diff --git a/openlp/core/lib/mailto/LICENSE b/openlp/core/lib/mailto/LICENSE new file mode 100644 index 000000000..d8ab2d8d2 --- /dev/null +++ b/openlp/core/lib/mailto/LICENSE @@ -0,0 +1,38 @@ +PSF LICENSE AGREEMENT FOR PYTHON 2.7.1 + + 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), + and the Individual or Organization ("Licensee") accessing and otherwise + using Python 2.7.1 software in source or binary form and its associated + documentation. + 2. Subject to the terms and conditions of this License Agreement, PSF hereby + grants Licensee a nonexclusive, royalty-free, world-wide license to + reproduce, analyze, test, perform and/or display publicly, prepare + derivative works, distribute, and otherwise use Python 2.7.1 alone or in any + derivative version, provided, however, that PSF's License Agreement and + PSF's notice of copyright, i.e., "Copyright (c) 2001-2010 Python Software + Foundation; All Rights Reserved" are retained in Python 2.7.1 alone or in + any derivative version prepared by Licensee. + 3. In the event Licensee prepares a derivative work that is based on or + incorporates Python 2.7.1 or any part thereof, and wants to make the + derivative work available to others as provided herein, then Licensee hereby + agrees to include in any such work a brief summary of the changes made to + Python 2.7.1. + 4. PSF is making Python 2.7.1 available to Licensee on an "AS IS" basis. PSF + MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF + EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION + OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT + THE USE OF PYTHON 2.7.1 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 2.7.1 FOR + ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF + MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 2.7.1, OR ANY DERIVATIVE + THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + 6. This License Agreement will automatically terminate upon a material breach + of its terms and conditions. + 7. Nothing in this License Agreement shall be deemed to create any relationship + of agency, partnership, or joint venture between PSF and Licensee. This + License Agreement does not grant permission to use PSF trademarks or trade + name in a trademark sense to endorse or promote products or services of + Licensee, or any third party. + 8. By copying, installing or otherwise using Python 2.7.1, Licensee agrees to + be bound by the terms and conditions of this License Agreement. + diff --git a/openlp/core/lib/mailto/__init__.py b/openlp/core/lib/mailto/__init__.py new file mode 100644 index 000000000..102c4c94a --- /dev/null +++ b/openlp/core/lib/mailto/__init__.py @@ -0,0 +1,321 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 + +############################################################################### +# Utilities for opening files or URLs in the registered default application # +# and for sending e-mail using the user's preferred composer. # +# --------------------------------------------------------------------------- # +# Copyright (c) 2007 Antonio Valentino # +# All rights reserved. # +# --------------------------------------------------------------------------- # +# This program offered under the PSF License as published by the Python # +# Software Foundation. # +# # +# The license text can be found at http://docs.python.org/license.html # +# # +# This code is taken from: http://code.activestate.com/recipes/511443 # +# Modified for use in OpenLP # +############################################################################### + +__version__ = u'1.1' +__all__ = [u'open', u'mailto'] + +import os +import sys +import webbrowser +import subprocess + +from email.Utils import encode_rfc2231 + +_controllers = {} +_open = None + + +class BaseController(object): + """ + Base class for open program controllers. + """ + + def __init__(self, name): + self.name = name + + def open(self, filename): + raise NotImplementedError + + +class Controller(BaseController): + """ + Controller for a generic open program. + """ + + def __init__(self, *args): + super(Controller, self).__init__(os.path.basename(args[0])) + self.args = list(args) + + def _invoke(self, cmdline): + if sys.platform[:3] == u'win': + closefds = False + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + else: + closefds = True + startupinfo = None + + if (os.environ.get(u'DISPLAY') or sys.platform[:3] == u'win' or \ + sys.platform == u'darwin'): + inout = file(os.devnull, u'r+') + else: + # for TTY programs, we need stdin/out + inout = None + + # if possible, put the child precess in separate process group, + # so keyboard interrupts don't affect child precess as well as + # Python + setsid = getattr(os, u'setsid', None) + if not setsid: + setsid = getattr(os, u'setpgrp', None) + + pipe = subprocess.Popen(cmdline, stdin=inout, stdout=inout, + stderr=inout, close_fds=closefds, preexec_fn=setsid, + startupinfo=startupinfo) + + # It is assumed that this kind of tools (gnome-open, kfmclient, + # exo-open, xdg-open and open for OSX) immediately exit after lauching + # the specific application + returncode = pipe.wait() + if hasattr(self, u'fixreturncode'): + returncode = self.fixreturncode(returncode) + return not returncode + + def open(self, filename): + if isinstance(filename, basestring): + cmdline = self.args + [filename] + else: + # assume it is a sequence + cmdline = self.args + filename + try: + return self._invoke(cmdline) + except OSError: + return False + + +# Platform support for Windows +if sys.platform[:3] == u'win': + + class Start(BaseController): + """ + Controller for the win32 start progam through os.startfile. + """ + + def open(self, filename): + try: + os.startfile(filename) + except WindowsError: + # [Error 22] No application is associated with the specified + # file for this operation: '' + return False + else: + return True + + _controllers[u'windows-default'] = Start(u'start') + _open = _controllers[u'windows-default'].open + + +# Platform support for MacOS +elif sys.platform == u'darwin': + _controllers[u'open']= Controller(u'open') + _open = _controllers[u'open'].open + + +# Platform support for Unix +else: + + import commands + + # @WARNING: use the private API of the webbrowser module + from webbrowser import _iscommand + + class KfmClient(Controller): + """ + Controller for the KDE kfmclient program. + """ + + def __init__(self, kfmclient=u'kfmclient'): + super(KfmClient, self).__init__(kfmclient, u'exec') + self.kde_version = self.detect_kde_version() + + def detect_kde_version(self): + kde_version = None + try: + info = commands.getoutput(u'kfmclient --version') + + for line in info.splitlines(): + if line.startswith(u'KDE'): + kde_version = line.split(u':')[-1].strip() + break + except (OSError, RuntimeError): + pass + + return kde_version + + def fixreturncode(self, returncode): + if returncode is not None and self.kde_version > u'3.5.4': + return returncode + else: + return os.EX_OK + + def detect_desktop_environment(): + """ + Checks for known desktop environments + + Return the desktop environments name, lowercase (kde, gnome, xfce) + or "generic" + """ + + desktop_environment = u'generic' + + if os.environ.get(u'KDE_FULL_SESSION') == u'true': + desktop_environment = u'kde' + elif os.environ.get(u'GNOME_DESKTOP_SESSION_ID'): + desktop_environment = u'gnome' + else: + try: + info = commands.getoutput(u'xprop -root _DT_SAVE_MODE') + if u' = "xfce4"' in info: + desktop_environment = u'xfce' + except (OSError, RuntimeError): + pass + + return desktop_environment + + + def register_X_controllers(): + if _iscommand(u'kfmclient'): + _controllers[u'kde-open'] = KfmClient() + + for command in (u'gnome-open', u'exo-open', u'xdg-open'): + if _iscommand(command): + _controllers[command] = Controller(command) + + + def get(): + controllers_map = { + u'gnome': u'gnome-open', + u'kde': u'kde-open', + u'xfce': u'exo-open', + } + + desktop_environment = detect_desktop_environment() + + try: + controller_name = controllers_map[desktop_environment] + return _controllers[controller_name].open + + except KeyError: + if _controllers.has_key(u'xdg-open'): + return _controllers[u'xdg-open'].open + else: + return webbrowser.open + + if os.environ.get(u'DISPLAY'): + register_X_controllers() + _open = get() + + +def open(filename): + """ + Open a file or an URL in the registered default application. + """ + + return _open(filename) + + +def _fix_addersses(**kwargs): + for headername in (u'address', u'to', u'cc', u'bcc'): + try: + headervalue = kwargs[headername] + if not headervalue: + del kwargs[headername] + continue + elif not isinstance(headervalue, basestring): + # assume it is a sequence + headervalue = u','.join(headervalue) + except KeyError: + pass + except TypeError: + raise TypeError(u'string or sequence expected for "%s", %s ' + u'found' % (headername, type(headervalue).__name__)) + else: + translation_map = {u'%': u'%25', u'&': u'%26', u'?': u'%3F'} + for char, replacement in translation_map.items(): + headervalue = headervalue.replace(char, replacement) + kwargs[headername] = headervalue + + return kwargs + + +def mailto_format(**kwargs): + """ + Compile mailto string from call parameters + """ + # @TODO: implement utf8 option + + kwargs = _fix_addersses(**kwargs) + parts = [] + for headername in (u'to', u'cc', u'bcc', u'subject', u'body', u'attach'): + if kwargs.has_key(headername): + headervalue = kwargs[headername] + if not headervalue: + continue + if headername in (u'address', u'to', u'cc', u'bcc'): + parts.append(u'%s=%s' % (headername, headervalue)) + else: + headervalue = encode_rfc2231(headervalue) # @TODO: check + parts.append(u'%s=%s' % (headername, headervalue)) + + mailto_string = u'mailto:%s' % kwargs.get(u'address', '') + if parts: + mailto_string = u'%s?%s' % (mailto_string, u'&'.join(parts)) + + return mailto_string + + +def mailto(address, to=None, cc=None, bcc=None, subject=None, body=None, + attach=None): + """ + Send an e-mail using the user's preferred composer. + + Open the user's preferred e-mail composer in order to send a mail to + address(es) that must follow the syntax of RFC822. Multiple addresses + may be provided (for address, cc and bcc parameters) as separate + arguments. + + All parameters provided are used to prefill corresponding fields in + the user's e-mail composer. The user will have the opportunity to + change any of this information before actually sending the e-mail. + + ``address`` + specify the destination recipient + + ``cc`` + specify a recipient to be copied on the e-mail + + ``bcc`` + specify a recipient to be blindly copied on the e-mail + + ``subject`` + specify a subject for the e-mail + + ``body`` + specify a body for the e-mail. Since the user will be able to make + changes before actually sending the e-mail, this can be used to provide + the user with a template for the e-mail text may contain linebreaks + + ``attach`` + specify an attachment for the e-mail. file must point to an existing + file + """ + + mailto_string = mailto_format(**locals()) + return open(mailto_string) + diff --git a/openlp/core/lib/plugin.py b/openlp/core/lib/plugin.py index 37ac3d74a..f73b58735 100644 --- a/openlp/core/lib/plugin.py +++ b/openlp/core/lib/plugin.py @@ -108,7 +108,7 @@ class Plugin(QtCore.QObject): """ log.info(u'loaded') - def __init__(self, name, version=None, plugin_helpers=None): + def __init__(self, name, version=None, pluginHelpers=None): """ This is the constructor for the plugin object. This provides an easy way for descendent plugins to populate common data. This method *must* @@ -124,7 +124,7 @@ class Plugin(QtCore.QObject): ``version`` Defaults to *None*. The version of the plugin. - ``plugin_helpers`` + ``pluginHelpers`` Defaults to *None*. A list of helper objects. """ QtCore.QObject.__init__(self) @@ -139,14 +139,14 @@ class Plugin(QtCore.QObject): self.status = PluginStatus.Inactive # Set up logging self.log = logging.getLogger(self.name) - self.previewController = plugin_helpers[u'preview'] - self.liveController = plugin_helpers[u'live'] - self.renderManager = plugin_helpers[u'render'] - self.serviceManager = plugin_helpers[u'service'] - self.settingsForm = plugin_helpers[u'settings form'] - self.mediadock = plugin_helpers[u'toolbox'] - self.pluginManager = plugin_helpers[u'pluginmanager'] - self.formparent = plugin_helpers[u'formparent'] + self.previewController = pluginHelpers[u'preview'] + self.liveController = pluginHelpers[u'live'] + self.renderManager = pluginHelpers[u'render'] + self.serviceManager = pluginHelpers[u'service'] + self.settingsForm = pluginHelpers[u'settings form'] + self.mediadock = pluginHelpers[u'toolbox'] + self.pluginManager = pluginHelpers[u'pluginmanager'] + self.formparent = pluginHelpers[u'formparent'] QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'%s_add_service_item' % self.name), self.processAddServiceEvent) diff --git a/openlp/core/ui/exceptiondialog.py b/openlp/core/ui/exceptiondialog.py index 49e2c8151..03bbde764 100644 --- a/openlp/core/ui/exceptiondialog.py +++ b/openlp/core/ui/exceptiondialog.py @@ -26,7 +26,7 @@ from PyQt4 import QtCore, QtGui -from openlp.core.lib import translate +from openlp.core.lib import translate, build_icon class Ui_ExceptionDialog(object): def setupUi(self, exceptionDialog): @@ -63,12 +63,27 @@ class Ui_ExceptionDialog(object): self.exceptionButtonBox.setStandardButtons(QtGui.QDialogButtonBox.Close) self.exceptionButtonBox.setObjectName(u'exceptionButtonBox') self.exceptionLayout.addWidget(self.exceptionButtonBox) + self.sendReportButton = QtGui.QPushButton(exceptionDialog) + self.sendReportButton.setIcon(build_icon( + u':/general/general_email.png')) + self.sendReportButton.setObjectName(u'sendReportButton') + self.exceptionButtonBox.addButton(self.sendReportButton, + QtGui.QDialogButtonBox.ActionRole) + self.saveReportButton = QtGui.QPushButton(exceptionDialog) + self.saveReportButton.setIcon(build_icon(u':/general/general_save.png')) + self.saveReportButton.setObjectName(u'saveReportButton') + self.exceptionButtonBox.addButton(self.saveReportButton, + QtGui.QDialogButtonBox.ActionRole) self.retranslateUi(exceptionDialog) QtCore.QObject.connect(self.exceptionButtonBox, QtCore.SIGNAL(u'accepted()'), exceptionDialog.accept) QtCore.QObject.connect(self.exceptionButtonBox, QtCore.SIGNAL(u'rejected()'), exceptionDialog.reject) + QtCore.QObject.connect(self.sendReportButton, + QtCore.SIGNAL(u'pressed()'), self.onSendReportButtonPressed) + QtCore.QObject.connect(self.saveReportButton, + QtCore.SIGNAL(u'pressed()'), self.onSaveReportButtonPressed) QtCore.QMetaObject.connectSlotsByName(exceptionDialog) def retranslateUi(self, exceptionDialog): @@ -80,3 +95,7 @@ class Ui_ExceptionDialog(object): 'developers, so please e-mail it to bugs@openlp.org, along with a ' 'detailed description of what you were doing when the problem ' 'occurred.')) + self.sendReportButton.setText(translate('OpenLP.ExceptionDialog', + 'Send E-Mail')) + self.saveReportButton.setText(translate('OpenLP.ExceptionDialog', + 'Save to File')) diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index b40c9e063..5fa0a7dd1 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -24,7 +24,38 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### -from PyQt4 import QtGui +import re +import os +import platform + +import sqlalchemy +import BeautifulSoup +from lxml import etree +from PyQt4 import Qt, QtCore, QtGui + +try: + from PyQt4.phonon import Phonon + phonon_version = Phonon.phononVersion() +except ImportError: + phonon_version = u'-' +try: + import chardet + chardet_version = chardet.__version__ +except ImportError: + chardet_version = u'-' +try: + import enchant + enchant_version = enchant.__version__ +except ImportError: + enchant_version = u'-' +try: + import sqlite + sqlite_version = sqlite.version +except ImportError: + sqlite_version = u'-' + +from openlp.core.lib import translate, SettingsManager +from openlp.core.lib.mailto import mailto from exceptiondialog import Ui_ExceptionDialog @@ -35,3 +66,78 @@ class ExceptionForm(QtGui.QDialog, Ui_ExceptionDialog): def __init__(self, parent): QtGui.QDialog.__init__(self, parent) self.setupUi(self) + self.settingsSection = u'crashreport' + + def _createReport(self): + openlp_version = self.parent().applicationVersion[u'full'] + traceback = unicode(self.exceptionTextEdit.toPlainText()) + system = unicode(translate('OpenLP.ExceptionForm', + 'Platform: %s\n')) % platform.platform() + libraries = u'Python: %s\n' % platform.python_version() + \ + u'Qt4: %s\n' % Qt.qVersion() + \ + u'Phonon: %s\n' % phonon_version + \ + u'PyQt4: %s\n' % Qt.PYQT_VERSION_STR + \ + u'SQLAlchemy: %s\n' % sqlalchemy.__version__ + \ + u'BeautifulSoup: %s\n' % BeautifulSoup.__version__ + \ + u'lxml: %s\n' % etree.__version__ + \ + u'Chardet: %s\n' % chardet_version + \ + u'PyEnchant: %s\n' % enchant_version + \ + u'PySQLite: %s\n' % sqlite_version + if platform.system() == u'Linux': + if os.environ.get(u'KDE_FULL_SESSION') == u'true': + system = system + u'Desktop: KDE SC\n' + elif os.environ.get(u'GNOME_DESKTOP_SESSION_ID'): + system = system + u'Desktop: GNOME\n' + return (openlp_version, traceback, system, libraries) + + def onSaveReportButtonPressed(self): + """ + Saving exception log and system informations to a file. + """ + report = unicode(translate('OpenLP.ExceptionForm', + '**OpenLP Bug Report**\n' + 'Version: %s\n\n' + '--- Exception Traceback ---\n%s\n' + '--- System information ---\n%s\n' + '--- Library Versions ---\n%s\n')) + filename = QtGui.QFileDialog.getSaveFileName(self, + translate('OpenLP.ExceptionForm', 'Save Crash Report'), + SettingsManager.get_last_dir(self.settingsSection), + translate('OpenLP.ExceptionForm', 'Text files (*.txt *.log *.text)')) + if filename: + filename = unicode(QtCore.QDir.toNativeSeparators(filename)) + SettingsManager.set_last_dir(self.settingsSection, os.path.dirname( + filename)) + report = report % self._createReport() + try: + file = open(filename, u'w') + try: + file.write(report) + except UnicodeError: + file.close() + file = open(filename, u'wb') + file.write(report.encode(u'utf-8')) + file.close() + except IOError: + log.exception(u'Failed to write crash report') + + def onSendReportButtonPressed(self): + """ + Opening systems default email client and inserting exception log and + system informations. + """ + body = unicode(translate('OpenLP.ExceptionForm', + '*OpenLP Bug Report*\n' + 'Version: %s\n\n' + '--- Please enter the report below this line. ---\n\n\n' + '--- Exception Traceback ---\n%s\n' + '--- System information ---\n%s\n' + '--- Library Versions ---\n%s\n')) + content = self._createReport() + for line in content[1].split(u'\n'): + if re.search(r'[/\\]openlp[/\\]', line): + source = re.sub(r'.*[/\\]openlp[/\\](.*)".*', r'\1', line) + if u':' in line: + exception = line.split(u'\n')[-1].split(u':')[0] + subject = u'Bug report: %s in %s' % (exception, source) + mailto(address=u'bugs@openlp.org', subject=subject, body=body % content) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 3833f1697..a319e005f 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -97,16 +97,16 @@ class Ui_MainWindow(object): self.ControlSplitter.setObjectName(u'ControlSplitter') self.MainContentLayout.addWidget(self.ControlSplitter) # Create slide controllers - self.PreviewController = SlideController(self, self.settingsmanager, + self.previewController = SlideController(self, self.settingsmanager, self.screens) - self.LiveController = SlideController(self, self.settingsmanager, + self.liveController = SlideController(self, self.settingsmanager, self.screens, True) previewVisible = QtCore.QSettings().value( u'user interface/preview panel', QtCore.QVariant(True)).toBool() - self.PreviewController.Panel.setVisible(previewVisible) + self.previewController.Panel.setVisible(previewVisible) liveVisible = QtCore.QSettings().value(u'user interface/live panel', QtCore.QVariant(True)).toBool() - self.LiveController.Panel.setVisible(liveVisible) + self.liveController.Panel.setVisible(liveVisible) # Create menu self.MenuBar = QtGui.QMenuBar(MainWindow) self.MenuBar.setGeometry(QtCore.QRect(0, 0, 1087, 27)) @@ -362,8 +362,8 @@ class Ui_MainWindow(object): """ Splitter between the Preview and Live Controllers. """ - self.LiveController.widthChanged() - self.PreviewController.widthChanged() + self.liveController.widthChanged() + self.previewController.widthChanged() def retranslateUi(self, MainWindow): """ @@ -548,8 +548,8 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): self.recentFiles = QtCore.QStringList() # Set up the path with plugins pluginpath = AppLocation.get_directory(AppLocation.PluginsDir) - self.plugin_manager = PluginManager(pluginpath) - self.plugin_helpers = {} + self.pluginManager = PluginManager(pluginpath) + self.pluginHelpers = {} # Set up the interface self.setupUi(self) # Load settings after setupUi so default UI sizes are overwritten @@ -633,33 +633,33 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): self.mediaDockManager = MediaDockManager(self.MediaToolBox) log.info(u'Load Plugins') # make the controllers available to the plugins - self.plugin_helpers[u'preview'] = self.PreviewController - self.plugin_helpers[u'live'] = self.LiveController - self.plugin_helpers[u'render'] = self.renderManager - self.plugin_helpers[u'service'] = self.ServiceManagerContents - self.plugin_helpers[u'settings form'] = self.settingsForm - self.plugin_helpers[u'toolbox'] = self.mediaDockManager - self.plugin_helpers[u'pluginmanager'] = self.plugin_manager - self.plugin_helpers[u'formparent'] = self - self.plugin_manager.find_plugins(pluginpath, self.plugin_helpers) + self.pluginHelpers[u'preview'] = self.previewController + self.pluginHelpers[u'live'] = self.liveController + self.pluginHelpers[u'render'] = self.renderManager + self.pluginHelpers[u'service'] = self.ServiceManagerContents + self.pluginHelpers[u'settings form'] = self.settingsForm + self.pluginHelpers[u'toolbox'] = self.mediaDockManager + self.pluginHelpers[u'pluginmanager'] = self.pluginManager + self.pluginHelpers[u'formparent'] = self + self.pluginManager.find_plugins(pluginpath, self.pluginHelpers) # hook methods have to happen after find_plugins. Find plugins needs # the controllers hence the hooks have moved from setupUI() to here # Find and insert settings tabs log.info(u'hook settings') - self.plugin_manager.hook_settings_tabs(self.settingsForm) + self.pluginManager.hook_settings_tabs(self.settingsForm) # Find and insert media manager items log.info(u'hook media') - self.plugin_manager.hook_media_manager(self.mediaDockManager) + self.pluginManager.hook_media_manager(self.mediaDockManager) # Call the hook method to pull in import menus. log.info(u'hook menus') - self.plugin_manager.hook_import_menu(self.FileImportMenu) + self.pluginManager.hook_import_menu(self.FileImportMenu) # Call the hook method to pull in export menus. - self.plugin_manager.hook_export_menu(self.FileExportMenu) + self.pluginManager.hook_export_menu(self.FileExportMenu) # Call the hook method to pull in tools menus. - self.plugin_manager.hook_tools_menu(self.ToolsMenu) + self.pluginManager.hook_tools_menu(self.ToolsMenu) # Call the initialise method to setup plugins. log.info(u'initialise plugins') - self.plugin_manager.initialise_plugins() + self.pluginManager.initialise_plugins() # Once all components are initialised load the Themes log.info(u'Load Themes') self.ThemeManagerContents.loadThemes() @@ -695,10 +695,10 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): Show the main form, as well as the display form """ QtGui.QWidget.show(self) - self.LiveController.display.setup() - self.PreviewController.display.setup() - if self.LiveController.display.isVisible(): - self.LiveController.display.setFocus() + self.liveController.display.setup() + self.previewController.display.setup() + if self.liveController.display.isVisible(): + self.liveController.display.setFocus() self.activateWindow() if QtCore.QSettings().value( self.generalSettingsSection + u'/auto open', @@ -723,7 +723,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): settings = QtCore.QSettings() if settings.value(u'%s/screen blank' % self.generalSettingsSection, QtCore.QVariant(False)).toBool(): - self.LiveController.mainDisplaySetBackground() + self.liveController.mainDisplaySetBackground() if settings.value(u'blank warning', QtCore.QVariant(False)).toBool(): QtGui.QMessageBox.question(self, @@ -852,11 +852,11 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): QtCore.QVariant(self.MediaToolBox.currentIndex())) # Call the cleanup method to shutdown plugins. log.info(u'cleanup plugins') - self.plugin_manager.finalise_plugins() + self.pluginManager.finalise_plugins() # Save settings self.saveSettings() # Close down the display - self.LiveController.display.close() + self.liveController.display.close() def serviceChanged(self, reset=False, serviceName=None): """ @@ -910,7 +910,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): True - Visible False - Hidden """ - self.PreviewController.Panel.setVisible(visible) + self.previewController.Panel.setVisible(visible) QtCore.QSettings().setValue(u'user interface/preview panel', QtCore.QVariant(visible)) self.ViewPreviewPanel.setChecked(visible) @@ -925,7 +925,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): True - Visible False - Hidden """ - self.LiveController.Panel.setVisible(visible) + self.liveController.Panel.setVisible(visible) QtCore.QSettings().setValue(u'user interface/live panel', QtCore.QVariant(visible)) self.ViewLivePanel.setChecked(visible) diff --git a/openlp/core/ui/pluginform.py b/openlp/core/ui/pluginform.py index a857caee7..3d3a814a0 100644 --- a/openlp/core/ui/pluginform.py +++ b/openlp/core/ui/pluginform.py @@ -61,7 +61,7 @@ class PluginForm(QtGui.QDialog, Ui_PluginViewDialog): self.programaticChange = True self._clearDetails() self.programaticChange = True - for plugin in self.parent.plugin_manager.plugins: + for plugin in self.parent.pluginManager.plugins: item = QtGui.QListWidgetItem(self.pluginListWidget) # We do this just to make 100% sure the status is an integer as # sometimes when it's loaded from the config, it isn't cast to int. @@ -110,7 +110,7 @@ class PluginForm(QtGui.QDialog, Ui_PluginViewDialog): plugin_name_plural = \ self.pluginListWidget.currentItem().text().split(u' ')[0] self.activePlugin = None - for plugin in self.parent.plugin_manager.plugins: + for plugin in self.parent.pluginManager.plugins: name_string = plugin.getString(StringContent.Name) if name_string[u'plural'] == plugin_name_plural: self.activePlugin = plugin diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 85dff6b91..32e46dffb 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -880,7 +880,7 @@ class ServiceManager(QtGui.QWidget): newItem.merge(item[u'service_item']) item[u'service_item'] = newItem self.repaintServiceList(itemcount + 1, 0) - self.parent.LiveController.replaceServiceManagerItem(newItem) + self.parent.liveController.replaceServiceManagerItem(newItem) self.parent.serviceChanged(False, self.serviceName) def addServiceItem(self, item, rebuild=False, expand=None, replace=False): @@ -902,7 +902,7 @@ class ServiceManager(QtGui.QWidget): item.merge(self.serviceItems[sitem][u'service_item']) self.serviceItems[sitem][u'service_item'] = item self.repaintServiceList(sitem + 1, 0) - self.parent.LiveController.replaceServiceManagerItem(item) + self.parent.liveController.replaceServiceManagerItem(item) else: # nothing selected for dnd if self.droppos == 0: @@ -923,7 +923,7 @@ class ServiceManager(QtGui.QWidget): self.repaintServiceList(self.droppos, 0) # if rebuilding list make sure live is fixed. if rebuild: - self.parent.LiveController.replaceServiceManagerItem(item) + self.parent.liveController.replaceServiceManagerItem(item) self.droppos = 0 self.parent.serviceChanged(False, self.serviceName) @@ -933,7 +933,7 @@ class ServiceManager(QtGui.QWidget): """ item, count = self.findServiceItem() if self.serviceItems[item][u'service_item'].is_valid: - self.parent.PreviewController.addServiceManagerItem( + self.parent.previewController.addServiceManagerItem( self.serviceItems[item][u'service_item'], count) else: QtGui.QMessageBox.critical(self, @@ -957,7 +957,7 @@ class ServiceManager(QtGui.QWidget): """ item, count = self.findServiceItem() if self.serviceItems[item][u'service_item'].is_valid: - self.parent.LiveController.addServiceManagerItem( + self.parent.liveController.addServiceManagerItem( self.serviceItems[item][u'service_item'], count) if QtCore.QSettings().value( self.parent.generalSettingsSection + u'/auto preview', @@ -966,9 +966,9 @@ class ServiceManager(QtGui.QWidget): if self.serviceItems and item < len(self.serviceItems) and \ self.serviceItems[item][u'service_item'].is_capable( ItemCapabilities.AllowsPreview): - self.parent.PreviewController.addServiceManagerItem( + self.parent.previewController.addServiceManagerItem( self.serviceItems[item][u'service_item'], 0) - self.parent.LiveController.PreviewListWidget.setFocus() + self.parent.liveController.PreviewListWidget.setFocus() else: QtGui.QMessageBox.critical(self, translate('OpenLP.ServiceManager', 'Missing Display Handler'), diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index f667c6d3c..6c7fb8538 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -957,7 +957,7 @@ class SlideController(QtGui.QWidget): """ row = self.PreviewListWidget.currentRow() if row > -1 and row < self.PreviewListWidget.rowCount(): - self.parent.LiveController.addServiceManagerItem( + self.parent.liveController.addServiceManagerItem( self.serviceItem, row) def onMediaStart(self, item): diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index da8597cc0..b6e8c3d7a 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -310,7 +310,7 @@ class ThemeManager(QtGui.QWidget): translate('OpenLP.ThemeManager', 'You are unable to delete the default theme.')) else: - for plugin in self.parent.plugin_manager.plugins: + for plugin in self.parent.pluginManager.plugins: if plugin.usesTheme(theme): QtGui.QMessageBox.critical(self, translate('OpenLP.ThemeManager', 'Error'), @@ -663,7 +663,7 @@ class ThemeManager(QtGui.QWidget): (QtGui.QMessageBox.Yes | QtGui.QMessageBox.No), QtGui.QMessageBox.No) if self.saveThemeName != u'': - for plugin in self.parent.plugin_manager.plugins: + for plugin in self.parent.pluginManager.plugins: if plugin.usesTheme(self.saveThemeName): plugin.renameTheme(self.saveThemeName, name) if unicode(self.serviceComboBox.currentText()) == name: diff --git a/openlp/plugins/alerts/forms/alertform.py b/openlp/plugins/alerts/forms/alertform.py index 377a43f6c..ff5072560 100644 --- a/openlp/plugins/alerts/forms/alertform.py +++ b/openlp/plugins/alerts/forms/alertform.py @@ -62,6 +62,9 @@ class AlertForm(QtGui.QDialog, Ui_AlertDialog): QtCore.SIGNAL(u'clicked(QModelIndex)'), self.onSingleClick) def loadList(self): + """ + Loads the list with alerts. + """ self.AlertListWidget.clear() alerts = self.manager.get_all_objects(AlertItem, order_by_ref=AlertItem.text) @@ -81,12 +84,16 @@ class AlertForm(QtGui.QDialog, Ui_AlertDialog): self.close() def onDeleteClick(self): + """ + Deletes the selected item. + """ item = self.AlertListWidget.currentItem() if item: item_id = (item.data(QtCore.Qt.UserRole)).toInt()[0] self.manager.delete_object(AlertItem, item_id) row = self.AlertListWidget.row(item) self.AlertListWidget.takeItem(row) + self.item_id = None self.AlertTextEdit.setText(u'') self.SaveButton.setEnabled(False) self.DeleteButton.setEnabled(False) @@ -96,8 +103,8 @@ class AlertForm(QtGui.QDialog, Ui_AlertDialog): QtGui.QMessageBox.information(self, translate('AlertsPlugin.AlertForm', 'New Alert'), translate('AlertsPlugin.AlertForm', 'You haven\'t specified ' - 'any text for your alert. Please type in some text before ' - 'clicking New.')) + 'any text for your alert. Please type in some text before ' + 'clicking New.')) else: alert = AlertItem() alert.text = unicode(self.AlertTextEdit.text()) @@ -107,7 +114,7 @@ class AlertForm(QtGui.QDialog, Ui_AlertDialog): def onSaveClick(self): """ - Save an alert + Save the alert, we are editing. """ if self.item_id: alert = self.manager.get_object(AlertItem, self.item_id) @@ -115,14 +122,14 @@ class AlertForm(QtGui.QDialog, Ui_AlertDialog): self.manager.save_object(alert) self.item_id = None self.loadList() - else: - self.onNewClick() def onTextChanged(self): """ Enable save button when data has been changed by editing the form """ - self.SaveButton.setEnabled(True) + # Only enable the button, if we are editing an item. + if self.item_id: + self.SaveButton.setEnabled(True) def onDoubleClick(self): """ @@ -131,8 +138,8 @@ class AlertForm(QtGui.QDialog, Ui_AlertDialog): items = self.AlertListWidget.selectedIndexes() for item in items: bitem = self.AlertListWidget.item(item.row()) - self.triggerAlert(bitem.text()) - self.AlertTextEdit.setText(bitem.text()) + self.triggerAlert(unicode(bitem.text())) + self.AlertTextEdit.setText(unicode(bitem.text())) self.item_id = (bitem.data(QtCore.Qt.UserRole)).toInt()[0] self.SaveButton.setEnabled(False) self.DeleteButton.setEnabled(True) @@ -145,13 +152,45 @@ class AlertForm(QtGui.QDialog, Ui_AlertDialog): items = self.AlertListWidget.selectedIndexes() for item in items: bitem = self.AlertListWidget.item(item.row()) - self.AlertTextEdit.setText(bitem.text()) + self.AlertTextEdit.setText(unicode(bitem.text())) self.item_id = (bitem.data(QtCore.Qt.UserRole)).toInt()[0] + # If the alert does not contain '<>' we clear the ParameterEdit field. + if unicode(self.AlertTextEdit.text()).find(u'<>') == -1: + self.ParameterEdit.setText(u'') self.SaveButton.setEnabled(False) self.DeleteButton.setEnabled(True) def triggerAlert(self, text): + """ + Prepares the alert text for displaying. + + ``text`` + The alert text (unicode). + """ if text: + # We found '<>' in the alert text, but the ParameterEdit field is + # empty. + if text.find(u'<>') != -1 and not self.ParameterEdit.text() and \ + QtGui.QMessageBox.question(self, translate( + 'AlertPlugin.AlertForm', 'No Parameter found'), + translate('AlertPlugin.AlertForm', 'You have not entered a ' + 'parameter to be replaced.\nDo you want to continue ' + 'anyway?'), + QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.No | + QtGui.QMessageBox.Yes)) == QtGui.QMessageBox.No: + self.ParameterEdit.setFocus() + return False + # The ParameterEdit field is not empty, but we have not found '<>' + # in the alert text. + elif text.find(u'<>') == -1 and self.ParameterEdit.text() and \ + QtGui.QMessageBox.question(self, translate( + 'AlertPlugin.AlertForm', 'No Placeholder found'), + translate('AlertPlugin.AlertForm', 'The alert text does not' + ' contain \'<>\'.\nDo want to continue anyway?'), + QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.No | + QtGui.QMessageBox.Yes)) == QtGui.QMessageBox.No: + self.ParameterEdit.setFocus() + return False text = text.replace(u'<>', unicode(self.ParameterEdit.text())) self.parent.alertsmanager.displayAlert(text) return True diff --git a/openlp/plugins/alerts/lib/alertsmanager.py b/openlp/plugins/alerts/lib/alertsmanager.py index a418c6fec..0f2eb4ec7 100644 --- a/openlp/plugins/alerts/lib/alertsmanager.py +++ b/openlp/plugins/alerts/lib/alertsmanager.py @@ -86,7 +86,7 @@ class AlertsManager(QtCore.QObject): text = self.alertList.pop(0) alertTab = self.parent.alertsTab self.parent.liveController.display.alert(text) - # check to see if we have a timer running + # Check to see if we have a timer running. if self.timer_id == 0: self.timer_id = self.startTimer(int(alertTab.timeout) * 1000) @@ -94,9 +94,9 @@ class AlertsManager(QtCore.QObject): """ Time has finished so if our time then request the next Alert if there is one and reset the timer. + ``event`` the QT event that has been triggered. - """ log.debug(u'timer event') if event.timerId() == self.timer_id: diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index 247e5e837..b7f4a3f0b 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -120,35 +120,55 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog): order_by_ref=Author.display_name) self.AuthorsSelectionComboItem.clear() self.AuthorsSelectionComboItem.addItem(u'') + self.authors = [] for author in authors: row = self.AuthorsSelectionComboItem.count() self.AuthorsSelectionComboItem.addItem(author.display_name) self.AuthorsSelectionComboItem.setItemData( row, QtCore.QVariant(author.id)) + self.authors.append(author.display_name) + completer = QtGui.QCompleter(self.authors) + completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) + self.AuthorsSelectionComboItem.setCompleter(completer) def loadTopics(self): topics = self.manager.get_all_objects(Topic, order_by_ref=Topic.name) self.SongTopicCombo.clear() self.SongTopicCombo.addItem(u'') + self.topics = [] for topic in topics: row = self.SongTopicCombo.count() self.SongTopicCombo.addItem(topic.name) + self.topics.append(topic.name) self.SongTopicCombo.setItemData(row, QtCore.QVariant(topic.id)) + completer = QtGui.QCompleter(self.topics) + completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) + self.SongTopicCombo.setCompleter(completer) def loadBooks(self): books = self.manager.get_all_objects(Book, order_by_ref=Book.name) self.SongbookCombo.clear() self.SongbookCombo.addItem(u'') + self.books = [] for book in books: row = self.SongbookCombo.count() self.SongbookCombo.addItem(book.name) + self.books.append(book.name) self.SongbookCombo.setItemData(row, QtCore.QVariant(book.id)) + completer = QtGui.QCompleter(self.books) + completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) + self.SongbookCombo.setCompleter(completer) def loadThemes(self, theme_list): self.ThemeSelectionComboItem.clear() self.ThemeSelectionComboItem.addItem(u'') + self.themes = [] for theme in theme_list: self.ThemeSelectionComboItem.addItem(theme) + self.themes.append(theme) + completer = QtGui.QCompleter(self.themes) + completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) + self.ThemeSelectionComboItem.setCompleter(completer) def newSong(self): log.debug(u'New Song') @@ -614,12 +634,29 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog): self.saveSong(True): Receiver.send_message(u'songs_preview') + def clearCaches(self): + """ + Free up autocompletion memory on dialog exit + """ + self.authors = [] + self.themes = [] + self.books = [] + self.topics = [] + def closePressed(self): + """ + Exit Dialog and do not save + """ Receiver.send_message(u'songs_edit_clear') + self.clearCaches() self.close() def accept(self): + """ + Exit Dialog and save soong if valid + """ log.debug(u'accept') + self.clearCaches() if not self.song: self.song = Song() item = int(self.SongbookCombo.currentIndex()) @@ -644,7 +681,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog): Get all the data from the widgets on the form, and then save it to the database. - ``preview`` + ``preview`` Should be ``True`` if the song is also previewed (boolean). """ self.song.title = unicode(self.TitleEditItem.text()) diff --git a/resources/images/general_email.png b/resources/images/general_email.png new file mode 100644 index 000000000..bb3cab8f3 Binary files /dev/null and b/resources/images/general_email.png differ diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index 2a3282461..e9ec5c0a3 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -39,6 +39,7 @@ general_new.png general_open.png general_save.png + general_email.png slide_close.png