forked from openlp/openlp
head
This commit is contained in:
commit
3f60c34440
@ -111,10 +111,10 @@ class OpenLP(QtGui.QApplication):
|
||||
# Decide how many screens we have and their size
|
||||
screens = ScreenList.create(self.desktop())
|
||||
# First time checks in settings
|
||||
has_run_wizard = Settings().value(u'general/has run wizard')
|
||||
has_run_wizard = Settings().value(u'core/has run wizard')
|
||||
if not has_run_wizard:
|
||||
if FirstTimeForm(screens).exec_() == QtGui.QDialog.Accepted:
|
||||
Settings().setValue(u'general/has run wizard', True)
|
||||
Settings().setValue(u'core/has run wizard', True)
|
||||
# Correct stylesheet bugs
|
||||
application_stylesheet = u''
|
||||
if not Settings().value(u'advanced/alternate rows'):
|
||||
@ -126,7 +126,7 @@ class OpenLP(QtGui.QApplication):
|
||||
application_stylesheet += NT_REPAIR_STYLESHEET
|
||||
if application_stylesheet:
|
||||
self.setStyleSheet(application_stylesheet)
|
||||
show_splash = Settings().value(u'general/show splash')
|
||||
show_splash = Settings().value(u'core/show splash')
|
||||
if show_splash:
|
||||
self.splash = SplashScreen()
|
||||
self.splash.show()
|
||||
@ -147,7 +147,7 @@ class OpenLP(QtGui.QApplication):
|
||||
self.processEvents()
|
||||
if not has_run_wizard:
|
||||
self.main_window.first_time()
|
||||
update_check = Settings().value(u'general/update check')
|
||||
update_check = Settings().value(u'core/update check')
|
||||
if update_check:
|
||||
VersionThread(self.main_window).start()
|
||||
self.main_window.is_display_blank()
|
||||
@ -305,8 +305,10 @@ def main(args=None):
|
||||
# Instance check
|
||||
if application.is_already_running():
|
||||
sys.exit()
|
||||
# Remove/convert obsolete settings.
|
||||
Settings().remove_obsolete_settings()
|
||||
# First time checks in settings
|
||||
if not Settings().value(u'general/has run wizard'):
|
||||
if not Settings().value(u'core/has run wizard'):
|
||||
if not FirstTimeLanguageForm().exec_():
|
||||
# if cancel then stop processing
|
||||
sys.exit()
|
||||
|
@ -103,6 +103,9 @@ class MediaManagerItem(QtGui.QWidget):
|
||||
self.retranslateUi()
|
||||
self.auto_select_id = -1
|
||||
Registry().register_function(u'%s_service_load' % self.plugin.name, self.service_load)
|
||||
# Need to use event as called across threads and UI is updated
|
||||
QtCore.QObject.connect(self, QtCore.SIGNAL(u'%s_go_live' % self.plugin.name), self.go_live_remote)
|
||||
QtCore.QObject.connect(self, QtCore.SIGNAL(u'%s_add_to_service' % self.plugin.name), self.add_to_service_remote)
|
||||
|
||||
def required_icons(self):
|
||||
"""
|
||||
@ -481,6 +484,15 @@ class MediaManagerItem(QtGui.QWidget):
|
||||
else:
|
||||
self.go_live()
|
||||
|
||||
def go_live_remote(self, message):
|
||||
"""
|
||||
Remote Call wrapper
|
||||
|
||||
``message``
|
||||
The passed data item_id:Remote.
|
||||
"""
|
||||
self.go_live(message[0], remote=message[1])
|
||||
|
||||
def go_live(self, item_id=None, remote=False):
|
||||
"""
|
||||
Make the currently selected item go live.
|
||||
@ -523,6 +535,15 @@ class MediaManagerItem(QtGui.QWidget):
|
||||
for item in items:
|
||||
self.add_to_service(item)
|
||||
|
||||
def add_to_service_remote(self, message):
|
||||
"""
|
||||
Remote Call wrapper
|
||||
|
||||
``message``
|
||||
The passed data item:Remote.
|
||||
"""
|
||||
self.add_to_service(message[0], remote=message[1])
|
||||
|
||||
def add_to_service(self, item=None, replace=None, remote=False):
|
||||
"""
|
||||
Add this item to the current service.
|
||||
|
@ -103,7 +103,7 @@ class Plugin(QtCore.QObject):
|
||||
``add_export_menu_Item(export_menu)``
|
||||
Add an item to the Export menu.
|
||||
|
||||
``create_settings_Tab()``
|
||||
``create_settings_tab()``
|
||||
Creates a new instance of SettingsTabItem to be used in the Settings
|
||||
dialog.
|
||||
|
||||
@ -252,7 +252,7 @@ class Plugin(QtCore.QObject):
|
||||
"""
|
||||
pass
|
||||
|
||||
def create_settings_Tab(self, parent):
|
||||
def create_settings_tab(self, parent):
|
||||
"""
|
||||
Create a tab for the settings window to display the configurable options
|
||||
for this plugin to the user.
|
||||
|
@ -153,7 +153,7 @@ class PluginManager(object):
|
||||
"""
|
||||
for plugin in self.plugins:
|
||||
if plugin.status is not PluginStatus.Disabled:
|
||||
plugin.create_settings_Tab(self.settings_form)
|
||||
plugin.create_settings_tab(self.settings_form)
|
||||
|
||||
def hook_import_menu(self):
|
||||
"""
|
||||
|
@ -247,15 +247,15 @@ class ScreenList(object):
|
||||
# Add the screen settings to the settings dict. This has to be done here due to cyclic dependency.
|
||||
# Do not do this anywhere else.
|
||||
screen_settings = {
|
||||
u'general/x position': self.current[u'size'].x(),
|
||||
u'general/y position': self.current[u'size'].y(),
|
||||
u'general/monitor': self.display_count - 1,
|
||||
u'general/height': self.current[u'size'].height(),
|
||||
u'general/width': self.current[u'size'].width()
|
||||
u'core/x position': self.current[u'size'].x(),
|
||||
u'core/y position': self.current[u'size'].y(),
|
||||
u'core/monitor': self.display_count - 1,
|
||||
u'core/height': self.current[u'size'].height(),
|
||||
u'core/width': self.current[u'size'].width()
|
||||
}
|
||||
Settings.extend_default_settings(screen_settings)
|
||||
settings = Settings()
|
||||
settings.beginGroup(u'general')
|
||||
settings.beginGroup(u'core')
|
||||
monitor = settings.value(u'monitor')
|
||||
self.set_current_display(monitor)
|
||||
self.display = settings.value(u'display on monitor')
|
||||
|
@ -62,12 +62,10 @@ class ItemCapabilities(object):
|
||||
tab when making the previous item live.
|
||||
|
||||
``CanEdit``
|
||||
The capability to allow the ServiceManager to allow the item to be
|
||||
edited
|
||||
The capability to allow the ServiceManager to allow the item to be edited
|
||||
|
||||
``CanMaintain``
|
||||
The capability to allow the ServiceManager to allow the item to be
|
||||
reordered.
|
||||
The capability to allow the ServiceManager to allow the item to be reordered.
|
||||
|
||||
``RequiresMedia``
|
||||
Determines is the service_item needs a Media Player
|
||||
|
@ -116,30 +116,29 @@ class Settings(QtCore.QSettings):
|
||||
u'advanced/x11 bypass wm': X11_BYPASS_DEFAULT,
|
||||
u'crashreport/last directory': u'',
|
||||
u'displayTags/html_tags': u'',
|
||||
u'general/audio repeat list': False,
|
||||
u'general/auto open': False,
|
||||
u'general/auto preview': False,
|
||||
u'general/audio start paused': True,
|
||||
u'general/auto unblank': False,
|
||||
u'general/blank warning': False,
|
||||
u'general/ccli number': u'',
|
||||
u'general/has run wizard': False,
|
||||
u'general/language': u'[en]',
|
||||
# This defaults to yesterday in order to force the update check to run when you've never run it before.
|
||||
u'general/last version test': datetime.datetime.now().date() - datetime.timedelta(days=1),
|
||||
u'general/loop delay': 5,
|
||||
u'general/recent files': [],
|
||||
u'general/save prompt': False,
|
||||
u'general/screen blank': False,
|
||||
u'general/show splash': True,
|
||||
u'general/songselect password': u'',
|
||||
u'general/songselect username': u'',
|
||||
u'general/update check': True,
|
||||
u'general/view mode': u'default',
|
||||
u'core/audio repeat list': False,
|
||||
u'core/auto open': False,
|
||||
u'core/auto preview': False,
|
||||
u'core/audio start paused': True,
|
||||
u'core/auto unblank': False,
|
||||
u'core/blank warning': False,
|
||||
u'core/ccli number': u'',
|
||||
u'core/has run wizard': False,
|
||||
u'core/language': u'[en]',
|
||||
u'core/last version test': u'',
|
||||
u'core/loop delay': 5,
|
||||
u'core/recent files': [],
|
||||
u'core/save prompt': False,
|
||||
u'core/screen blank': False,
|
||||
u'core/show splash': True,
|
||||
u'core/songselect password': u'',
|
||||
u'core/songselect username': u'',
|
||||
u'core/update check': True,
|
||||
u'core/view mode': u'default',
|
||||
# The other display settings (display position and dimensions) are defined in the ScreenList class due to a
|
||||
# circular dependency.
|
||||
u'general/display on monitor': True,
|
||||
u'general/override position': False,
|
||||
u'core/display on monitor': True,
|
||||
u'core/override position': False,
|
||||
u'images/background color': u'#000000',
|
||||
u'media/players': u'webkit',
|
||||
u'media/override player': QtCore.Qt.Unchecked,
|
||||
@ -304,7 +303,7 @@ class Settings(QtCore.QSettings):
|
||||
# Changed during 1.9.x development.
|
||||
(u'bibles/bookname language', u'bibles/book name language', []),
|
||||
(u'general/enable slide loop', u'advanced/slide limits', [(SlideLimits.Wrap, True), (SlideLimits.End, False)]),
|
||||
(u'songs/ccli number', u'general/ccli number', []),
|
||||
(u'songs/ccli number', u'core/ccli number', []),
|
||||
(u'media/use phonon', u'', []),
|
||||
# Changed during 2.1.x development.
|
||||
(u'advanced/stylesheet fix', u'', []),
|
||||
@ -315,7 +314,34 @@ class Settings(QtCore.QSettings):
|
||||
(u'songs/last directory 1', u'songs/last directory import', []),
|
||||
(u'songusage/last directory 1', u'songusage/last directory export', []),
|
||||
(u'user interface/mainwindow splitter geometry', u'user interface/main window splitter geometry', []),
|
||||
(u'shortcuts/makeLive', u'shortcuts/make_live', [])
|
||||
(u'shortcuts/makeLive', u'shortcuts/make_live', []),
|
||||
(u'general/audio repeat list', u'core/audio repeat list', []),
|
||||
(u'general/auto open', u'core/auto open', []),
|
||||
(u'general/auto preview', u'core/auto preview', []),
|
||||
(u'general/audio start paused', u'core/audio start paused', []),
|
||||
(u'general/auto unblank', u'core/auto unblank', []),
|
||||
(u'general/blank warning', u'core/blank warning', []),
|
||||
(u'general/ccli number', u'core/ccli number', []),
|
||||
(u'general/has run wizard', u'core/has run wizard', []),
|
||||
(u'general/language', u'core/language', []),
|
||||
(u'general/last version test', u'core/last version test', []),
|
||||
(u'general/loop delay', u'core/loop delay', []),
|
||||
(u'general/recent files', u'core/recent files', []),
|
||||
(u'general/save prompt', u'core/save prompt', []),
|
||||
(u'general/screen blank', u'core/screen blank', []),
|
||||
(u'general/show splash', u'core/show splash', []),
|
||||
(u'general/songselect password', u'core/songselect password', []),
|
||||
(u'general/songselect username', u'core/songselect username', []),
|
||||
(u'general/update check', u'core/update check', []),
|
||||
(u'general/view mode', u'core/view mode', []),
|
||||
(u'general/display on monitor', u'core/display on monitor', []),
|
||||
(u'general/override position', u'core/override position', []),
|
||||
(u'general/x position', u'core/x position', []),
|
||||
(u'general/y position', u'core/y position', []),
|
||||
(u'general/monitor', u'core/monitor', []),
|
||||
(u'general/height', u'core/height', []),
|
||||
(u'general/monitor', u'core/monitor', []),
|
||||
(u'general/width', u'core/width', [])
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
|
@ -69,6 +69,19 @@ try:
|
||||
MAKO_VERSION = mako.__version__
|
||||
except ImportError:
|
||||
MAKO_VERSION = u'-'
|
||||
try:
|
||||
import icu
|
||||
try:
|
||||
ICU_VERSION = icu.VERSION
|
||||
except AttributeError:
|
||||
ICU_VERSION = u'OK'
|
||||
except ImportError:
|
||||
ICU_VERSION = u'-'
|
||||
try:
|
||||
import cherrypy
|
||||
CHERRYPY_VERSION = cherrypy.__version__
|
||||
except ImportError:
|
||||
CHERRYPY_VERSION = u'-'
|
||||
try:
|
||||
import uno
|
||||
arg = uno.createUnoStruct(u'com.sun.star.beans.PropertyValue')
|
||||
@ -143,6 +156,8 @@ class ExceptionForm(QtGui.QDialog, Ui_ExceptionDialog):
|
||||
u'PyEnchant: %s\n' % ENCHANT_VERSION + \
|
||||
u'PySQLite: %s\n' % SQLITE_VERSION + \
|
||||
u'Mako: %s\n' % MAKO_VERSION + \
|
||||
u'CherryPy: %s\n' % CHERRYPY_VERSION + \
|
||||
u'pyICU: %s\n' % ICU_VERSION + \
|
||||
u'pyUNO bridge: %s\n' % UNO_VERSION + \
|
||||
u'VLC: %s\n' % VLC_VERSION
|
||||
if platform.system() == u'Linux':
|
||||
|
@ -118,7 +118,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard):
|
||||
check_directory_exists(os.path.join(unicode(gettempdir(), get_filesystem_encoding()), u'openlp'))
|
||||
self.noInternetFinishButton.setVisible(False)
|
||||
# Check if this is a re-run of the wizard.
|
||||
self.hasRunWizard = Settings().value(u'general/has run wizard')
|
||||
self.hasRunWizard = Settings().value(u'core/has run wizard')
|
||||
# Sort out internet access for downloads
|
||||
if self.web_access:
|
||||
songs = self.config.get(u'songs', u'languages')
|
||||
@ -252,7 +252,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard):
|
||||
self.application.set_busy_cursor()
|
||||
self._performWizard()
|
||||
self.application.set_normal_cursor()
|
||||
Settings().setValue(u'general/has run wizard', True)
|
||||
Settings().setValue(u'core/has run wizard', True)
|
||||
self.close()
|
||||
|
||||
def urlGetFile(self, url, fpath):
|
||||
@ -459,7 +459,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard):
|
||||
self.urlGetFile(u'%s%s' % (self.web, theme), os.path.join(themes_destination, theme))
|
||||
# Set Default Display
|
||||
if self.displayComboBox.currentIndex() != -1:
|
||||
Settings().setValue(u'General/monitor', self.displayComboBox.currentIndex())
|
||||
Settings().setValue(u'core/monitor', self.displayComboBox.currentIndex())
|
||||
self.screens.set_current_display(self.displayComboBox.currentIndex())
|
||||
# Set Global Theme
|
||||
if self.themeComboBox.currentIndex() != -1:
|
||||
|
@ -49,7 +49,7 @@ class GeneralTab(SettingsTab):
|
||||
self.screens = ScreenList()
|
||||
self.icon_path = u':/icon/openlp-logo-16x16.png'
|
||||
general_translated = translate('OpenLP.GeneralTab', 'General')
|
||||
SettingsTab.__init__(self, parent, u'General', general_translated)
|
||||
SettingsTab.__init__(self, parent, u'Core', general_translated)
|
||||
|
||||
def setupUi(self):
|
||||
"""
|
||||
|
@ -357,7 +357,7 @@ class MainDisplay(Display):
|
||||
# Single screen active
|
||||
if self.screens.display_count == 1:
|
||||
# Only make visible if setting enabled.
|
||||
if Settings().value(u'general/display on monitor'):
|
||||
if Settings().value(u'core/display on monitor'):
|
||||
self.setVisible(True)
|
||||
else:
|
||||
self.setVisible(True)
|
||||
@ -405,7 +405,7 @@ class MainDisplay(Display):
|
||||
self.footer(service_item.foot_text)
|
||||
# if was hidden keep it hidden
|
||||
if self.hide_mode and self.is_live and not service_item.is_media():
|
||||
if Settings().value(u'general/auto unblank'):
|
||||
if Settings().value(u'core/auto unblank'):
|
||||
Registry().execute(u'slidecontroller_live_unblank')
|
||||
else:
|
||||
self.hide_display(self.hide_mode)
|
||||
@ -427,7 +427,7 @@ class MainDisplay(Display):
|
||||
log.debug(u'hide_display mode = %d', mode)
|
||||
if self.screens.display_count == 1:
|
||||
# Only make visible if setting enabled.
|
||||
if not Settings().value(u'general/display on monitor'):
|
||||
if not Settings().value(u'core/display on monitor'):
|
||||
return
|
||||
if mode == HideMode.Screen:
|
||||
self.frame.evaluateJavaScript(u'show_blank("desktop");')
|
||||
@ -450,7 +450,7 @@ class MainDisplay(Display):
|
||||
log.debug(u'show_display')
|
||||
if self.screens.display_count == 1:
|
||||
# Only make visible if setting enabled.
|
||||
if not Settings().value(u'general/display on monitor'):
|
||||
if not Settings().value(u'core/display on monitor'):
|
||||
return
|
||||
self.frame.evaluateJavaScript('show_blank("show");')
|
||||
if self.isHidden():
|
||||
|
@ -476,7 +476,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
|
||||
self.arguments = self.application.args
|
||||
# Set up settings sections for the main application (not for use by plugins).
|
||||
self.ui_settings_section = u'user interface'
|
||||
self.general_settings_section = u'general'
|
||||
self.general_settings_section = u'core'
|
||||
self.advanced_settings_section = u'advanced'
|
||||
self.shortcuts_settings_section = u'shortcuts'
|
||||
self.service_manager_settings_section = u'servicemanager'
|
||||
@ -491,7 +491,6 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
|
||||
self.new_data_path = None
|
||||
self.copy_data = False
|
||||
Settings().set_up_default_values()
|
||||
Settings().remove_obsolete_settings()
|
||||
self.service_not_saved = False
|
||||
self.about_form = AboutForm(self)
|
||||
self.media_controller = MediaController()
|
||||
|
@ -415,7 +415,7 @@ class MediaController(object):
|
||||
elif not hidden or controller.media_info.is_background or service_item.will_auto_start:
|
||||
autoplay = True
|
||||
# Unblank on load set
|
||||
elif Settings().value(u'general/auto unblank'):
|
||||
elif Settings().value(u'core/auto unblank'):
|
||||
autoplay = True
|
||||
if autoplay:
|
||||
if not self.media_play(controller):
|
||||
|
@ -273,7 +273,6 @@ class ServiceManagerDialog(object):
|
||||
Registry().register_function(u'config_screen_changed', self.regenerate_service_Items)
|
||||
Registry().register_function(u'theme_update_global', self.theme_change)
|
||||
Registry().register_function(u'mediaitem_suffix_reset', self.reset_supported_suffixes)
|
||||
Registry().register_function(u'servicemanager_set_item', self.on_set_item)
|
||||
|
||||
def drag_enter_event(self, event):
|
||||
"""
|
||||
@ -315,6 +314,8 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
|
||||
self.layout.setSpacing(0)
|
||||
self.layout.setMargin(0)
|
||||
self.setup_ui(self)
|
||||
# Need to use event as called across threads and UI is updated
|
||||
QtCore.QObject.connect(self, QtCore.SIGNAL(u'servicemanager_set_item'), self.on_set_item)
|
||||
|
||||
def set_modified(self, modified=True):
|
||||
"""
|
||||
@ -993,7 +994,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
|
||||
|
||||
def on_set_item(self, message):
|
||||
"""
|
||||
Called by a signal to select a specific item.
|
||||
Called by a signal to select a specific item and make it live usually from remote.
|
||||
"""
|
||||
self.set_item(int(message))
|
||||
|
||||
|
@ -96,6 +96,7 @@ class SettingsForm(QtGui.QDialog, Ui_SettingsDialog):
|
||||
"""
|
||||
Process the form saving the settings
|
||||
"""
|
||||
log.debug(u'Processing settings exit')
|
||||
for tabIndex in range(self.stacked_layout.count()):
|
||||
self.stacked_layout.widget(tabIndex).save()
|
||||
# if the display of image background are changing we need to regenerate the image cache
|
||||
|
@ -44,6 +44,8 @@ from openlp.core.utils.actions import ActionList, CategoryOrder
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Threshold which has to be trespassed to toggle.
|
||||
HIDE_MENU_THRESHOLD = 27
|
||||
AUDIO_TIME_LABEL_STYLESHEET = u'background-color: palette(background); ' \
|
||||
u'border-top-color: palette(shadow); ' \
|
||||
u'border-left-color: palette(shadow); ' \
|
||||
@ -358,8 +360,9 @@ class SlideController(DisplayController):
|
||||
# Signals
|
||||
self.preview_list_widget.clicked.connect(self.onSlideSelected)
|
||||
if self.is_live:
|
||||
# Need to use event as called across threads and UI is updated
|
||||
QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_toggle_display'), self.toggle_display)
|
||||
Registry().register_function(u'slidecontroller_live_spin_delay', self.receive_spin_delay)
|
||||
Registry().register_function(u'slidecontroller_toggle_display', self.toggle_display)
|
||||
self.toolbar.set_widget_visible(self.loop_list, False)
|
||||
self.toolbar.set_widget_visible(self.wide_menu, False)
|
||||
else:
|
||||
@ -371,13 +374,16 @@ class SlideController(DisplayController):
|
||||
else:
|
||||
self.preview_list_widget.addActions([self.nextItem, self.previous_item])
|
||||
Registry().register_function(u'slidecontroller_%s_stop_loop' % self.type_prefix, self.on_stop_loop)
|
||||
Registry().register_function(u'slidecontroller_%s_next' % self.type_prefix, self.on_slide_selected_next)
|
||||
Registry().register_function(u'slidecontroller_%s_previous' % self.type_prefix, self.on_slide_selected_previous)
|
||||
Registry().register_function(u'slidecontroller_%s_change' % self.type_prefix, self.on_slide_change)
|
||||
Registry().register_function(u'slidecontroller_%s_set' % self.type_prefix, self.on_slide_selected_index)
|
||||
Registry().register_function(u'slidecontroller_%s_blank' % self.type_prefix, self.on_slide_blank)
|
||||
Registry().register_function(u'slidecontroller_%s_unblank' % self.type_prefix, self.on_slide_unblank)
|
||||
Registry().register_function(u'slidecontroller_update_slide_limits', self.update_slide_limits)
|
||||
QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_%s_set' % self.type_prefix),
|
||||
self.on_slide_selected_index)
|
||||
QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_%s_next' % self.type_prefix),
|
||||
self.on_slide_selected_next)
|
||||
QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_%s_previous' % self.type_prefix),
|
||||
self.on_slide_selected_previous)
|
||||
|
||||
def _slideShortcutActivated(self):
|
||||
"""
|
||||
@ -588,12 +594,12 @@ class SlideController(DisplayController):
|
||||
if self.is_live:
|
||||
# Space used by the toolbar.
|
||||
used_space = self.toolbar.size().width() + self.hide_menu.size().width()
|
||||
# The + 40 is needed to prevent flickering. This can be considered a "buffer".
|
||||
if width > used_space + 40 and self.hide_menu.isVisible():
|
||||
# Add the threshold to prevent flickering.
|
||||
if width > used_space + HIDE_MENU_THRESHOLD and self.hide_menu.isVisible():
|
||||
self.toolbar.set_widget_visible(self.narrow_menu, False)
|
||||
self.toolbar.set_widget_visible(self.wide_menu)
|
||||
# The - 40 is needed to prevent flickering. This can be considered a "buffer".
|
||||
elif width < used_space - 40 and not self.hide_menu.isVisible():
|
||||
# Take away a threshold to prevent flickering.
|
||||
elif width < used_space - HIDE_MENU_THRESHOLD and not self.hide_menu.isVisible():
|
||||
self.toolbar.set_widget_visible(self.wide_menu, False)
|
||||
self.toolbar.set_widget_visible(self.narrow_menu)
|
||||
|
||||
|
@ -44,7 +44,7 @@ from openlp.core.lib.theme import ThemeXML, BackgroundType, VerticalType, Backgr
|
||||
from openlp.core.lib.ui import critical_error_message_box, create_widget_action
|
||||
from openlp.core.theme import Theme
|
||||
from openlp.core.ui import FileRenameForm, ThemeForm
|
||||
from openlp.core.utils import AppLocation, delete_file, locale_compare, get_filesystem_encoding
|
||||
from openlp.core.utils import AppLocation, delete_file, get_locale_key, get_filesystem_encoding
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -418,7 +418,7 @@ class ThemeManager(QtGui.QWidget):
|
||||
self.theme_list_widget.clear()
|
||||
files = AppLocation.get_files(self.settings_section, u'.png')
|
||||
# Sort the themes by its name considering language specific
|
||||
files.sort(key=lambda file_name: unicode(file_name), cmp=locale_compare)
|
||||
files.sort(key=lambda file_name: get_locale_key(unicode(file_name)))
|
||||
# now process the file list of png files
|
||||
for name in files:
|
||||
# check to see file is in theme root directory
|
||||
|
@ -38,6 +38,7 @@ import re
|
||||
from subprocess import Popen, PIPE
|
||||
import sys
|
||||
import urllib2
|
||||
import icu
|
||||
|
||||
from PyQt4 import QtGui, QtCore
|
||||
|
||||
@ -56,10 +57,12 @@ from openlp.core.lib import translate
|
||||
log = logging.getLogger(__name__)
|
||||
APPLICATION_VERSION = {}
|
||||
IMAGES_FILTER = None
|
||||
ICU_COLLATOR = None
|
||||
UNO_CONNECTION_TYPE = u'pipe'
|
||||
#UNO_CONNECTION_TYPE = u'socket'
|
||||
CONTROL_CHARS = re.compile(r'[\x00-\x1F\x7F-\x9F]', re.UNICODE)
|
||||
INVALID_FILE_CHARS = re.compile(r'[\\/:\*\?"<>\|\+\[\]%]', re.UNICODE)
|
||||
DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+', re.UNICODE)
|
||||
|
||||
|
||||
class VersionThread(QtCore.QThread):
|
||||
@ -184,7 +187,7 @@ def check_latest_version(current_version):
|
||||
settings = Settings()
|
||||
settings.beginGroup(u'general')
|
||||
last_test = settings.value(u'last version test')
|
||||
this_test = datetime.now().date()
|
||||
this_test = unicode(datetime.now().date())
|
||||
settings.setValue(u'last version test', this_test)
|
||||
settings.endGroup()
|
||||
# Tell the main window whether there will ever be data to display
|
||||
@ -244,8 +247,7 @@ def get_images_filter():
|
||||
global IMAGES_FILTER
|
||||
if not IMAGES_FILTER:
|
||||
log.debug(u'Generating images filter.')
|
||||
formats = [unicode(fmt)
|
||||
for fmt in QtGui.QImageReader.supportedImageFormats()]
|
||||
formats = map(unicode, QtGui.QImageReader.supportedImageFormats())
|
||||
visible_formats = u'(*.%s)' % u'; *.'.join(formats)
|
||||
actual_formats = u'(*.%s)' % u' *.'.join(formats)
|
||||
IMAGES_FILTER = u'%s %s %s' % (translate('OpenLP', 'Image Files'), visible_formats, actual_formats)
|
||||
@ -379,21 +381,32 @@ def format_time(text, local_time):
|
||||
return re.sub('\%[a-zA-Z]', match_formatting, text)
|
||||
|
||||
|
||||
def locale_compare(string1, string2):
|
||||
def get_locale_key(string):
|
||||
"""
|
||||
Compares two strings according to the current locale settings.
|
||||
|
||||
As any other compare function, returns a negative, or a positive value,
|
||||
or 0, depending on whether string1 collates before or after string2 or
|
||||
is equal to it. Comparison is case insensitive.
|
||||
Creates a key for case insensitive, locale aware string sorting.
|
||||
"""
|
||||
# Function locale.strcoll() from standard Python library does not work properly on Windows.
|
||||
return locale.strcoll(string1.lower(), string2.lower())
|
||||
string = string.lower()
|
||||
# For Python 3 on platforms other than Windows ICU is not necessary. In those cases locale.strxfrm(str) can be used.
|
||||
global ICU_COLLATOR
|
||||
if ICU_COLLATOR is None:
|
||||
from languagemanager import LanguageManager
|
||||
locale = LanguageManager.get_language()
|
||||
icu_locale = icu.Locale(locale)
|
||||
ICU_COLLATOR = icu.Collator.createInstance(icu_locale)
|
||||
return ICU_COLLATOR.getSortKey(string)
|
||||
|
||||
|
||||
# For performance reasons provide direct reference to compare function without wrapping it in another function making
|
||||
# the string lowercase. This is needed for sorting songs.
|
||||
locale_direct_compare = locale.strcoll
|
||||
def get_natural_key(string):
|
||||
"""
|
||||
Generate a key for locale aware natural string sorting.
|
||||
Returns a list of string compare keys and integers.
|
||||
"""
|
||||
key = DIGITS_OR_NONDIGITS.findall(string)
|
||||
key = [int(part) if part.isdigit() else get_locale_key(part) for part in key]
|
||||
# Python 3 does not support comparision of different types anymore. So make sure, that we do not compare str and int.
|
||||
#if string[0].isdigit():
|
||||
# return [''] + key
|
||||
return key
|
||||
|
||||
|
||||
from applocation import AppLocation
|
||||
@ -403,4 +416,4 @@ from actions import ActionList
|
||||
|
||||
__all__ = [u'AppLocation', u'ActionList', u'LanguageManager', u'get_application_version', u'check_latest_version',
|
||||
u'add_actions', u'get_filesystem_encoding', u'get_web_page', u'get_uno_command', u'get_uno_instance',
|
||||
u'delete_file', u'clean_filename', u'format_time', u'locale_compare', u'locale_direct_compare']
|
||||
u'delete_file', u'clean_filename', u'format_time', u'get_locale_key', u'get_natural_key']
|
||||
|
@ -98,7 +98,7 @@ class LanguageManager(object):
|
||||
"""
|
||||
Retrieve a saved language to use from settings
|
||||
"""
|
||||
language = Settings().value(u'general/language')
|
||||
language = Settings().value(u'core/language')
|
||||
language = str(language)
|
||||
log.info(u'Language file: \'%s\' Loaded from conf file' % language)
|
||||
if re.match(r'[[].*[]]', language):
|
||||
@ -128,7 +128,7 @@ class LanguageManager(object):
|
||||
language = unicode(qm_list[action_name])
|
||||
if LanguageManager.auto_language:
|
||||
language = u'[%s]' % language
|
||||
Settings().setValue(u'general/language', language)
|
||||
Settings().setValue(u'core/language', language)
|
||||
log.info(u'Language file: \'%s\' written to conf file' % language)
|
||||
if message:
|
||||
QtGui.QMessageBox.information(None,
|
||||
|
@ -49,10 +49,12 @@ class AlertsManager(QtCore.QObject):
|
||||
|
||||
def __init__(self, parent):
|
||||
QtCore.QObject.__init__(self, parent)
|
||||
Registry().register(u'alerts_manager', self)
|
||||
self.timer_id = 0
|
||||
self.alert_list = []
|
||||
Registry().register_function(u'live_display_active', self.generate_alert)
|
||||
Registry().register_function(u'alerts_text', self.alert_text)
|
||||
QtCore.QObject.connect(self, QtCore.SIGNAL(u'alerts_text'), self.alert_text)
|
||||
|
||||
def alert_text(self, message):
|
||||
"""
|
||||
|
@ -38,7 +38,7 @@ from openlp.core.lib import Settings, UiStrings, translate
|
||||
from openlp.core.lib.db import delete_database
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.core.ui.wizard import OpenLPWizard, WizardStrings
|
||||
from openlp.core.utils import AppLocation, locale_compare
|
||||
from openlp.core.utils import AppLocation, get_locale_key
|
||||
from openlp.plugins.bibles.lib.manager import BibleFormat
|
||||
from openlp.plugins.bibles.lib.db import BiblesResourcesDB, clean_filename
|
||||
|
||||
@ -455,7 +455,7 @@ class BibleImportForm(OpenLPWizard):
|
||||
"""
|
||||
self.webTranslationComboBox.clear()
|
||||
bibles = self.web_bible_list[index].keys()
|
||||
bibles.sort(cmp=locale_compare)
|
||||
bibles.sort(key=get_locale_key)
|
||||
self.webTranslationComboBox.addItems(bibles)
|
||||
|
||||
def onOsisBrowseButtonClicked(self):
|
||||
|
@ -714,6 +714,7 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, pre
|
||||
Registry().get(u'application').process_events()
|
||||
return soup
|
||||
|
||||
|
||||
def send_error_message(error_type):
|
||||
"""
|
||||
Send a standard error message informing the user of an issue.
|
||||
|
@ -36,7 +36,7 @@ from openlp.core.lib import Registry, MediaManagerItem, ItemCapabilities, Servic
|
||||
from openlp.core.lib.searchedit import SearchEdit
|
||||
from openlp.core.lib.ui import set_case_insensitive_completer, create_horizontal_adjusting_combo_box, \
|
||||
critical_error_message_box, find_and_set_in_combo_box, build_icon
|
||||
from openlp.core.utils import locale_compare
|
||||
from openlp.core.utils import get_locale_key
|
||||
from openlp.plugins.bibles.forms import BibleImportForm, EditBibleForm
|
||||
from openlp.plugins.bibles.lib import LayoutStyle, DisplayStyle, VerseReferenceList, get_reference_separator, \
|
||||
LanguageSelection, BibleStrings
|
||||
@ -325,7 +325,7 @@ class BibleMediaItem(MediaManagerItem):
|
||||
# Get all bibles and sort the list.
|
||||
bibles = self.plugin.manager.get_bibles().keys()
|
||||
bibles = filter(None, bibles)
|
||||
bibles.sort(cmp=locale_compare)
|
||||
bibles.sort(key=get_locale_key)
|
||||
# Load the bibles into the combo boxes.
|
||||
self.quickVersionComboBox.addItems(bibles)
|
||||
self.quickSecondComboBox.addItems(bibles)
|
||||
@ -461,7 +461,7 @@ class BibleMediaItem(MediaManagerItem):
|
||||
for book in book_data:
|
||||
data = BiblesResourcesDB.get_book_by_id(book.book_reference_id)
|
||||
books.append(data[u'name'] + u' ')
|
||||
books.sort(cmp=locale_compare)
|
||||
books.sort(key=get_locale_key)
|
||||
set_case_insensitive_completer(books, self.quickSearchEdit)
|
||||
|
||||
def on_import_click(self):
|
||||
|
@ -35,7 +35,7 @@ from sqlalchemy import Column, Table, types
|
||||
from sqlalchemy.orm import mapper
|
||||
|
||||
from openlp.core.lib.db import BaseModel, init_db
|
||||
from openlp.core.utils import locale_compare
|
||||
from openlp.core.utils import get_locale_key
|
||||
|
||||
class CustomSlide(BaseModel):
|
||||
"""
|
||||
@ -44,11 +44,10 @@ class CustomSlide(BaseModel):
|
||||
# By default sort the customs by its title considering language specific
|
||||
# characters.
|
||||
def __lt__(self, other):
|
||||
r = locale_compare(self.title, other.title)
|
||||
return True if r < 0 else False
|
||||
return get_locale_key(self.title) < get_locale_key(other.title)
|
||||
|
||||
def __eq__(self, other):
|
||||
return 0 == locale_compare(self.title, other.title)
|
||||
return get_locale_key(self.title) == get_locale_key(other.title)
|
||||
|
||||
|
||||
def init_schema(url):
|
||||
|
@ -27,6 +27,6 @@
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
The :mod:`images` module provides the Images plugin. The Images plugin
|
||||
provides the facility to display images from OpenLP.
|
||||
The :mod:`images` module provides the Images plugin. The Images plugin provides the facility to display images from
|
||||
OpenLP.
|
||||
"""
|
||||
|
@ -27,20 +27,16 @@
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
Forms in OpenLP are made up of two classes. One class holds all the graphical
|
||||
elements, like buttons and lists, and the other class holds all the functional
|
||||
code, like slots and loading and saving.
|
||||
Forms in OpenLP are made up of two classes. One class holds all the graphical elements, like buttons and lists, and the
|
||||
other class holds all the functional code, like slots and loading and saving.
|
||||
|
||||
The first class, commonly known as the **Dialog** class, is typically named
|
||||
``Ui_<name>Dialog``. It is a slightly modified version of the class that the
|
||||
``pyuic4`` command produces from Qt4's .ui file. Typical modifications will be
|
||||
converting most strings from "" to u'' and using OpenLP's ``translate()``
|
||||
function for translating strings.
|
||||
The first class, commonly known as the **Dialog** class, is typically named ``Ui_<name>Dialog``. It is a slightly
|
||||
modified version of the class that the ``pyuic4`` command produces from Qt4's .ui file. Typical modifications will be
|
||||
converting most strings from "" to u'' and using OpenLP's ``translate()`` function for translating strings.
|
||||
|
||||
The second class, commonly known as the **Form** class, is typically named
|
||||
``<name>Form``. This class is the one which is instantiated and used. It uses
|
||||
dual inheritance to inherit from (usually) QtGui.QDialog and the Ui class
|
||||
mentioned above, like so::
|
||||
The second class, commonly known as the **Form** class, is typically named ``<name>Form``. This class is the one which
|
||||
is instantiated and used. It uses dual inheritance to inherit from (usually) QtGui.QDialog and the Ui class mentioned
|
||||
above, like so::
|
||||
|
||||
class AuthorsForm(QtGui.QDialog, Ui_AuthorsDialog):
|
||||
|
||||
@ -48,9 +44,8 @@ mentioned above, like so::
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
|
||||
This allows OpenLP to use ``self.object`` for all the GUI elements while keeping
|
||||
them separate from the functionality, so that it is easier to recreate the GUI
|
||||
from the .ui files later if necessary.
|
||||
This allows OpenLP to use ``self.object`` for all the GUI elements while keeping them separate from the functionality,
|
||||
so that it is easier to recreate the GUI from the .ui files later if necessary.
|
||||
"""
|
||||
|
||||
from addgroupform import AddGroupForm
|
||||
|
@ -47,16 +47,16 @@ class AddGroupForm(QtGui.QDialog, Ui_AddGroupDialog):
|
||||
|
||||
def exec_(self, clear=True, show_top_level_group=False, selected_group=None):
|
||||
"""
|
||||
Show the form
|
||||
Show the form.
|
||||
|
||||
``clear``
|
||||
Set to False if the text input box should not be cleared when showing the dialog (default: True)
|
||||
Set to False if the text input box should not be cleared when showing the dialog (default: True).
|
||||
|
||||
``show_top_level_group``
|
||||
Set to True when "-- Top level group --" should be showed as first item (default: False)
|
||||
Set to True when "-- Top level group --" should be showed as first item (default: False).
|
||||
|
||||
``selected_group``
|
||||
The ID of the group that should be selected by default when showing the dialog
|
||||
The ID of the group that should be selected by default when showing the dialog.
|
||||
"""
|
||||
if clear:
|
||||
self.name_edit.clear()
|
||||
@ -72,7 +72,7 @@ class AddGroupForm(QtGui.QDialog, Ui_AddGroupDialog):
|
||||
|
||||
def accept(self):
|
||||
"""
|
||||
Override the accept() method from QDialog to make sure something is entered in the text input box
|
||||
Override the accept() method from QDialog to make sure something is entered in the text input box.
|
||||
"""
|
||||
if not self.name_edit.text():
|
||||
critical_error_message_box(message=translate('ImagePlugin.AddGroupForm',
|
||||
|
@ -48,10 +48,10 @@ class ChooseGroupForm(QtGui.QDialog, Ui_ChooseGroupDialog):
|
||||
Show the form
|
||||
|
||||
``selected_group``
|
||||
The ID of the group that should be selected by default when showing the dialog
|
||||
The ID of the group that should be selected by default when showing the dialog.
|
||||
"""
|
||||
if selected_group is not None:
|
||||
for i in range(self.group_combobox.count()):
|
||||
if self.group_combobox.itemData(i) == selected_group:
|
||||
self.group_combobox.setCurrentIndex(i)
|
||||
for index in range(self.group_combobox.count()):
|
||||
if self.group_combobox.itemData(index) == selected_group:
|
||||
self.group_combobox.setCurrentIndex(index)
|
||||
return QtGui.QDialog.exec_(self)
|
||||
|
@ -70,10 +70,10 @@ class ImagePlugin(Plugin):
|
||||
|
||||
def app_startup(self):
|
||||
"""
|
||||
Perform tasks on application startup
|
||||
Perform tasks on application startup.
|
||||
"""
|
||||
Plugin.app_startup(self)
|
||||
# Convert old settings-based image list to the database
|
||||
# Convert old settings-based image list to the database.
|
||||
files_from_config = Settings().get_files_from_config(self)
|
||||
if files_from_config:
|
||||
log.debug(u'Importing images list from old config: %s' % files_from_config)
|
||||
@ -93,7 +93,7 @@ class ImagePlugin(Plugin):
|
||||
|
||||
def set_plugin_text_strings(self):
|
||||
"""
|
||||
Called to define all translatable texts of the plugin
|
||||
Called to define all translatable texts of the plugin.
|
||||
"""
|
||||
## Name PluginList ##
|
||||
self.text_strings[StringContent.Name] = {
|
||||
@ -117,8 +117,8 @@ class ImagePlugin(Plugin):
|
||||
|
||||
def config_update(self):
|
||||
"""
|
||||
Triggered by saving and changing the image border. Sets the images in image manager to require updates.
|
||||
Actual update is triggered by the last part of saving the config.
|
||||
Triggered by saving and changing the image border. Sets the images in image manager to require updates. Actual
|
||||
update is triggered by the last part of saving the config.
|
||||
"""
|
||||
log.info(u'Images config_update')
|
||||
background = QtGui.QColor(Settings().value(self.settings_section + u'/background color'))
|
||||
|
@ -27,7 +27,7 @@
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
The :mod:`db` module provides the database and schema that is the backend for the Images plugin
|
||||
The :mod:`db` module provides the database and schema that is the backend for the Images plugin.
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, ForeignKey, Table, types
|
||||
@ -38,14 +38,14 @@ from openlp.core.lib.db import BaseModel, init_db
|
||||
|
||||
class ImageGroups(BaseModel):
|
||||
"""
|
||||
ImageGroups model
|
||||
ImageGroups model.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ImageFilenames(BaseModel):
|
||||
"""
|
||||
ImageFilenames model
|
||||
ImageFilenames model.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
@ -36,10 +36,11 @@ from openlp.core.lib import ItemCapabilities, MediaManagerItem, Registry, Servic
|
||||
StringContent, TreeWidgetWithDnD, UiStrings, build_icon, check_directory_exists, check_item_selected, \
|
||||
create_thumb, translate, validate_thumb
|
||||
from openlp.core.lib.ui import create_widget_action, critical_error_message_box
|
||||
from openlp.core.utils import AppLocation, delete_file, locale_compare, get_images_filter
|
||||
from openlp.core.utils import AppLocation, delete_file, get_locale_key, get_images_filter
|
||||
from openlp.plugins.images.forms import AddGroupForm, ChooseGroupForm
|
||||
from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -60,24 +61,23 @@ class ImageMediaItem(MediaManagerItem):
|
||||
self.fill_groups_combobox(self.choose_group_form.group_combobox)
|
||||
self.fill_groups_combobox(self.add_group_form.parent_group_combobox)
|
||||
Registry().register_function(u'live_theme_changed', self.live_theme_changed)
|
||||
# Allow DnD from the desktop
|
||||
# Allow DnD from the desktop.
|
||||
self.list_view.activateDnD()
|
||||
|
||||
def retranslateUi(self):
|
||||
self.on_new_prompt = translate('ImagePlugin.MediaItem',
|
||||
'Select Image(s)')
|
||||
self.on_new_prompt = translate('ImagePlugin.MediaItem', 'Select Image(s)')
|
||||
file_formats = get_images_filter()
|
||||
self.on_new_file_masks = u'%s;;%s (*.*) (*)' % (file_formats, UiStrings().AllFiles)
|
||||
self.addGroupAction.setText(UiStrings().AddGroup)
|
||||
self.addGroupAction.setToolTip(UiStrings().AddGroup)
|
||||
self.replaceAction.setText(UiStrings().ReplaceBG)
|
||||
self.replaceAction.setToolTip(UiStrings().ReplaceLiveBG)
|
||||
self.resetAction.setText(UiStrings().ResetBG)
|
||||
self.resetAction.setToolTip(UiStrings().ResetLiveBG)
|
||||
self.replace_action.setText(UiStrings().ReplaceBG)
|
||||
self.replace_action.setToolTip(UiStrings().ReplaceLiveBG)
|
||||
self.reset_action.setText(UiStrings().ResetBG)
|
||||
self.reset_action.setToolTip(UiStrings().ResetLiveBG)
|
||||
|
||||
def required_icons(self):
|
||||
"""
|
||||
Set which icons the media manager tab should show
|
||||
Set which icons the media manager tab should show.
|
||||
"""
|
||||
MediaManagerItem.required_icons(self)
|
||||
self.has_file_icon = True
|
||||
@ -94,13 +94,13 @@ class ImageMediaItem(MediaManagerItem):
|
||||
self.servicePath = os.path.join(AppLocation.get_section_data_path(self.settings_section), u'thumbnails')
|
||||
check_directory_exists(self.servicePath)
|
||||
# Load images from the database
|
||||
self.loadFullList(
|
||||
self.load_full_list(
|
||||
self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename), initial_load=True)
|
||||
|
||||
def add_list_view_to_toolbar(self):
|
||||
"""
|
||||
Creates the main widget for listing items the media item is tracking.
|
||||
This method overloads MediaManagerItem.add_list_view_to_toolbar
|
||||
Creates the main widget for listing items the media item is tracking. This method overloads
|
||||
MediaManagerItem.add_list_view_to_toolbar.
|
||||
"""
|
||||
# Add the List widget
|
||||
self.list_view = TreeWidgetWithDnD(self, self.plugin.name)
|
||||
@ -155,44 +155,41 @@ class ImageMediaItem(MediaManagerItem):
|
||||
self.list_view.doubleClicked.connect(self.on_double_clicked)
|
||||
self.list_view.itemSelectionChanged.connect(self.on_selection_change)
|
||||
self.list_view.customContextMenuRequested.connect(self.context_menu)
|
||||
self.list_view.addAction(self.replaceAction)
|
||||
self.list_view.addAction(self.replace_action)
|
||||
|
||||
def add_custom_context_actions(self):
|
||||
"""
|
||||
Add custom actions to the context menu
|
||||
Add custom actions to the context menu.
|
||||
"""
|
||||
create_widget_action(self.list_view, separator=True)
|
||||
create_widget_action(self.list_view,
|
||||
text=UiStrings().AddGroup,
|
||||
icon=u':/images/image_new_group.png',
|
||||
triggers=self.onAddGroupClick)
|
||||
text=UiStrings().AddGroup, icon=u':/images/image_new_group.png', triggers=self.on_add_group_click)
|
||||
create_widget_action(self.list_view,
|
||||
text=self.plugin.get_string(StringContent.Load)[u'tooltip'],
|
||||
icon=u':/general/general_open.png',
|
||||
triggers=self.on_file_click)
|
||||
icon=u':/general/general_open.png', triggers=self.on_file_click)
|
||||
|
||||
def add_start_header_bar(self):
|
||||
"""
|
||||
Add custom buttons to the start of the toolbar
|
||||
Add custom buttons to the start of the toolbar.
|
||||
"""
|
||||
self.addGroupAction = self.toolbar.add_toolbar_action(u'addGroupAction',
|
||||
icon=u':/images/image_new_group.png', triggers=self.onAddGroupClick)
|
||||
icon=u':/images/image_new_group.png', triggers=self.on_add_group_click)
|
||||
|
||||
def add_end_header_bar(self):
|
||||
"""
|
||||
Add custom buttons to the end of the toolbar
|
||||
"""
|
||||
self.replaceAction = self.toolbar.add_toolbar_action(u'replaceAction',
|
||||
icon=u':/slides/slide_blank.png', triggers=self.onReplaceClick)
|
||||
self.resetAction = self.toolbar.add_toolbar_action(u'resetAction',
|
||||
icon=u':/system/system_close.png', visible=False, triggers=self.onResetClick)
|
||||
self.replace_action = self.toolbar.add_toolbar_action(u'replace_action',
|
||||
icon=u':/slides/slide_blank.png', triggers=self.on_replace_click)
|
||||
self.reset_action = self.toolbar.add_toolbar_action(u'reset_action',
|
||||
icon=u':/system/system_close.png', visible=False, triggers=self.on_reset_click)
|
||||
|
||||
def recursively_delete_group(self, image_group):
|
||||
"""
|
||||
Recursively deletes a group and all groups and images in it
|
||||
Recursively deletes a group and all groups and images in it.
|
||||
|
||||
``image_group``
|
||||
The ImageGroups instance of the group that will be deleted
|
||||
The ImageGroups instance of the group that will be deleted.
|
||||
"""
|
||||
images = self.manager.get_all_objects(ImageFilenames, ImageFilenames.group_id == image_group.id)
|
||||
for image in images:
|
||||
@ -205,7 +202,7 @@ class ImageMediaItem(MediaManagerItem):
|
||||
|
||||
def on_delete_click(self):
|
||||
"""
|
||||
Remove an image item from the list
|
||||
Remove an image item from the list.
|
||||
"""
|
||||
# Turn off auto preview triggers.
|
||||
self.list_view.blockSignals(True)
|
||||
@ -246,16 +243,16 @@ class ImageMediaItem(MediaManagerItem):
|
||||
|
||||
def add_sub_groups(self, group_list, parent_group_id):
|
||||
"""
|
||||
Recursively add subgroups to the given parent group in a QTreeWidget
|
||||
Recursively add subgroups to the given parent group in a QTreeWidget.
|
||||
|
||||
``group_list``
|
||||
The List object that contains all QTreeWidgetItems
|
||||
The List object that contains all QTreeWidgetItems.
|
||||
|
||||
``parent_group_id``
|
||||
The ID of the group that will be added recursively
|
||||
The ID of the group that will be added recursively.
|
||||
"""
|
||||
image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == parent_group_id)
|
||||
image_groups.sort(cmp=locale_compare, key=lambda group_object: group_object.group_name)
|
||||
image_groups.sort(key=lambda group_object: get_locale_key(group_object.group_name))
|
||||
folder_icon = build_icon(u':/images/image_group.png')
|
||||
for image_group in image_groups:
|
||||
group = QtGui.QTreeWidgetItem()
|
||||
@ -271,35 +268,35 @@ class ImageMediaItem(MediaManagerItem):
|
||||
|
||||
def fill_groups_combobox(self, combobox, parent_group_id=0, prefix=''):
|
||||
"""
|
||||
Recursively add groups to the combobox in the 'Add group' dialog
|
||||
Recursively add groups to the combobox in the 'Add group' dialog.
|
||||
|
||||
``combobox``
|
||||
The QComboBox to add the options to
|
||||
The QComboBox to add the options to.
|
||||
|
||||
``parent_group_id``
|
||||
The ID of the group that will be added
|
||||
The ID of the group that will be added.
|
||||
|
||||
``prefix``
|
||||
A string containing the prefix that will be added in front of the groupname for each level of the tree
|
||||
A string containing the prefix that will be added in front of the groupname for each level of the tree.
|
||||
"""
|
||||
if parent_group_id == 0:
|
||||
combobox.clear()
|
||||
combobox.top_level_group_added = False
|
||||
image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == parent_group_id)
|
||||
image_groups.sort(cmp=locale_compare, key=lambda group_object: group_object.group_name)
|
||||
image_groups.sort(key=lambda group_object: get_locale_key(group_object.group_name))
|
||||
for image_group in image_groups:
|
||||
combobox.addItem(prefix + image_group.group_name, image_group.id)
|
||||
self.fill_groups_combobox(combobox, image_group.id, prefix + ' ')
|
||||
|
||||
def expand_group(self, group_id, root_item=None):
|
||||
"""
|
||||
Expand groups in the widget recursively
|
||||
Expand groups in the widget recursively.
|
||||
|
||||
``group_id``
|
||||
The ID of the group that will be expanded
|
||||
The ID of the group that will be expanded.
|
||||
|
||||
``root_item``
|
||||
This option is only used for recursion purposes
|
||||
This option is only used for recursion purposes.
|
||||
"""
|
||||
return_value = False
|
||||
if root_item is None:
|
||||
@ -314,31 +311,31 @@ class ImageMediaItem(MediaManagerItem):
|
||||
return True
|
||||
return return_value
|
||||
|
||||
def loadFullList(self, images, initial_load=False, open_group=None):
|
||||
def load_full_list(self, images, initial_load=False, open_group=None):
|
||||
"""
|
||||
Replace the list of images and groups in the interface.
|
||||
|
||||
``images``
|
||||
A List of ImageFilenames objects that will be used to reload the mediamanager list
|
||||
A List of ImageFilenames objects that will be used to reload the mediamanager list.
|
||||
|
||||
``initial_load``
|
||||
When set to False, the busy cursor and progressbar will be shown while loading images
|
||||
When set to False, the busy cursor and progressbar will be shown while loading images.
|
||||
|
||||
``open_group``
|
||||
ImageGroups object of the group that must be expanded after reloading the list in the interface
|
||||
ImageGroups object of the group that must be expanded after reloading the list in the interface.
|
||||
"""
|
||||
if not initial_load:
|
||||
self.application.set_busy_cursor()
|
||||
self.main_window.display_progress_bar(len(images))
|
||||
self.list_view.clear()
|
||||
# Load the list of groups and add them to the treeView
|
||||
# Load the list of groups and add them to the treeView.
|
||||
group_items = {}
|
||||
self.add_sub_groups(group_items, parent_group_id=0)
|
||||
if open_group is not None:
|
||||
self.expand_group(open_group.id)
|
||||
# Sort the images by its filename considering language specific
|
||||
# Sort the images by its filename considering language specific.
|
||||
# characters.
|
||||
images.sort(cmp=locale_compare, key=lambda image_object: os.path.split(unicode(image_object.filename))[1])
|
||||
images.sort(key=lambda image_object: get_locale_key(os.path.split(unicode(image_object.filename))[1]))
|
||||
for imageFile in images:
|
||||
log.debug(u'Loading image: %s', imageFile.filename)
|
||||
filename = os.path.split(imageFile.filename)[1]
|
||||
@ -455,7 +452,7 @@ class ImageMediaItem(MediaManagerItem):
|
||||
self.main_window.display_progress_bar(len(images))
|
||||
# Save the new images in the database
|
||||
self.save_new_images_list(images, group_id=parent_group.id, reload_list=False)
|
||||
self.loadFullList(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename),
|
||||
self.load_full_list(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename),
|
||||
initial_load=initial_load, open_group=parent_group)
|
||||
self.application.set_normal_cursor()
|
||||
|
||||
@ -482,7 +479,7 @@ class ImageMediaItem(MediaManagerItem):
|
||||
self.manager.save_object(imageFile)
|
||||
self.main_window.increment_progress_bar()
|
||||
if reload_list and images_list:
|
||||
self.loadFullList(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename))
|
||||
self.load_full_list(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename))
|
||||
|
||||
def dnd_move_internal(self, target):
|
||||
"""
|
||||
@ -525,12 +522,12 @@ class ImageMediaItem(MediaManagerItem):
|
||||
group_items.append(item)
|
||||
if isinstance(item.data(0, QtCore.Qt.UserRole), ImageFilenames):
|
||||
image_items.append(item)
|
||||
group_items.sort(cmp=locale_compare, key=lambda item: item.text(0))
|
||||
group_items.sort(key=lambda item: get_locale_key(item.text(0)))
|
||||
target_group.addChildren(group_items)
|
||||
image_items.sort(cmp=locale_compare, key=lambda item: item.text(0))
|
||||
image_items.sort(key=lambda item: get_locale_key(item.text(0)))
|
||||
target_group.addChildren(image_items)
|
||||
|
||||
def generate_slide_data(self, service_item, item=None, xmlVersion=False,
|
||||
def generate_slide_data(self, service_item, item=None, xml_version=False,
|
||||
remote=False, context=ServiceItemContext.Service):
|
||||
"""
|
||||
Generate the slide data. Needs to be implemented by the plugin.
|
||||
@ -608,7 +605,7 @@ class ImageMediaItem(MediaManagerItem):
|
||||
else:
|
||||
return False
|
||||
|
||||
def onAddGroupClick(self):
|
||||
def on_add_group_click(self):
|
||||
"""
|
||||
Called to add a new group
|
||||
"""
|
||||
@ -629,7 +626,7 @@ class ImageMediaItem(MediaManagerItem):
|
||||
group_name=self.add_group_form.name_edit.text())
|
||||
if not self.check_group_exists(new_group):
|
||||
if self.manager.save_object(new_group):
|
||||
self.loadFullList(self.manager.get_all_objects(ImageFilenames,
|
||||
self.load_full_list(self.manager.get_all_objects(ImageFilenames,
|
||||
order_by_ref=ImageFilenames.filename))
|
||||
self.expand_group(new_group.id)
|
||||
self.fill_groups_combobox(self.choose_group_form.group_combobox)
|
||||
@ -638,23 +635,22 @@ class ImageMediaItem(MediaManagerItem):
|
||||
critical_error_message_box(
|
||||
message=translate('ImagePlugin.AddGroupForm', 'Could not add the new group.'))
|
||||
else:
|
||||
critical_error_message_box(
|
||||
message=translate('ImagePlugin.AddGroupForm', 'This group already exists.'))
|
||||
critical_error_message_box(message=translate('ImagePlugin.AddGroupForm', 'This group already exists.'))
|
||||
|
||||
def onResetClick(self):
|
||||
def on_reset_click(self):
|
||||
"""
|
||||
Called to reset the Live background with the image selected,
|
||||
Called to reset the Live background with the image selected.
|
||||
"""
|
||||
self.resetAction.setVisible(False)
|
||||
self.reset_action.setVisible(False)
|
||||
self.live_controller.display.reset_image()
|
||||
|
||||
def live_theme_changed(self):
|
||||
"""
|
||||
Triggered by the change of theme in the slide controller
|
||||
Triggered by the change of theme in the slide controller.
|
||||
"""
|
||||
self.resetAction.setVisible(False)
|
||||
self.reset_action.setVisible(False)
|
||||
|
||||
def onReplaceClick(self):
|
||||
def on_replace_click(self):
|
||||
"""
|
||||
Called to replace Live backgound with the image selected.
|
||||
"""
|
||||
@ -663,12 +659,12 @@ class ImageMediaItem(MediaManagerItem):
|
||||
background = QtGui.QColor(Settings().value(self.settings_section + u'/background color'))
|
||||
bitem = self.list_view.selectedItems()[0]
|
||||
if not isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageFilenames):
|
||||
# Only continue when an image is selected
|
||||
# Only continue when an image is selected.
|
||||
return
|
||||
filename = bitem.data(0, QtCore.Qt.UserRole).filename
|
||||
if os.path.exists(filename):
|
||||
if self.live_controller.display.direct_image(filename, background):
|
||||
self.resetAction.setVisible(True)
|
||||
self.reset_action.setVisible(True)
|
||||
else:
|
||||
critical_error_message_box(UiStrings().LiveBGError,
|
||||
translate('ImagePlugin.MediaItem', 'There was no display item to amend.'))
|
||||
|
@ -27,8 +27,7 @@
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
The :mod:`media` module provides the Media plugin which allows OpenLP to
|
||||
display videos. The media supported depends not only on the Python support
|
||||
but also extensively on the codecs installed on the underlying operating system
|
||||
being picked up and usable by Python.
|
||||
The :mod:`media` module provides the Media plugin which allows OpenLP to display videos. The media supported depends not
|
||||
only on the Python support but also extensively on the codecs installed on the underlying operating system being picked
|
||||
up and usable by Python.
|
||||
"""
|
||||
|
@ -37,15 +37,18 @@ from openlp.core.lib import ItemCapabilities, MediaManagerItem,MediaType, Regist
|
||||
from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box
|
||||
from openlp.core.ui import DisplayController, Display, DisplayControllerType
|
||||
from openlp.core.ui.media import get_media_players, set_media_players
|
||||
from openlp.core.utils import AppLocation, locale_compare
|
||||
from openlp.core.utils import AppLocation, get_locale_key
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
CLAPPERBOARD = u':/media/slidecontroller_multimedia.png'
|
||||
VIDEO = build_icon(QtGui.QImage(u':/media/media_video.png'))
|
||||
AUDIO = build_icon(QtGui.QImage(u':/media/media_audio.png'))
|
||||
DVDICON = build_icon(QtGui.QImage(u':/media/media_video.png'))
|
||||
ERROR = build_icon(QtGui.QImage(u':/general/general_delete.png'))
|
||||
VIDEO_ICON = build_icon(QtGui.QImage(u':/media/media_video.png'))
|
||||
AUDIO_ICON = build_icon(QtGui.QImage(u':/media/media_audio.png'))
|
||||
DVD_ICON = build_icon(QtGui.QImage(u':/media/media_video.png'))
|
||||
ERROR_ICON = build_icon(QtGui.QImage(u':/general/general_delete.png'))
|
||||
|
||||
|
||||
class MediaMediaItem(MediaManagerItem):
|
||||
"""
|
||||
@ -79,12 +82,12 @@ class MediaMediaItem(MediaManagerItem):
|
||||
|
||||
def retranslateUi(self):
|
||||
self.on_new_prompt = translate('MediaPlugin.MediaItem', 'Select Media')
|
||||
self.replaceAction.setText(UiStrings().ReplaceBG)
|
||||
self.replaceAction.setToolTip(UiStrings().ReplaceLiveBG)
|
||||
self.resetAction.setText(UiStrings().ResetBG)
|
||||
self.resetAction.setToolTip(UiStrings().ResetLiveBG)
|
||||
self.replace_action.setText(UiStrings().ReplaceBG)
|
||||
self.replace_action.setToolTip(UiStrings().ReplaceLiveBG)
|
||||
self.reset_action.setText(UiStrings().ResetBG)
|
||||
self.reset_action.setToolTip(UiStrings().ResetLiveBG)
|
||||
self.automatic = UiStrings().Automatic
|
||||
self.displayTypeLabel.setText(translate('MediaPlugin.MediaItem', 'Use Player:'))
|
||||
self.display_type_label.setText(translate('MediaPlugin.MediaItem', 'Use Player:'))
|
||||
self.rebuild_players()
|
||||
|
||||
def required_icons(self):
|
||||
@ -98,27 +101,28 @@ class MediaMediaItem(MediaManagerItem):
|
||||
|
||||
def add_list_view_to_toolbar(self):
|
||||
MediaManagerItem.add_list_view_to_toolbar(self)
|
||||
self.list_view.addAction(self.replaceAction)
|
||||
self.list_view.addAction(self.replace_action)
|
||||
|
||||
def add_end_header_bar(self):
|
||||
# Replace backgrounds do not work at present so remove functionality.
|
||||
self.replaceAction = self.toolbar.add_toolbar_action(u'replaceAction', icon=u':/slides/slide_blank.png',
|
||||
self.replace_action = self.toolbar.add_toolbar_action(u'replace_action', icon=u':/slides/slide_blank.png',
|
||||
triggers=self.onReplaceClick)
|
||||
self.resetAction = self.toolbar.add_toolbar_action(u'resetAction', icon=u':/system/system_close.png',
|
||||
self.reset_action = self.toolbar.add_toolbar_action(u'reset_action', icon=u':/system/system_close.png',
|
||||
visible=False, triggers=self.onResetClick)
|
||||
self.mediaWidget = QtGui.QWidget(self)
|
||||
self.mediaWidget.setObjectName(u'mediaWidget')
|
||||
self.displayLayout = QtGui.QFormLayout(self.mediaWidget)
|
||||
self.displayLayout.setMargin(self.displayLayout.spacing())
|
||||
self.displayLayout.setObjectName(u'displayLayout')
|
||||
self.displayTypeLabel = QtGui.QLabel(self.mediaWidget)
|
||||
self.displayTypeLabel.setObjectName(u'displayTypeLabel')
|
||||
self.displayTypeComboBox = create_horizontal_adjusting_combo_box(self.mediaWidget, u'displayTypeComboBox')
|
||||
self.displayTypeLabel.setBuddy(self.displayTypeComboBox)
|
||||
self.displayLayout.addRow(self.displayTypeLabel, self.displayTypeComboBox)
|
||||
# Add the Media widget to the page layout
|
||||
self.page_layout.addWidget(self.mediaWidget)
|
||||
self.displayTypeComboBox.currentIndexChanged.connect(self.overridePlayerChanged)
|
||||
self.media_widget = QtGui.QWidget(self)
|
||||
self.media_widget.setObjectName(u'media_widget')
|
||||
self.display_layout = QtGui.QFormLayout(self.media_widget)
|
||||
self.display_layout.setMargin(self.display_layout.spacing())
|
||||
self.display_layout.setObjectName(u'display_layout')
|
||||
self.display_type_label = QtGui.QLabel(self.media_widget)
|
||||
self.display_type_label.setObjectName(u'display_type_label')
|
||||
self.display_type_combo_box = create_horizontal_adjusting_combo_box(
|
||||
self.media_widget, u'display_type_combo_box')
|
||||
self.display_type_label.setBuddy(self.display_type_combo_box)
|
||||
self.display_layout.addRow(self.display_type_label, self.display_type_combo_box)
|
||||
# Add the Media widget to the page layout.
|
||||
self.page_layout.addWidget(self.media_widget)
|
||||
self.display_type_combo_box.currentIndexChanged.connect(self.overridePlayerChanged)
|
||||
|
||||
def overridePlayerChanged(self, index):
|
||||
player = get_media_players()[0]
|
||||
@ -132,13 +136,13 @@ class MediaMediaItem(MediaManagerItem):
|
||||
Called to reset the Live background with the media selected,
|
||||
"""
|
||||
self.media_controller.media_reset(self.live_controller)
|
||||
self.resetAction.setVisible(False)
|
||||
self.reset_action.setVisible(False)
|
||||
|
||||
def video_background_replaced(self):
|
||||
"""
|
||||
Triggered by main display on change of serviceitem.
|
||||
"""
|
||||
self.resetAction.setVisible(False)
|
||||
self.reset_action.setVisible(False)
|
||||
|
||||
def onReplaceClick(self):
|
||||
"""
|
||||
@ -155,7 +159,7 @@ class MediaMediaItem(MediaManagerItem):
|
||||
(path, name) = os.path.split(filename)
|
||||
service_item.add_from_command(path, name,CLAPPERBOARD)
|
||||
if self.media_controller.video(DisplayControllerType.Live, service_item, video_behind_text=True):
|
||||
self.resetAction.setVisible(True)
|
||||
self.reset_action.setVisible(True)
|
||||
else:
|
||||
critical_error_message_box(UiStrings().LiveBGError,
|
||||
translate('MediaPlugin.MediaItem', 'There was no display item to amend.'))
|
||||
@ -164,7 +168,7 @@ class MediaMediaItem(MediaManagerItem):
|
||||
translate('MediaPlugin.MediaItem',
|
||||
'There was a problem replacing your background, the media file "%s" no longer exists.') % filename)
|
||||
|
||||
def generate_slide_data(self, service_item, item=None, xmlVersion=False, remote=False,
|
||||
def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False,
|
||||
context=ServiceItemContext.Live):
|
||||
"""
|
||||
Generate the slide data. Needs to be implemented by the plugin.
|
||||
@ -181,7 +185,7 @@ class MediaMediaItem(MediaManagerItem):
|
||||
translate('MediaPlugin.MediaItem', 'Missing Media File'),
|
||||
translate('MediaPlugin.MediaItem', 'The file %s no longer exists.') % filename)
|
||||
return False
|
||||
service_item.title = self.displayTypeComboBox.currentText()
|
||||
service_item.title = self.display_type_combo_box.currentText()
|
||||
service_item.shortname = service_item.title
|
||||
(path, name) = os.path.split(filename)
|
||||
service_item.add_from_command(path, name, CLAPPERBOARD)
|
||||
@ -209,8 +213,7 @@ class MediaMediaItem(MediaManagerItem):
|
||||
|
||||
def rebuild_players(self):
|
||||
"""
|
||||
Rebuild the tab in the media manager when changes are made in
|
||||
the settings
|
||||
Rebuild the tab in the media manager when changes are made in the settings.
|
||||
"""
|
||||
self.populateDisplayTypes()
|
||||
self.on_new_file_masks = translate('MediaPlugin.MediaItem', 'Videos (%s);;Audio (%s);;%s (*)') % (
|
||||
@ -222,29 +225,27 @@ class MediaMediaItem(MediaManagerItem):
|
||||
|
||||
def populateDisplayTypes(self):
|
||||
"""
|
||||
Load the combobox with the enabled media players,
|
||||
allowing user to select a specific player if settings allow
|
||||
Load the combobox with the enabled media players, allowing user to select a specific player if settings allow.
|
||||
"""
|
||||
# block signals to avoid unnecessary overridePlayerChanged Signals
|
||||
# while combo box creation
|
||||
self.displayTypeComboBox.blockSignals(True)
|
||||
self.displayTypeComboBox.clear()
|
||||
# block signals to avoid unnecessary overridePlayerChanged Signals while combo box creation
|
||||
self.display_type_combo_box.blockSignals(True)
|
||||
self.display_type_combo_box.clear()
|
||||
usedPlayers, overridePlayer = get_media_players()
|
||||
media_players = self.media_controller.media_players
|
||||
currentIndex = 0
|
||||
for player in usedPlayers:
|
||||
# load the drop down selection
|
||||
self.displayTypeComboBox.addItem(media_players[player].original_name)
|
||||
self.display_type_combo_box.addItem(media_players[player].original_name)
|
||||
if overridePlayer == player:
|
||||
currentIndex = len(self.displayTypeComboBox)
|
||||
if self.displayTypeComboBox.count() > 1:
|
||||
self.displayTypeComboBox.insertItem(0, self.automatic)
|
||||
self.displayTypeComboBox.setCurrentIndex(currentIndex)
|
||||
currentIndex = len(self.display_type_combo_box)
|
||||
if self.display_type_combo_box.count() > 1:
|
||||
self.display_type_combo_box.insertItem(0, self.automatic)
|
||||
self.display_type_combo_box.setCurrentIndex(currentIndex)
|
||||
if overridePlayer:
|
||||
self.mediaWidget.show()
|
||||
self.media_widget.show()
|
||||
else:
|
||||
self.mediaWidget.hide()
|
||||
self.displayTypeComboBox.blockSignals(False)
|
||||
self.media_widget.hide()
|
||||
self.display_type_combo_box.blockSignals(False)
|
||||
|
||||
def on_delete_click(self):
|
||||
"""
|
||||
@ -261,40 +262,40 @@ class MediaMediaItem(MediaManagerItem):
|
||||
def load_list(self, media, target_group=None):
|
||||
# Sort the media by its filename considering language specific
|
||||
# characters.
|
||||
media.sort(cmp=locale_compare, key=lambda filename: os.path.split(unicode(filename))[1])
|
||||
media.sort(key=lambda filename: get_locale_key(os.path.split(unicode(filename))[1]))
|
||||
for track in media:
|
||||
track_info = QtCore.QFileInfo(track)
|
||||
if not os.path.exists(track):
|
||||
filename = os.path.split(unicode(track))[1]
|
||||
item_name = QtGui.QListWidgetItem(filename)
|
||||
item_name.setIcon(ERROR)
|
||||
item_name.setIcon(ERROR_ICON)
|
||||
item_name.setData(QtCore.Qt.UserRole, track)
|
||||
elif track_info.isFile():
|
||||
filename = os.path.split(unicode(track))[1]
|
||||
item_name = QtGui.QListWidgetItem(filename)
|
||||
if u'*.%s' % (filename.split(u'.')[-1].lower()) in self.media_controller.audio_extensions_list:
|
||||
item_name.setIcon(AUDIO)
|
||||
item_name.setIcon(AUDIO_ICON)
|
||||
else:
|
||||
item_name.setIcon(VIDEO)
|
||||
item_name.setIcon(VIDEO_ICON)
|
||||
item_name.setData(QtCore.Qt.UserRole, track)
|
||||
else:
|
||||
filename = os.path.split(unicode(track))[1]
|
||||
item_name = QtGui.QListWidgetItem(filename)
|
||||
item_name.setIcon(build_icon(DVDICON))
|
||||
item_name.setIcon(build_icon(DVD_ICON))
|
||||
item_name.setData(QtCore.Qt.UserRole, track)
|
||||
item_name.setToolTip(track)
|
||||
self.list_view.addItem(item_name)
|
||||
|
||||
def getList(self, type=MediaType.Audio):
|
||||
def get_list(self, type=MediaType.Audio):
|
||||
media = Settings().value(self.settings_section + u'/media files')
|
||||
media.sort(cmp=locale_compare, key=lambda filename: os.path.split(unicode(filename))[1])
|
||||
ext = []
|
||||
media.sort(key=lambda filename: get_locale_key(os.path.split(unicode(filename))[1]))
|
||||
extension = []
|
||||
if type == MediaType.Audio:
|
||||
ext = self.media_controller.audio_extensions_list
|
||||
extension = self.media_controller.audio_extensions_list
|
||||
else:
|
||||
ext = self.media_controller.video_extensions_list
|
||||
ext = map(lambda x: x[1:], ext)
|
||||
media = filter(lambda x: os.path.splitext(x)[1] in ext, media)
|
||||
extension = self.media_controller.video_extensions_list
|
||||
extension = map(lambda x: x[1:], extension)
|
||||
media = filter(lambda x: os.path.splitext(x)[1] in extension, media)
|
||||
return media
|
||||
|
||||
def search(self, string, showError):
|
||||
|
@ -34,8 +34,10 @@ from PyQt4 import QtCore
|
||||
from openlp.core.lib import Plugin, Registry, StringContent, Settings, build_icon, translate
|
||||
from openlp.plugins.media.lib import MediaMediaItem, MediaTab
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Some settings starting with "media" are in core, because they are needed for core functionality.
|
||||
__default_settings__ = {
|
||||
u'media/media auto start': QtCore.Qt.Unchecked,
|
||||
@ -54,7 +56,7 @@ class MediaPlugin(Plugin):
|
||||
# passed with drag and drop messages
|
||||
self.dnd_id = u'Media'
|
||||
|
||||
def create_settings_Tab(self, parent):
|
||||
def create_settings_tab(self, parent):
|
||||
"""
|
||||
Create the settings Tab
|
||||
"""
|
||||
@ -94,7 +96,7 @@ class MediaPlugin(Plugin):
|
||||
|
||||
def finalise(self):
|
||||
"""
|
||||
Time to tidy up on exit
|
||||
Time to tidy up on exit.
|
||||
"""
|
||||
log.info(u'Media Finalising')
|
||||
self.media_controller.finalise()
|
||||
@ -102,19 +104,19 @@ class MediaPlugin(Plugin):
|
||||
|
||||
def get_display_css(self):
|
||||
"""
|
||||
Add css style sheets to htmlbuilder
|
||||
Add css style sheets to htmlbuilder.
|
||||
"""
|
||||
return self.media_controller.get_media_display_css()
|
||||
|
||||
def get_display_javascript(self):
|
||||
"""
|
||||
Add javascript functions to htmlbuilder
|
||||
Add javascript functions to htmlbuilder.
|
||||
"""
|
||||
return self.media_controller.get_media_display_javascript()
|
||||
|
||||
def get_display_html(self):
|
||||
"""
|
||||
Add html code to htmlbuilder
|
||||
Add html code to htmlbuilder.
|
||||
"""
|
||||
return self.media_controller.get_media_display_html()
|
||||
|
||||
|
@ -27,6 +27,6 @@
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
The :mod:`presentations` module provides the Presentations plugin which allows
|
||||
OpenLP to show presentations from most popular presentation packages.
|
||||
The :mod:`presentations` module provides the Presentations plugin which allows OpenLP to show presentations from most
|
||||
popular presentation packages.
|
||||
"""
|
||||
|
@ -62,13 +62,14 @@ from openlp.core.lib import ScreenList
|
||||
from openlp.core.utils import delete_file, get_uno_command, get_uno_instance
|
||||
from presentationcontroller import PresentationController, PresentationDocument
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImpressController(PresentationController):
|
||||
"""
|
||||
Class to control interactions with Impress presentations.
|
||||
It creates the runtime environment, loads and closes the presentation as
|
||||
well as triggering the correct activities based on the users input
|
||||
Class to control interactions with Impress presentations. It creates the runtime environment, loads and closes the
|
||||
presentation as well as triggering the correct activities based on the users input.
|
||||
"""
|
||||
log.info(u'ImpressController loaded')
|
||||
|
||||
@ -79,14 +80,14 @@ class ImpressController(PresentationController):
|
||||
log.debug(u'Initialising')
|
||||
PresentationController.__init__(self, plugin, u'Impress', ImpressDocument)
|
||||
self.supports = [u'odp']
|
||||
self.alsosupports = [u'ppt', u'pps', u'pptx', u'ppsx']
|
||||
self.also_supports = [u'ppt', u'pps', u'pptx', u'ppsx']
|
||||
self.process = None
|
||||
self.desktop = None
|
||||
self.manager = None
|
||||
|
||||
def check_available(self):
|
||||
"""
|
||||
Impress is able to run on this machine
|
||||
Impress is able to run on this machine.
|
||||
"""
|
||||
log.debug(u'check_available')
|
||||
if os.name == u'nt':
|
||||
@ -96,9 +97,8 @@ class ImpressController(PresentationController):
|
||||
|
||||
def start_process(self):
|
||||
"""
|
||||
Loads a running version of OpenOffice in the background.
|
||||
It is not displayed to the user but is available to the UNO interface
|
||||
when required.
|
||||
Loads a running version of OpenOffice in the background. It is not displayed to the user but is available to the
|
||||
UNO interface when required.
|
||||
"""
|
||||
log.debug(u'start process Openoffice')
|
||||
if os.name == u'nt':
|
||||
@ -113,8 +113,7 @@ class ImpressController(PresentationController):
|
||||
|
||||
def get_uno_desktop(self):
|
||||
"""
|
||||
On non-Windows platforms, use Uno. Get the OpenOffice desktop
|
||||
which will be used to manage impress
|
||||
On non-Windows platforms, use Uno. Get the OpenOffice desktop which will be used to manage impress.
|
||||
"""
|
||||
log.debug(u'get UNO Desktop Openoffice')
|
||||
uno_instance = None
|
||||
@ -132,8 +131,7 @@ class ImpressController(PresentationController):
|
||||
loop += 1
|
||||
try:
|
||||
self.manager = uno_instance.ServiceManager
|
||||
log.debug(u'get UNO Desktop Openoffice - createInstanceWithContext'
|
||||
u' - Desktop')
|
||||
log.debug(u'get UNO Desktop Openoffice - createInstanceWithContext - Desktop')
|
||||
desktop = self.manager.createInstanceWithContext("com.sun.star.frame.Desktop", uno_instance)
|
||||
return desktop
|
||||
except:
|
||||
@ -142,8 +140,7 @@ class ImpressController(PresentationController):
|
||||
|
||||
def get_com_desktop(self):
|
||||
"""
|
||||
On Windows platforms, use COM. Return the desktop object which
|
||||
will be used to manage Impress
|
||||
On Windows platforms, use COM. Return the desktop object which will be used to manage Impress.
|
||||
"""
|
||||
log.debug(u'get COM Desktop OpenOffice')
|
||||
if not self.manager:
|
||||
@ -157,7 +154,7 @@ class ImpressController(PresentationController):
|
||||
|
||||
def get_com_servicemanager(self):
|
||||
"""
|
||||
Return the OOo service manager for windows
|
||||
Return the OOo service manager for windows.
|
||||
"""
|
||||
log.debug(u'get_com_servicemanager openoffice')
|
||||
try:
|
||||
@ -168,7 +165,7 @@ class ImpressController(PresentationController):
|
||||
|
||||
def kill(self):
|
||||
"""
|
||||
Called at system exit to clean up any running presentations
|
||||
Called at system exit to clean up any running presentations.
|
||||
"""
|
||||
log.debug(u'Kill OpenOffice')
|
||||
while self.docs:
|
||||
@ -203,12 +200,12 @@ class ImpressController(PresentationController):
|
||||
|
||||
class ImpressDocument(PresentationDocument):
|
||||
"""
|
||||
Class which holds information and controls a single presentation
|
||||
Class which holds information and controls a single presentation.
|
||||
"""
|
||||
|
||||
def __init__(self, controller, presentation):
|
||||
"""
|
||||
Constructor, store information about the file and initialise
|
||||
Constructor, store information about the file and initialise.
|
||||
"""
|
||||
log.debug(u'Init Presentation OpenOffice')
|
||||
PresentationDocument.__init__(self, controller, presentation)
|
||||
@ -218,11 +215,9 @@ class ImpressDocument(PresentationDocument):
|
||||
|
||||
def load_presentation(self):
|
||||
"""
|
||||
Called when a presentation is added to the SlideController.
|
||||
It builds the environment, starts communcations with the background
|
||||
OpenOffice task started earlier. If OpenOffice is not present is is
|
||||
started. Once the environment is available the presentation is loaded
|
||||
and started.
|
||||
Called when a presentation is added to the SlideController. It builds the environment, starts communcations with
|
||||
the background OpenOffice task started earlier. If OpenOffice is not present is is started. Once the environment
|
||||
is available the presentation is loaded and started.
|
||||
"""
|
||||
log.debug(u'Load Presentation OpenOffice')
|
||||
if os.name == u'nt':
|
||||
@ -239,13 +234,12 @@ class ImpressDocument(PresentationDocument):
|
||||
self.desktop = desktop
|
||||
properties = []
|
||||
if os.name != u'nt':
|
||||
# Recent versions of Impress on Windows won't start the presentation
|
||||
# if it starts as minimized. It seems OK on Linux though.
|
||||
# Recent versions of Impress on Windows won't start the presentation if it starts as minimized. It seems OK
|
||||
# on Linux though.
|
||||
properties.append(self.create_property(u'Minimized', True))
|
||||
properties = tuple(properties)
|
||||
try:
|
||||
self.document = desktop.loadComponentFromURL(url, u'_blank',
|
||||
0, properties)
|
||||
self.document = desktop.loadComponentFromURL(url, u'_blank', 0, properties)
|
||||
except:
|
||||
log.warn(u'Failed to load presentation %s' % url)
|
||||
return False
|
||||
@ -262,33 +256,33 @@ class ImpressDocument(PresentationDocument):
|
||||
|
||||
def create_thumbnails(self):
|
||||
"""
|
||||
Create thumbnail images for presentation
|
||||
Create thumbnail images for presentation.
|
||||
"""
|
||||
log.debug(u'create thumbnails OpenOffice')
|
||||
if self.check_thumbnails():
|
||||
return
|
||||
if os.name == u'nt':
|
||||
thumbdirurl = u'file:///' + self.get_temp_folder().replace(u'\\', u'/') \
|
||||
thumb_dir_url = u'file:///' + self.get_temp_folder().replace(u'\\', u'/') \
|
||||
.replace(u':', u'|').replace(u' ', u'%20')
|
||||
else:
|
||||
thumbdirurl = uno.systemPathToFileUrl(self.get_temp_folder())
|
||||
props = []
|
||||
props.append(self.create_property(u'FilterName', u'impress_png_Export'))
|
||||
props = tuple(props)
|
||||
thumb_dir_url = uno.systemPathToFileUrl(self.get_temp_folder())
|
||||
properties = []
|
||||
properties.append(self.create_property(u'FilterName', u'impress_png_Export'))
|
||||
properties = tuple(properties)
|
||||
doc = self.document
|
||||
pages = doc.getDrawPages()
|
||||
if not pages:
|
||||
return
|
||||
if not os.path.isdir(self.get_temp_folder()):
|
||||
os.makedirs(self.get_temp_folder())
|
||||
for idx in range(pages.getCount()):
|
||||
page = pages.getByIndex(idx)
|
||||
for index in range(pages.getCount()):
|
||||
page = pages.getByIndex(index)
|
||||
doc.getCurrentController().setCurrentPage(page)
|
||||
urlpath = u'%s/%s.png' % (thumbdirurl, unicode(idx + 1))
|
||||
path = os.path.join(self.get_temp_folder(), unicode(idx + 1) + u'.png')
|
||||
url_path = u'%s/%s.png' % (thumb_dir_url, unicode(index + 1))
|
||||
path = os.path.join(self.get_temp_folder(), unicode(index + 1) + u'.png')
|
||||
try:
|
||||
doc.storeToURL(urlpath, props)
|
||||
self.convert_thumbnail(path, idx + 1)
|
||||
doc.storeToURL(url_path, properties)
|
||||
self.convert_thumbnail(path, index + 1)
|
||||
delete_file(path)
|
||||
except ErrorCodeIOException, exception:
|
||||
log.exception(u'ERROR! ErrorCodeIOException %d' % exception.ErrCode)
|
||||
@ -297,23 +291,21 @@ class ImpressDocument(PresentationDocument):
|
||||
|
||||
def create_property(self, name, value):
|
||||
"""
|
||||
Create an OOo style property object which are passed into some
|
||||
Uno methods
|
||||
Create an OOo style property object which are passed into some Uno methods.
|
||||
"""
|
||||
log.debug(u'create property OpenOffice')
|
||||
if os.name == u'nt':
|
||||
prop = self.controller.manager.Bridge_GetStruct(u'com.sun.star.beans.PropertyValue')
|
||||
property_object = self.controller.manager.Bridge_GetStruct(u'com.sun.star.beans.PropertyValue')
|
||||
else:
|
||||
prop = PropertyValue()
|
||||
prop.Name = name
|
||||
prop.Value = value
|
||||
return prop
|
||||
property_object = PropertyValue()
|
||||
property_object.Name = name
|
||||
property_object.Value = value
|
||||
return property_object
|
||||
|
||||
def close_presentation(self):
|
||||
"""
|
||||
Close presentation and clean up objects
|
||||
Triggered by new object being added to SlideController or OpenLP
|
||||
being shutdown
|
||||
Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being
|
||||
shutdown.
|
||||
"""
|
||||
log.debug(u'close Presentation OpenOffice')
|
||||
if self.document:
|
||||
@ -329,7 +321,7 @@ class ImpressDocument(PresentationDocument):
|
||||
|
||||
def is_loaded(self):
|
||||
"""
|
||||
Returns true if a presentation is loaded
|
||||
Returns true if a presentation is loaded.
|
||||
"""
|
||||
log.debug(u'is loaded OpenOffice')
|
||||
if self.presentation is None or self.document is None:
|
||||
@ -346,7 +338,7 @@ class ImpressDocument(PresentationDocument):
|
||||
|
||||
def is_active(self):
|
||||
"""
|
||||
Returns true if a presentation is active and running
|
||||
Returns true if a presentation is active and running.
|
||||
"""
|
||||
log.debug(u'is active OpenOffice')
|
||||
if not self.is_loaded():
|
||||
@ -355,21 +347,21 @@ class ImpressDocument(PresentationDocument):
|
||||
|
||||
def unblank_screen(self):
|
||||
"""
|
||||
Unblanks the screen
|
||||
Unblanks the screen.
|
||||
"""
|
||||
log.debug(u'unblank screen OpenOffice')
|
||||
return self.control.resume()
|
||||
|
||||
def blank_screen(self):
|
||||
"""
|
||||
Blanks the screen
|
||||
Blanks the screen.
|
||||
"""
|
||||
log.debug(u'blank screen OpenOffice')
|
||||
self.control.blankScreen(0)
|
||||
|
||||
def is_blank(self):
|
||||
"""
|
||||
Returns true if screen is blank
|
||||
Returns true if screen is blank.
|
||||
"""
|
||||
log.debug(u'is blank OpenOffice')
|
||||
if self.control and self.control.isRunning():
|
||||
@ -379,7 +371,7 @@ class ImpressDocument(PresentationDocument):
|
||||
|
||||
def stop_presentation(self):
|
||||
"""
|
||||
Stop the presentation, remove from screen
|
||||
Stop the presentation, remove from screen.
|
||||
"""
|
||||
log.debug(u'stop presentation OpenOffice')
|
||||
# deactivate should hide the screen according to docs, but doesn't
|
||||
@ -389,18 +381,17 @@ class ImpressDocument(PresentationDocument):
|
||||
|
||||
def start_presentation(self):
|
||||
"""
|
||||
Start the presentation from the beginning
|
||||
Start the presentation from the beginning.
|
||||
"""
|
||||
log.debug(u'start presentation OpenOffice')
|
||||
if self.control is None or not self.control.isRunning():
|
||||
self.presentation.start()
|
||||
self.control = self.presentation.getController()
|
||||
# start() returns before the Component is ready.
|
||||
# Try for 15 seconds
|
||||
i = 1
|
||||
while not self.control and i < 150:
|
||||
# start() returns before the Component is ready. Try for 15 seconds.
|
||||
sleep_count = 1
|
||||
while not self.control and sleep_count < 150:
|
||||
time.sleep(0.1)
|
||||
i += 1
|
||||
sleep_count += 1
|
||||
self.control = self.presentation.getController()
|
||||
else:
|
||||
self.control.activate()
|
||||
@ -408,25 +399,25 @@ class ImpressDocument(PresentationDocument):
|
||||
|
||||
def get_slide_number(self):
|
||||
"""
|
||||
Return the current slide number on the screen, from 1
|
||||
Return the current slide number on the screen, from 1.
|
||||
"""
|
||||
return self.control.getCurrentSlideIndex() + 1
|
||||
|
||||
def get_slide_count(self):
|
||||
"""
|
||||
Return the total number of slides
|
||||
Return the total number of slides.
|
||||
"""
|
||||
return self.document.getDrawPages().getCount()
|
||||
|
||||
def goto_slide(self, slideno):
|
||||
"""
|
||||
Go to a specific slide (from 1)
|
||||
Go to a specific slide (from 1).
|
||||
"""
|
||||
self.control.gotoSlideIndex(slideno-1)
|
||||
|
||||
def next_step(self):
|
||||
"""
|
||||
Triggers the next effect of slide on the running presentation
|
||||
Triggers the next effect of slide on the running presentation.
|
||||
"""
|
||||
is_paused = self.control.isPaused()
|
||||
self.control.gotoNextEffect()
|
||||
@ -436,7 +427,7 @@ class ImpressDocument(PresentationDocument):
|
||||
|
||||
def previous_step(self):
|
||||
"""
|
||||
Triggers the previous slide on the running presentation
|
||||
Triggers the previous slide on the running presentation.
|
||||
"""
|
||||
self.control.gotoPreviousSlide()
|
||||
|
||||
@ -470,8 +461,8 @@ class ImpressDocument(PresentationDocument):
|
||||
page = pages.getByIndex(slide_no - 1)
|
||||
if notes:
|
||||
page = page.getNotesPage()
|
||||
for idx in range(page.getCount()):
|
||||
shape = page.getByIndex(idx)
|
||||
for index in range(page.getCount()):
|
||||
shape = page.getByIndex(index)
|
||||
if shape.supportsService("com.sun.star.drawing.Text"):
|
||||
text += shape.getString() + '\n'
|
||||
return text
|
||||
|
@ -35,17 +35,20 @@ from PyQt4 import QtCore, QtGui
|
||||
from openlp.core.lib import MediaManagerItem, Registry, ItemCapabilities, ServiceItemContext, Settings, UiStrings, \
|
||||
build_icon, check_item_selected, create_thumb, translate, validate_thumb
|
||||
from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box
|
||||
from openlp.core.utils import locale_compare
|
||||
from openlp.core.utils import get_locale_key
|
||||
from openlp.plugins.presentations.lib import MessageListener
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
ERROR = QtGui.QImage(u':/general/general_delete.png')
|
||||
|
||||
ERROR_IMAGE = QtGui.QImage(u':/general/general_delete.png')
|
||||
|
||||
|
||||
class PresentationMediaItem(MediaManagerItem):
|
||||
"""
|
||||
This is the Presentation media manager item for Presentation Items.
|
||||
It can present files using Openoffice and Powerpoint
|
||||
This is the Presentation media manager item for Presentation Items. It can present files using Openoffice and
|
||||
Powerpoint
|
||||
"""
|
||||
log.info(u'Presentations Media Item loaded')
|
||||
|
||||
@ -71,25 +74,25 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
"""
|
||||
self.on_new_prompt = translate('PresentationPlugin.MediaItem', 'Select Presentation(s)')
|
||||
self.Automatic = translate('PresentationPlugin.MediaItem', 'Automatic')
|
||||
self.displayTypeLabel.setText(translate('PresentationPlugin.MediaItem', 'Present using:'))
|
||||
self.display_type_label.setText(translate('PresentationPlugin.MediaItem', 'Present using:'))
|
||||
|
||||
def build_file_mask_string(self):
|
||||
"""
|
||||
Build the list of file extensions to be used in the Open file dialog
|
||||
Build the list of file extensions to be used in the Open file dialog.
|
||||
"""
|
||||
fileType = u''
|
||||
file_type = u''
|
||||
for controller in self.controllers:
|
||||
if self.controllers[controller].enabled():
|
||||
types = self.controllers[controller].supports + self.controllers[controller].alsosupports
|
||||
for type in types:
|
||||
if fileType.find(type) == -1:
|
||||
fileType += u'*.%s ' % type
|
||||
self.service_manager.supported_suffixes(type)
|
||||
self.on_new_file_masks = translate('PresentationPlugin.MediaItem', 'Presentations (%s)') % fileType
|
||||
file_types = self.controllers[controller].supports + self.controllers[controller].also_supports
|
||||
for file_type in file_types:
|
||||
if file_type.find(file_type) == -1:
|
||||
file_type += u'*.%s ' % file_type
|
||||
self.service_manager.supported_suffixes(file_type)
|
||||
self.on_new_file_masks = translate('PresentationPlugin.MediaItem', 'Presentations (%s)') % file_type
|
||||
|
||||
def required_icons(self):
|
||||
"""
|
||||
Set which icons the media manager tab should show
|
||||
Set which icons the media manager tab should show.
|
||||
"""
|
||||
MediaManagerItem.required_icons(self)
|
||||
self.has_file_icon = True
|
||||
@ -98,21 +101,21 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
|
||||
def add_end_header_bar(self):
|
||||
"""
|
||||
Display custom media manager items for presentations
|
||||
Display custom media manager items for presentations.
|
||||
"""
|
||||
self.presentationWidget = QtGui.QWidget(self)
|
||||
self.presentationWidget.setObjectName(u'presentationWidget')
|
||||
self.displayLayout = QtGui.QFormLayout(self.presentationWidget)
|
||||
self.displayLayout.setMargin(self.displayLayout.spacing())
|
||||
self.displayLayout.setObjectName(u'displayLayout')
|
||||
self.displayTypeLabel = QtGui.QLabel(self.presentationWidget)
|
||||
self.displayTypeLabel.setObjectName(u'displayTypeLabel')
|
||||
self.displayTypeComboBox = create_horizontal_adjusting_combo_box(self.presentationWidget,
|
||||
u'displayTypeComboBox')
|
||||
self.displayTypeLabel.setBuddy(self.displayTypeComboBox)
|
||||
self.displayLayout.addRow(self.displayTypeLabel, self.displayTypeComboBox)
|
||||
# Add the Presentation widget to the page layout
|
||||
self.page_layout.addWidget(self.presentationWidget)
|
||||
self.presentation_widget = QtGui.QWidget(self)
|
||||
self.presentation_widget.setObjectName(u'presentation_widget')
|
||||
self.display_layout = QtGui.QFormLayout(self.presentation_widget)
|
||||
self.display_layout.setMargin(self.display_layout.spacing())
|
||||
self.display_layout.setObjectName(u'display_layout')
|
||||
self.display_type_label = QtGui.QLabel(self.presentation_widget)
|
||||
self.display_type_label.setObjectName(u'display_type_label')
|
||||
self.display_type_combo_box = create_horizontal_adjusting_combo_box(self.presentation_widget,
|
||||
u'display_type_combo_box')
|
||||
self.display_type_label.setBuddy(self.display_type_combo_box)
|
||||
self.display_layout.addRow(self.display_type_label, self.display_type_combo_box)
|
||||
# Add the Presentation widget to the page layout.
|
||||
self.page_layout.addWidget(self.presentation_widget)
|
||||
|
||||
def initialise(self):
|
||||
"""
|
||||
@ -120,56 +123,54 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
"""
|
||||
self.list_view.setIconSize(QtCore.QSize(88, 50))
|
||||
files = Settings().value(self.settings_section + u'/presentations files')
|
||||
self.load_list(files, initialLoad=True)
|
||||
self.load_list(files, initial_load=True)
|
||||
self.populate_display_types()
|
||||
|
||||
def populate_display_types(self):
|
||||
"""
|
||||
Load the combobox with the enabled presentation controllers,
|
||||
allowing user to select a specific app if settings allow
|
||||
Load the combobox with the enabled presentation controllers, allowing user to select a specific app if settings
|
||||
allow.
|
||||
"""
|
||||
self.displayTypeComboBox.clear()
|
||||
self.display_type_combo_box.clear()
|
||||
for item in self.controllers:
|
||||
# load the drop down selection
|
||||
if self.controllers[item].enabled():
|
||||
self.displayTypeComboBox.addItem(item)
|
||||
if self.displayTypeComboBox.count() > 1:
|
||||
self.displayTypeComboBox.insertItem(0, self.Automatic)
|
||||
self.displayTypeComboBox.setCurrentIndex(0)
|
||||
self.display_type_combo_box.addItem(item)
|
||||
if self.display_type_combo_box.count() > 1:
|
||||
self.display_type_combo_box.insertItem(0, self.Automatic)
|
||||
self.display_type_combo_box.setCurrentIndex(0)
|
||||
if Settings().value(self.settings_section + u'/override app') == QtCore.Qt.Checked:
|
||||
self.presentationWidget.show()
|
||||
self.presentation_widget.show()
|
||||
else:
|
||||
self.presentationWidget.hide()
|
||||
self.presentation_widget.hide()
|
||||
|
||||
def load_list(self, files, target_group=None, initialLoad=False):
|
||||
def load_list(self, files, target_group=None, initial_load=False):
|
||||
"""
|
||||
Add presentations into the media manager
|
||||
This is called both on initial load of the plugin to populate with
|
||||
existing files, and when the user adds new files via the media manager
|
||||
Add presentations into the media manager. This is called both on initial load of the plugin to populate with
|
||||
existing files, and when the user adds new files via the media manager.
|
||||
"""
|
||||
currlist = self.get_file_list()
|
||||
titles = [os.path.split(file)[1] for file in currlist]
|
||||
current_list = self.get_file_list()
|
||||
titles = [os.path.split(file)[1] for file in current_list]
|
||||
self.application.set_busy_cursor()
|
||||
if not initialLoad:
|
||||
if not initial_load:
|
||||
self.main_window.display_progress_bar(len(files))
|
||||
# Sort the presentations by its filename considering language specific characters.
|
||||
files.sort(cmp=locale_compare,
|
||||
key=lambda filename: os.path.split(unicode(filename))[1])
|
||||
files.sort(key=lambda filename: get_locale_key(os.path.split(unicode(filename))[1]))
|
||||
for file in files:
|
||||
if not initialLoad:
|
||||
if not initial_load:
|
||||
self.main_window.increment_progress_bar()
|
||||
if currlist.count(file) > 0:
|
||||
if current_list.count(file) > 0:
|
||||
continue
|
||||
filename = os.path.split(unicode(file))[1]
|
||||
if not os.path.exists(file):
|
||||
item_name = QtGui.QListWidgetItem(filename)
|
||||
item_name.setIcon(build_icon(ERROR))
|
||||
item_name.setIcon(build_icon(ERROR_IMAGE))
|
||||
item_name.setData(QtCore.Qt.UserRole, file)
|
||||
item_name.setToolTip(file)
|
||||
self.list_view.addItem(item_name)
|
||||
else:
|
||||
if titles.count(filename) > 0:
|
||||
if not initialLoad:
|
||||
if not initial_load:
|
||||
critical_error_message_box(translate('PresentationPlugin.MediaItem', 'File Exists'),
|
||||
translate('PresentationPlugin.MediaItem',
|
||||
'A presentation with that filename already exists.')
|
||||
@ -181,7 +182,7 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
doc = controller.add_document(unicode(file))
|
||||
thumb = os.path.join(doc.get_thumbnail_folder(), u'icon.png')
|
||||
preview = doc.get_thumbnail_path(1, True)
|
||||
if not preview and not initialLoad:
|
||||
if not preview and not initial_load:
|
||||
doc.load_presentation()
|
||||
preview = doc.get_thumbnail_path(1, True)
|
||||
doc.close_presentation()
|
||||
@ -193,7 +194,7 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
else:
|
||||
icon = create_thumb(preview, thumb)
|
||||
else:
|
||||
if initialLoad:
|
||||
if initial_load:
|
||||
icon = build_icon(u':/general/general_delete.png')
|
||||
else:
|
||||
critical_error_message_box(UiStrings().UnsupportedFile,
|
||||
@ -204,13 +205,13 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
item_name.setIcon(icon)
|
||||
item_name.setToolTip(file)
|
||||
self.list_view.addItem(item_name)
|
||||
if not initialLoad:
|
||||
if not initial_load:
|
||||
self.main_window.finished_progress_bar()
|
||||
self.application.set_normal_cursor()
|
||||
|
||||
def on_delete_click(self):
|
||||
"""
|
||||
Remove a presentation item from the list
|
||||
Remove a presentation item from the list.
|
||||
"""
|
||||
if check_item_selected(self.list_view, UiStrings().SelectDelete):
|
||||
items = self.list_view.selectedIndexes()
|
||||
@ -231,12 +232,11 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
self.list_view.takeItem(row)
|
||||
Settings().setValue(self.settings_section + u'/presentations files', self.get_file_list())
|
||||
|
||||
def generate_slide_data(self, service_item, item=None, xmlVersion=False,
|
||||
def generate_slide_data(self, service_item, item=None, xml_version=False,
|
||||
remote=False, context=ServiceItemContext.Service):
|
||||
"""
|
||||
Load the relevant information for displaying the presentation
|
||||
in the slidecontroller. In the case of powerpoints, an image
|
||||
for each slide
|
||||
Load the relevant information for displaying the presentation in the slidecontroller. In the case of
|
||||
powerpoints, an image for each slide.
|
||||
"""
|
||||
if item:
|
||||
items = [item]
|
||||
@ -244,8 +244,8 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
items = self.list_view.selectedItems()
|
||||
if len(items) > 1:
|
||||
return False
|
||||
service_item.title = self.displayTypeComboBox.currentText()
|
||||
service_item.shortname = self.displayTypeComboBox.currentText()
|
||||
service_item.title = self.display_type_combo_box.currentText()
|
||||
service_item.shortname = self.display_type_combo_box.currentText()
|
||||
service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay)
|
||||
service_item.add_capability(ItemCapabilities.HasDetailedTitleDisplay)
|
||||
shortname = service_item.shortname
|
||||
@ -288,26 +288,24 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
|
||||
def findControllerByType(self, filename):
|
||||
"""
|
||||
Determine the default application controller to use for the selected
|
||||
file type. This is used if "Automatic" is set as the preferred
|
||||
controller. Find the first (alphabetic) enabled controller which
|
||||
"supports" the extension. If none found, then look for a controller
|
||||
which "also supports" it instead.
|
||||
Determine the default application controller to use for the selected file type. This is used if "Automatic" is
|
||||
set as the preferred controller. Find the first (alphabetic) enabled controller which "supports" the extension.
|
||||
If none found, then look for a controller which "also supports" it instead.
|
||||
"""
|
||||
filetype = os.path.splitext(filename)[1][1:]
|
||||
if not filetype:
|
||||
file_type = os.path.splitext(filename)[1][1:]
|
||||
if not file_type:
|
||||
return None
|
||||
for controller in self.controllers:
|
||||
if self.controllers[controller].enabled():
|
||||
if filetype in self.controllers[controller].supports:
|
||||
if file_type in self.controllers[controller].supports:
|
||||
return controller
|
||||
for controller in self.controllers:
|
||||
if self.controllers[controller].enabled():
|
||||
if filetype in self.controllers[controller].alsosupports:
|
||||
if file_type in self.controllers[controller].also_supports:
|
||||
return controller
|
||||
return None
|
||||
|
||||
def search(self, string, showError):
|
||||
def search(self, string, show_error):
|
||||
files = Settings().value(self.settings_section + u'/presentations files')
|
||||
results = []
|
||||
string = string.lower()
|
||||
|
@ -38,8 +38,8 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class Controller(object):
|
||||
"""
|
||||
This is the Presentation listener who acts on events from the slide
|
||||
controller and passes the messages on the the correct presentation handlers
|
||||
This is the Presentation listener who acts on events from the slide controller and passes the messages on the the
|
||||
correct presentation handlers.
|
||||
"""
|
||||
log.info(u'Controller loaded')
|
||||
|
||||
@ -54,9 +54,8 @@ class Controller(object):
|
||||
|
||||
def add_handler(self, controller, file, hide_mode, slide_no):
|
||||
"""
|
||||
Add a handler, which is an instance of a presentation and
|
||||
slidecontroller combination. If the slidecontroller has a display
|
||||
then load the presentation.
|
||||
Add a handler, which is an instance of a presentation and slidecontroller combination. If the slidecontroller
|
||||
has a display then load the presentation.
|
||||
"""
|
||||
log.debug(u'Live = %s, add_handler %s' % (self.is_live, file))
|
||||
self.controller = controller
|
||||
@ -86,8 +85,7 @@ class Controller(object):
|
||||
|
||||
def activate(self):
|
||||
"""
|
||||
Active the presentation, and show it on the screen.
|
||||
Use the last slide number.
|
||||
Active the presentation, and show it on the screen. Use the last slide number.
|
||||
"""
|
||||
log.debug(u'Live = %s, activate' % self.is_live)
|
||||
if not self.doc:
|
||||
@ -130,7 +128,7 @@ class Controller(object):
|
||||
|
||||
def first(self):
|
||||
"""
|
||||
Based on the handler passed at startup triggers the first slide
|
||||
Based on the handler passed at startup triggers the first slide.
|
||||
"""
|
||||
log.debug(u'Live = %s, first' % self.is_live)
|
||||
if not self.doc:
|
||||
@ -148,7 +146,7 @@ class Controller(object):
|
||||
|
||||
def last(self):
|
||||
"""
|
||||
Based on the handler passed at startup triggers the last slide
|
||||
Based on the handler passed at startup triggers the last slide.
|
||||
"""
|
||||
log.debug(u'Live = %s, last' % self.is_live)
|
||||
if not self.doc:
|
||||
@ -166,7 +164,7 @@ class Controller(object):
|
||||
|
||||
def next(self):
|
||||
"""
|
||||
Based on the handler passed at startup triggers the next slide event
|
||||
Based on the handler passed at startup triggers the next slide event.
|
||||
"""
|
||||
log.debug(u'Live = %s, next' % self.is_live)
|
||||
if not self.doc:
|
||||
@ -182,9 +180,8 @@ class Controller(object):
|
||||
return
|
||||
if not self.activate():
|
||||
return
|
||||
# The "End of slideshow" screen is after the last slide
|
||||
# Note, we can't just stop on the last slide, since it may
|
||||
# contain animations that need to be stepped through.
|
||||
# The "End of slideshow" screen is after the last slide. Note, we can't just stop on the last slide, since it
|
||||
# may contain animations that need to be stepped through.
|
||||
if self.doc.slidenumber > self.doc.get_slide_count():
|
||||
return
|
||||
self.doc.next_step()
|
||||
@ -192,7 +189,7 @@ class Controller(object):
|
||||
|
||||
def previous(self):
|
||||
"""
|
||||
Based on the handler passed at startup triggers the previous slide event
|
||||
Based on the handler passed at startup triggers the previous slide event.
|
||||
"""
|
||||
log.debug(u'Live = %s, previous' % self.is_live)
|
||||
if not self.doc:
|
||||
@ -213,7 +210,7 @@ class Controller(object):
|
||||
|
||||
def shutdown(self):
|
||||
"""
|
||||
Based on the handler passed at startup triggers slide show to shut down
|
||||
Based on the handler passed at startup triggers slide show to shut down.
|
||||
"""
|
||||
log.debug(u'Live = %s, shutdown' % self.is_live)
|
||||
if not self.doc:
|
||||
@ -223,7 +220,7 @@ class Controller(object):
|
||||
|
||||
def blank(self, hide_mode):
|
||||
"""
|
||||
Instruct the controller to blank the presentation
|
||||
Instruct the controller to blank the presentation.
|
||||
"""
|
||||
log.debug(u'Live = %s, blank' % self.is_live)
|
||||
self.hide_mode = hide_mode
|
||||
@ -244,7 +241,7 @@ class Controller(object):
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Instruct the controller to stop and hide the presentation
|
||||
Instruct the controller to stop and hide the presentation.
|
||||
"""
|
||||
log.debug(u'Live = %s, stop' % self.is_live)
|
||||
self.hide_mode = HideMode.Screen
|
||||
@ -260,7 +257,7 @@ class Controller(object):
|
||||
|
||||
def unblank(self):
|
||||
"""
|
||||
Instruct the controller to unblank the presentation
|
||||
Instruct the controller to unblank the presentation.
|
||||
"""
|
||||
log.debug(u'Live = %s, unblank' % self.is_live)
|
||||
self.hide_mode = None
|
||||
@ -283,8 +280,8 @@ class Controller(object):
|
||||
|
||||
class MessageListener(object):
|
||||
"""
|
||||
This is the Presentation listener who acts on events from the slide
|
||||
controller and passes the messages on the the correct presentation handlers
|
||||
This is the Presentation listener who acts on events from the slide controller and passes the messages on the the
|
||||
correct presentation handlers
|
||||
"""
|
||||
log.info(u'Message Listener loaded')
|
||||
|
||||
@ -310,12 +307,11 @@ class MessageListener(object):
|
||||
|
||||
def startup(self, message):
|
||||
"""
|
||||
Start of new presentation
|
||||
Save the handler as any new presentations start here
|
||||
Start of new presentation. Save the handler as any new presentations start here
|
||||
"""
|
||||
log.debug(u'Startup called with message %s' % message)
|
||||
is_live = message[1]
|
||||
item = message[0]
|
||||
log.debug(u'Startup called with message %s' % message)
|
||||
hide_mode = message[2]
|
||||
file = item.get_frame_path()
|
||||
self.handler = item.title
|
||||
@ -331,7 +327,7 @@ class MessageListener(object):
|
||||
|
||||
def slide(self, message):
|
||||
"""
|
||||
React to the message to move to a specific slide
|
||||
React to the message to move to a specific slide.
|
||||
"""
|
||||
is_live = message[1]
|
||||
slide = message[2]
|
||||
@ -342,7 +338,7 @@ class MessageListener(object):
|
||||
|
||||
def first(self, message):
|
||||
"""
|
||||
React to the message to move to the first slide
|
||||
React to the message to move to the first slide.
|
||||
"""
|
||||
is_live = message[1]
|
||||
if is_live:
|
||||
@ -352,7 +348,7 @@ class MessageListener(object):
|
||||
|
||||
def last(self, message):
|
||||
"""
|
||||
React to the message to move to the last slide
|
||||
React to the message to move to the last slide.
|
||||
"""
|
||||
is_live = message[1]
|
||||
if is_live:
|
||||
@ -362,7 +358,7 @@ class MessageListener(object):
|
||||
|
||||
def next(self, message):
|
||||
"""
|
||||
React to the message to move to the next animation/slide
|
||||
React to the message to move to the next animation/slide.
|
||||
"""
|
||||
is_live = message[1]
|
||||
if is_live:
|
||||
@ -372,7 +368,7 @@ class MessageListener(object):
|
||||
|
||||
def previous(self, message):
|
||||
"""
|
||||
React to the message to move to the previous animation/slide
|
||||
React to the message to move to the previous animation/slide.
|
||||
"""
|
||||
is_live = message[1]
|
||||
if is_live:
|
||||
@ -382,8 +378,7 @@ class MessageListener(object):
|
||||
|
||||
def shutdown(self, message):
|
||||
"""
|
||||
React to message to shutdown the presentation. I.e. end the show
|
||||
and close the file
|
||||
React to message to shutdown the presentation. I.e. end the show and close the file.
|
||||
"""
|
||||
is_live = message[1]
|
||||
if is_live:
|
||||
@ -393,7 +388,7 @@ class MessageListener(object):
|
||||
|
||||
def hide(self, message):
|
||||
"""
|
||||
React to the message to show the desktop
|
||||
React to the message to show the desktop.
|
||||
"""
|
||||
is_live = message[1]
|
||||
if is_live:
|
||||
@ -401,7 +396,7 @@ class MessageListener(object):
|
||||
|
||||
def blank(self, message):
|
||||
"""
|
||||
React to the message to blank the display
|
||||
React to the message to blank the display.
|
||||
"""
|
||||
is_live = message[1]
|
||||
hide_mode = message[2]
|
||||
@ -410,7 +405,7 @@ class MessageListener(object):
|
||||
|
||||
def unblank(self, message):
|
||||
"""
|
||||
React to the message to unblank the display
|
||||
React to the message to unblank the display.
|
||||
"""
|
||||
is_live = message[1]
|
||||
if is_live:
|
||||
@ -418,9 +413,7 @@ class MessageListener(object):
|
||||
|
||||
def timeout(self):
|
||||
"""
|
||||
The presentation may be timed or might be controlled by the
|
||||
application directly, rather than through OpenLP. Poll occasionally
|
||||
to check which slide is currently displayed so the slidecontroller
|
||||
view can be updated
|
||||
The presentation may be timed or might be controlled by the application directly, rather than through OpenLP.
|
||||
Poll occasionally to check which slide is currently displayed so the slidecontroller view can be updated.
|
||||
"""
|
||||
self.live_handler.poll()
|
||||
|
@ -26,7 +26,10 @@
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
"""
|
||||
This modul is for controlling powerpiont. PPT API documentation:
|
||||
`http://msdn.microsoft.com/en-us/library/aa269321(office.10).aspx`_
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
|
||||
@ -39,16 +42,14 @@ if os.name == u'nt':
|
||||
from openlp.core.lib import ScreenList
|
||||
from presentationcontroller import PresentationController, PresentationDocument
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# PPT API documentation:
|
||||
# http://msdn.microsoft.com/en-us/library/aa269321(office.10).aspx
|
||||
|
||||
class PowerpointController(PresentationController):
|
||||
"""
|
||||
Class to control interactions with PowerPoint Presentations
|
||||
It creates the runtime Environment , Loads the and Closes the Presentation
|
||||
As well as triggering the correct activities based on the users input
|
||||
Class to control interactions with PowerPoint Presentations. It creates the runtime Environment , Loads the and
|
||||
Closes the Presentation. As well as triggering the correct activities based on the users input.
|
||||
"""
|
||||
log.info(u'PowerpointController loaded')
|
||||
|
||||
@ -63,7 +64,7 @@ class PowerpointController(PresentationController):
|
||||
|
||||
def check_available(self):
|
||||
"""
|
||||
PowerPoint is able to run on this machine
|
||||
PowerPoint is able to run on this machine.
|
||||
"""
|
||||
log.debug(u'check_available')
|
||||
if os.name == u'nt':
|
||||
@ -77,7 +78,7 @@ class PowerpointController(PresentationController):
|
||||
if os.name == u'nt':
|
||||
def start_process(self):
|
||||
"""
|
||||
Loads PowerPoint process
|
||||
Loads PowerPoint process.
|
||||
"""
|
||||
log.debug(u'start_process')
|
||||
if not self.process:
|
||||
@ -87,7 +88,7 @@ class PowerpointController(PresentationController):
|
||||
|
||||
def kill(self):
|
||||
"""
|
||||
Called at system exit to clean up any running presentations
|
||||
Called at system exit to clean up any running presentations.
|
||||
"""
|
||||
log.debug(u'Kill powerpoint')
|
||||
while self.docs:
|
||||
@ -105,12 +106,12 @@ class PowerpointController(PresentationController):
|
||||
|
||||
class PowerpointDocument(PresentationDocument):
|
||||
"""
|
||||
Class which holds information and controls a single presentation
|
||||
Class which holds information and controls a single presentation.
|
||||
"""
|
||||
|
||||
def __init__(self, controller, presentation):
|
||||
"""
|
||||
Constructor, store information about the file and initialise
|
||||
Constructor, store information about the file and initialise.
|
||||
"""
|
||||
log.debug(u'Init Presentation Powerpoint')
|
||||
PresentationDocument.__init__(self, controller, presentation)
|
||||
@ -118,8 +119,8 @@ class PowerpointDocument(PresentationDocument):
|
||||
|
||||
def load_presentation(self):
|
||||
"""
|
||||
Called when a presentation is added to the SlideController.
|
||||
Opens the PowerPoint file using the process created earlier.
|
||||
Called when a presentation is added to the SlideController. Opens the PowerPoint file using the process created
|
||||
earlier.
|
||||
"""
|
||||
log.debug(u'load_presentation')
|
||||
if not self.controller.process or not self.controller.process.Visible:
|
||||
@ -142,20 +143,19 @@ class PowerpointDocument(PresentationDocument):
|
||||
self.presentation.Slides[n].Copy()
|
||||
thumbnail = QApplication.clipboard.image()
|
||||
|
||||
However, for the moment, we want a physical file since it makes life
|
||||
easier elsewhere.
|
||||
However, for the moment, we want a physical file since it makes life easier elsewhere.
|
||||
"""
|
||||
log.debug(u'create_thumbnails')
|
||||
if self.check_thumbnails():
|
||||
return
|
||||
for num in range(self.presentation.Slides.Count):
|
||||
self.presentation.Slides(num + 1).Export(os.path.join(
|
||||
self.get_thumbnail_folder(), 'slide%d.png' % (num + 1)), 'png', 320, 240)
|
||||
self.presentation.Slides(num + 1).Export(
|
||||
os.path.join(self.get_thumbnail_folder(), 'slide%d.png' % (num + 1)), 'png', 320, 240)
|
||||
|
||||
def close_presentation(self):
|
||||
"""
|
||||
Close presentation and clean up objects. This is triggered by a new
|
||||
object being added to SlideController or OpenLP being shut down.
|
||||
Close presentation and clean up objects. This is triggered by a new object being added to SlideController or
|
||||
OpenLP being shut down.
|
||||
"""
|
||||
log.debug(u'ClosePresentation')
|
||||
if self.presentation:
|
||||
@ -182,7 +182,6 @@ class PowerpointDocument(PresentationDocument):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def is_active(self):
|
||||
"""
|
||||
Returns ``True`` if a presentation is currently active.
|
||||
@ -253,15 +252,14 @@ class PowerpointDocument(PresentationDocument):
|
||||
dpi = win32ui.GetForegroundWindow().GetDC().GetDeviceCaps(88)
|
||||
except win32ui.error:
|
||||
dpi = 96
|
||||
rect = ScreenList().current[u'size']
|
||||
size = ScreenList().current[u'size']
|
||||
ppt_window = self.presentation.SlideShowSettings.Run()
|
||||
if not ppt_window:
|
||||
return
|
||||
ppt_window.Top = rect.y() * 72 / dpi
|
||||
ppt_window.Height = rect.height() * 72 / dpi
|
||||
ppt_window.Left = rect.x() * 72 / dpi
|
||||
ppt_window.Width = rect.width() * 72 / dpi
|
||||
|
||||
ppt_window.Top = size.y() * 72 / dpi
|
||||
ppt_window.Height = size.height() * 72 / dpi
|
||||
ppt_window.Left = size.x() * 72 / dpi
|
||||
ppt_window.Width = size.width() * 72 / dpi
|
||||
|
||||
def get_slide_number(self):
|
||||
"""
|
||||
@ -318,6 +316,7 @@ class PowerpointDocument(PresentationDocument):
|
||||
"""
|
||||
return _get_text_from_shapes(self.presentation.Slides(slide_no).NotesPage.Shapes)
|
||||
|
||||
|
||||
def _get_text_from_shapes(shapes):
|
||||
"""
|
||||
Returns any text extracted from the shapes on a presentation slide.
|
||||
@ -326,8 +325,8 @@ def _get_text_from_shapes(shapes):
|
||||
A set of shapes to search for text.
|
||||
"""
|
||||
text = ''
|
||||
for idx in range(shapes.Count):
|
||||
shape = shapes(idx + 1)
|
||||
for index in range(shapes.Count):
|
||||
shape = shapes(index + 1)
|
||||
if shape.HasTextFrame:
|
||||
text += shape.TextFrame.TextRange.Text + '\n'
|
||||
return text
|
||||
|
@ -37,13 +37,14 @@ if os.name == u'nt':
|
||||
from openlp.core.lib import ScreenList
|
||||
from presentationcontroller import PresentationController, PresentationDocument
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PptviewController(PresentationController):
|
||||
"""
|
||||
Class to control interactions with PowerPoint Viewer Presentations
|
||||
It creates the runtime Environment , Loads the and Closes the Presentation
|
||||
As well as triggering the correct activities based on the users input
|
||||
Class to control interactions with PowerPoint Viewer Presentations. It creates the runtime Environment , Loads the
|
||||
and Closes the Presentation. As well as triggering the correct activities based on the users input
|
||||
"""
|
||||
log.info(u'PPTViewController loaded')
|
||||
|
||||
@ -58,7 +59,7 @@ class PptviewController(PresentationController):
|
||||
|
||||
def check_available(self):
|
||||
"""
|
||||
PPT Viewer is able to run on this machine
|
||||
PPT Viewer is able to run on this machine.
|
||||
"""
|
||||
log.debug(u'check_available')
|
||||
if os.name != u'nt':
|
||||
@ -68,7 +69,7 @@ class PptviewController(PresentationController):
|
||||
if os.name == u'nt':
|
||||
def check_installed(self):
|
||||
"""
|
||||
Check the viewer is installed
|
||||
Check the viewer is installed.
|
||||
"""
|
||||
log.debug(u'Check installed')
|
||||
try:
|
||||
@ -79,14 +80,14 @@ class PptviewController(PresentationController):
|
||||
|
||||
def start_process(self):
|
||||
"""
|
||||
Loads the PPTVIEWLIB library
|
||||
Loads the PPTVIEWLIB library.
|
||||
"""
|
||||
if self.process:
|
||||
return
|
||||
log.debug(u'start PPTView')
|
||||
dllpath = os.path.join(self.plugin_manager.base_path, u'presentations', u'lib', u'pptviewlib',
|
||||
u'pptviewlib.dll')
|
||||
self.process = cdll.LoadLibrary(dllpath)
|
||||
dll_path = os.path.join(
|
||||
self.plugin_manager.base_path, u'presentations', u'lib', u'pptviewlib', u'pptviewlib.dll')
|
||||
self.process = cdll.LoadLibrary(dll_path)
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
self.process.SetDebug(1)
|
||||
|
||||
@ -101,33 +102,32 @@ class PptviewController(PresentationController):
|
||||
|
||||
class PptviewDocument(PresentationDocument):
|
||||
"""
|
||||
Class which holds information and controls a single presentation
|
||||
Class which holds information and controls a single presentation.
|
||||
"""
|
||||
def __init__(self, controller, presentation):
|
||||
"""
|
||||
Constructor, store information about the file and initialise
|
||||
Constructor, store information about the file and initialise.
|
||||
"""
|
||||
log.debug(u'Init Presentation PowerPoint')
|
||||
PresentationDocument.__init__(self, controller, presentation)
|
||||
self.presentation = None
|
||||
self.pptid = None
|
||||
self.ppt_id = None
|
||||
self.blanked = False
|
||||
self.hidden = False
|
||||
|
||||
def load_presentation(self):
|
||||
"""
|
||||
Called when a presentation is added to the SlideController.
|
||||
It builds the environment, starts communication with the background
|
||||
PptView task started earlier.
|
||||
Called when a presentation is added to the SlideController. It builds the environment, starts communication with
|
||||
the background PptView task started earlier.
|
||||
"""
|
||||
log.debug(u'LoadPresentation')
|
||||
rect = ScreenList().current[u'size']
|
||||
rect = RECT(rect.x(), rect.y(), rect.right(), rect.bottom())
|
||||
size = ScreenList().current[u'size']
|
||||
rect = RECT(size.x(), size.y(), size.right(), size.bottom())
|
||||
filepath = str(self.filepath.replace(u'/', u'\\'))
|
||||
if not os.path.isdir(self.get_temp_folder()):
|
||||
os.makedirs(self.get_temp_folder())
|
||||
self.pptid = self.controller.process.OpenPPT(filepath, None, rect, str(self.get_temp_folder()) + '\\slide')
|
||||
if self.pptid >= 0:
|
||||
self.ppt_id = self.controller.process.OpenPPT(filepath, None, rect, str(self.get_temp_folder()) + '\\slide')
|
||||
if self.ppt_id >= 0:
|
||||
self.create_thumbnails()
|
||||
self.stop_presentation()
|
||||
return True
|
||||
@ -136,8 +136,7 @@ class PptviewDocument(PresentationDocument):
|
||||
|
||||
def create_thumbnails(self):
|
||||
"""
|
||||
PPTviewLib creates large BMP's, but we want small PNG's for consistency.
|
||||
Convert them here.
|
||||
PPTviewLib creates large BMP's, but we want small PNG's for consistency. Convert them here.
|
||||
"""
|
||||
log.debug(u'create_thumbnails')
|
||||
if self.check_thumbnails():
|
||||
@ -149,21 +148,20 @@ class PptviewDocument(PresentationDocument):
|
||||
|
||||
def close_presentation(self):
|
||||
"""
|
||||
Close presentation and clean up objects
|
||||
Triggered by new object being added to SlideController orOpenLP
|
||||
being shut down
|
||||
Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being
|
||||
shut down.
|
||||
"""
|
||||
log.debug(u'ClosePresentation')
|
||||
if self.controller.process:
|
||||
self.controller.process.ClosePPT(self.pptid)
|
||||
self.pptid = -1
|
||||
self.controller.process.ClosePPT(self.ppt_id)
|
||||
self.ppt_id = -1
|
||||
self.controller.remove_doc(self)
|
||||
|
||||
def is_loaded(self):
|
||||
"""
|
||||
Returns true if a presentation is loaded
|
||||
Returns true if a presentation is loaded.
|
||||
"""
|
||||
if self.pptid < 0:
|
||||
if self.ppt_id < 0:
|
||||
return False
|
||||
if self.get_slide_count() < 0:
|
||||
return False
|
||||
@ -171,74 +169,74 @@ class PptviewDocument(PresentationDocument):
|
||||
|
||||
def is_active(self):
|
||||
"""
|
||||
Returns true if a presentation is currently active
|
||||
Returns true if a presentation is currently active.
|
||||
"""
|
||||
return self.is_loaded() and not self.hidden
|
||||
|
||||
def blank_screen(self):
|
||||
"""
|
||||
Blanks the screen
|
||||
Blanks the screen.
|
||||
"""
|
||||
self.controller.process.Blank(self.pptid)
|
||||
self.controller.process.Blank(self.ppt_id)
|
||||
self.blanked = True
|
||||
|
||||
def unblank_screen(self):
|
||||
"""
|
||||
Unblanks (restores) the presentation
|
||||
Unblanks (restores) the presentation.
|
||||
"""
|
||||
self.controller.process.Unblank(self.pptid)
|
||||
self.controller.process.Unblank(self.ppt_id)
|
||||
self.blanked = False
|
||||
|
||||
def is_blank(self):
|
||||
"""
|
||||
Returns true if screen is blank
|
||||
Returns true if screen is blank.
|
||||
"""
|
||||
log.debug(u'is blank OpenOffice')
|
||||
return self.blanked
|
||||
|
||||
def stop_presentation(self):
|
||||
"""
|
||||
Stops the current presentation and hides the output
|
||||
Stops the current presentation and hides the output.
|
||||
"""
|
||||
self.hidden = True
|
||||
self.controller.process.Stop(self.pptid)
|
||||
self.controller.process.Stop(self.ppt_id)
|
||||
|
||||
def start_presentation(self):
|
||||
"""
|
||||
Starts a presentation from the beginning
|
||||
Starts a presentation from the beginning.
|
||||
"""
|
||||
if self.hidden:
|
||||
self.hidden = False
|
||||
self.controller.process.Resume(self.pptid)
|
||||
self.controller.process.Resume(self.ppt_id)
|
||||
else:
|
||||
self.controller.process.RestartShow(self.pptid)
|
||||
self.controller.process.RestartShow(self.ppt_id)
|
||||
|
||||
def get_slide_number(self):
|
||||
"""
|
||||
Returns the current slide number
|
||||
Returns the current slide number.
|
||||
"""
|
||||
return self.controller.process.GetCurrentSlide(self.pptid)
|
||||
return self.controller.process.GetCurrentSlide(self.ppt_id)
|
||||
|
||||
def get_slide_count(self):
|
||||
"""
|
||||
Returns total number of slides
|
||||
Returns total number of slides.
|
||||
"""
|
||||
return self.controller.process.GetSlideCount(self.pptid)
|
||||
return self.controller.process.GetSlideCount(self.ppt_id)
|
||||
|
||||
def goto_slide(self, slideno):
|
||||
"""
|
||||
Moves to a specific slide in the presentation
|
||||
Moves to a specific slide in the presentation.
|
||||
"""
|
||||
self.controller.process.GotoSlide(self.pptid, slideno)
|
||||
self.controller.process.GotoSlide(self.ppt_id, slideno)
|
||||
|
||||
def next_step(self):
|
||||
"""
|
||||
Triggers the next effect of slide on the running presentation
|
||||
Triggers the next effect of slide on the running presentation.
|
||||
"""
|
||||
self.controller.process.NextStep(self.pptid)
|
||||
self.controller.process.NextStep(self.ppt_id)
|
||||
|
||||
def previous_step(self):
|
||||
"""
|
||||
Triggers the previous slide on the running presentation
|
||||
Triggers the previous slide on the running presentation.
|
||||
"""
|
||||
self.controller.process.PrevStep(self.pptid)
|
||||
self.controller.process.PrevStep(self.ppt_id)
|
||||
|
@ -40,9 +40,8 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class PresentationDocument(object):
|
||||
"""
|
||||
Base class for presentation documents to inherit from.
|
||||
Loads and closes the presentation as well as triggering the correct
|
||||
activities based on the users input
|
||||
Base class for presentation documents to inherit from. Loads and closes the presentation as well as triggering the
|
||||
correct activities based on the users input
|
||||
|
||||
**Hook Functions**
|
||||
|
||||
@ -131,20 +130,17 @@ class PresentationDocument(object):
|
||||
"""
|
||||
The location where thumbnail images will be stored
|
||||
"""
|
||||
return os.path.join(
|
||||
self.controller.thumbnail_folder, self.get_file_name())
|
||||
return os.path.join(self.controller.thumbnail_folder, self.get_file_name())
|
||||
|
||||
def get_temp_folder(self):
|
||||
"""
|
||||
The location where thumbnail images will be stored
|
||||
"""
|
||||
return os.path.join(
|
||||
self.controller.temp_folder, self.get_file_name())
|
||||
return os.path.join(self.controller.temp_folder, self.get_file_name())
|
||||
|
||||
def check_thumbnails(self):
|
||||
"""
|
||||
Returns ``True`` if the thumbnail images exist and are more recent than
|
||||
the powerpoint file.
|
||||
Returns ``True`` if the thumbnail images exist and are more recent than the powerpoint file.
|
||||
"""
|
||||
lastimage = self.get_thumbnail_path(self.get_slide_count(), True)
|
||||
if not (lastimage and os.path.isfile(lastimage)):
|
||||
@ -153,8 +149,7 @@ class PresentationDocument(object):
|
||||
|
||||
def close_presentation(self):
|
||||
"""
|
||||
Close presentation and clean up objects
|
||||
Triggered by new object being added to SlideController
|
||||
Close presentation and clean up objects. Triggered by new object being added to SlideController
|
||||
"""
|
||||
self.controller.close_presentation()
|
||||
|
||||
@ -223,8 +218,8 @@ class PresentationDocument(object):
|
||||
|
||||
def next_step(self):
|
||||
"""
|
||||
Triggers the next effect of slide on the running presentation
|
||||
This might be the next animation on the current slide, or the next slide
|
||||
Triggers the next effect of slide on the running presentation. This might be the next animation on the current
|
||||
slide, or the next slide
|
||||
"""
|
||||
pass
|
||||
|
||||
@ -236,8 +231,7 @@ class PresentationDocument(object):
|
||||
|
||||
def convert_thumbnail(self, file, idx):
|
||||
"""
|
||||
Convert the slide image the application made to a standard 320x240
|
||||
.png image.
|
||||
Convert the slide image the application made to a standard 320x240 .png image.
|
||||
"""
|
||||
if self.check_thumbnails():
|
||||
return
|
||||
@ -297,17 +291,14 @@ class PresentationDocument(object):
|
||||
|
||||
class PresentationController(object):
|
||||
"""
|
||||
This class is used to control interactions with presentation applications
|
||||
by creating a runtime environment. This is a base class for presentation
|
||||
controllers to inherit from.
|
||||
This class is used to control interactions with presentation applications by creating a runtime environment. This is
|
||||
a base class for presentation controllers to inherit from.
|
||||
|
||||
To create a new controller, take a copy of this file and name it so it ends
|
||||
with ``controller.py``, i.e. ``foobarcontroller.py``. Make sure it inherits
|
||||
:class:`~openlp.plugins.presentations.lib.presentationcontroller.PresentationController`,
|
||||
and then fill in the blanks. If possible try to make sure it loads on all
|
||||
platforms, usually by using :mod:``os.name`` checks, although
|
||||
``__init__``, ``check_available`` and ``presentation_deleted`` should
|
||||
always be implemented.
|
||||
To create a new controller, take a copy of this file and name it so it ends with ``controller.py``, i.e.
|
||||
``foobarcontroller.py``. Make sure it inherits
|
||||
:class:`~openlp.plugins.presentations.lib.presentationcontroller.PresentationController`, and then fill in the
|
||||
blanks. If possible try to make sure it loads on all platforms, usually by using :mod:``os.name`` checks, although
|
||||
``__init__``, ``check_available`` and ``presentation_deleted`` should always be implemented.
|
||||
|
||||
See :class:`~openlp.plugins.presentations.lib.impresscontroller.ImpressController`,
|
||||
:class:`~openlp.plugins.presentations.lib.powerpointcontroller.PowerpointController` or
|
||||
@ -317,36 +308,34 @@ class PresentationController(object):
|
||||
**Basic Attributes**
|
||||
|
||||
``name``
|
||||
The name that appears in the options and the media manager
|
||||
The name that appears in the options and the media manager.
|
||||
|
||||
``enabled``
|
||||
The controller is enabled
|
||||
The controller is enabled.
|
||||
|
||||
``available``
|
||||
The controller is available on this machine. Set by init via
|
||||
call to check_available
|
||||
The controller is available on this machine. Set by init via call to check_available.
|
||||
|
||||
``plugin``
|
||||
The presentationplugin object
|
||||
The presentationplugin object.
|
||||
|
||||
``supports``
|
||||
The primary native file types this application supports
|
||||
The primary native file types this application supports.
|
||||
|
||||
``alsosupports``
|
||||
Other file types the application can import, although not necessarily
|
||||
the first choice due to potential incompatibilities
|
||||
Other file types the application can import, although not necessarily the first choice due to potential
|
||||
incompatibilities.
|
||||
|
||||
**Hook Functions**
|
||||
|
||||
``kill()``
|
||||
Called at system exit to clean up any running presentations
|
||||
Called at system exit to clean up any running presentations.
|
||||
|
||||
``check_available()``
|
||||
Returns True if presentation application is installed/can run on this
|
||||
machine
|
||||
Returns True if presentation application is installed/can run on this machine.
|
||||
|
||||
``presentation_deleted()``
|
||||
Deletes presentation specific files, e.g. thumbnails
|
||||
Deletes presentation specific files, e.g. thumbnails.
|
||||
|
||||
"""
|
||||
log.info(u'PresentationController loaded')
|
||||
@ -354,9 +343,8 @@ class PresentationController(object):
|
||||
def __init__(self, plugin=None, name=u'PresentationController',
|
||||
document_class=PresentationDocument):
|
||||
"""
|
||||
This is the constructor for the presentationcontroller object. This
|
||||
provides an easy way for descendent plugins to populate common data.
|
||||
This method *must* be overridden, like so::
|
||||
This is the constructor for the presentationcontroller object. This provides an easy way for descendent plugins
|
||||
to populate common data. This method *must* be overridden, like so::
|
||||
|
||||
class MyPresentationController(PresentationController):
|
||||
def __init__(self, plugin):
|
||||
@ -399,28 +387,26 @@ class PresentationController(object):
|
||||
|
||||
def check_available(self):
|
||||
"""
|
||||
Presentation app is able to run on this machine
|
||||
Presentation app is able to run on this machine.
|
||||
"""
|
||||
return False
|
||||
|
||||
def start_process(self):
|
||||
"""
|
||||
Loads a running version of the presentation application in the
|
||||
background.
|
||||
Loads a running version of the presentation application in the background.
|
||||
"""
|
||||
pass
|
||||
|
||||
def kill(self):
|
||||
"""
|
||||
Called at system exit to clean up any running presentations and
|
||||
close the application
|
||||
Called at system exit to clean up any running presentations and close the application.
|
||||
"""
|
||||
log.debug(u'Kill')
|
||||
self.close_presentation()
|
||||
|
||||
def add_document(self, name):
|
||||
"""
|
||||
Called when a new presentation document is opened
|
||||
Called when a new presentation document is opened.
|
||||
"""
|
||||
document = self.document_class(self, name)
|
||||
self.docs.append(document)
|
||||
@ -428,7 +414,7 @@ class PresentationController(object):
|
||||
|
||||
def remove_doc(self, doc=None):
|
||||
"""
|
||||
Called to remove an open document from the collection
|
||||
Called to remove an open document from the collection.
|
||||
"""
|
||||
log.debug(u'remove_doc Presentation')
|
||||
if doc is None:
|
||||
|
@ -91,8 +91,7 @@ class PresentationTab(SettingsTab):
|
||||
if checkbox.isEnabled():
|
||||
checkbox.setText(controller.name)
|
||||
else:
|
||||
checkbox.setText(
|
||||
translate('PresentationPlugin.PresentationTab', '%s (unavailable)') % controller.name)
|
||||
checkbox.setText(translate('PresentationPlugin.PresentationTab', '%s (unavailable)') % controller.name)
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
@ -106,8 +105,8 @@ class PresentationTab(SettingsTab):
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Save the settings. If the tab hasn't been made visible to the user then there is nothing to do,
|
||||
so exit. This removes the need to start presentation applications unnecessarily.
|
||||
Save the settings. If the tab hasn't been made visible to the user then there is nothing to do, so exit. This
|
||||
removes the need to start presentation applications unnecessarily.
|
||||
"""
|
||||
if not self.activated:
|
||||
return
|
||||
|
@ -27,8 +27,8 @@
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
The :mod:`presentationplugin` module provides the ability for OpenLP to display
|
||||
presentations from a variety of document formats.
|
||||
The :mod:`presentationplugin` module provides the ability for OpenLP to display presentations from a variety of document
|
||||
formats.
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
@ -39,8 +39,10 @@ from openlp.core.lib import Plugin, StringContent, build_icon, translate
|
||||
from openlp.core.utils import AppLocation
|
||||
from openlp.plugins.presentations.lib import PresentationController, PresentationMediaItem, PresentationTab
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
__default_settings__ = {
|
||||
u'presentations/override app': QtCore.Qt.Unchecked,
|
||||
u'presentations/Impress': QtCore.Qt.Checked,
|
||||
@ -52,9 +54,8 @@ __default_settings__ = {
|
||||
|
||||
class PresentationPlugin(Plugin):
|
||||
"""
|
||||
This plugin allowed a Presentation to be opened, controlled and displayed
|
||||
on the output display. The plugin controls third party applications such
|
||||
as OpenOffice.org Impress, Microsoft PowerPoint and the PowerPoint viewer
|
||||
This plugin allowed a Presentation to be opened, controlled and displayed on the output display. The plugin controls
|
||||
third party applications such as OpenOffice.org Impress, Microsoft PowerPoint and the PowerPoint viewer.
|
||||
"""
|
||||
log = logging.getLogger(u'PresentationPlugin')
|
||||
|
||||
@ -69,18 +70,16 @@ class PresentationPlugin(Plugin):
|
||||
self.icon_path = u':/plugins/plugin_presentations.png'
|
||||
self.icon = build_icon(self.icon_path)
|
||||
|
||||
def create_settings_Tab(self, parent):
|
||||
def create_settings_tab(self, parent):
|
||||
"""
|
||||
Create the settings Tab
|
||||
Create the settings Tab.
|
||||
"""
|
||||
visible_name = self.get_string(StringContent.VisibleName)
|
||||
self.settings_tab = PresentationTab(parent, self.name, visible_name[u'title'], self.controllers,
|
||||
self.icon_path)
|
||||
self.settings_tab = PresentationTab(parent, self.name, visible_name[u'title'], self.controllers, self.icon_path)
|
||||
|
||||
def initialise(self):
|
||||
"""
|
||||
Initialise the plugin. Determine which controllers are enabled
|
||||
are start their processes.
|
||||
Initialise the plugin. Determine which controllers are enabled are start their processes.
|
||||
"""
|
||||
log.info(u'Presentations Initialising')
|
||||
Plugin.initialise(self)
|
||||
@ -95,8 +94,8 @@ class PresentationPlugin(Plugin):
|
||||
|
||||
def finalise(self):
|
||||
"""
|
||||
Finalise the plugin. Ask all the enabled presentation applications
|
||||
to close down their applications and release resources.
|
||||
Finalise the plugin. Ask all the enabled presentation applications to close down their applications and release
|
||||
resources.
|
||||
"""
|
||||
log.info(u'Plugin Finalise')
|
||||
# Ask each controller to tidy up.
|
||||
@ -108,26 +107,23 @@ class PresentationPlugin(Plugin):
|
||||
|
||||
def create_media_manager_item(self):
|
||||
"""
|
||||
Create the Media Manager List
|
||||
Create the Media Manager List.
|
||||
"""
|
||||
self.media_item = PresentationMediaItem(
|
||||
self.main_window.media_dock_manager.media_dock, self, self.icon, self.controllers)
|
||||
|
||||
def register_controllers(self, controller):
|
||||
"""
|
||||
Register each presentation controller (Impress, PPT etc) and store for later use
|
||||
Register each presentation controller (Impress, PPT etc) and store for later use.
|
||||
"""
|
||||
self.controllers[controller.name] = controller
|
||||
|
||||
def check_pre_conditions(self):
|
||||
"""
|
||||
Check to see if we have any presentation software available
|
||||
If Not do not install the plugin.
|
||||
Check to see if we have any presentation software available. If not do not install the plugin.
|
||||
"""
|
||||
log.debug(u'check_pre_conditions')
|
||||
controller_dir = os.path.join(
|
||||
AppLocation.get_directory(AppLocation.PluginsDir),
|
||||
u'presentations', u'lib')
|
||||
controller_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'presentations', u'lib')
|
||||
for filename in os.listdir(controller_dir):
|
||||
if filename.endswith(u'controller.py') and not filename == 'presentationcontroller.py':
|
||||
path = os.path.join(controller_dir, filename)
|
||||
@ -146,7 +142,7 @@ class PresentationPlugin(Plugin):
|
||||
|
||||
def about(self):
|
||||
"""
|
||||
Return information about this plugin
|
||||
Return information about this plugin.
|
||||
"""
|
||||
about_text = translate('PresentationPlugin', '<strong>Presentation '
|
||||
'Plugin</strong><br />The presentation plugin provides the '
|
||||
@ -157,7 +153,7 @@ class PresentationPlugin(Plugin):
|
||||
|
||||
def set_plugin_text_strings(self):
|
||||
"""
|
||||
Called to define all translatable texts of the plugin
|
||||
Called to define all translatable texts of the plugin.
|
||||
"""
|
||||
## Name PluginList ##
|
||||
self.text_strings[StringContent.Name] = {
|
||||
|
@ -147,7 +147,7 @@ window.OpenLP = {
|
||||
},
|
||||
pollServer: function () {
|
||||
$.getJSON(
|
||||
"/api/poll",
|
||||
"/stage/api/poll",
|
||||
function (data, status) {
|
||||
var prevItem = OpenLP.currentItem;
|
||||
OpenLP.currentSlide = data.results.slide;
|
||||
|
@ -26,7 +26,7 @@
|
||||
window.OpenLP = {
|
||||
loadService: function (event) {
|
||||
$.getJSON(
|
||||
"/api/service/list",
|
||||
"/stage/api/service/list",
|
||||
function (data, status) {
|
||||
OpenLP.nextSong = "";
|
||||
$("#notes").html("");
|
||||
@ -46,7 +46,7 @@ window.OpenLP = {
|
||||
},
|
||||
loadSlides: function (event) {
|
||||
$.getJSON(
|
||||
"/api/controller/live/text",
|
||||
"/stage/api/controller/live/text",
|
||||
function (data, status) {
|
||||
OpenLP.currentSlides = data.results.slides;
|
||||
OpenLP.currentSlide = 0;
|
||||
@ -137,7 +137,7 @@ window.OpenLP = {
|
||||
},
|
||||
pollServer: function () {
|
||||
$.getJSON(
|
||||
"/api/poll",
|
||||
"/stage/api/poll",
|
||||
function (data, status) {
|
||||
OpenLP.updateClock(data);
|
||||
if (OpenLP.currentItem != data.results.item ||
|
||||
|
@ -43,7 +43,7 @@ the remotes.
|
||||
``/files/{filename}``
|
||||
Serve a static file.
|
||||
|
||||
``/api/poll``
|
||||
``/stage/api/poll``
|
||||
Poll to see if there are any changes. Returns a JSON-encoded dict of
|
||||
any changes that occurred::
|
||||
|
||||
@ -119,122 +119,198 @@ import os
|
||||
import re
|
||||
import urllib
|
||||
import urlparse
|
||||
import cherrypy
|
||||
|
||||
from PyQt4 import QtCore, QtNetwork
|
||||
from mako.template import Template
|
||||
from PyQt4 import QtCore
|
||||
|
||||
from openlp.core.lib import Registry, Settings, PluginStatus, StringContent
|
||||
|
||||
from openlp.core.utils import AppLocation, translate
|
||||
|
||||
from cherrypy._cpcompat import sha, ntob
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HttpResponse(object):
|
||||
def make_sha_hash(password):
|
||||
"""
|
||||
A simple object to encapsulate a pseudo-http response.
|
||||
Create an encrypted password for the given password.
|
||||
"""
|
||||
code = '200 OK'
|
||||
content = ''
|
||||
headers = {
|
||||
'Content-Type': 'text/html; charset="utf-8"\r\n'
|
||||
}
|
||||
return sha(ntob(password)).hexdigest()
|
||||
|
||||
def __init__(self, content='', headers=None, code=None):
|
||||
if headers is None:
|
||||
headers = {}
|
||||
self.content = content
|
||||
for key, value in headers.iteritems():
|
||||
self.headers[key] = value
|
||||
if code:
|
||||
self.code = code
|
||||
|
||||
def fetch_password(username):
|
||||
"""
|
||||
Fetch the password for a provided user.
|
||||
"""
|
||||
if username != Settings().value(u'remotes/user id'):
|
||||
return None
|
||||
return make_sha_hash(Settings().value(u'remotes/password'))
|
||||
|
||||
|
||||
class HttpServer(object):
|
||||
"""
|
||||
Ability to control OpenLP via a web browser.
|
||||
This class controls the Cherrypy server and configuration.
|
||||
"""
|
||||
def __init__(self, plugin):
|
||||
_cp_config = {
|
||||
'tools.sessions.on': True,
|
||||
'tools.auth.on': True
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialise the httpserver, and start the server.
|
||||
Initialise the http server, and start the server.
|
||||
"""
|
||||
log.debug(u'Initialise httpserver')
|
||||
self.plugin = plugin
|
||||
self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html')
|
||||
self.connections = []
|
||||
self.start_tcp()
|
||||
self.settings_section = u'remotes'
|
||||
self.router = HttpRouter()
|
||||
|
||||
def start_tcp(self):
|
||||
def start_server(self):
|
||||
"""
|
||||
Start the http server, use the port in the settings default to 4316.
|
||||
Listen out for slide and song changes so they can be broadcast to
|
||||
clients. Listen out for socket connections.
|
||||
Start the http server based on configuration.
|
||||
"""
|
||||
log.debug(u'Start TCP server')
|
||||
port = Settings().value(self.plugin.settings_section + u'/port')
|
||||
address = Settings().value(self.plugin.settings_section + u'/ip address')
|
||||
self.server = QtNetwork.QTcpServer()
|
||||
self.server.listen(QtNetwork.QHostAddress(address), port)
|
||||
self.server.newConnection.connect(self.new_connection)
|
||||
log.debug(u'TCP listening on port %d' % port)
|
||||
log.debug(u'Start CherryPy server')
|
||||
# Define to security levels and inject the router code
|
||||
self.root = self.Public()
|
||||
self.root.files = self.Files()
|
||||
self.root.stage = self.Stage()
|
||||
self.root.router = self.router
|
||||
self.root.files.router = self.router
|
||||
self.root.stage.router = self.router
|
||||
cherrypy.tree.mount(self.root, '/', config=self.define_config())
|
||||
# Turn off the flood of access messages cause by poll
|
||||
cherrypy.log.access_log.propagate = False
|
||||
cherrypy.engine.start()
|
||||
|
||||
def new_connection(self):
|
||||
def define_config(self):
|
||||
"""
|
||||
A new http connection has been made. Create a client object to handle
|
||||
communication.
|
||||
Define the configuration of the server.
|
||||
"""
|
||||
log.debug(u'new http connection')
|
||||
socket = self.server.nextPendingConnection()
|
||||
if socket:
|
||||
self.connections.append(HttpConnection(self, socket))
|
||||
if Settings().value(self.settings_section + u'/https enabled'):
|
||||
port = Settings().value(self.settings_section + u'/https port')
|
||||
address = Settings().value(self.settings_section + u'/ip address')
|
||||
local_data = AppLocation.get_directory(AppLocation.DataDir)
|
||||
cherrypy.config.update({u'server.socket_host': str(address),
|
||||
u'server.socket_port': port,
|
||||
u'server.ssl_certificate': os.path.join(local_data, u'remotes', u'openlp.crt'),
|
||||
u'server.ssl_private_key': os.path.join(local_data, u'remotes', u'openlp.key')})
|
||||
else:
|
||||
port = Settings().value(self.settings_section + u'/port')
|
||||
address = Settings().value(self.settings_section + u'/ip address')
|
||||
cherrypy.config.update({u'server.socket_host': str(address)})
|
||||
cherrypy.config.update({u'server.socket_port': port})
|
||||
cherrypy.config.update({u'environment': u'embedded'})
|
||||
cherrypy.config.update({u'engine.autoreload_on': False})
|
||||
directory_config = {u'/': {u'tools.staticdir.on': True,
|
||||
u'tools.staticdir.dir': self.router.html_dir,
|
||||
u'tools.basic_auth.on': Settings().value(u'remotes/authentication enabled'),
|
||||
u'tools.basic_auth.realm': u'OpenLP Remote Login',
|
||||
u'tools.basic_auth.users': fetch_password,
|
||||
u'tools.basic_auth.encrypt': make_sha_hash},
|
||||
u'/files': {u'tools.staticdir.on': True,
|
||||
u'tools.staticdir.dir': self.router.html_dir,
|
||||
u'tools.basic_auth.on': False},
|
||||
u'/stage': {u'tools.staticdir.on': True,
|
||||
u'tools.staticdir.dir': self.router.html_dir,
|
||||
u'tools.basic_auth.on': False}}
|
||||
return directory_config
|
||||
|
||||
def close_connection(self, connection):
|
||||
class Public(object):
|
||||
"""
|
||||
The connection has been closed. Clean up
|
||||
Main access class with may have security enabled on it.
|
||||
"""
|
||||
log.debug(u'close http connection')
|
||||
if connection in self.connections:
|
||||
self.connections.remove(connection)
|
||||
@cherrypy.expose
|
||||
def default(self, *args, **kwargs):
|
||||
self.router.request_data = None
|
||||
if isinstance(kwargs, dict):
|
||||
self.router.request_data = kwargs.get(u'data', None)
|
||||
url = urlparse.urlparse(cherrypy.url())
|
||||
return self.router.process_http_request(url.path, *args)
|
||||
|
||||
class Files(object):
|
||||
"""
|
||||
Provides access to files and has no security available. These are read only accesses
|
||||
"""
|
||||
@cherrypy.expose
|
||||
def default(self, *args, **kwargs):
|
||||
url = urlparse.urlparse(cherrypy.url())
|
||||
return self.router.process_http_request(url.path, *args)
|
||||
|
||||
class Stage(object):
|
||||
"""
|
||||
Stageview is read only so security is not relevant and would reduce it's usability
|
||||
"""
|
||||
@cherrypy.expose
|
||||
def default(self, *args, **kwargs):
|
||||
url = urlparse.urlparse(cherrypy.url())
|
||||
return self.router.process_http_request(url.path, *args)
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close down the http server.
|
||||
"""
|
||||
log.debug(u'close http server')
|
||||
self.server.close()
|
||||
cherrypy.engine.exit()
|
||||
|
||||
|
||||
class HttpConnection(object):
|
||||
class HttpRouter(object):
|
||||
"""
|
||||
A single connection, this handles communication between the server
|
||||
and the client.
|
||||
This code is called by the HttpServer upon a request and it processes it based on the routing table.
|
||||
"""
|
||||
def __init__(self, parent, socket):
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialise the http connection. Listen out for socket signals.
|
||||
Initialise the router
|
||||
"""
|
||||
log.debug(u'Initialise HttpConnection: %s' % socket.peerAddress())
|
||||
self.socket = socket
|
||||
self.parent = parent
|
||||
self.routes = [
|
||||
(u'^/$', self.serve_file),
|
||||
(u'^/(stage)$', self.serve_file),
|
||||
(r'^/files/(.*)$', self.serve_file),
|
||||
(r'^/api/poll$', self.poll),
|
||||
(r'^/stage/api/poll$', self.poll),
|
||||
(r'^/api/controller/(live|preview)/(.*)$', self.controller),
|
||||
(r'^/stage/api/controller/(live|preview)/(.*)$', self.controller),
|
||||
(r'^/api/service/(.*)$', self.service),
|
||||
(r'^/stage/api/service/(.*)$', self.service),
|
||||
(r'^/api/display/(hide|show|blank|theme|desktop)$', self.display),
|
||||
(r'^/api/alert$', self.alert),
|
||||
(r'^/api/plugin/(search)$', self.pluginInfo),
|
||||
(r'^/api/plugin/(search)$', self.plugin_info),
|
||||
(r'^/api/(.*)/search$', self.search),
|
||||
(r'^/api/(.*)/live$', self.go_live),
|
||||
(r'^/api/(.*)/add$', self.add_to_service)
|
||||
]
|
||||
self.socket.readyRead.connect(self.ready_read)
|
||||
self.socket.disconnected.connect(self.disconnected)
|
||||
self.translate()
|
||||
self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html')
|
||||
|
||||
def process_http_request(self, url_path, *args):
|
||||
"""
|
||||
Common function to process HTTP requests
|
||||
|
||||
``url_path``
|
||||
The requested URL.
|
||||
|
||||
``*args``
|
||||
Any passed data.
|
||||
"""
|
||||
response = None
|
||||
for route, func in self.routes:
|
||||
match = re.match(route, url_path)
|
||||
if match:
|
||||
log.debug('Route "%s" matched "%s"', route, url_path)
|
||||
args = []
|
||||
for param in match.groups():
|
||||
args.append(param)
|
||||
response = func(*args)
|
||||
break
|
||||
if response:
|
||||
return response
|
||||
else:
|
||||
return self._http_not_found()
|
||||
|
||||
def _get_service_items(self):
|
||||
"""
|
||||
Read the service item in use and return the data as a json object
|
||||
"""
|
||||
service_items = []
|
||||
if self.live_controller.service_item:
|
||||
current_unique_identifier = self.live_controller.service_item.unique_identifier
|
||||
@ -281,40 +357,6 @@ class HttpConnection(object):
|
||||
'slides': translate('RemotePlugin.Mobile', 'Slides')
|
||||
}
|
||||
|
||||
def ready_read(self):
|
||||
"""
|
||||
Data has been sent from the client. Respond to it
|
||||
"""
|
||||
log.debug(u'ready to read socket')
|
||||
if self.socket.canReadLine():
|
||||
data = str(self.socket.readLine())
|
||||
try:
|
||||
log.debug(u'received: ' + data)
|
||||
except UnicodeDecodeError:
|
||||
# Malicious request containing non-ASCII characters.
|
||||
self.close()
|
||||
return
|
||||
words = data.split(' ')
|
||||
response = None
|
||||
if words[0] == u'GET':
|
||||
url = urlparse.urlparse(words[1])
|
||||
self.url_params = urlparse.parse_qs(url.query)
|
||||
# Loop through the routes we set up earlier and execute them
|
||||
for route, func in self.routes:
|
||||
match = re.match(route, url.path)
|
||||
if match:
|
||||
log.debug('Route "%s" matched "%s"', route, url.path)
|
||||
args = []
|
||||
for param in match.groups():
|
||||
args.append(param)
|
||||
response = func(*args)
|
||||
break
|
||||
if response:
|
||||
self.send_response(response)
|
||||
else:
|
||||
self.send_response(HttpResponse(code='404 Not Found'))
|
||||
self.close()
|
||||
|
||||
def serve_file(self, filename=None):
|
||||
"""
|
||||
Send a file to the socket. For now, just a subset of file types
|
||||
@ -329,9 +371,9 @@ class HttpConnection(object):
|
||||
filename = u'index.html'
|
||||
elif filename == u'stage':
|
||||
filename = u'stage.html'
|
||||
path = os.path.normpath(os.path.join(self.parent.html_dir, filename))
|
||||
if not path.startswith(self.parent.html_dir):
|
||||
return HttpResponse(code=u'404 Not Found')
|
||||
path = os.path.normpath(os.path.join(self.html_dir, filename))
|
||||
if not path.startswith(self.html_dir):
|
||||
return self._http_not_found()
|
||||
ext = os.path.splitext(filename)[1]
|
||||
html = None
|
||||
if ext == u'.html':
|
||||
@ -360,11 +402,12 @@ class HttpConnection(object):
|
||||
content = file_handle.read()
|
||||
except IOError:
|
||||
log.exception(u'Failed to open %s' % path)
|
||||
return HttpResponse(code=u'404 Not Found')
|
||||
return self._http_not_found()
|
||||
finally:
|
||||
if file_handle:
|
||||
file_handle.close()
|
||||
return HttpResponse(content, {u'Content-Type': mimetype})
|
||||
cherrypy.response.headers['Content-Type'] = mimetype
|
||||
return content
|
||||
|
||||
def poll(self):
|
||||
"""
|
||||
@ -379,18 +422,20 @@ class HttpConnection(object):
|
||||
u'theme': self.live_controller.theme_screen.isChecked(),
|
||||
u'display': self.live_controller.desktop_screen.isChecked()
|
||||
}
|
||||
return HttpResponse(json.dumps({u'results': result}), {u'Content-Type': u'application/json'})
|
||||
cherrypy.response.headers['Content-Type'] = u'application/json'
|
||||
return json.dumps({u'results': result})
|
||||
|
||||
def display(self, action):
|
||||
"""
|
||||
Hide or show the display screen.
|
||||
This is a cross Thread call and UI is updated so Events need to be used.
|
||||
|
||||
``action``
|
||||
This is the action, either ``hide`` or ``show``.
|
||||
"""
|
||||
Registry().execute(u'slidecontroller_toggle_display', action)
|
||||
return HttpResponse(json.dumps({u'results': {u'success': True}}),
|
||||
{u'Content-Type': u'application/json'})
|
||||
self.live_controller.emit(QtCore.SIGNAL(u'slidecontroller_toggle_display'), action)
|
||||
cherrypy.response.headers['Content-Type'] = u'application/json'
|
||||
return json.dumps({u'results': {u'success': True}})
|
||||
|
||||
def alert(self):
|
||||
"""
|
||||
@ -399,16 +444,16 @@ class HttpConnection(object):
|
||||
plugin = self.plugin_manager.get_plugin_by_name("alerts")
|
||||
if plugin.status == PluginStatus.Active:
|
||||
try:
|
||||
text = json.loads(self.url_params[u'data'][0])[u'request'][u'text']
|
||||
text = json.loads(self.request_data)[u'request'][u'text']
|
||||
except KeyError, ValueError:
|
||||
return HttpResponse(code=u'400 Bad Request')
|
||||
return self._http_bad_request()
|
||||
text = urllib.unquote(text)
|
||||
Registry().execute(u'alerts_text', [text])
|
||||
self.alerts_manager.emit(QtCore.SIGNAL(u'alerts_text'), [text])
|
||||
success = True
|
||||
else:
|
||||
success = False
|
||||
return HttpResponse(json.dumps({u'results': {u'success': success}}),
|
||||
{u'Content-Type': u'application/json'})
|
||||
cherrypy.response.headers['Content-Type'] = u'application/json'
|
||||
return json.dumps({u'results': {u'success': success}})
|
||||
|
||||
def controller(self, display_type, action):
|
||||
"""
|
||||
@ -444,44 +489,44 @@ class HttpConnection(object):
|
||||
if current_item:
|
||||
json_data[u'results'][u'item'] = self.live_controller.service_item.unique_identifier
|
||||
else:
|
||||
if self.url_params and self.url_params.get(u'data'):
|
||||
if self.request_data:
|
||||
try:
|
||||
data = json.loads(self.url_params[u'data'][0])
|
||||
data = json.loads(self.request_data)[u'request'][u'id']
|
||||
except KeyError, ValueError:
|
||||
return HttpResponse(code=u'400 Bad Request')
|
||||
return self._http_bad_request()
|
||||
log.info(data)
|
||||
# This slot expects an int within a list.
|
||||
id = data[u'request'][u'id']
|
||||
Registry().execute(event, [id])
|
||||
self.live_controller.emit(QtCore.SIGNAL(event), [data])
|
||||
else:
|
||||
Registry().execute(event)
|
||||
self.live_controller.emit(QtCore.SIGNAL(event))
|
||||
json_data = {u'results': {u'success': True}}
|
||||
return HttpResponse(json.dumps(json_data), {u'Content-Type': u'application/json'})
|
||||
cherrypy.response.headers['Content-Type'] = u'application/json'
|
||||
return json.dumps(json_data)
|
||||
|
||||
def service(self, action):
|
||||
"""
|
||||
Handles requests for service items
|
||||
Handles requests for service items in the service manager
|
||||
|
||||
``action``
|
||||
The action to perform.
|
||||
"""
|
||||
event = u'servicemanager_%s' % action
|
||||
if action == u'list':
|
||||
return HttpResponse(json.dumps({u'results': {u'items': self._get_service_items()}}),
|
||||
{u'Content-Type': u'application/json'})
|
||||
else:
|
||||
cherrypy.response.headers['Content-Type'] = u'application/json'
|
||||
return json.dumps({u'results': {u'items': self._get_service_items()}})
|
||||
event += u'_item'
|
||||
if self.url_params and self.url_params.get(u'data'):
|
||||
if self.request_data:
|
||||
try:
|
||||
data = json.loads(self.url_params[u'data'][0])
|
||||
except KeyError, ValueError:
|
||||
return HttpResponse(code=u'400 Bad Request')
|
||||
Registry().execute(event, data[u'request'][u'id'])
|
||||
data = json.loads(self.request_data)[u'request'][u'id']
|
||||
except KeyError:
|
||||
return self._http_bad_request()
|
||||
self.service_manager.emit(QtCore.SIGNAL(event), data)
|
||||
else:
|
||||
Registry().execute(event)
|
||||
return HttpResponse(json.dumps({u'results': {u'success': True}}), {u'Content-Type': u'application/json'})
|
||||
cherrypy.response.headers['Content-Type'] = u'application/json'
|
||||
return json.dumps({u'results': {u'success': True}})
|
||||
|
||||
def pluginInfo(self, action):
|
||||
def plugin_info(self, action):
|
||||
"""
|
||||
Return plugin related information, based on the action.
|
||||
|
||||
@ -493,8 +538,9 @@ class HttpConnection(object):
|
||||
searches = []
|
||||
for plugin in self.plugin_manager.plugins:
|
||||
if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search:
|
||||
searches.append([plugin.name, unicode(plugin.textStrings[StringContent.Name][u'plural'])])
|
||||
return HttpResponse(json.dumps({u'results': {u'items': searches}}), {u'Content-Type': u'application/json'})
|
||||
searches.append([plugin.name, unicode(plugin.text_strings[StringContent.Name][u'plural'])])
|
||||
cherrypy.response.headers['Content-Type'] = u'application/json'
|
||||
return json.dumps({u'results': {u'items': searches}})
|
||||
|
||||
def search(self, plugin_name):
|
||||
"""
|
||||
@ -504,69 +550,63 @@ class HttpConnection(object):
|
||||
The plugin name to search in.
|
||||
"""
|
||||
try:
|
||||
text = json.loads(self.url_params[u'data'][0])[u'request'][u'text']
|
||||
text = json.loads(self.request_data)[u'request'][u'text']
|
||||
except KeyError, ValueError:
|
||||
return HttpResponse(code=u'400 Bad Request')
|
||||
return self._http_bad_request()
|
||||
text = urllib.unquote(text)
|
||||
plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
|
||||
if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search:
|
||||
results = plugin.media_item.search(text, False)
|
||||
else:
|
||||
results = []
|
||||
return HttpResponse(json.dumps({u'results': {u'items': results}}), {u'Content-Type': u'application/json'})
|
||||
cherrypy.response.headers['Content-Type'] = u'application/json'
|
||||
return json.dumps({u'results': {u'items': results}})
|
||||
|
||||
def go_live(self, plugin_name):
|
||||
"""
|
||||
Go live on an item of type ``plugin``.
|
||||
"""
|
||||
try:
|
||||
id = json.loads(self.url_params[u'data'][0])[u'request'][u'id']
|
||||
id = json.loads(self.request_data)[u'request'][u'id']
|
||||
except KeyError, ValueError:
|
||||
return HttpResponse(code=u'400 Bad Request')
|
||||
return self._http_bad_request()
|
||||
plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
|
||||
if plugin.status == PluginStatus.Active and plugin.media_item:
|
||||
plugin.media_item.go_live(id, remote=True)
|
||||
return HttpResponse(code=u'200 OK')
|
||||
plugin.media_item.emit(QtCore.SIGNAL(u'%s_go_live' % plugin_name), [id, True])
|
||||
return self._http_success()
|
||||
|
||||
def add_to_service(self, plugin_name):
|
||||
"""
|
||||
Add item of type ``plugin_name`` to the end of the service.
|
||||
"""
|
||||
try:
|
||||
id = json.loads(self.url_params[u'data'][0])[u'request'][u'id']
|
||||
id = json.loads(self.request_data)[u'request'][u'id']
|
||||
except KeyError, ValueError:
|
||||
return HttpResponse(code=u'400 Bad Request')
|
||||
return self._http_bad_request()
|
||||
plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
|
||||
if plugin.status == PluginStatus.Active and plugin.media_item:
|
||||
item_id = plugin.media_item.createItemFromId(id)
|
||||
plugin.media_item.add_to_service(item_id, remote=True)
|
||||
return HttpResponse(code=u'200 OK')
|
||||
item_id = plugin.media_item.create_item_from_id(id)
|
||||
plugin.media_item.emit(QtCore.SIGNAL(u'%s_add_to_service' % plugin_name), [item_id, True])
|
||||
self._http_success()
|
||||
|
||||
def send_response(self, response):
|
||||
http = u'HTTP/1.1 %s\r\n' % response.code
|
||||
for header, value in response.headers.iteritems():
|
||||
http += '%s: %s\r\n' % (header, value)
|
||||
http += '\r\n'
|
||||
self.socket.write(http)
|
||||
self.socket.write(response.content)
|
||||
def _http_success(self):
|
||||
"""
|
||||
Set the HTTP success return code.
|
||||
"""
|
||||
cherrypy.response.status = 200
|
||||
|
||||
def disconnected(self):
|
||||
def _http_bad_request(self):
|
||||
"""
|
||||
The client has disconnected. Tidy up
|
||||
Set the HTTP bad response return code.
|
||||
"""
|
||||
log.debug(u'socket disconnected')
|
||||
self.close()
|
||||
cherrypy.response.status = 400
|
||||
|
||||
def close(self):
|
||||
def _http_not_found(self):
|
||||
"""
|
||||
The server has closed the connection. Tidy up
|
||||
Set the HTTP not found return code.
|
||||
"""
|
||||
if not self.socket:
|
||||
return
|
||||
log.debug(u'close socket')
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
self.parent.close_connection(self)
|
||||
cherrypy.response.status = 404
|
||||
cherrypy.response.body = ["<html><body>Sorry, an error occurred </body></html>"]
|
||||
|
||||
def _get_service_manager(self):
|
||||
"""
|
||||
@ -597,3 +637,13 @@ class HttpConnection(object):
|
||||
return self._plugin_manager
|
||||
|
||||
plugin_manager = property(_get_plugin_manager)
|
||||
|
||||
def _get_alerts_manager(self):
|
||||
"""
|
||||
Adds the alerts manager to the class dynamically
|
||||
"""
|
||||
if not hasattr(self, u'_alerts_manager'):
|
||||
self._alerts_manager = Registry().get(u'alerts_manager')
|
||||
return self._alerts_manager
|
||||
|
||||
alerts_manager = property(_get_alerts_manager)
|
||||
|
@ -27,9 +27,12 @@
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
import os.path
|
||||
|
||||
from PyQt4 import QtCore, QtGui, QtNetwork
|
||||
|
||||
from openlp.core.lib import Registry, Settings, SettingsTab, translate
|
||||
from openlp.core.lib import Settings, SettingsTab, translate
|
||||
from openlp.core.utils import AppLocation
|
||||
|
||||
|
||||
ZERO_URL = u'0.0.0.0'
|
||||
@ -53,32 +56,84 @@ class RemoteTab(SettingsTab):
|
||||
self.address_label.setObjectName(u'address_label')
|
||||
self.address_edit = QtGui.QLineEdit(self.server_settings_group_box)
|
||||
self.address_edit.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed)
|
||||
self.address_edit.setValidator(QtGui.QRegExpValidator(QtCore.QRegExp(
|
||||
u'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'), self))
|
||||
self.address_edit.setValidator(QtGui.QRegExpValidator(QtCore.QRegExp(u'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'),
|
||||
self))
|
||||
self.address_edit.setObjectName(u'address_edit')
|
||||
self.server_settings_layout.addRow(self.address_label, self.address_edit)
|
||||
self.twelve_hour_check_box = QtGui.QCheckBox(self.server_settings_group_box)
|
||||
self.twelve_hour_check_box.setObjectName(u'twelve_hour_check_box')
|
||||
self.server_settings_layout.addRow(self.twelve_hour_check_box)
|
||||
self.port_label = QtGui.QLabel(self.server_settings_group_box)
|
||||
self.left_layout.addWidget(self.server_settings_group_box)
|
||||
self.http_settings_group_box = QtGui.QGroupBox(self.left_column)
|
||||
self.http_settings_group_box.setObjectName(u'http_settings_group_box')
|
||||
self.http_setting_layout = QtGui.QFormLayout(self.http_settings_group_box)
|
||||
self.http_setting_layout.setObjectName(u'http_setting_layout')
|
||||
self.port_label = QtGui.QLabel(self.http_settings_group_box)
|
||||
self.port_label.setObjectName(u'port_label')
|
||||
self.port_spin_box = QtGui.QSpinBox(self.server_settings_group_box)
|
||||
self.port_spin_box = QtGui.QSpinBox(self.http_settings_group_box)
|
||||
self.port_spin_box.setMaximum(32767)
|
||||
self.port_spin_box.setObjectName(u'port_spin_box')
|
||||
self.server_settings_layout.addRow(self.port_label, self.port_spin_box)
|
||||
self.remote_url_label = QtGui.QLabel(self.server_settings_group_box)
|
||||
self.http_setting_layout.addRow(self.port_label, self.port_spin_box)
|
||||
self.remote_url_label = QtGui.QLabel(self.http_settings_group_box)
|
||||
self.remote_url_label.setObjectName(u'remote_url_label')
|
||||
self.remote_url = QtGui.QLabel(self.server_settings_group_box)
|
||||
self.remote_url = QtGui.QLabel(self.http_settings_group_box)
|
||||
self.remote_url.setObjectName(u'remote_url')
|
||||
self.remote_url.setOpenExternalLinks(True)
|
||||
self.server_settings_layout.addRow(self.remote_url_label, self.remote_url)
|
||||
self.stage_url_label = QtGui.QLabel(self.server_settings_group_box)
|
||||
self.http_setting_layout.addRow(self.remote_url_label, self.remote_url)
|
||||
self.stage_url_label = QtGui.QLabel(self.http_settings_group_box)
|
||||
self.stage_url_label.setObjectName(u'stage_url_label')
|
||||
self.stage_url = QtGui.QLabel(self.server_settings_group_box)
|
||||
self.stage_url = QtGui.QLabel(self.http_settings_group_box)
|
||||
self.stage_url.setObjectName(u'stage_url')
|
||||
self.stage_url.setOpenExternalLinks(True)
|
||||
self.server_settings_layout.addRow(self.stage_url_label, self.stage_url)
|
||||
self.left_layout.addWidget(self.server_settings_group_box)
|
||||
self.http_setting_layout.addRow(self.stage_url_label, self.stage_url)
|
||||
self.left_layout.addWidget(self.http_settings_group_box)
|
||||
self.https_settings_group_box = QtGui.QGroupBox(self.left_column)
|
||||
self.https_settings_group_box.setCheckable(True)
|
||||
self.https_settings_group_box.setChecked(False)
|
||||
self.https_settings_group_box.setObjectName(u'https_settings_group_box')
|
||||
self.https_settings_layout = QtGui.QFormLayout(self.https_settings_group_box)
|
||||
self.https_settings_layout.setObjectName(u'https_settings_layout')
|
||||
self.https_error_label = QtGui.QLabel(self.https_settings_group_box)
|
||||
self.https_error_label.setVisible(False)
|
||||
self.https_error_label.setWordWrap(True)
|
||||
self.https_error_label.setObjectName(u'https_error_label')
|
||||
self.https_settings_layout.addRow(self.https_error_label)
|
||||
self.https_port_label = QtGui.QLabel(self.https_settings_group_box)
|
||||
self.https_port_label.setObjectName(u'https_port_label')
|
||||
self.https_port_spin_box = QtGui.QSpinBox(self.https_settings_group_box)
|
||||
self.https_port_spin_box.setMaximum(32767)
|
||||
self.https_port_spin_box.setObjectName(u'https_port_spin_box')
|
||||
self.https_settings_layout.addRow(self.https_port_label, self.https_port_spin_box)
|
||||
self.remote_https_url = QtGui.QLabel(self.https_settings_group_box)
|
||||
self.remote_https_url.setObjectName(u'remote_http_url')
|
||||
self.remote_https_url.setOpenExternalLinks(True)
|
||||
self.remote_https_url_label = QtGui.QLabel(self.https_settings_group_box)
|
||||
self.remote_https_url_label.setObjectName(u'remote_http_url_label')
|
||||
self.https_settings_layout.addRow(self.remote_https_url_label, self.remote_https_url)
|
||||
self.stage_https_url_label = QtGui.QLabel(self.http_settings_group_box)
|
||||
self.stage_https_url_label.setObjectName(u'stage_https_url_label')
|
||||
self.stage_https_url = QtGui.QLabel(self.https_settings_group_box)
|
||||
self.stage_https_url.setObjectName(u'stage_https_url')
|
||||
self.stage_https_url.setOpenExternalLinks(True)
|
||||
self.https_settings_layout.addRow(self.stage_https_url_label, self.stage_https_url)
|
||||
self.left_layout.addWidget(self.https_settings_group_box)
|
||||
self.user_login_group_box = QtGui.QGroupBox(self.left_column)
|
||||
self.user_login_group_box.setCheckable(True)
|
||||
self.user_login_group_box.setChecked(False)
|
||||
self.user_login_group_box.setObjectName(u'user_login_group_box')
|
||||
self.user_login_layout = QtGui.QFormLayout(self.user_login_group_box)
|
||||
self.user_login_layout.setObjectName(u'user_login_layout')
|
||||
self.user_id_label = QtGui.QLabel(self.user_login_group_box)
|
||||
self.user_id_label.setObjectName(u'user_id_label')
|
||||
self.user_id = QtGui.QLineEdit(self.user_login_group_box)
|
||||
self.user_id.setObjectName(u'user_id')
|
||||
self.user_login_layout.addRow(self.user_id_label, self.user_id)
|
||||
self.password_label = QtGui.QLabel(self.user_login_group_box)
|
||||
self.password_label.setObjectName(u'password_label')
|
||||
self.password = QtGui.QLineEdit(self.user_login_group_box)
|
||||
self.password.setObjectName(u'password')
|
||||
self.user_login_layout.addRow(self.password_label, self.password)
|
||||
self.left_layout.addWidget(self.user_login_group_box)
|
||||
self.android_app_group_box = QtGui.QGroupBox(self.right_column)
|
||||
self.android_app_group_box.setObjectName(u'android_app_group_box')
|
||||
self.right_layout.addWidget(self.android_app_group_box)
|
||||
@ -96,9 +151,11 @@ class RemoteTab(SettingsTab):
|
||||
self.qr_layout.addWidget(self.qr_description_label)
|
||||
self.left_layout.addStretch()
|
||||
self.right_layout.addStretch()
|
||||
self.twelve_hour_check_box.stateChanged.connect(self.onTwelveHourCheckBoxChanged)
|
||||
self.twelve_hour_check_box.stateChanged.connect(self.on_twelve_hour_check_box_changed)
|
||||
self.address_edit.textChanged.connect(self.set_urls)
|
||||
self.port_spin_box.valueChanged.connect(self.set_urls)
|
||||
self.https_port_spin_box.valueChanged.connect(self.set_urls)
|
||||
self.https_settings_group_box.clicked.connect(self.https_changed)
|
||||
|
||||
def retranslateUi(self):
|
||||
self.server_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Server Settings'))
|
||||
@ -112,8 +169,21 @@ class RemoteTab(SettingsTab):
|
||||
'Scan the QR code or click <a href="https://play.google.com/store/'
|
||||
'apps/details?id=org.openlp.android">download</a> to install the '
|
||||
'Android app from Google Play.'))
|
||||
self.https_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'HTTPS Server'))
|
||||
self.https_error_label.setText(translate('RemotePlugin.RemoteTab',
|
||||
'Could not find an SSL certificate. The HTTPS server will not be available unless an SSL certificate '
|
||||
'is found. Please see the manual for more information.'))
|
||||
self.https_port_label.setText(self.port_label.text())
|
||||
self.remote_https_url_label.setText(self.remote_url_label.text())
|
||||
self.stage_https_url_label.setText(self.stage_url_label.text())
|
||||
self.user_login_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'User Authentication'))
|
||||
self.user_id_label.setText(translate('RemotePlugin.RemoteTab', 'User id:'))
|
||||
self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:'))
|
||||
|
||||
def set_urls(self):
|
||||
"""
|
||||
Update the display based on the data input on the screen
|
||||
"""
|
||||
ip_address = u'localhost'
|
||||
if self.address_edit.text() == ZERO_URL:
|
||||
interfaces = QtNetwork.QNetworkInterface.allInterfaces()
|
||||
@ -129,31 +199,73 @@ class RemoteTab(SettingsTab):
|
||||
break
|
||||
else:
|
||||
ip_address = self.address_edit.text()
|
||||
url = u'http://%s:%s/' % (ip_address, self.port_spin_box.value())
|
||||
self.remote_url.setText(u'<a href="%s">%s</a>' % (url, url))
|
||||
url += u'stage'
|
||||
self.stage_url.setText(u'<a href="%s">%s</a>' % (url, url))
|
||||
http_url = u'http://%s:%s/' % (ip_address, self.port_spin_box.value())
|
||||
https_url = u'https://%s:%s/' % (ip_address, self.https_port_spin_box.value())
|
||||
self.remote_url.setText(u'<a href="%s">%s</a>' % (http_url, http_url))
|
||||
self.remote_https_url.setText(u'<a href="%s">%s</a>' % (https_url, https_url))
|
||||
http_url += u'stage'
|
||||
https_url += u'stage'
|
||||
self.stage_url.setText(u'<a href="%s">%s</a>' % (http_url, http_url))
|
||||
self.stage_https_url.setText(u'<a href="%s">%s</a>' % (https_url, https_url))
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
Load the configuration and update the server configuration if necessary
|
||||
"""
|
||||
self.port_spin_box.setValue(Settings().value(self.settings_section + u'/port'))
|
||||
self.https_port_spin_box.setValue(Settings().value(self.settings_section + u'/https port'))
|
||||
self.address_edit.setText(Settings().value(self.settings_section + u'/ip address'))
|
||||
self.twelve_hour = Settings().value(self.settings_section + u'/twelve hour')
|
||||
self.twelve_hour_check_box.setChecked(self.twelve_hour)
|
||||
local_data = AppLocation.get_directory(AppLocation.DataDir)
|
||||
if not os.path.exists(os.path.join(local_data, u'remotes', u'openlp.crt')) or \
|
||||
not os.path.exists(os.path.join(local_data, u'remotes', u'openlp.key')):
|
||||
self.https_settings_group_box.setChecked(False)
|
||||
self.https_settings_group_box.setEnabled(False)
|
||||
self.https_error_label.setVisible(True)
|
||||
else:
|
||||
self.https_settings_group_box.setChecked(Settings().value(self.settings_section + u'/https enabled'))
|
||||
self.https_settings_group_box.setEnabled(True)
|
||||
self.https_error_label.setVisible(False)
|
||||
self.user_login_group_box.setChecked(Settings().value(self.settings_section + u'/authentication enabled'))
|
||||
self.user_id.setText(Settings().value(self.settings_section + u'/user id'))
|
||||
self.password.setText(Settings().value(self.settings_section + u'/password'))
|
||||
self.set_urls()
|
||||
self.https_changed()
|
||||
|
||||
def save(self):
|
||||
changed = False
|
||||
"""
|
||||
Save the configuration and update the server configuration if necessary
|
||||
"""
|
||||
if Settings().value(self.settings_section + u'/ip address') != self.address_edit.text() or \
|
||||
Settings().value(self.settings_section + u'/port') != self.port_spin_box.value():
|
||||
changed = True
|
||||
Settings().value(self.settings_section + u'/port') != self.port_spin_box.value() or \
|
||||
Settings().value(self.settings_section + u'/https port') != self.https_port_spin_box.value() or \
|
||||
Settings().value(self.settings_section + u'/https enabled') != \
|
||||
self.https_settings_group_box.isChecked() or \
|
||||
Settings().value(self.settings_section + u'/authentication enabled') != \
|
||||
self.user_login_group_box.isChecked():
|
||||
self.settings_form.register_post_process(u'remotes_config_updated')
|
||||
Settings().setValue(self.settings_section + u'/port', self.port_spin_box.value())
|
||||
Settings().setValue(self.settings_section + u'/https port', self.https_port_spin_box.value())
|
||||
Settings().setValue(self.settings_section + u'/https enabled', self.https_settings_group_box.isChecked())
|
||||
Settings().setValue(self.settings_section + u'/ip address', self.address_edit.text())
|
||||
Settings().setValue(self.settings_section + u'/twelve hour', self.twelve_hour)
|
||||
if changed:
|
||||
Registry().execute(u'remotes_config_updated')
|
||||
Settings().setValue(self.settings_section + u'/authentication enabled', self.user_login_group_box.isChecked())
|
||||
Settings().setValue(self.settings_section + u'/user id', self.user_id.text())
|
||||
Settings().setValue(self.settings_section + u'/password', self.password.text())
|
||||
|
||||
def onTwelveHourCheckBoxChanged(self, check_state):
|
||||
def on_twelve_hour_check_box_changed(self, check_state):
|
||||
"""
|
||||
Toggle the 12 hour check box.
|
||||
"""
|
||||
self.twelve_hour = False
|
||||
# we have a set value convert to True/False
|
||||
if check_state == QtCore.Qt.Checked:
|
||||
self.twelve_hour = True
|
||||
|
||||
def https_changed(self):
|
||||
"""
|
||||
Invert the HTTP group box based on Https group settings
|
||||
"""
|
||||
self.http_settings_group_box.setEnabled(not self.https_settings_group_box.isChecked())
|
||||
|
||||
|
@ -29,6 +29,8 @@
|
||||
|
||||
import logging
|
||||
|
||||
from PyQt4 import QtGui
|
||||
|
||||
from openlp.core.lib import Plugin, StringContent, translate, build_icon
|
||||
from openlp.plugins.remotes.lib import RemoteTab, HttpServer
|
||||
|
||||
@ -37,6 +39,11 @@ log = logging.getLogger(__name__)
|
||||
__default_settings__ = {
|
||||
u'remotes/twelve hour': True,
|
||||
u'remotes/port': 4316,
|
||||
u'remotes/https port': 4317,
|
||||
u'remotes/https enabled': False,
|
||||
u'remotes/user id': u'openlp',
|
||||
u'remotes/password': u'password',
|
||||
u'remotes/authentication enabled': False,
|
||||
u'remotes/ip address': u'0.0.0.0'
|
||||
}
|
||||
|
||||
@ -60,7 +67,8 @@ class RemotesPlugin(Plugin):
|
||||
"""
|
||||
log.debug(u'initialise')
|
||||
Plugin.initialise(self)
|
||||
self.server = HttpServer(self)
|
||||
self.server = HttpServer()
|
||||
self.server.start_server()
|
||||
|
||||
def finalise(self):
|
||||
"""
|
||||
@ -70,6 +78,7 @@ class RemotesPlugin(Plugin):
|
||||
Plugin.finalise(self)
|
||||
if self.server:
|
||||
self.server.close()
|
||||
self.server = None
|
||||
|
||||
def about(self):
|
||||
"""
|
||||
@ -99,5 +108,6 @@ class RemotesPlugin(Plugin):
|
||||
"""
|
||||
Called when Config is changed to restart the server on new address or port
|
||||
"""
|
||||
self.finalise()
|
||||
self.initialise()
|
||||
log.debug(u'remote config changed')
|
||||
self.main_window.information_message(translate('RemotePlugin', 'Configuration Change'),
|
||||
translate('RemotePlugin', 'OpenLP will need to be restarted for the Remote changes to become active.'))
|
||||
|
@ -320,7 +320,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog):
|
||||
for plugin in self.plugin_manager.plugins:
|
||||
if plugin.name == u'media' and plugin.status == PluginStatus.Active:
|
||||
self.from_media_button.setVisible(True)
|
||||
self.media_form.populateFiles(plugin.media_item.getList(MediaType.Audio))
|
||||
self.media_form.populateFiles(plugin.media_item.get_list(MediaType.Audio))
|
||||
break
|
||||
|
||||
def new_song(self):
|
||||
|
@ -37,7 +37,6 @@ from PyQt4 import QtCore, QtGui
|
||||
from openlp.core.lib import Registry, UiStrings, create_separated_list, build_icon, translate
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.core.ui.wizard import OpenLPWizard, WizardStrings
|
||||
from openlp.plugins.songs.lib import natcmp
|
||||
from openlp.plugins.songs.lib.db import Song
|
||||
from openlp.plugins.songs.lib.openlyricsexport import OpenLyricsExport
|
||||
|
||||
@ -222,7 +221,7 @@ class SongExportForm(OpenLPWizard):
|
||||
# Load the list of songs.
|
||||
self.application.set_busy_cursor()
|
||||
songs = self.plugin.manager.get_all_objects(Song)
|
||||
songs.sort(cmp=natcmp, key=lambda song: song.sort_key)
|
||||
songs.sort(key=lambda song: song.sort_key)
|
||||
for song in songs:
|
||||
# No need to export temporary songs.
|
||||
if song.temporary:
|
||||
|
@ -34,7 +34,7 @@ import re
|
||||
from PyQt4 import QtGui
|
||||
|
||||
from openlp.core.lib import translate
|
||||
from openlp.core.utils import CONTROL_CHARS, locale_direct_compare
|
||||
from openlp.core.utils import CONTROL_CHARS
|
||||
from db import Author
|
||||
from ui import SongStrings
|
||||
|
||||
@ -168,6 +168,7 @@ class VerseType(object):
|
||||
translate('SongsPlugin.VerseType', 'Intro'),
|
||||
translate('SongsPlugin.VerseType', 'Ending'),
|
||||
translate('SongsPlugin.VerseType', 'Other')]
|
||||
|
||||
translated_tags = [name[0].lower() for name in translated_names]
|
||||
|
||||
@staticmethod
|
||||
@ -592,37 +593,3 @@ def strip_rtf(text, default_encoding=None):
|
||||
text = u''.join(out)
|
||||
return text, default_encoding
|
||||
|
||||
|
||||
def natcmp(a, b):
|
||||
"""
|
||||
Natural string comparison which mimics the behaviour of Python's internal cmp function.
|
||||
"""
|
||||
if len(a) <= len(b):
|
||||
for i, key in enumerate(a):
|
||||
if isinstance(key, int) and isinstance(b[i], int):
|
||||
result = cmp(key, b[i])
|
||||
elif isinstance(key, int) and not isinstance(b[i], int):
|
||||
result = locale_direct_compare(str(key), b[i])
|
||||
elif not isinstance(key, int) and isinstance(b[i], int):
|
||||
result = locale_direct_compare(key, str(b[i]))
|
||||
else:
|
||||
result = locale_direct_compare(key, b[i])
|
||||
if result != 0:
|
||||
return result
|
||||
if len(a) == len(b):
|
||||
return 0
|
||||
else:
|
||||
return -1
|
||||
else:
|
||||
for i, key in enumerate(b):
|
||||
if isinstance(a[i], int) and isinstance(key, int):
|
||||
result = cmp(a[i], key)
|
||||
elif isinstance(a[i], int) and not isinstance(key, int):
|
||||
result = locale_direct_compare(str(a[i]), key)
|
||||
elif not isinstance(a[i], int) and isinstance(key, int):
|
||||
result = locale_direct_compare(a[i], str(key))
|
||||
else:
|
||||
result = locale_direct_compare(a[i], key)
|
||||
if result != 0:
|
||||
return result
|
||||
return 1
|
||||
|
@ -38,6 +38,7 @@ from sqlalchemy.orm import mapper, relation, reconstructor
|
||||
from sqlalchemy.sql.expression import func
|
||||
|
||||
from openlp.core.lib.db import BaseModel, init_db
|
||||
from openlp.core.utils import get_natural_key
|
||||
|
||||
|
||||
class Author(BaseModel):
|
||||
@ -69,36 +70,15 @@ class Song(BaseModel):
|
||||
def __init__(self):
|
||||
self.sort_key = ()
|
||||
|
||||
def _try_int(self, s):
|
||||
"""
|
||||
Convert to integer if possible.
|
||||
"""
|
||||
try:
|
||||
return int(s)
|
||||
except:
|
||||
return s.lower()
|
||||
|
||||
def _natsort_key(self, s):
|
||||
"""
|
||||
Used internally to get a tuple by which s is sorted.
|
||||
"""
|
||||
return map(self._try_int, re.findall(r'(\d+|\D+)', s))
|
||||
|
||||
# This decorator tells sqlalchemy to call this method everytime
|
||||
# any data on this object is updated.
|
||||
|
||||
@reconstructor
|
||||
def init_on_load(self):
|
||||
"""
|
||||
Precompute a tuple to be used for sorting.
|
||||
Precompute a natural sorting, locale aware sorting key.
|
||||
|
||||
Song sorting is performance sensitive operation.
|
||||
To get maximum speed lets precompute the string
|
||||
used for comparison.
|
||||
To get maximum speed lets precompute the sorting key.
|
||||
"""
|
||||
# Avoid the overhead of converting string to lowercase and to QString
|
||||
# with every call to sort().
|
||||
self.sort_key = self._natsort_key(self.title)
|
||||
self.sort_key = get_natural_key(self.title)
|
||||
|
||||
|
||||
class Topic(BaseModel):
|
||||
|
@ -43,7 +43,7 @@ from openlp.plugins.songs.forms.editsongform import EditSongForm
|
||||
from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm
|
||||
from openlp.plugins.songs.forms.songimportform import SongImportForm
|
||||
from openlp.plugins.songs.forms.songexportform import SongExportForm
|
||||
from openlp.plugins.songs.lib import VerseType, clean_string, natcmp
|
||||
from openlp.plugins.songs.lib import VerseType, clean_string
|
||||
from openlp.plugins.songs.lib.db import Author, Song, Book, MediaFile
|
||||
from openlp.plugins.songs.lib.ui import SongStrings
|
||||
from openlp.plugins.songs.lib.xml import OpenLyrics, SongXML
|
||||
@ -225,7 +225,7 @@ class SongMediaItem(MediaManagerItem):
|
||||
log.debug(u'display results Song')
|
||||
self.save_auto_select_id()
|
||||
self.list_view.clear()
|
||||
searchresults.sort(cmp=natcmp, key=lambda song: song.sort_key)
|
||||
searchresults.sort(key=lambda song: song.sort_key)
|
||||
for song in searchresults:
|
||||
# Do not display temporary songs
|
||||
if song.temporary:
|
||||
@ -467,9 +467,9 @@ class SongMediaItem(MediaManagerItem):
|
||||
service_item.raw_footer.append(song.title)
|
||||
service_item.raw_footer.append(create_separated_list(author_list))
|
||||
service_item.raw_footer.append(song.copyright)
|
||||
if Settings().value(u'general/ccli number'):
|
||||
if Settings().value(u'core/ccli number'):
|
||||
service_item.raw_footer.append(translate('SongsPlugin.MediaItem', 'CCLI License: ') +
|
||||
Settings().value(u'general/ccli number'))
|
||||
Settings().value(u'core/ccli number'))
|
||||
service_item.audit = [
|
||||
song.title, author_list, song.copyright, unicode(song.ccli_number)
|
||||
]
|
||||
|
@ -260,6 +260,9 @@ class SongImport(QtCore.QObject):
|
||||
elif int(verse_def[1:]) > self.verseCounts[verse_def[0]]:
|
||||
self.verseCounts[verse_def[0]] = int(verse_def[1:])
|
||||
self.verses.append([verse_def, verse_text.rstrip(), lang])
|
||||
# A verse_def refers to all verses with that name, adding it once adds every instance, so do not add if already
|
||||
# used.
|
||||
if verse_def not in self.verseOrderListGenerated:
|
||||
self.verseOrderListGenerated.append(verse_def)
|
||||
|
||||
def repeatVerse(self):
|
||||
|
@ -32,6 +32,7 @@ SongShow Plus songs into the OpenLP database.
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
import re
|
||||
import struct
|
||||
|
||||
from openlp.core.ui.wizard import WizardStrings
|
||||
@ -44,43 +45,36 @@ COPYRIGHT = 3
|
||||
CCLI_NO = 5
|
||||
VERSE = 12
|
||||
CHORUS = 20
|
||||
BRIDGE = 24
|
||||
TOPIC = 29
|
||||
COMMENTS = 30
|
||||
VERSE_ORDER = 31
|
||||
SONG_BOOK = 35
|
||||
SONG_NUMBER = 36
|
||||
CUSTOM_VERSE = 37
|
||||
BRIDGE = 24
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class SongShowPlusImport(SongImport):
|
||||
"""
|
||||
The :class:`SongShowPlusImport` class provides the ability to import song
|
||||
files from SongShow Plus.
|
||||
The :class:`SongShowPlusImport` class provides the ability to import song files from SongShow Plus.
|
||||
|
||||
**SongShow Plus Song File Format:**
|
||||
|
||||
The SongShow Plus song file format is as follows:
|
||||
|
||||
* Each piece of data in the song file has some information that precedes
|
||||
it.
|
||||
* Each piece of data in the song file has some information that precedes it.
|
||||
* The general format of this data is as follows:
|
||||
4 Bytes, forming a 32 bit number, a key if you will, this describes what
|
||||
the data is (see blockKey below)
|
||||
4 Bytes, forming a 32 bit number, which is the number of bytes until the
|
||||
next block starts
|
||||
4 Bytes, forming a 32 bit number, a key if you will, this describes what the data is (see blockKey below)
|
||||
4 Bytes, forming a 32 bit number, which is the number of bytes until the next block starts
|
||||
1 Byte, which tells how many bytes follows
|
||||
1 or 4 Bytes, describes how long the string is, if its 1 byte, the string
|
||||
is less than 255
|
||||
1 or 4 Bytes, describes how long the string is, if its 1 byte, the string is less than 255
|
||||
The next bytes are the actual data.
|
||||
The next block of data follows on.
|
||||
|
||||
This description does differ for verses. Which includes extra bytes
|
||||
stating the verse type or number. In some cases a "custom" verse is used,
|
||||
in that case, this block will in include 2 strings, with the associated
|
||||
string length descriptors. The first string is the name of the verse, the
|
||||
second is the verse content.
|
||||
This description does differ for verses. Which includes extra bytes stating the verse type or number. In some cases
|
||||
a "custom" verse is used, in that case, this block will in include 2 strings, with the associated string length
|
||||
descriptors. The first string is the name of the verse, the second is the verse content.
|
||||
|
||||
The file is ended with four null bytes.
|
||||
|
||||
@ -88,8 +82,9 @@ class SongShowPlusImport(SongImport):
|
||||
|
||||
* .sbsong
|
||||
"""
|
||||
otherList = {}
|
||||
otherCount = 0
|
||||
|
||||
other_count = 0
|
||||
other_list = {}
|
||||
|
||||
def __init__(self, manager, **kwargs):
|
||||
"""
|
||||
@ -107,9 +102,9 @@ class SongShowPlusImport(SongImport):
|
||||
for file in self.import_source:
|
||||
if self.stop_import_flag:
|
||||
return
|
||||
self.sspVerseOrderList = []
|
||||
other_count = 0
|
||||
other_list = {}
|
||||
self.ssp_verse_order_list = []
|
||||
self.other_count = 0
|
||||
self.other_list = {}
|
||||
file_name = os.path.split(file)[1]
|
||||
self.import_wizard.increment_progress_bar(WizardStrings.ImportingType % file_name, 0)
|
||||
song_data = open(file, 'rb')
|
||||
@ -162,34 +157,37 @@ class SongShowPlusImport(SongImport):
|
||||
elif block_key == COMMENTS:
|
||||
self.comments = unicode(data, u'cp1252')
|
||||
elif block_key == VERSE_ORDER:
|
||||
verse_tag = self.toOpenLPVerseTag(data, True)
|
||||
verse_tag = self.to_openlp_verse_tag(data, True)
|
||||
if verse_tag:
|
||||
if not isinstance(verse_tag, unicode):
|
||||
verse_tag = unicode(verse_tag, u'cp1252')
|
||||
self.sspVerseOrderList.append(verse_tag)
|
||||
self.ssp_verse_order_list.append(verse_tag)
|
||||
elif block_key == SONG_BOOK:
|
||||
self.songBookName = unicode(data, u'cp1252')
|
||||
elif block_key == SONG_NUMBER:
|
||||
self.songNumber = ord(data)
|
||||
elif block_key == CUSTOM_VERSE:
|
||||
verse_tag = self.toOpenLPVerseTag(verse_name)
|
||||
verse_tag = self.to_openlp_verse_tag(verse_name)
|
||||
self.addVerse(unicode(data, u'cp1252'), verse_tag)
|
||||
else:
|
||||
log.debug("Unrecognised blockKey: %s, data: %s" % (block_key, data))
|
||||
song_data.seek(next_block_starts)
|
||||
self.verseOrderList = self.sspVerseOrderList
|
||||
self.verseOrderList = self.ssp_verse_order_list
|
||||
song_data.close()
|
||||
if not self.finish():
|
||||
self.logError(file)
|
||||
|
||||
def toOpenLPVerseTag(self, verse_name, ignore_unique=False):
|
||||
if verse_name.find(" ") != -1:
|
||||
verse_parts = verse_name.split(" ")
|
||||
verse_type = verse_parts[0]
|
||||
verse_number = verse_parts[1]
|
||||
def to_openlp_verse_tag(self, verse_name, ignore_unique=False):
|
||||
# Have we got any digits? If so, verse number is everything from the digits to the end (OpenLP does not have
|
||||
# concept of part verses, so just ignore any non integers on the end (including floats))
|
||||
match = re.match(r'(\D*)(\d+)', verse_name)
|
||||
if match:
|
||||
verse_type = match.group(1).strip()
|
||||
verse_number = match.group(2)
|
||||
else:
|
||||
# otherwise we assume number 1 and take the whole prefix as the verse tag
|
||||
verse_type = verse_name
|
||||
verse_number = "1"
|
||||
verse_number = u'1'
|
||||
verse_type = verse_type.lower()
|
||||
if verse_type == "verse":
|
||||
verse_tag = VerseType.tags[VerseType.Verse]
|
||||
@ -200,11 +198,11 @@ class SongShowPlusImport(SongImport):
|
||||
elif verse_type == "pre-chorus":
|
||||
verse_tag = VerseType.tags[VerseType.PreChorus]
|
||||
else:
|
||||
if verse_name not in self.otherList:
|
||||
if verse_name not in self.other_list:
|
||||
if ignore_unique:
|
||||
return None
|
||||
self.otherCount += 1
|
||||
self.otherList[verse_name] = str(self.otherCount)
|
||||
self.other_count += 1
|
||||
self.other_list[verse_name] = str(self.other_count)
|
||||
verse_tag = VerseType.tags[VerseType.Other]
|
||||
verse_number = self.otherList[verse_name]
|
||||
verse_number = self.other_list[verse_name]
|
||||
return verse_tag + verse_number
|
||||
|
@ -27,7 +27,6 @@
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
The :mod:`songusage` module contains the Song Usage plugin. The Song Usage
|
||||
plugin provides auditing capabilities for reporting the songs you are using to
|
||||
copyright license organisations.
|
||||
The :mod:`songusage` module contains the Song Usage plugin. The Song Usage plugin provides auditing capabilities for
|
||||
reporting the songs you are using to copyright license organisations.
|
||||
"""
|
||||
|
@ -55,7 +55,8 @@ class Ui_SongUsageDeleteDialog(object):
|
||||
self.retranslateUi(song_usage_delete_dialog)
|
||||
|
||||
def retranslateUi(self, song_usage_delete_dialog):
|
||||
song_usage_delete_dialog.setWindowTitle(translate('SongUsagePlugin.SongUsageDeleteForm', 'Delete Song Usage Data'))
|
||||
song_usage_delete_dialog.setWindowTitle(
|
||||
translate('SongUsagePlugin.SongUsageDeleteForm', 'Delete Song Usage Data'))
|
||||
self.delete_label.setText(
|
||||
translate('SongUsagePlugin.SongUsageDeleteForm', 'Select the date up to which the song usage data '
|
||||
'should be deleted. All data recorded before this date will be permanently deleted.'))
|
||||
|
@ -81,7 +81,8 @@ class Ui_SongUsageDetailDialog(object):
|
||||
self.save_file_push_button.clicked.connect(song_usage_detail_dialog.define_output_location)
|
||||
|
||||
def retranslateUi(self, song_usage_detail_dialog):
|
||||
song_usage_detail_dialog.setWindowTitle(translate('SongUsagePlugin.SongUsageDetailForm', 'Song Usage Extraction'))
|
||||
song_usage_detail_dialog.setWindowTitle(
|
||||
translate('SongUsagePlugin.SongUsageDetailForm', 'Song Usage Extraction'))
|
||||
self.date_range_group_box.setTitle(translate('SongUsagePlugin.SongUsageDetailForm', 'Select Date Range'))
|
||||
self.to_label.setText(translate('SongUsagePlugin.SongUsageDetailForm', 'to'))
|
||||
self.file_group_box.setTitle(translate('SongUsagePlugin.SongUsageDetailForm', 'Report Location'))
|
||||
|
@ -36,12 +36,14 @@ from sqlalchemy.orm import mapper
|
||||
|
||||
from openlp.core.lib.db import BaseModel, init_db
|
||||
|
||||
|
||||
class SongUsageItem(BaseModel):
|
||||
"""
|
||||
SongUsageItem model
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def init_schema(url):
|
||||
"""
|
||||
Setup the songusage database connection and initialise the database schema
|
||||
|
@ -76,12 +76,10 @@ class SongUsagePlugin(Plugin):
|
||||
|
||||
def add_tools_menu_item(self, tools_menu):
|
||||
"""
|
||||
Give the SongUsage plugin the opportunity to add items to the
|
||||
**Tools** menu.
|
||||
Give the SongUsage plugin the opportunity to add items to the **Tools** menu.
|
||||
|
||||
``tools_menu``
|
||||
The actual **Tools** menu item, so that your actions can
|
||||
use it as their parent.
|
||||
The actual **Tools** menu item, so that your actions can use it as their parent.
|
||||
"""
|
||||
log.info(u'add tools menu')
|
||||
self.toolsMenu = tools_menu
|
||||
@ -218,8 +216,8 @@ class SongUsagePlugin(Plugin):
|
||||
self.song_usage_detail_form.exec_()
|
||||
|
||||
def about(self):
|
||||
about_text = translate('SongUsagePlugin', '<strong>SongUsage Plugin'
|
||||
'</strong><br />This plugin tracks the usage of songs in services.')
|
||||
about_text = translate('SongUsagePlugin',
|
||||
'<strong>SongUsage Plugin</strong><br />This plugin tracks the usage of songs in services.')
|
||||
return about_text
|
||||
|
||||
def set_plugin_text_strings(self):
|
||||
|
@ -81,8 +81,10 @@ MODULES = [
|
||||
'enchant',
|
||||
'bs4',
|
||||
'mako',
|
||||
'cherrypy',
|
||||
'migrate',
|
||||
'uno',
|
||||
'icu',
|
||||
]
|
||||
|
||||
|
||||
|
@ -15,7 +15,7 @@ class TestLib(TestCase):
|
||||
"""
|
||||
Test the str_to_bool function with boolean input
|
||||
"""
|
||||
#GIVEN: A boolean value set to true
|
||||
# GIVEN: A boolean value set to true
|
||||
true_boolean = True
|
||||
|
||||
# WHEN: We "convert" it to a bool
|
||||
@ -25,7 +25,7 @@ class TestLib(TestCase):
|
||||
assert isinstance(true_result, bool), u'The result should be a boolean'
|
||||
assert true_result is True, u'The result should be True'
|
||||
|
||||
#GIVEN: A boolean value set to false
|
||||
# GIVEN: A boolean value set to false
|
||||
false_boolean = False
|
||||
|
||||
# WHEN: We "convert" it to a bool
|
||||
|
@ -74,7 +74,7 @@ class TestPluginManager(TestCase):
|
||||
# WHEN: We run hook_settings_tabs()
|
||||
plugin_manager.hook_settings_tabs()
|
||||
|
||||
# THEN: The create_settings_Tab() method should have been called
|
||||
# THEN: The hook_settings_tabs() method should have been called
|
||||
assert mocked_plugin.create_media_manager_item.call_count == 0, \
|
||||
u'The create_media_manager_item() method should not have been called.'
|
||||
|
||||
@ -94,8 +94,8 @@ class TestPluginManager(TestCase):
|
||||
# WHEN: We run hook_settings_tabs()
|
||||
plugin_manager.hook_settings_tabs()
|
||||
|
||||
# THEN: The create_settings_Tab() method should not have been called, but the plugins lists should be the same
|
||||
assert mocked_plugin.create_settings_Tab.call_count == 0, \
|
||||
# THEN: The create_settings_tab() method should not have been called, but the plugins lists should be the same
|
||||
assert mocked_plugin.create_settings_tab.call_count == 0, \
|
||||
u'The create_media_manager_item() method should not have been called.'
|
||||
self.assertEqual(mocked_settings_form.plugin_manager.plugins, plugin_manager.plugins,
|
||||
u'The plugins on the settings form should be the same as the plugins in the plugin manager')
|
||||
@ -117,7 +117,7 @@ class TestPluginManager(TestCase):
|
||||
plugin_manager.hook_settings_tabs()
|
||||
|
||||
# THEN: The create_media_manager_item() method should have been called with the mocked settings form
|
||||
assert mocked_plugin.create_settings_Tab.call_count == 1, \
|
||||
assert mocked_plugin.create_settings_tab.call_count == 1, \
|
||||
u'The create_media_manager_item() method should have been called once.'
|
||||
self.assertEqual(mocked_settings_form.plugin_manager.plugins, plugin_manager.plugins,
|
||||
u'The plugins on the settings form should be the same as the plugins in the plugin manager')
|
||||
@ -135,8 +135,8 @@ class TestPluginManager(TestCase):
|
||||
# WHEN: We run hook_settings_tabs()
|
||||
plugin_manager.hook_settings_tabs()
|
||||
|
||||
# THEN: The create_settings_Tab() method should have been called
|
||||
mocked_plugin.create_settings_Tab.assert_called_with(self.mocked_settings_form)
|
||||
# THEN: The create_settings_tab() method should have been called
|
||||
mocked_plugin.create_settings_tab.assert_called_with(self.mocked_settings_form)
|
||||
|
||||
def hook_import_menu_with_disabled_plugin_test(self):
|
||||
"""
|
||||
|
@ -276,5 +276,7 @@ class TestServiceItem(TestCase):
|
||||
first_line = items[0]
|
||||
except IOError:
|
||||
first_line = u''
|
||||
finally:
|
||||
open_file.close()
|
||||
return first_line
|
||||
|
||||
|
@ -11,7 +11,9 @@ from PyQt4 import QtGui
|
||||
|
||||
|
||||
class TestSettings(TestCase):
|
||||
|
||||
"""
|
||||
Test the functions in the Settings module
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Create the UI
|
||||
@ -35,16 +37,16 @@ class TestSettings(TestCase):
|
||||
# GIVEN: A new Settings setup
|
||||
|
||||
# WHEN reading a setting for the first time
|
||||
default_value = Settings().value(u'general/has run wizard')
|
||||
default_value = Settings().value(u'core/has run wizard')
|
||||
|
||||
# THEN the default value is returned
|
||||
assert default_value is False, u'The default value should be False'
|
||||
|
||||
# WHEN a new value is saved into config
|
||||
Settings().setValue(u'general/has run wizard', True)
|
||||
Settings().setValue(u'core/has run wizard', True)
|
||||
|
||||
# THEN the new value is returned when re-read
|
||||
assert Settings().value(u'general/has run wizard') is True, u'The saved value should have been returned'
|
||||
assert Settings().value(u'core/has run wizard') is True, u'The saved value should have been returned'
|
||||
|
||||
def settings_override_test(self):
|
||||
"""
|
||||
|
@ -6,6 +6,7 @@ from unittest import TestCase
|
||||
|
||||
from openlp.core.lib import UiStrings
|
||||
|
||||
|
||||
class TestUiStrings(TestCase):
|
||||
|
||||
def check_same_instance_test(self):
|
||||
|
@ -30,8 +30,10 @@ class TestAppLocation(TestCase):
|
||||
mocked_get_directory.return_value = u'test/dir'
|
||||
mocked_check_directory_exists.return_value = True
|
||||
mocked_os.path.normpath.return_value = u'test/dir'
|
||||
|
||||
# WHEN: we call AppLocation.get_data_path()
|
||||
data_path = AppLocation.get_data_path()
|
||||
|
||||
# THEN: check that all the correct methods were called, and the result is correct
|
||||
mocked_settings.contains.assert_called_with(u'advanced/data path')
|
||||
mocked_get_directory.assert_called_with(AppLocation.DataDir)
|
||||
@ -49,8 +51,10 @@ class TestAppLocation(TestCase):
|
||||
mocked_settings.contains.return_value = True
|
||||
mocked_settings.value.return_value.toString.return_value = u'custom/dir'
|
||||
mocked_os.path.normpath.return_value = u'custom/dir'
|
||||
|
||||
# WHEN: we call AppLocation.get_data_path()
|
||||
data_path = AppLocation.get_data_path()
|
||||
|
||||
# THEN: the mocked Settings methods were called and the value returned was our set up value
|
||||
mocked_settings.contains.assert_called_with(u'advanced/data path')
|
||||
mocked_settings.value.assert_called_with(u'advanced/data path')
|
||||
@ -100,8 +104,10 @@ class TestAppLocation(TestCase):
|
||||
# GIVEN: A mocked out AppLocation.get_data_path()
|
||||
mocked_get_data_path.return_value = u'test/dir'
|
||||
mocked_check_directory_exists.return_value = True
|
||||
|
||||
# WHEN: we call AppLocation.get_data_path()
|
||||
data_path = AppLocation.get_section_data_path(u'section')
|
||||
|
||||
# THEN: check that all the correct methods were called, and the result is correct
|
||||
mocked_check_directory_exists.assert_called_with(u'test/dir/section')
|
||||
assert data_path == u'test/dir/section', u'Result should be "test/dir/section"'
|
||||
@ -112,8 +118,10 @@ class TestAppLocation(TestCase):
|
||||
"""
|
||||
with patch(u'openlp.core.utils.applocation._get_frozen_path') as mocked_get_frozen_path:
|
||||
mocked_get_frozen_path.return_value = u'app/dir'
|
||||
|
||||
# WHEN: We call AppLocation.get_directory
|
||||
directory = AppLocation.get_directory(AppLocation.AppDir)
|
||||
|
||||
# THEN:
|
||||
assert directory == u'app/dir', u'Directory should be "app/dir"'
|
||||
|
||||
@ -130,8 +138,10 @@ class TestAppLocation(TestCase):
|
||||
mocked_get_frozen_path.return_value = u'plugins/dir'
|
||||
mocked_sys.frozen = 1
|
||||
mocked_sys.argv = ['openlp']
|
||||
|
||||
# WHEN: We call AppLocation.get_directory
|
||||
directory = AppLocation.get_directory(AppLocation.PluginsDir)
|
||||
|
||||
# THEN:
|
||||
assert directory == u'plugins/dir', u'Directory should be "plugins/dir"'
|
||||
|
||||
|
@ -5,7 +5,9 @@ from unittest import TestCase
|
||||
|
||||
from mock import patch
|
||||
|
||||
from openlp.core.utils import get_filesystem_encoding, _get_frozen_path
|
||||
from openlp.core.utils import clean_filename, get_filesystem_encoding, _get_frozen_path, get_locale_key, \
|
||||
get_natural_key, split_filename
|
||||
|
||||
|
||||
class TestUtils(TestCase):
|
||||
"""
|
||||
@ -56,3 +58,76 @@ class TestUtils(TestCase):
|
||||
# THEN: The frozen parameter is returned
|
||||
assert _get_frozen_path(u'frozen', u'not frozen') == u'frozen', u'Should return "frozen"'
|
||||
|
||||
def split_filename_with_file_path_test(self):
|
||||
"""
|
||||
Test the split_filename() function with a path to a file
|
||||
"""
|
||||
# GIVEN: A path to a file.
|
||||
file_path = u'/home/user/myfile.txt'
|
||||
wanted_result = (u'/home/user', u'myfile.txt')
|
||||
with patch(u'openlp.core.utils.os.path.isfile') as mocked_is_file:
|
||||
mocked_is_file.return_value = True
|
||||
|
||||
# WHEN: Split the file name.
|
||||
result = split_filename(file_path)
|
||||
|
||||
# THEN: A tuple should be returned.
|
||||
assert result == wanted_result, u'A tuple with the directory and file name should have been returned.'
|
||||
|
||||
def split_filename_with_dir_path_test(self):
|
||||
"""
|
||||
Test the split_filename() function with a path to a directory
|
||||
"""
|
||||
# GIVEN: A path to a dir.
|
||||
file_path = u'/home/user/mydir'
|
||||
wanted_result = (u'/home/user/mydir', u'')
|
||||
with patch(u'openlp.core.utils.os.path.isfile') as mocked_is_file:
|
||||
mocked_is_file.return_value = False
|
||||
|
||||
# WHEN: Split the file name.
|
||||
result = split_filename(file_path)
|
||||
|
||||
# THEN: A tuple should be returned.
|
||||
assert result == wanted_result, \
|
||||
u'A two-entry tuple with the directory and file name (empty) should have been returned.'
|
||||
|
||||
def clean_filename_test(self):
|
||||
"""
|
||||
Test the clean_filename() function
|
||||
"""
|
||||
# GIVEN: A invalid file name and the valid file name.
|
||||
invalid_name = u'A_file_with_invalid_characters_[\\/:\*\?"<>\|\+\[\]%].py'
|
||||
wanted_name = u'A_file_with_invalid_characters______________________.py'
|
||||
|
||||
# WHEN: Clean the name.
|
||||
result = clean_filename(invalid_name)
|
||||
|
||||
# THEN: The file name should be cleaned.
|
||||
assert result == wanted_name, u'The file name should not contain any special characters.'
|
||||
|
||||
def get_locale_key_test(self):
|
||||
"""
|
||||
Test the get_locale_key(string) function
|
||||
"""
|
||||
with patch(u'openlp.core.utils.languagemanager.LanguageManager.get_language') as mocked_get_language:
|
||||
# GIVEN: The language is German
|
||||
# 0x00C3 (A with diaresis) should be sorted as "A". 0x00DF (sharp s) should be sorted as "ss".
|
||||
mocked_get_language.return_value = u'de'
|
||||
unsorted_list = [u'Auszug', u'Aushang', u'\u00C4u\u00DFerung']
|
||||
# WHEN: We sort the list and use get_locale_key() to generate the sorting keys
|
||||
# THEN: We get a properly sorted list
|
||||
test_passes = sorted(unsorted_list, key=get_locale_key) == [u'Aushang', u'\u00C4u\u00DFerung', u'Auszug']
|
||||
assert test_passes, u'Strings should be sorted properly'
|
||||
|
||||
def get_natural_key_test(self):
|
||||
"""
|
||||
Test the get_natural_key(string) function
|
||||
"""
|
||||
with patch(u'openlp.core.utils.languagemanager.LanguageManager.get_language') as mocked_get_language:
|
||||
# GIVEN: The language is English (a language, which sorts digits before letters)
|
||||
mocked_get_language.return_value = u'en'
|
||||
unsorted_list = [u'item 10a', u'item 3b', u'1st item']
|
||||
# WHEN: We sort the list and use get_natural_key() to generate the sorting keys
|
||||
# THEN: We get a properly sorted list
|
||||
test_passes = sorted(unsorted_list, key=get_natural_key) == [u'1st item', u'item 3b', u'item 10a']
|
||||
assert test_passes, u'Numbers should be sorted naturally'
|
||||
|
@ -35,7 +35,7 @@ class TestImageMediaItem(TestCase):
|
||||
"""
|
||||
# GIVEN: An empty image_list
|
||||
image_list = []
|
||||
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.loadFullList') as mocked_loadFullList:
|
||||
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_loadFullList:
|
||||
self.media_item.manager = MagicMock()
|
||||
|
||||
# WHEN: We run save_new_images_list with the empty list
|
||||
@ -47,37 +47,37 @@ class TestImageMediaItem(TestCase):
|
||||
|
||||
def save_new_images_list_single_image_with_reload_test(self):
|
||||
"""
|
||||
Test that the save_new_images_list() calls loadFullList() when reload_list is set to True
|
||||
Test that the save_new_images_list() calls load_full_list() when reload_list is set to True
|
||||
"""
|
||||
# GIVEN: A list with 1 image
|
||||
image_list = [ u'test_image.jpg' ]
|
||||
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.loadFullList') as mocked_loadFullList:
|
||||
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_loadFullList:
|
||||
ImageFilenames.filename = ''
|
||||
self.media_item.manager = MagicMock()
|
||||
|
||||
# WHEN: We run save_new_images_list with reload_list=True
|
||||
self.media_item.save_new_images_list(image_list, reload_list=True)
|
||||
|
||||
# THEN: loadFullList() should have been called
|
||||
assert mocked_loadFullList.call_count == 1, u'loadFullList() should have been called'
|
||||
# THEN: load_full_list() should have been called
|
||||
assert mocked_loadFullList.call_count == 1, u'load_full_list() should have been called'
|
||||
|
||||
# CLEANUP: Remove added attribute from ImageFilenames
|
||||
delattr(ImageFilenames, 'filename')
|
||||
|
||||
def save_new_images_list_single_image_without_reload_test(self):
|
||||
"""
|
||||
Test that the save_new_images_list() doesn't call loadFullList() when reload_list is set to False
|
||||
Test that the save_new_images_list() doesn't call load_full_list() when reload_list is set to False
|
||||
"""
|
||||
# GIVEN: A list with 1 image
|
||||
image_list = [ u'test_image.jpg' ]
|
||||
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.loadFullList') as mocked_loadFullList:
|
||||
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_loadFullList:
|
||||
self.media_item.manager = MagicMock()
|
||||
|
||||
# WHEN: We run save_new_images_list with reload_list=False
|
||||
self.media_item.save_new_images_list(image_list, reload_list=False)
|
||||
|
||||
# THEN: loadFullList() should not have been called
|
||||
assert mocked_loadFullList.call_count == 0, u'loadFullList() should not have been called'
|
||||
# THEN: load_full_list() should not have been called
|
||||
assert mocked_loadFullList.call_count == 0, u'load_full_list() should not have been called'
|
||||
|
||||
def save_new_images_list_multiple_images_test(self):
|
||||
"""
|
||||
@ -85,15 +85,15 @@ class TestImageMediaItem(TestCase):
|
||||
"""
|
||||
# GIVEN: A list with 3 images
|
||||
image_list = [ u'test_image_1.jpg', u'test_image_2.jpg', u'test_image_3.jpg' ]
|
||||
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.loadFullList') as mocked_loadFullList:
|
||||
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_loadFullList:
|
||||
self.media_item.manager = MagicMock()
|
||||
|
||||
# WHEN: We run save_new_images_list with the list of 3 images
|
||||
self.media_item.save_new_images_list(image_list, reload_list=False)
|
||||
|
||||
# THEN: loadFullList() should not have been called
|
||||
# THEN: load_full_list() should not have been called
|
||||
assert self.media_item.manager.save_object.call_count == 3, \
|
||||
u'loadFullList() should have been called three times'
|
||||
u'load_full_list() should have been called three times'
|
||||
|
||||
def save_new_images_list_other_objects_in_list_test(self):
|
||||
"""
|
||||
@ -101,12 +101,12 @@ class TestImageMediaItem(TestCase):
|
||||
"""
|
||||
# GIVEN: A list with images and objects
|
||||
image_list = [ u'test_image_1.jpg', None, True, ImageFilenames(), 'test_image_2.jpg' ]
|
||||
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.loadFullList') as mocked_loadFullList:
|
||||
with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') as mocked_loadFullList:
|
||||
self.media_item.manager = MagicMock()
|
||||
|
||||
# WHEN: We run save_new_images_list with the list of images and objects
|
||||
self.media_item.save_new_images_list(image_list, reload_list=False)
|
||||
|
||||
# THEN: loadFullList() should not have been called
|
||||
# THEN: load_full_list() should not have been called
|
||||
assert self.media_item.manager.save_object.call_count == 2, \
|
||||
u'loadFullList() should have been called only once'
|
||||
u'load_full_list() should have been called only once'
|
||||
|
0
tests/functional/openlp_plugins/remotes/__init__.py
Normal file
0
tests/functional/openlp_plugins/remotes/__init__.py
Normal file
108
tests/functional/openlp_plugins/remotes/test_remotetab.py
Normal file
108
tests/functional/openlp_plugins/remotes/test_remotetab.py
Normal file
@ -0,0 +1,108 @@
|
||||
"""
|
||||
This module contains tests for the lib submodule of the Remotes plugin.
|
||||
"""
|
||||
import os
|
||||
|
||||
from unittest import TestCase
|
||||
from tempfile import mkstemp
|
||||
from mock import patch
|
||||
|
||||
from openlp.core.lib import Settings
|
||||
from openlp.plugins.remotes.lib.remotetab import RemoteTab
|
||||
|
||||
from PyQt4 import QtGui
|
||||
|
||||
__default_settings__ = {
|
||||
u'remotes/twelve hour': True,
|
||||
u'remotes/port': 4316,
|
||||
u'remotes/https port': 4317,
|
||||
u'remotes/https enabled': False,
|
||||
u'remotes/user id': u'openlp',
|
||||
u'remotes/password': u'password',
|
||||
u'remotes/authentication enabled': False,
|
||||
u'remotes/ip address': u'0.0.0.0'
|
||||
}
|
||||
|
||||
ZERO_URL = u'0.0.0.0'
|
||||
|
||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..', u'..', u'resources'))
|
||||
|
||||
|
||||
class TestRemoteTab(TestCase):
|
||||
"""
|
||||
Test the functions in the :mod:`lib` module.
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Create the UI
|
||||
"""
|
||||
fd, self.ini_file = mkstemp(u'.ini')
|
||||
Settings().set_filename(self.ini_file)
|
||||
self.application = QtGui.QApplication.instance()
|
||||
Settings().extend_default_settings(__default_settings__)
|
||||
self.parent = QtGui.QMainWindow()
|
||||
self.form = RemoteTab(self.parent, u'Remotes', None, None)
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Delete all the C++ objects at the end so that we don't have a segfault
|
||||
"""
|
||||
del self.application
|
||||
del self.parent
|
||||
del self.form
|
||||
os.unlink(self.ini_file)
|
||||
|
||||
def set_basic_urls_test(self):
|
||||
"""
|
||||
Test the set_urls function with standard defaults
|
||||
"""
|
||||
# GIVEN: A mocked location
|
||||
with patch(u'openlp.core.utils.applocation.Settings') as mocked_class, \
|
||||
patch(u'openlp.core.utils.AppLocation.get_directory') as mocked_get_directory, \
|
||||
patch(u'openlp.core.utils.applocation.check_directory_exists') as mocked_check_directory_exists, \
|
||||
patch(u'openlp.core.utils.applocation.os') as mocked_os:
|
||||
# GIVEN: A mocked out Settings class and a mocked out AppLocation.get_directory()
|
||||
mocked_settings = mocked_class.return_value
|
||||
mocked_settings.contains.return_value = False
|
||||
mocked_get_directory.return_value = u'test/dir'
|
||||
mocked_check_directory_exists.return_value = True
|
||||
mocked_os.path.normpath.return_value = u'test/dir'
|
||||
|
||||
# WHEN: when the set_urls is called having reloaded the form.
|
||||
self.form.load()
|
||||
self.form.set_urls()
|
||||
# THEN: the following screen values should be set
|
||||
self.assertEqual(self.form.address_edit.text(), ZERO_URL, u'The default URL should be set on the screen')
|
||||
self.assertEqual(self.form.https_settings_group_box.isEnabled(), False,
|
||||
u'The Https box should not be enabled')
|
||||
self.assertEqual(self.form.https_settings_group_box.isChecked(), False,
|
||||
u'The Https checked box should note be Checked')
|
||||
self.assertEqual(self.form.user_login_group_box.isChecked(), False,
|
||||
u'The authentication box should not be enabled')
|
||||
|
||||
def set_certificate_urls_test(self):
|
||||
"""
|
||||
Test the set_urls function with certificate available
|
||||
"""
|
||||
# GIVEN: A mocked location
|
||||
with patch(u'openlp.core.utils.applocation.Settings') as mocked_class, \
|
||||
patch(u'openlp.core.utils.AppLocation.get_directory') as mocked_get_directory, \
|
||||
patch(u'openlp.core.utils.applocation.check_directory_exists') as mocked_check_directory_exists, \
|
||||
patch(u'openlp.core.utils.applocation.os') as mocked_os:
|
||||
# GIVEN: A mocked out Settings class and a mocked out AppLocation.get_directory()
|
||||
mocked_settings = mocked_class.return_value
|
||||
mocked_settings.contains.return_value = False
|
||||
mocked_get_directory.return_value = TEST_PATH
|
||||
mocked_check_directory_exists.return_value = True
|
||||
mocked_os.path.normpath.return_value = TEST_PATH
|
||||
|
||||
# WHEN: when the set_urls is called having reloaded the form.
|
||||
self.form.load()
|
||||
self.form.set_urls()
|
||||
# THEN: the following screen values should be set
|
||||
self.assertEqual(self.form.http_settings_group_box.isEnabled(), True,
|
||||
u'The Http group box should be enabled')
|
||||
self.assertEqual(self.form.https_settings_group_box.isChecked(), False,
|
||||
u'The Https checked box should be Checked')
|
||||
self.assertEqual(self.form.https_settings_group_box.isEnabled(), True,
|
||||
u'The Https box should be enabled')
|
99
tests/functional/openlp_plugins/remotes/test_router.py
Normal file
99
tests/functional/openlp_plugins/remotes/test_router.py
Normal file
@ -0,0 +1,99 @@
|
||||
"""
|
||||
This module contains tests for the lib submodule of the Remotes plugin.
|
||||
"""
|
||||
import os
|
||||
|
||||
from unittest import TestCase
|
||||
from tempfile import mkstemp
|
||||
from mock import MagicMock
|
||||
|
||||
from openlp.core.lib import Settings
|
||||
from openlp.plugins.remotes.lib.httpserver import HttpRouter, fetch_password, make_sha_hash
|
||||
from PyQt4 import QtGui
|
||||
|
||||
__default_settings__ = {
|
||||
u'remotes/twelve hour': True,
|
||||
u'remotes/port': 4316,
|
||||
u'remotes/https port': 4317,
|
||||
u'remotes/https enabled': False,
|
||||
u'remotes/user id': u'openlp',
|
||||
u'remotes/password': u'password',
|
||||
u'remotes/authentication enabled': False,
|
||||
u'remotes/ip address': u'0.0.0.0'
|
||||
}
|
||||
|
||||
|
||||
class TestRouter(TestCase):
|
||||
"""
|
||||
Test the functions in the :mod:`lib` module.
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Create the UI
|
||||
"""
|
||||
fd, self.ini_file = mkstemp(u'.ini')
|
||||
Settings().set_filename(self.ini_file)
|
||||
self.application = QtGui.QApplication.instance()
|
||||
Settings().extend_default_settings(__default_settings__)
|
||||
self.router = HttpRouter()
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Delete all the C++ objects at the end so that we don't have a segfault
|
||||
"""
|
||||
del self.application
|
||||
os.unlink(self.ini_file)
|
||||
|
||||
def fetch_password_unknown_test(self):
|
||||
"""
|
||||
Test the fetch password code with an unknown userid
|
||||
"""
|
||||
# GIVEN: A default configuration
|
||||
# WHEN: called with the defined userid
|
||||
password = fetch_password(u'itwinkle')
|
||||
|
||||
# THEN: the function should return None
|
||||
self.assertEqual(password, None, u'The result for fetch_password should be None')
|
||||
|
||||
def fetch_password_known_test(self):
|
||||
"""
|
||||
Test the fetch password code with the defined userid
|
||||
"""
|
||||
# GIVEN: A default configuration
|
||||
# WHEN: called with the defined userid
|
||||
password = fetch_password(u'openlp')
|
||||
required_password = make_sha_hash(u'password')
|
||||
|
||||
# THEN: the function should return the correct password
|
||||
self.assertEqual(password, required_password, u'The result for fetch_password should be the defined password')
|
||||
|
||||
def sha_password_encrypter_test(self):
|
||||
"""
|
||||
Test hash password function
|
||||
"""
|
||||
# GIVEN: A default configuration
|
||||
# WHEN: called with the defined userid
|
||||
required_password = make_sha_hash(u'password')
|
||||
test_value = u'5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8'
|
||||
|
||||
# THEN: the function should return the correct password
|
||||
self.assertEqual(required_password, test_value,
|
||||
u'The result for make_sha_hash should return the correct encrypted password')
|
||||
|
||||
def process_http_request_test(self):
|
||||
"""
|
||||
Test the router control functionality
|
||||
"""
|
||||
# GIVEN: A testing set of Routes
|
||||
mocked_function = MagicMock()
|
||||
test_route = [
|
||||
(r'^/stage/api/poll$', mocked_function),
|
||||
]
|
||||
self.router.routes = test_route
|
||||
|
||||
# WHEN: called with a poll route
|
||||
self.router.process_http_request(u'/stage/api/poll', None)
|
||||
|
||||
# THEN: the function should have been called only once
|
||||
assert mocked_function.call_count == 1, \
|
||||
u'The mocked function should have been matched and called once.'
|
235
tests/functional/openlp_plugins/songs/test_songshowplusimport.py
Normal file
235
tests/functional/openlp_plugins/songs/test_songshowplusimport.py
Normal file
@ -0,0 +1,235 @@
|
||||
"""
|
||||
This module contains tests for the SongShow Plus song importer.
|
||||
"""
|
||||
|
||||
import os
|
||||
from unittest import TestCase
|
||||
from mock import patch, MagicMock
|
||||
|
||||
from openlp.plugins.songs.lib import VerseType
|
||||
from openlp.plugins.songs.lib.songshowplusimport import SongShowPlusImport
|
||||
|
||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'../../../resources/songshowplussongs'))
|
||||
SONG_TEST_DATA = {u'Amazing Grace.sbsong':
|
||||
{u'title': u'Amazing Grace (Demonstration)',
|
||||
u'authors': [u'John Newton', u'Edwin Excell', u'John P. Rees'],
|
||||
u'copyright': u'Public Domain ',
|
||||
u'ccli_number': 22025,
|
||||
u'verses':
|
||||
[(u'Amazing grace! How sweet the sound!\r\nThat saved a wretch like me!\r\n'
|
||||
u'I once was lost, but now am found;\r\nWas blind, but now I see.', u'v1'),
|
||||
(u'\'Twas grace that taught my heart to fear,\r\nAnd grace my fears relieved.\r\n'
|
||||
u'How precious did that grace appear,\r\nThe hour I first believed.', u'v2'),
|
||||
(u'The Lord has promised good to me,\r\nHis Word my hope secures.\r\n'
|
||||
u'He will my shield and portion be\r\nAs long as life endures.', u'v3'),
|
||||
(u'Thro\' many dangers, toils and snares\r\nI have already come.\r\n'
|
||||
u'\'Tis grace that brought me safe thus far,\r\nAnd grace will lead me home.', u'v4'),
|
||||
(u'When we\'ve been there ten thousand years,\r\nBright shining as the sun,\r\n'
|
||||
u'We\'ve no less days to sing God\'s praise,\r\nThan when we first begun.', u'v5')],
|
||||
u'topics': [u'Assurance', u'Grace', u'Praise', u'Salvation'],
|
||||
u'comments': u'\n\n\n',
|
||||
u'song_book_name': u'Demonstration Songs',
|
||||
u'song_number': 0,
|
||||
u'verse_order_list': []},
|
||||
u'Beautiful Garden Of Prayer.sbsong':
|
||||
{u'title': u'Beautiful Garden Of Prayer (Demonstration)',
|
||||
u'authors': [u'Eleanor Allen Schroll', u'James H. Fillmore'],
|
||||
u'copyright': u'Public Domain ',
|
||||
u'ccli_number': 60252,
|
||||
u'verses':
|
||||
[(u'There\'s a garden where Jesus is waiting,\r\nThere\'s a place that is wondrously fair.\r\n'
|
||||
u'For it glows with the light of His presence,\r\n\'Tis the beautiful garden of prayer.', u'v1'),
|
||||
(u'There\'s a garden where Jesus is waiting,\r\nAnd I go with my burden and care.\r\n'
|
||||
u'Just to learn from His lips, words of comfort,\r\nIn the beautiful garden of prayer.', u'v2'),
|
||||
(u'There\'s a garden where Jesus is waiting,\r\nAnd He bids you to come meet Him there,\r\n'
|
||||
u'Just to bow and receive a new blessing,\r\nIn the beautiful garden of prayer.', u'v3'),
|
||||
(u'O the beautiful garden, the garden of prayer,\r\nO the beautiful garden of prayer.\r\n'
|
||||
u'There my Savior awaits, and He opens the gates\r\nTo the beautiful garden of prayer.', u'c1')],
|
||||
u'topics': [u'Devotion', u'Prayer'],
|
||||
u'comments': u'',
|
||||
u'song_book_name': u'',
|
||||
u'song_number': 0,
|
||||
u'verse_order_list': []}}
|
||||
|
||||
|
||||
class TestSongShowPlusImport(TestCase):
|
||||
"""
|
||||
Test the functions in the :mod:`songshowplusimport` module.
|
||||
"""
|
||||
def create_importer_test(self):
|
||||
"""
|
||||
Test creating an instance of the SongShow Plus file importer
|
||||
"""
|
||||
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
|
||||
with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport'):
|
||||
mocked_manager = MagicMock()
|
||||
|
||||
# WHEN: An importer object is created
|
||||
importer = SongShowPlusImport(mocked_manager)
|
||||
|
||||
# THEN: The importer object should not be None
|
||||
self.assertIsNotNone(importer, u'Import should not be none')
|
||||
|
||||
def invalid_import_source_test(self):
|
||||
"""
|
||||
Test SongShowPlusImport.doImport handles different invalid import_source values
|
||||
"""
|
||||
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
|
||||
with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport'):
|
||||
mocked_manager = MagicMock()
|
||||
mocked_import_wizard = MagicMock()
|
||||
importer = SongShowPlusImport(mocked_manager)
|
||||
importer.import_wizard = mocked_import_wizard
|
||||
importer.stop_import_flag = True
|
||||
|
||||
# WHEN: Import source is not a list
|
||||
for source in [u'not a list', 0]:
|
||||
importer.import_source = source
|
||||
|
||||
# THEN: doImport should return none and the progress bar maximum should not be set.
|
||||
self.assertIsNone(importer.doImport(), u'doImport should return None when import_source is not a list')
|
||||
self.assertEquals(mocked_import_wizard.progress_bar.setMaximum.called, False,
|
||||
u'setMaxium on import_wizard.progress_bar should not have been called')
|
||||
|
||||
def valid_import_source_test(self):
|
||||
"""
|
||||
Test SongShowPlusImport.doImport handles different invalid import_source values
|
||||
"""
|
||||
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
|
||||
with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport'):
|
||||
mocked_manager = MagicMock()
|
||||
mocked_import_wizard = MagicMock()
|
||||
importer = SongShowPlusImport(mocked_manager)
|
||||
importer.import_wizard = mocked_import_wizard
|
||||
importer.stop_import_flag = True
|
||||
|
||||
# WHEN: Import source is a list
|
||||
importer.import_source = [u'List', u'of', u'files']
|
||||
|
||||
# THEN: doImport should return none and the progress bar setMaximum should be called with the length of
|
||||
# import_source.
|
||||
self.assertIsNone(importer.doImport(),
|
||||
u'doImport should return None when import_source is a list and stop_import_flag is True')
|
||||
mocked_import_wizard.progress_bar.setMaximum.assert_called_with(len(importer.import_source))
|
||||
|
||||
def to_openlp_verse_tag_test(self):
|
||||
"""
|
||||
Test to_openlp_verse_tag method by simulating adding a verse
|
||||
"""
|
||||
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
|
||||
with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport'):
|
||||
mocked_manager = MagicMock()
|
||||
importer = SongShowPlusImport(mocked_manager)
|
||||
|
||||
# WHEN: Supplied with the following arguments replicating verses being added
|
||||
test_values = [(u'Verse 1', VerseType.tags[VerseType.Verse] + u'1'),
|
||||
(u'Verse 2', VerseType.tags[VerseType.Verse] + u'2'),
|
||||
(u'verse1', VerseType.tags[VerseType.Verse] + u'1'),
|
||||
(u'Verse', VerseType.tags[VerseType.Verse] + u'1'),
|
||||
(u'Verse1', VerseType.tags[VerseType.Verse] + u'1'),
|
||||
(u'chorus 1', VerseType.tags[VerseType.Chorus] + u'1'),
|
||||
(u'bridge 1', VerseType.tags[VerseType.Bridge] + u'1'),
|
||||
(u'pre-chorus 1', VerseType.tags[VerseType.PreChorus] + u'1'),
|
||||
(u'different 1', VerseType.tags[VerseType.Other] + u'1'),
|
||||
(u'random 1', VerseType.tags[VerseType.Other] + u'2')]
|
||||
|
||||
# THEN: The returned value should should correlate with the input arguments
|
||||
for original_tag, openlp_tag in test_values:
|
||||
self.assertEquals(importer.to_openlp_verse_tag(original_tag), openlp_tag,
|
||||
u'SongShowPlusImport.to_openlp_verse_tag should return "%s" when called with "%s"'
|
||||
% (openlp_tag, original_tag))
|
||||
|
||||
def to_openlp_verse_tag_verse_order_test(self):
|
||||
"""
|
||||
Test to_openlp_verse_tag method by simulating adding a verse to the verse order
|
||||
"""
|
||||
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
|
||||
with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport'):
|
||||
mocked_manager = MagicMock()
|
||||
importer = SongShowPlusImport(mocked_manager)
|
||||
|
||||
# WHEN: Supplied with the following arguments replicating a verse order being added
|
||||
test_values = [(u'Verse 1', VerseType.tags[VerseType.Verse] + u'1'),
|
||||
(u'Verse 2', VerseType.tags[VerseType.Verse] + u'2'),
|
||||
(u'verse1', VerseType.tags[VerseType.Verse] + u'1'),
|
||||
(u'Verse', VerseType.tags[VerseType.Verse] + u'1'),
|
||||
(u'Verse1', VerseType.tags[VerseType.Verse] + u'1'),
|
||||
(u'chorus 1', VerseType.tags[VerseType.Chorus] + u'1'),
|
||||
(u'bridge 1', VerseType.tags[VerseType.Bridge] + u'1'),
|
||||
(u'pre-chorus 1', VerseType.tags[VerseType.PreChorus] + u'1'),
|
||||
(u'different 1', VerseType.tags[VerseType.Other] + u'1'),
|
||||
(u'random 1', VerseType.tags[VerseType.Other] + u'2'),
|
||||
(u'unused 2', None)]
|
||||
|
||||
# THEN: The returned value should should correlate with the input arguments
|
||||
for original_tag, openlp_tag in test_values:
|
||||
self.assertEquals(importer.to_openlp_verse_tag(original_tag, ignore_unique=True), openlp_tag,
|
||||
u'SongShowPlusImport.to_openlp_verse_tag should return "%s" when called with "%s"'
|
||||
% (openlp_tag, original_tag))
|
||||
|
||||
def file_import_test(self):
|
||||
"""
|
||||
Test the actual import of real song files and check that the imported data is correct.
|
||||
"""
|
||||
|
||||
# GIVEN: Test files with a mocked out SongImport class, a mocked out "manager", a mocked out "import_wizard",
|
||||
# and mocked out "author", "add_copyright", "add_verse", "finish" methods.
|
||||
with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport'):
|
||||
for song_file in SONG_TEST_DATA:
|
||||
mocked_manager = MagicMock()
|
||||
mocked_import_wizard = MagicMock()
|
||||
mocked_parse_author = MagicMock()
|
||||
mocked_add_copyright = MagicMock()
|
||||
mocked_add_verse = MagicMock()
|
||||
mocked_finish = MagicMock()
|
||||
mocked_finish.return_value = True
|
||||
importer = SongShowPlusImport(mocked_manager)
|
||||
importer.import_wizard = mocked_import_wizard
|
||||
importer.stop_import_flag = False
|
||||
importer.parse_author = mocked_parse_author
|
||||
importer.addCopyright = mocked_add_copyright
|
||||
importer.addVerse = mocked_add_verse
|
||||
importer.finish = mocked_finish
|
||||
importer.topics = []
|
||||
|
||||
# WHEN: Importing each file
|
||||
importer.import_source = [os.path.join(TEST_PATH, song_file)]
|
||||
title = SONG_TEST_DATA[song_file][u'title']
|
||||
author_calls = SONG_TEST_DATA[song_file][u'authors']
|
||||
song_copyright = SONG_TEST_DATA[song_file][u'copyright']
|
||||
ccli_number = SONG_TEST_DATA[song_file][u'ccli_number']
|
||||
add_verse_calls = SONG_TEST_DATA[song_file][u'verses']
|
||||
topics = SONG_TEST_DATA[song_file][u'topics']
|
||||
comments = SONG_TEST_DATA[song_file][u'comments']
|
||||
song_book_name = SONG_TEST_DATA[song_file][u'song_book_name']
|
||||
song_number = SONG_TEST_DATA[song_file][u'song_number']
|
||||
verse_order_list = SONG_TEST_DATA[song_file][u'verse_order_list']
|
||||
|
||||
# THEN: doImport should return none, the song data should be as expected, and finish should have been
|
||||
# called.
|
||||
self.assertIsNone(importer.doImport(), u'doImport should return None when it has completed')
|
||||
self.assertEquals(importer.title, title, u'title for %s should be "%s"' % (song_file, title))
|
||||
for author in author_calls:
|
||||
mocked_parse_author.assert_any_call(author)
|
||||
if song_copyright:
|
||||
mocked_add_copyright.assert_called_with(song_copyright)
|
||||
if ccli_number:
|
||||
self.assertEquals(importer.ccliNumber, ccli_number, u'ccliNumber for %s should be %s'
|
||||
% (song_file, ccli_number))
|
||||
for verse_text, verse_tag in add_verse_calls:
|
||||
mocked_add_verse.assert_any_call(verse_text, verse_tag)
|
||||
if topics:
|
||||
self.assertEquals(importer.topics, topics, u'topics for %s should be %s' % (song_file, topics))
|
||||
if comments:
|
||||
self.assertEquals(importer.comments, comments, u'comments for %s should be "%s"'
|
||||
% (song_file, comments))
|
||||
if song_book_name:
|
||||
self.assertEquals(importer.songBookName, song_book_name, u'songBookName for %s should be "%s"'
|
||||
% (song_file, song_book_name))
|
||||
if song_number:
|
||||
self.assertEquals(importer.songNumber, song_number, u'songNumber for %s should be %s'
|
||||
% (song_file, song_number))
|
||||
if verse_order_list:
|
||||
self.assertEquals(importer.verseOrderList, [], u'verseOrderList for %s should be %s'
|
||||
% (song_file, verse_order_list))
|
||||
mocked_finish.assert_called_with()
|
@ -36,17 +36,27 @@ class TestEditCustomForm(TestCase):
|
||||
del self.main_window
|
||||
del self.app
|
||||
|
||||
def load_themes_test(self):
|
||||
"""
|
||||
Test the load_themes() method.
|
||||
"""
|
||||
# GIVEN: A theme list.
|
||||
theme_list = [u'First Theme', u'Second Theme']
|
||||
|
||||
# WHEN: Show the dialog and add pass a theme list.
|
||||
self.form.load_themes(theme_list)
|
||||
|
||||
# THEN: There should be three items in the combo box.
|
||||
assert self.form.theme_combo_box.count() == 3, u'There should be three items (themes) in the combo box.'
|
||||
|
||||
def load_custom_test(self):
|
||||
"""
|
||||
Test the load_custom() method.
|
||||
"""
|
||||
# GIVEN: A mocked QDialog.exec_() method
|
||||
with patch(u'PyQt4.QtGui.QDialog.exec_') as mocked_exec:
|
||||
# WHEN: Show the dialog and create a new custom item.
|
||||
self.form.exec_()
|
||||
# WHEN: Create a new custom item.
|
||||
self.form.load_custom(0)
|
||||
|
||||
#THEN: The line edits should not contain any text.
|
||||
# THEN: The line edits should not contain any text.
|
||||
self.assertEqual(self.form.title_edit.text(), u'', u'The title edit should be empty')
|
||||
self.assertEqual(self.form.credit_edit.text(), u'', u'The credit edit should be empty')
|
||||
|
||||
@ -57,10 +67,10 @@ class TestEditCustomForm(TestCase):
|
||||
"""
|
||||
# GIVEN: A mocked QDialog.exec_() method
|
||||
with patch(u'PyQt4.QtGui.QDialog.exec_') as mocked_exec:
|
||||
# WHEN: Show the dialog and add a new slide.
|
||||
self.form.exec_()
|
||||
# WHEN: Add a new slide.
|
||||
QtTest.QTest.mouseClick(self.form.add_button, QtCore.Qt.LeftButton)
|
||||
#THEN: One slide should be added.
|
||||
|
||||
# THEN: One slide should be added.
|
||||
assert self.form.slide_list_view.count() == 1, u'There should be one slide added.'
|
||||
|
||||
def validate_not_valid_part1_test(self):
|
||||
|
0
tests/interfaces/openlp_plugins/remotes/__init__.py
Normal file
0
tests/interfaces/openlp_plugins/remotes/__init__.py
Normal file
138
tests/interfaces/openlp_plugins/remotes/test_server.py
Normal file
138
tests/interfaces/openlp_plugins/remotes/test_server.py
Normal file
@ -0,0 +1,138 @@
|
||||
"""
|
||||
This module contains tests for the lib submodule of the Remotes plugin.
|
||||
"""
|
||||
import os
|
||||
|
||||
from unittest import TestCase
|
||||
from tempfile import mkstemp
|
||||
from mock import MagicMock
|
||||
import urllib2
|
||||
import cherrypy
|
||||
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
|
||||
from openlp.core.lib import Settings
|
||||
from openlp.plugins.remotes.lib.httpserver import HttpServer
|
||||
from PyQt4 import QtGui
|
||||
|
||||
__default_settings__ = {
|
||||
u'remotes/twelve hour': True,
|
||||
u'remotes/port': 4316,
|
||||
u'remotes/https port': 4317,
|
||||
u'remotes/https enabled': False,
|
||||
u'remotes/user id': u'openlp',
|
||||
u'remotes/password': u'password',
|
||||
u'remotes/authentication enabled': False,
|
||||
u'remotes/ip address': u'0.0.0.0'
|
||||
}
|
||||
|
||||
|
||||
class TestRouter(TestCase):
|
||||
"""
|
||||
Test the functions in the :mod:`lib` module.
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Create the UI
|
||||
"""
|
||||
fd, self.ini_file = mkstemp(u'.ini')
|
||||
Settings().set_filename(self.ini_file)
|
||||
self.application = QtGui.QApplication.instance()
|
||||
Settings().extend_default_settings(__default_settings__)
|
||||
self.server = HttpServer()
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Delete all the C++ objects at the end so that we don't have a segfault
|
||||
"""
|
||||
del self.application
|
||||
os.unlink(self.ini_file)
|
||||
self.server.close()
|
||||
|
||||
def start_server(self):
|
||||
"""
|
||||
Common function to start server then mock out the router. CherryPy crashes if you mock before you start
|
||||
"""
|
||||
self.server.start_server()
|
||||
self.server.router = MagicMock()
|
||||
self.server.router.process_http_request = process_http_request
|
||||
|
||||
def start_default_server_test(self):
|
||||
"""
|
||||
Test the default server serves the correct initial page
|
||||
"""
|
||||
# GIVEN: A default configuration
|
||||
Settings().setValue(u'remotes/authentication enabled', False)
|
||||
self.start_server()
|
||||
|
||||
# WHEN: called the route location
|
||||
code, page = call_remote_server(u'http://localhost:4316')
|
||||
|
||||
# THEN: default title will be returned
|
||||
self.assertEqual(BeautifulSoup(page).title.text, u'OpenLP 2.1 Remote',
|
||||
u'The default menu should be returned')
|
||||
|
||||
def start_authenticating_server_test(self):
|
||||
"""
|
||||
Test the default server serves the correctly with authentication
|
||||
"""
|
||||
# GIVEN: A default authorised configuration
|
||||
Settings().setValue(u'remotes/authentication enabled', True)
|
||||
self.start_server()
|
||||
|
||||
# WHEN: called the route location with no user details
|
||||
code, page = call_remote_server(u'http://localhost:4316')
|
||||
|
||||
# THEN: then server will ask for details
|
||||
self.assertEqual(code, 401, u'The basic authorisation request should be returned')
|
||||
|
||||
# WHEN: called the route location with user details
|
||||
code, page = call_remote_server(u'http://localhost:4316', u'openlp', u'password')
|
||||
|
||||
# THEN: default title will be returned
|
||||
self.assertEqual(BeautifulSoup(page).title.text, u'OpenLP 2.1 Remote',
|
||||
u'The default menu should be returned')
|
||||
|
||||
# WHEN: called the route location with incorrect user details
|
||||
code, page = call_remote_server(u'http://localhost:4316', u'itwinkle', u'password')
|
||||
|
||||
# THEN: then server will ask for details
|
||||
self.assertEqual(code, 401, u'The basic authorisation request should be returned')
|
||||
|
||||
|
||||
def call_remote_server(url, username=None, password=None):
|
||||
"""
|
||||
Helper function
|
||||
|
||||
``username``
|
||||
The username.
|
||||
|
||||
``password``
|
||||
The password.
|
||||
"""
|
||||
if username:
|
||||
passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
||||
passman.add_password(None, url, username, password)
|
||||
authhandler = urllib2.HTTPBasicAuthHandler(passman)
|
||||
opener = urllib2.build_opener(authhandler)
|
||||
urllib2.install_opener(opener)
|
||||
try:
|
||||
page = urllib2.urlopen(url)
|
||||
return 0, page.read()
|
||||
except urllib2.HTTPError, e:
|
||||
return e.code, u''
|
||||
|
||||
|
||||
def process_http_request(url_path, *args):
|
||||
"""
|
||||
Override function to make the Mock work but does nothing.
|
||||
|
||||
``Url_path``
|
||||
The url_path.
|
||||
|
||||
``*args``
|
||||
Some args.
|
||||
"""
|
||||
cherrypy.response.status = 200
|
||||
return None
|
||||
|
0
tests/resources/remotes/openlp.crt
Normal file
0
tests/resources/remotes/openlp.crt
Normal file
0
tests/resources/remotes/openlp.key
Normal file
0
tests/resources/remotes/openlp.key
Normal file
BIN
tests/resources/songshowplussongs/Amazing Grace.sbsong
Normal file
BIN
tests/resources/songshowplussongs/Amazing Grace.sbsong
Normal file
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user