diff --git a/openlp.py b/openlp.py index 02287cd5e..9bccc526f 100755 --- a/openlp.py +++ b/openlp.py @@ -46,6 +46,7 @@ if __name__ == '__main__': """ Instantiate and run the application. """ + faulthandler.enable() set_up_fault_handling() # Add support for using multiprocessing from frozen Windows executable (built using PyInstaller), # see https://docs.python.org/3/library/multiprocessing.html#multiprocessing.freeze_support diff --git a/openlp/core/api/http/server.py b/openlp/core/api/http/server.py index fad135f2b..782940f2d 100644 --- a/openlp/core/api/http/server.py +++ b/openlp/core/api/http/server.py @@ -39,9 +39,9 @@ from openlp.core.api.http import application from openlp.core.api.poll import Poller from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import UiStrings -from openlp.core.common.mixins import OpenLPMixin, RegistryMixin +from openlp.core.common.mixins import LogMixin, RegistryProperties from openlp.core.common.path import create_paths -from openlp.core.common.registry import RegistryProperties, Registry +from openlp.core.common.registry import Registry, RegistryBase from openlp.core.common.settings import Settings from openlp.core.common.i18n import translate @@ -73,7 +73,7 @@ class HttpWorker(QtCore.QObject): pass -class HttpServer(RegistryMixin, RegistryProperties, OpenLPMixin): +class HttpServer(RegistryBase, RegistryProperties, LogMixin): """ Wrapper round a server instance """ diff --git a/openlp/core/api/poll.py b/openlp/core/api/poll.py index 5b3fb33c4..d2d36f60a 100644 --- a/openlp/core/api/poll.py +++ b/openlp/core/api/poll.py @@ -23,7 +23,7 @@ import json from openlp.core.common.httputils import get_web_page -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.core.common.settings import Settings diff --git a/openlp/core/api/websockets.py b/openlp/core/api/websockets.py index d64fdf3cc..90dca8208 100644 --- a/openlp/core/api/websockets.py +++ b/openlp/core/api/websockets.py @@ -31,8 +31,8 @@ import time from PyQt5 import QtCore -from openlp.core.common.mixins import OpenLPMixin -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import LogMixin, RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings log = logging.getLogger(__name__) @@ -61,7 +61,7 @@ class WebSocketWorker(QtCore.QObject): self.ws_server.stop = True -class WebSocketServer(RegistryProperties, OpenLPMixin): +class WebSocketServer(RegistryProperties, LogMixin): """ Wrapper round a server instance """ diff --git a/openlp/core/app.py b/openlp/core/app.py index 7c3938cfe..19943e3f0 100644 --- a/openlp/core/app.py +++ b/openlp/core/app.py @@ -38,7 +38,7 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.common import is_macosx, is_win from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import LanguageManager, UiStrings, translate -from openlp.core.common.mixins import OpenLPMixin +from openlp.core.common.mixins import LogMixin from openlp.core.common.path import create_paths, copytree from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings @@ -59,7 +59,7 @@ __all__ = ['OpenLP', 'main'] log = logging.getLogger() -class OpenLP(OpenLPMixin, QtWidgets.QApplication): +class OpenLP(QtWidgets.QApplication, LogMixin): """ The core application class. This class inherits from Qt's QApplication class in order to provide the core of the application. diff --git a/openlp/core/common/mixins.py b/openlp/core/common/mixins.py index 6a39b8f2c..1bc6907a0 100644 --- a/openlp/core/common/mixins.py +++ b/openlp/core/common/mixins.py @@ -25,25 +25,29 @@ Provide Error Handling and login Services import logging import inspect -from openlp.core.common import trace_error_handler, de_hump +from openlp.core.common import is_win, trace_error_handler from openlp.core.common.registry import Registry DO_NOT_TRACE_EVENTS = ['timerEvent', 'paintEvent', 'drag_enter_event', 'drop_event', 'on_controller_size_changed', 'preview_size_changed', 'resizeEvent'] -class OpenLPMixin(object): +class LogMixin(object): """ Base Calling object for OpenLP classes. """ - def __init__(self, *args, **kwargs): - super(OpenLPMixin, self).__init__(*args, **kwargs) - self.logger = logging.getLogger("%s.%s" % (self.__module__, self.__class__.__name__)) - if self.logger.getEffectiveLevel() == logging.DEBUG: - for name, m in inspect.getmembers(self, inspect.ismethod): - if name not in DO_NOT_TRACE_EVENTS: - if not name.startswith("_") and not name.startswith("log"): - setattr(self, name, self.logging_wrapper(m, self)) + @property + def logger(self): + if hasattr(self, '_logger') and self._logger: + return self._logger + else: + self._logger = logging.getLogger("%s.%s" % (self.__module__, self.__class__.__name__)) + if self._logger.getEffectiveLevel() == logging.DEBUG: + for name, m in inspect.getmembers(self, inspect.ismethod): + if name not in DO_NOT_TRACE_EVENTS: + if not name.startswith("_") and not name.startswith("log"): + setattr(self, name, self.logging_wrapper(m, self)) + return self._logger def logging_wrapper(self, func, parent): """ @@ -93,30 +97,127 @@ class OpenLPMixin(object): self.logger.exception(message) -class RegistryMixin(object): +class RegistryProperties(object): """ This adds registry components to classes to use at run time. """ - def __init__(self, parent): + @property + def application(self): """ - Register the class and bootstrap hooks. + Adds the openlp to the class dynamically. + Windows needs to access the application in a dynamic manner. """ - try: - super(RegistryMixin, self).__init__(parent) - except TypeError: - super(RegistryMixin, self).__init__() - Registry().register(de_hump(self.__class__.__name__), self) - Registry().register_function('bootstrap_initialise', self.bootstrap_initialise) - Registry().register_function('bootstrap_post_set_up', self.bootstrap_post_set_up) + if is_win(): + return Registry().get('application') + else: + if not hasattr(self, '_application') or not self._application: + self._application = Registry().get('application') + return self._application - def bootstrap_initialise(self): + @property + def plugin_manager(self): """ - Dummy method to be overridden + Adds the plugin manager to the class dynamically """ - pass + if not hasattr(self, '_plugin_manager') or not self._plugin_manager: + self._plugin_manager = Registry().get('plugin_manager') + return self._plugin_manager - def bootstrap_post_set_up(self): + @property + def image_manager(self): """ - Dummy method to be overridden + Adds the image manager to the class dynamically """ - pass + if not hasattr(self, '_image_manager') or not self._image_manager: + self._image_manager = Registry().get('image_manager') + return self._image_manager + + @property + def media_controller(self): + """ + Adds the media controller to the class dynamically + """ + if not hasattr(self, '_media_controller') or not self._media_controller: + self._media_controller = Registry().get('media_controller') + return self._media_controller + + @property + def service_manager(self): + """ + Adds the service manager to the class dynamically + """ + if not hasattr(self, '_service_manager') or not self._service_manager: + self._service_manager = Registry().get('service_manager') + return self._service_manager + + @property + def preview_controller(self): + """ + Adds the preview controller to the class dynamically + """ + if not hasattr(self, '_preview_controller') or not self._preview_controller: + self._preview_controller = Registry().get('preview_controller') + return self._preview_controller + + @property + def live_controller(self): + """ + Adds the live controller to the class dynamically + """ + if not hasattr(self, '_live_controller') or not self._live_controller: + self._live_controller = Registry().get('live_controller') + return self._live_controller + + @property + def main_window(self): + """ + Adds the main window to the class dynamically + """ + if not hasattr(self, '_main_window') or not self._main_window: + self._main_window = Registry().get('main_window') + return self._main_window + + @property + def renderer(self): + """ + Adds the Renderer to the class dynamically + """ + if not hasattr(self, '_renderer') or not self._renderer: + self._renderer = Registry().get('renderer') + return self._renderer + + @property + def theme_manager(self): + """ + Adds the theme manager to the class dynamically + """ + if not hasattr(self, '_theme_manager') or not self._theme_manager: + self._theme_manager = Registry().get('theme_manager') + return self._theme_manager + + @property + def settings_form(self): + """ + Adds the settings form to the class dynamically + """ + if not hasattr(self, '_settings_form') or not self._settings_form: + self._settings_form = Registry().get('settings_form') + return self._settings_form + + @property + def alerts_manager(self): + """ + Adds the alerts manager to the class dynamically + """ + if not hasattr(self, '_alerts_manager') or not self._alerts_manager: + self._alerts_manager = Registry().get('alerts_manager') + return self._alerts_manager + + @property + def projector_manager(self): + """ + Adds the projector manager to the class dynamically + """ + if not hasattr(self, '_projector_manager') or not self._projector_manager: + self._projector_manager = Registry().get('projector_manager') + return self._projector_manager diff --git a/openlp/core/common/registry.py b/openlp/core/common/registry.py index 5cbe881ba..71978ae5d 100644 --- a/openlp/core/common/registry.py +++ b/openlp/core/common/registry.py @@ -25,7 +25,7 @@ Provide Registry Services import logging import sys -from openlp.core.common import is_win, trace_error_handler +from openlp.core.common import de_hump, trace_error_handler log = logging.getLogger(__name__) @@ -61,6 +61,15 @@ class Registry(object): registry.initialising = True return registry + @classmethod + def destroy(cls): + """ + Destroy the Registry. + """ + if cls.__instance__.running_under_test: + del cls.__instance__ + cls.__instance__ = None + def get(self, key): """ Extracts the registry value from the list based on the key passed in @@ -178,128 +187,30 @@ class Registry(object): del self.working_flags[key] -class RegistryProperties(object): +class RegistryBase(object): """ This adds registry components to classes to use at run time. """ + def __init__(self, *args, **kwargs): + """ + Register the class and bootstrap hooks. + """ + try: + super().__init__(*args, **kwargs) + except TypeError: + super().__init__() + Registry().register(de_hump(self.__class__.__name__), self) + Registry().register_function('bootstrap_initialise', self.bootstrap_initialise) + Registry().register_function('bootstrap_post_set_up', self.bootstrap_post_set_up) - @property - def application(self): + def bootstrap_initialise(self): """ - Adds the openlp to the class dynamically. - Windows needs to access the application in a dynamic manner. + Dummy method to be overridden """ - if is_win(): - return Registry().get('application') - else: - if not hasattr(self, '_application') or not self._application: - self._application = Registry().get('application') - return self._application + pass - @property - def plugin_manager(self): + def bootstrap_post_set_up(self): """ - Adds the plugin manager to the class dynamically + Dummy method to be overridden """ - if not hasattr(self, '_plugin_manager') or not self._plugin_manager: - self._plugin_manager = Registry().get('plugin_manager') - return self._plugin_manager - - @property - def image_manager(self): - """ - Adds the image manager to the class dynamically - """ - if not hasattr(self, '_image_manager') or not self._image_manager: - self._image_manager = Registry().get('image_manager') - return self._image_manager - - @property - def media_controller(self): - """ - Adds the media controller to the class dynamically - """ - if not hasattr(self, '_media_controller') or not self._media_controller: - self._media_controller = Registry().get('media_controller') - return self._media_controller - - @property - def service_manager(self): - """ - Adds the service manager to the class dynamically - """ - if not hasattr(self, '_service_manager') or not self._service_manager: - self._service_manager = Registry().get('service_manager') - return self._service_manager - - @property - def preview_controller(self): - """ - Adds the preview controller to the class dynamically - """ - if not hasattr(self, '_preview_controller') or not self._preview_controller: - self._preview_controller = Registry().get('preview_controller') - return self._preview_controller - - @property - def live_controller(self): - """ - Adds the live controller to the class dynamically - """ - if not hasattr(self, '_live_controller') or not self._live_controller: - self._live_controller = Registry().get('live_controller') - return self._live_controller - - @property - def main_window(self): - """ - Adds the main window to the class dynamically - """ - if not hasattr(self, '_main_window') or not self._main_window: - self._main_window = Registry().get('main_window') - return self._main_window - - @property - def renderer(self): - """ - Adds the Renderer to the class dynamically - """ - if not hasattr(self, '_renderer') or not self._renderer: - self._renderer = Registry().get('renderer') - return self._renderer - - @property - def theme_manager(self): - """ - Adds the theme manager to the class dynamically - """ - if not hasattr(self, '_theme_manager') or not self._theme_manager: - self._theme_manager = Registry().get('theme_manager') - return self._theme_manager - - @property - def settings_form(self): - """ - Adds the settings form to the class dynamically - """ - if not hasattr(self, '_settings_form') or not self._settings_form: - self._settings_form = Registry().get('settings_form') - return self._settings_form - - @property - def alerts_manager(self): - """ - Adds the alerts manager to the class dynamically - """ - if not hasattr(self, '_alerts_manager') or not self._alerts_manager: - self._alerts_manager = Registry().get('alerts_manager') - return self._alerts_manager - - @property - def projector_manager(self): - """ - Adds the projector manager to the class dynamically - """ - if not hasattr(self, '_projector_manager') or not self._projector_manager: - self._projector_manager = Registry().get('projector_manager') - return self._projector_manager + pass diff --git a/openlp/core/display/renderer.py b/openlp/core/display/renderer.py index fd040d9c6..9dd7f3d1f 100644 --- a/openlp/core/display/renderer.py +++ b/openlp/core/display/renderer.py @@ -25,9 +25,9 @@ import re from string import Template from PyQt5 import QtGui, QtCore, QtWebKitWidgets -from openlp.core.common.mixins import OpenLPMixin, RegistryMixin +from openlp.core.common.mixins import LogMixin, RegistryProperties from openlp.core.common.path import path_to_str -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.registry import Registry, RegistryBase from openlp.core.common.settings import Settings from openlp.core.display.screens import ScreenList from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ServiceItem, expand_tags, build_chords_css, \ @@ -46,7 +46,7 @@ VERSE_FOR_LINE_COUNT = '\n'.join(map(str, range(100))) FOOTER = ['Arky Arky (Unknown)', 'Public Domain', 'CCLI 123456'] -class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties): +class Renderer(RegistryBase, LogMixin, RegistryProperties): """ Class to pull all Renderer interactions into one place. The plugins will call helper methods to do the rendering but this class will provide display defense code. diff --git a/openlp/core/lib/db.py b/openlp/core/lib/db.py index 698b1d73e..9e3c5aff9 100644 --- a/openlp/core/lib/db.py +++ b/openlp/core/lib/db.py @@ -350,6 +350,7 @@ class Manager(object): resulting in the plugin_name being used. :param upgrade_mod: The upgrade_schema function for this database """ + super().__init__() self.is_dirty = False self.session = None self.db_url = None diff --git a/tests/interfaces/openlp_core_api/__init__.py b/openlp/core/lib/json/__init__.py similarity index 100% rename from tests/interfaces/openlp_core_api/__init__.py rename to openlp/core/lib/json/__init__.py diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index dda12e10e..425fa0523 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -30,14 +30,15 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.path import Path, path_to_str, str_to_path -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.lib import ServiceItem, StringContent, ServiceItemContext -from openlp.core.lib.searchedit import SearchEdit from openlp.core.lib.ui import create_widget_action, critical_error_message_box -from openlp.core.ui.lib.filedialog import FileDialog -from openlp.core.ui.lib.listwidgetwithdnd import ListWidgetWithDnD -from openlp.core.ui.lib.toolbar import OpenLPToolbar +from openlp.core.widgets.dialogs import FileDialog +from openlp.core.widgets.edits import SearchEdit +from openlp.core.widgets.toolbar import OpenLPToolbar +from openlp.core.widgets.views import ListWidgetWithDnD log = logging.getLogger(__name__) diff --git a/openlp/core/lib/plugin.py b/openlp/core/lib/plugin.py index 059b7a314..f155b3ce7 100644 --- a/openlp/core/lib/plugin.py +++ b/openlp/core/lib/plugin.py @@ -26,9 +26,10 @@ import logging from PyQt5 import QtCore -from openlp.core.common.registry import Registry, RegistryProperties -from openlp.core.common.settings import Settings from openlp.core.common.i18n import UiStrings +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry +from openlp.core.common.settings import Settings from openlp.core.version import get_version log = logging.getLogger(__name__) diff --git a/openlp/core/lib/pluginmanager.py b/openlp/core/lib/pluginmanager.py index 1bdb020aa..061788b25 100644 --- a/openlp/core/lib/pluginmanager.py +++ b/openlp/core/lib/pluginmanager.py @@ -26,12 +26,12 @@ import os from openlp.core.common import extension_loader from openlp.core.common.applocation import AppLocation -from openlp.core.common.registry import RegistryProperties -from openlp.core.common.mixins import OpenLPMixin, RegistryMixin +from openlp.core.common.mixins import LogMixin, RegistryProperties +from openlp.core.common.registry import RegistryBase from openlp.core.lib import Plugin, PluginStatus -class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties): +class PluginManager(RegistryBase, LogMixin, RegistryProperties): """ This is the Plugin manager, which loads all the plugins, and executes all the hooks, as and when necessary. diff --git a/openlp/core/lib/projector/db.py b/openlp/core/lib/projector/db.py index 223159a51..fa8934ae2 100644 --- a/openlp/core/lib/projector/db.py +++ b/openlp/core/lib/projector/db.py @@ -49,7 +49,7 @@ from openlp.core.lib.projector import upgrade Base = declarative_base(MetaData()) -class CommonBase(object): +class CommonMixin(object): """ Base class to automate table name and ID column. """ @@ -60,7 +60,7 @@ class CommonBase(object): id = Column(Integer, primary_key=True) -class Manufacturer(CommonBase, Base): +class Manufacturer(Base, CommonMixin): """ Projector manufacturer table. @@ -85,7 +85,7 @@ class Manufacturer(CommonBase, Base): lazy='joined') -class Model(CommonBase, Base): +class Model(Base, CommonMixin): """ Projector model table. @@ -113,7 +113,7 @@ class Model(CommonBase, Base): lazy='joined') -class Source(CommonBase, Base): +class Source(Base, CommonMixin): """ Projector video source table. @@ -140,7 +140,7 @@ class Source(CommonBase, Base): text = Column(String(30)) -class Projector(CommonBase, Base): +class Projector(Base, CommonMixin): """ Projector table. @@ -213,7 +213,7 @@ class Projector(CommonBase, Base): lazy='joined') -class ProjectorSource(CommonBase, Base): +class ProjectorSource(Base, CommonMixin): """ Projector local source table This table allows mapping specific projector source input to a local diff --git a/openlp/core/lib/projector/pjlink.py b/openlp/core/lib/projector/pjlink.py index 2eb4da32c..2272b971f 100644 --- a/openlp/core/lib/projector/pjlink.py +++ b/openlp/core/lib/projector/pjlink.py @@ -514,7 +514,7 @@ class PJLinkCommands(object): self.sw_version_received = data -class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): +class PJLink(QtNetwork.QTcpSocket, PJLinkCommands): """ Socket service for PJLink TCP socket. """ diff --git a/openlp/core/lib/searchedit.py b/openlp/core/lib/searchedit.py deleted file mode 100644 index db45486f9..000000000 --- a/openlp/core/lib/searchedit.py +++ /dev/null @@ -1,175 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2017 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### - -import logging - -from PyQt5 import QtCore, QtWidgets - -from openlp.core.common.settings import Settings -from openlp.core.lib import build_icon -from openlp.core.lib.ui import create_widget_action - -log = logging.getLogger(__name__) - - -class SearchEdit(QtWidgets.QLineEdit): - """ - This is a specialised QLineEdit with a "clear" button inside for searches. - """ - searchTypeChanged = QtCore.pyqtSignal(QtCore.QVariant) - cleared = QtCore.pyqtSignal() - - def __init__(self, parent, settings_section): - """ - Constructor. - """ - super().__init__(parent) - self.settings_section = settings_section - self._current_search_type = -1 - self.clear_button = QtWidgets.QToolButton(self) - self.clear_button.setIcon(build_icon(':/system/clear_shortcut.png')) - self.clear_button.setCursor(QtCore.Qt.ArrowCursor) - self.clear_button.setStyleSheet('QToolButton { border: none; padding: 0px; }') - self.clear_button.resize(18, 18) - self.clear_button.hide() - self.clear_button.clicked.connect(self._on_clear_button_clicked) - self.textChanged.connect(self._on_search_edit_text_changed) - self._update_style_sheet() - self.setAcceptDrops(False) - - def _update_style_sheet(self): - """ - Internal method to update the stylesheet depending on which widgets are available and visible. - """ - frame_width = self.style().pixelMetric(QtWidgets.QStyle.PM_DefaultFrameWidth) - right_padding = self.clear_button.width() + frame_width - if hasattr(self, 'menu_button'): - left_padding = self.menu_button.width() - stylesheet = 'QLineEdit {{ padding-left:{left}px; padding-right: {right}px; }} '.format(left=left_padding, - right=right_padding) - else: - stylesheet = 'QLineEdit {{ padding-right: {right}px; }} '.format(right=right_padding) - self.setStyleSheet(stylesheet) - msz = self.minimumSizeHint() - self.setMinimumSize(max(msz.width(), self.clear_button.width() + (frame_width * 2) + 2), - max(msz.height(), self.clear_button.height() + (frame_width * 2) + 2)) - - def resizeEvent(self, event): - """ - Reimplemented method to react to resizing of the widget. - - :param event: The event that happened. - """ - size = self.clear_button.size() - frame_width = self.style().pixelMetric(QtWidgets.QStyle.PM_DefaultFrameWidth) - self.clear_button.move(self.rect().right() - frame_width - size.width(), - (self.rect().bottom() + 1 - size.height()) // 2) - if hasattr(self, 'menu_button'): - size = self.menu_button.size() - self.menu_button.move(self.rect().left() + frame_width + 2, (self.rect().bottom() + 1 - size.height()) // 2) - - def current_search_type(self): - """ - Readonly property to return the current search type. - """ - return self._current_search_type - - def set_current_search_type(self, identifier): - """ - Set a new current search type. - - :param identifier: The search type identifier (int). - """ - menu = self.menu_button.menu() - for action in menu.actions(): - if identifier == action.data(): - self.setPlaceholderText(action.placeholder_text) - self.menu_button.setDefaultAction(action) - self._current_search_type = identifier - Settings().setValue('{section}/last used search type'.format(section=self.settings_section), identifier) - self.searchTypeChanged.emit(identifier) - return True - - def set_search_types(self, items): - """ - A list of tuples to be used in the search type menu. The first item in the list will be preselected as the - default. - - :param items: The list of tuples to use. The tuples should contain an integer identifier, an icon (QIcon - instance or string) and a title for the item in the menu. In short, they should look like this:: - - (, , , <place holder text>) - - For instance:: - - (1, <QIcon instance>, "Titles", "Search Song Titles...") - - Or:: - - (2, ":/songs/authors.png", "Authors", "Search Authors...") - """ - menu = QtWidgets.QMenu(self) - for identifier, icon, title, placeholder in items: - action = create_widget_action( - menu, text=title, icon=icon, data=identifier, triggers=self._on_menu_action_triggered) - action.placeholder_text = placeholder - if not hasattr(self, 'menu_button'): - self.menu_button = QtWidgets.QToolButton(self) - self.menu_button.setIcon(build_icon(':/system/clear_shortcut.png')) - self.menu_button.setCursor(QtCore.Qt.ArrowCursor) - self.menu_button.setPopupMode(QtWidgets.QToolButton.InstantPopup) - self.menu_button.setStyleSheet('QToolButton { border: none; padding: 0px 10px 0px 0px; }') - self.menu_button.resize(QtCore.QSize(28, 18)) - self.menu_button.setMenu(menu) - self.set_current_search_type( - Settings().value('{section}/last used search type'.format(section=self.settings_section))) - self.menu_button.show() - self._update_style_sheet() - - def _on_search_edit_text_changed(self, text): - """ - Internally implemented slot to react to when the text in the line edit has changed so that we can show or hide - the clear button. - - :param text: A :class:`~PyQt5.QtCore.QString` instance which represents the text in the line edit. - """ - self.clear_button.setVisible(bool(text)) - - def _on_clear_button_clicked(self): - """ - Internally implemented slot to react to the clear button being clicked to clear the line edit. Once it has - cleared the line edit, it emits the ``cleared()`` signal so that an application can react to the clearing of the - line edit. - """ - self.clear() - self.cleared.emit() - - def _on_menu_action_triggered(self): - """ - Internally implemented slot to react to the select of one of the search types in the menu. Once it has set the - correct action on the button, and set the current search type (using the list of identifiers provided by the - developer), the ``searchTypeChanged(int)`` signal is emitted with the identifier. - """ - for action in self.menu_button.menu().actions(): - # Why is this needed? - action.setChecked(False) - self.set_current_search_type(self.sender().data()) diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index 22b1f9b5f..3a824d424 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -35,7 +35,7 @@ from PyQt5 import QtGui from openlp.core.common import md5_hash from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import translate -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.core.common.settings import Settings from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, expand_chords diff --git a/openlp/core/lib/settingstab.py b/openlp/core/lib/settingstab.py index 4b259465f..06009ee3d 100644 --- a/openlp/core/lib/settingstab.py +++ b/openlp/core/lib/settingstab.py @@ -25,7 +25,7 @@ own tab to the settings dialog. """ from PyQt5 import QtWidgets -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties class SettingsTab(QtWidgets.QWidget, RegistryProperties): diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index 9581dc6f9..2d434c0c6 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -32,8 +32,9 @@ from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import UiStrings, format_time, translate from openlp.core.common.settings import Settings from openlp.core.lib import SettingsTab, build_icon -from openlp.core.ui.lib import PathEdit, PathType from openlp.core.ui.style import HAS_DARK_STYLE +from openlp.core.widgets.edits import PathEdit +from openlp.core.widgets.enums import PathEditType log = logging.getLogger(__name__) @@ -122,7 +123,7 @@ class AdvancedTab(SettingsTab): self.data_directory_layout.setObjectName('data_directory_layout') self.data_directory_new_label = QtWidgets.QLabel(self.data_directory_group_box) self.data_directory_new_label.setObjectName('data_directory_current_label') - self.data_directory_path_edit = PathEdit(self.data_directory_group_box, path_type=PathType.Directories, + self.data_directory_path_edit = PathEdit(self.data_directory_group_box, path_type=PathEditType.Directories, default_path=AppLocation.get_directory(AppLocation.DataDir)) self.data_directory_layout.addRow(self.data_directory_new_label, self.data_directory_path_edit) self.new_data_directory_has_files_label = QtWidgets.QLabel(self.data_directory_group_box) diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index 1ceeed989..70fe2c416 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -72,10 +72,10 @@ except ImportError: from openlp.core.common import is_linux from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.core.common.settings import Settings from openlp.core.ui.exceptiondialog import Ui_ExceptionDialog -from openlp.core.ui.lib.filedialog import FileDialog +from openlp.core.widgets.dialogs import FileDialog from openlp.core.version import get_version diff --git a/openlp/core/ui/filerenameform.py b/openlp/core/ui/filerenameform.py index d6a519240..249ea1f10 100644 --- a/openlp/core/ui/filerenameform.py +++ b/openlp/core/ui/filerenameform.py @@ -25,7 +25,8 @@ The file rename dialog. from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import translate -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.ui.filerenamedialog import Ui_FileRenameDialog diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index ea98577c8..37fac2dd4 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -38,7 +38,8 @@ from openlp.core.common import clean_button_text, trace_error_handler from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import translate from openlp.core.common.path import Path, create_paths -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.lib import PluginStatus, build_icon from openlp.core.lib.ui import critical_error_message_box diff --git a/openlp/core/ui/generaltab.py b/openlp/core/ui/generaltab.py index 8488f13a1..a908ac91d 100644 --- a/openlp/core/ui/generaltab.py +++ b/openlp/core/ui/generaltab.py @@ -33,7 +33,8 @@ from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.display.screens import ScreenList from openlp.core.lib import SettingsTab -from openlp.core.ui.lib import ColorButton, PathEdit +from openlp.core.widgets.buttons import ColorButton +from openlp.core.widgets.edits import PathEdit log = logging.getLogger(__name__) diff --git a/openlp/core/ui/lib/historycombobox.py b/openlp/core/ui/lib/historycombobox.py deleted file mode 100644 index 6320bc383..000000000 --- a/openlp/core/ui/lib/historycombobox.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2017 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### -""" -The :mod:`~openlp.core.ui.lib.historycombobox` module contains the HistoryComboBox widget -""" - -from PyQt5 import QtCore, QtWidgets - - -class HistoryComboBox(QtWidgets.QComboBox): - """ - The :class:`~openlp.core.common.historycombobox.HistoryComboBox` widget emulates the QLineEdit ``returnPressed`` - signal for when the :kbd:`Enter` or :kbd:`Return` keys are pressed, and saves anything that is typed into the edit - box into its list. - """ - returnPressed = QtCore.pyqtSignal() - - def __init__(self, parent=None): - """ - Initialise the combo box, setting duplicates to False and the insert policy to insert items at the top. - - :param parent: The parent widget - """ - super().__init__(parent) - self.setDuplicatesEnabled(False) - self.setEditable(True) - self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) - self.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop) - - def keyPressEvent(self, event): - """ - Override the inherited keyPressEvent method to emit the ``returnPressed`` signal and to save the current text to - the dropdown list. - - :param event: The keyboard event - """ - # Handle Enter and Return ourselves - if event.key() == QtCore.Qt.Key_Enter or event.key() == QtCore.Qt.Key_Return: - # Emit the returnPressed signal - self.returnPressed.emit() - # Save the current text to the dropdown list - if self.currentText() and self.findText(self.currentText()) == -1: - self.insertItem(0, self.currentText()) - # Let the parent handle any keypress events - super().keyPressEvent(event) - - def focusOutEvent(self, event): - """ - Override the inherited focusOutEvent to save the current text to the dropdown list. - - :param event: The focus event - """ - # Save the current text to the dropdown list - if self.currentText() and self.findText(self.currentText()) == -1: - self.insertItem(0, self.currentText()) - # Let the parent handle any keypress events - super().focusOutEvent(event) - - def getItems(self): - """ - Get all the items from the history - - :return: A list of strings - """ - return [self.itemText(i) for i in range(self.count())] diff --git a/openlp/core/ui/lib/listwidgetwithdnd.py b/openlp/core/ui/lib/listwidgetwithdnd.py deleted file mode 100755 index 5648ff8f4..000000000 --- a/openlp/core/ui/lib/listwidgetwithdnd.py +++ /dev/null @@ -1,153 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2017 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### -""" -Extend QListWidget to handle drag and drop functionality -""" -import os - -from PyQt5 import QtCore, QtGui, QtWidgets - -from openlp.core.common.i18n import UiStrings -from openlp.core.common.registry import Registry - - -class ListWidgetWithDnD(QtWidgets.QListWidget): - """ - Provide a list widget to store objects and handle drag and drop events - """ - def __init__(self, parent=None, name=''): - """ - Initialise the list widget - """ - super().__init__(parent) - self.mime_data_text = name - self.no_results_text = UiStrings().NoResults - self.setSpacing(1) - self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) - self.setAlternatingRowColors(True) - self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - - def activateDnD(self): - """ - Activate DnD of widget - """ - self.setAcceptDrops(True) - self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) - Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file) - - def clear(self, search_while_typing=False): - """ - Re-implement clear, so that we can customise feedback when using 'Search as you type' - - :param search_while_typing: True if we want to display the customised message - :return: None - """ - if search_while_typing: - self.no_results_text = UiStrings().ShortResults - else: - self.no_results_text = UiStrings().NoResults - super().clear() - - def mouseMoveEvent(self, event): - """ - Drag and drop event does not care what data is selected as the recipient will use events to request the data - move just tell it what plugin to call - """ - if event.buttons() != QtCore.Qt.LeftButton: - event.ignore() - return - if not self.selectedItems(): - event.ignore() - return - drag = QtGui.QDrag(self) - mime_data = QtCore.QMimeData() - drag.setMimeData(mime_data) - mime_data.setText(self.mime_data_text) - drag.exec(QtCore.Qt.CopyAction) - - def dragEnterEvent(self, event): - """ - When something is dragged into this object, check if you should be able to drop it in here. - """ - if event.mimeData().hasUrls(): - event.accept() - else: - event.ignore() - - def dragMoveEvent(self, event): - """ - Make an object droppable, and set it to copy the contents of the object, not move it. - """ - if event.mimeData().hasUrls(): - event.setDropAction(QtCore.Qt.CopyAction) - event.accept() - else: - event.ignore() - - def dropEvent(self, event): - """ - Receive drop event check if it is a file and process it if it is. - - :param event: Handle of the event pint passed - """ - if event.mimeData().hasUrls(): - event.setDropAction(QtCore.Qt.CopyAction) - event.accept() - files = [] - for url in event.mimeData().urls(): - local_file = os.path.normpath(url.toLocalFile()) - if os.path.isfile(local_file): - files.append(local_file) - elif os.path.isdir(local_file): - listing = os.listdir(local_file) - for file in listing: - files.append(os.path.join(local_file, file)) - Registry().execute('{mime_data}_dnd'.format(mime_data=self.mime_data_text), - {'files': files, 'target': self.itemAt(event.pos())}) - else: - event.ignore() - - def allItems(self): - """ - An generator to list all the items in the widget - - :return: a generator - """ - for row in range(self.count()): - yield self.item(row) - - def paintEvent(self, event): - """ - Re-implement paintEvent so that we can add 'No Results' text when the listWidget is empty. - - :param event: A QPaintEvent - :return: None - """ - super().paintEvent(event) - if not self.count(): - viewport = self.viewport() - painter = QtGui.QPainter(viewport) - font = QtGui.QFont() - font.setItalic(True) - painter.setFont(font) - painter.drawText(QtCore.QRect(0, 0, viewport.width(), viewport.height()), - (QtCore.Qt.AlignHCenter | QtCore.Qt.TextWordWrap), self.no_results_text) diff --git a/openlp/core/ui/lib/pathedit.py b/openlp/core/ui/lib/pathedit.py deleted file mode 100644 index 7b28c47ba..000000000 --- a/openlp/core/ui/lib/pathedit.py +++ /dev/null @@ -1,196 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2017 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### -from enum import Enum - -from PyQt5 import QtCore, QtWidgets - -from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.path import Path, path_to_str, str_to_path -from openlp.core.lib import build_icon -from openlp.core.ui.lib.filedialog import FileDialog - - -class PathType(Enum): - Files = 1 - Directories = 2 - - -class PathEdit(QtWidgets.QWidget): - """ - The :class:`~openlp.core.ui.lib.pathedit.PathEdit` class subclasses QWidget to create a custom widget for use when - a file or directory needs to be selected. - """ - pathChanged = QtCore.pyqtSignal(Path) - - def __init__(self, parent=None, path_type=PathType.Files, default_path=None, dialog_caption=None, show_revert=True): - """ - Initialise the PathEdit widget - - :param QtWidget.QWidget | None: The parent of the widget. This is just passed to the super method. - :param str dialog_caption: Used to customise the caption in the QFileDialog. - :param openlp.core.common.path.Path default_path: The default path. This is set as the path when the revert - button is clicked - :param bool show_revert: Used to determine if the 'revert button' should be visible. - :rtype: None - """ - super().__init__(parent) - self.default_path = default_path - self.dialog_caption = dialog_caption - self._path_type = path_type - self._path = None - self.filters = '{all_files} (*)'.format(all_files=UiStrings().AllFiles) - self._setup(show_revert) - - def _setup(self, show_revert): - """ - Set up the widget - :param bool show_revert: Show or hide the revert button - :rtype: None - """ - widget_layout = QtWidgets.QHBoxLayout() - widget_layout.setContentsMargins(0, 0, 0, 0) - self.line_edit = QtWidgets.QLineEdit(self) - widget_layout.addWidget(self.line_edit) - self.browse_button = QtWidgets.QToolButton(self) - self.browse_button.setIcon(build_icon(':/general/general_open.png')) - widget_layout.addWidget(self.browse_button) - self.revert_button = QtWidgets.QToolButton(self) - self.revert_button.setIcon(build_icon(':/general/general_revert.png')) - self.revert_button.setVisible(show_revert) - widget_layout.addWidget(self.revert_button) - self.setLayout(widget_layout) - # Signals and Slots - self.browse_button.clicked.connect(self.on_browse_button_clicked) - self.revert_button.clicked.connect(self.on_revert_button_clicked) - self.line_edit.editingFinished.connect(self.on_line_edit_editing_finished) - self.update_button_tool_tips() - - @property - def path(self): - """ - A property getter method to return the selected path. - - :return: The selected path - :rtype: openlp.core.common.path.Path - """ - return self._path - - @path.setter - def path(self, path): - """ - A Property setter method to set the selected path - - :param openlp.core.common.path.Path path: The path to set the widget to - :rtype: None - """ - self._path = path - text = path_to_str(path) - self.line_edit.setText(text) - self.line_edit.setToolTip(text) - - @property - def path_type(self): - """ - A property getter method to return the path_type. Path type allows you to sepecify if the user is restricted to - selecting a file or directory. - - :return: The type selected - :rtype: PathType - """ - return self._path_type - - @path_type.setter - def path_type(self, path_type): - """ - A Property setter method to set the path type - - :param PathType path_type: The type of path to select - :rtype: None - """ - self._path_type = path_type - self.update_button_tool_tips() - - def update_button_tool_tips(self): - """ - Called to update the tooltips on the buttons. This is changing path types, and when the widget is initalised - - :rtype: None - """ - if self._path_type == PathType.Directories: - self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for directory.')) - self.revert_button.setToolTip(translate('OpenLP.PathEdit', 'Revert to default directory.')) - else: - self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for file.')) - self.revert_button.setToolTip(translate('OpenLP.PathEdit', 'Revert to default file.')) - - def on_browse_button_clicked(self): - """ - A handler to handle a click on the browse button. - - Show the QFileDialog and process the input from the user - - :rtype: None - """ - caption = self.dialog_caption - path = None - if self._path_type == PathType.Directories: - if not caption: - caption = translate('OpenLP.PathEdit', 'Select Directory') - path = FileDialog.getExistingDirectory(self, caption, self._path, FileDialog.ShowDirsOnly) - elif self._path_type == PathType.Files: - if not caption: - caption = self.dialog_caption = translate('OpenLP.PathEdit', 'Select File') - path, filter_used = FileDialog.getOpenFileName(self, caption, self._path, self.filters) - if path: - self.on_new_path(path) - - def on_revert_button_clicked(self): - """ - A handler to handle a click on the revert button. - - Set the new path to the value of the default_path instance variable. - - :rtype: None - """ - self.on_new_path(self.default_path) - - def on_line_edit_editing_finished(self): - """ - A handler to handle when the line edit has finished being edited. - - :rtype: None - """ - path = str_to_path(self.line_edit.text()) - self.on_new_path(path) - - def on_new_path(self, path): - """ - A method called to validate and set a new path. - - Emits the pathChanged Signal - - :param openlp.core.common.path.Path path: The new path - :rtype: None - """ - if self._path != path: - self.path = path - self.pathChanged.emit(path) diff --git a/openlp/core/ui/lib/spelltextedit.py b/openlp/core/ui/lib/spelltextedit.py deleted file mode 100644 index d0fc25af2..000000000 --- a/openlp/core/ui/lib/spelltextedit.py +++ /dev/null @@ -1,205 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2017 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### -""" -The :mod:`~openlp.core.lib.spelltextedit` module contains a classes to add spell checking to an edit widget. -""" - -import logging -import re - -try: - import enchant - from enchant import DictNotFoundError - from enchant.errors import Error - ENCHANT_AVAILABLE = True -except ImportError: - ENCHANT_AVAILABLE = False - -# based on code from http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check - -from PyQt5 import QtCore, QtGui, QtWidgets - -from openlp.core.common.i18n import translate -from openlp.core.lib import FormattingTags -from openlp.core.lib.ui import create_action - -log = logging.getLogger(__name__) - - -class SpellTextEdit(QtWidgets.QPlainTextEdit): - """ - Spell checking widget based on QPlanTextEdit. - """ - def __init__(self, parent=None, formatting_tags_allowed=True): - """ - Constructor. - """ - global ENCHANT_AVAILABLE - super(SpellTextEdit, self).__init__(parent) - self.formatting_tags_allowed = formatting_tags_allowed - # Default dictionary based on the current locale. - if ENCHANT_AVAILABLE: - try: - self.dictionary = enchant.Dict() - self.highlighter = Highlighter(self.document()) - self.highlighter.spelling_dictionary = self.dictionary - except (Error, DictNotFoundError): - ENCHANT_AVAILABLE = False - log.debug('Could not load default dictionary') - - def mousePressEvent(self, event): - """ - Handle mouse clicks within the text edit region. - """ - if event.button() == QtCore.Qt.RightButton: - # Rewrite the mouse event to a left button event so the cursor is moved to the location of the pointer. - event = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, - event.pos(), QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier) - QtWidgets.QPlainTextEdit.mousePressEvent(self, event) - - def contextMenuEvent(self, event): - """ - Provide the context menu for the text edit region. - """ - popup_menu = self.createStandardContextMenu() - # Select the word under the cursor. - cursor = self.textCursor() - # only select text if not already selected - if not cursor.hasSelection(): - cursor.select(QtGui.QTextCursor.WordUnderCursor) - self.setTextCursor(cursor) - # Add menu with available languages. - if ENCHANT_AVAILABLE: - lang_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Language:')) - for lang in enchant.list_languages(): - action = create_action(lang_menu, lang, text=lang, checked=lang == self.dictionary.tag) - lang_menu.addAction(action) - popup_menu.insertSeparator(popup_menu.actions()[0]) - popup_menu.insertMenu(popup_menu.actions()[0], lang_menu) - lang_menu.triggered.connect(self.set_language) - # Check if the selected word is misspelled and offer spelling suggestions if it is. - if ENCHANT_AVAILABLE and self.textCursor().hasSelection(): - text = self.textCursor().selectedText() - if not self.dictionary.check(text): - spell_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Spelling Suggestions')) - for word in self.dictionary.suggest(text): - action = SpellAction(word, spell_menu) - action.correct.connect(self.correct_word) - spell_menu.addAction(action) - # Only add the spelling suggests to the menu if there are suggestions. - if spell_menu.actions(): - popup_menu.insertMenu(popup_menu.actions()[0], spell_menu) - tag_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Formatting Tags')) - if self.formatting_tags_allowed: - for html in FormattingTags.get_html_tags(): - action = SpellAction(html['desc'], tag_menu) - action.correct.connect(self.html_tag) - tag_menu.addAction(action) - popup_menu.insertSeparator(popup_menu.actions()[0]) - popup_menu.insertMenu(popup_menu.actions()[0], tag_menu) - popup_menu.exec(event.globalPos()) - - def set_language(self, action): - """ - Changes the language for this spelltextedit. - - :param action: The action. - """ - self.dictionary = enchant.Dict(action.text()) - self.highlighter.spelling_dictionary = self.dictionary - self.highlighter.highlightBlock(self.toPlainText()) - self.highlighter.rehighlight() - - def correct_word(self, word): - """ - Replaces the selected text with word. - """ - cursor = self.textCursor() - cursor.beginEditBlock() - cursor.removeSelectedText() - cursor.insertText(word) - cursor.endEditBlock() - - def html_tag(self, tag): - """ - Replaces the selected text with word. - """ - tag = tag.replace('&', '') - for html in FormattingTags.get_html_tags(): - if tag == html['desc']: - cursor = self.textCursor() - if self.textCursor().hasSelection(): - text = cursor.selectedText() - cursor.beginEditBlock() - cursor.removeSelectedText() - cursor.insertText(html['start tag']) - cursor.insertText(text) - cursor.insertText(html['end tag']) - cursor.endEditBlock() - else: - cursor = self.textCursor() - cursor.insertText(html['start tag']) - cursor.insertText(html['end tag']) - - -class Highlighter(QtGui.QSyntaxHighlighter): - """ - Provides a text highlighter for pointing out spelling errors in text. - """ - WORDS = r'(?iu)[\w\']+' - - def __init__(self, *args): - """ - Constructor - """ - super(Highlighter, self).__init__(*args) - self.spelling_dictionary = None - - def highlightBlock(self, text): - """ - Highlight mis spelt words in a block of text. - - Note, this is a Qt hook. - """ - if not self.spelling_dictionary: - return - text = str(text) - char_format = QtGui.QTextCharFormat() - char_format.setUnderlineColor(QtCore.Qt.red) - char_format.setUnderlineStyle(QtGui.QTextCharFormat.SpellCheckUnderline) - for word_object in re.finditer(self.WORDS, text): - if not self.spelling_dictionary.check(word_object.group()): - self.setFormat(word_object.start(), word_object.end() - word_object.start(), char_format) - - -class SpellAction(QtWidgets.QAction): - """ - A special QAction that returns the text in a signal. - """ - correct = QtCore.pyqtSignal(str) - - def __init__(self, *args): - """ - Constructor - """ - super(SpellAction, self).__init__(*args) - self.triggered.connect(lambda x: self.correct.emit(self.text())) diff --git a/openlp/core/ui/lib/treewidgetwithdnd.py b/openlp/core/ui/lib/treewidgetwithdnd.py deleted file mode 100644 index 792fa8ab8..000000000 --- a/openlp/core/ui/lib/treewidgetwithdnd.py +++ /dev/null @@ -1,145 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2017 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### -""" -Extend QTreeWidget to handle drag and drop functionality -""" -import os - -from PyQt5 import QtCore, QtGui, QtWidgets - -from openlp.core.common import is_win -from openlp.core.common.registry import Registry - - -class TreeWidgetWithDnD(QtWidgets.QTreeWidget): - """ - Provide a tree widget to store objects and handle drag and drop events - """ - def __init__(self, parent=None, name=''): - """ - Initialise the tree widget - """ - super(TreeWidgetWithDnD, self).__init__(parent) - self.mime_data_text = name - self.allow_internal_dnd = False - self.header().close() - self.default_indentation = self.indentation() - self.setIndentation(0) - self.setAnimated(True) - - def activateDnD(self): - """ - Activate DnD of widget - """ - self.setAcceptDrops(True) - self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) - Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file) - Registry().register_function(('%s_dnd_internal' % self.mime_data_text), self.parent().dnd_move_internal) - - def mouseMoveEvent(self, event): - """ - Drag and drop event does not care what data is selected as the recipient will use events to request the data - move just tell it what plugin to call - - :param event: The event that occurred - """ - if event.buttons() != QtCore.Qt.LeftButton: - event.ignore() - return - if not self.selectedItems(): - event.ignore() - return - drag = QtGui.QDrag(self) - mime_data = QtCore.QMimeData() - drag.setMimeData(mime_data) - mime_data.setText(self.mime_data_text) - drag.exec(QtCore.Qt.CopyAction) - - def dragEnterEvent(self, event): - """ - Receive drag enter event, check if it is a file or internal object and allow it if it is. - - :param event: The event that occurred - """ - if event.mimeData().hasUrls(): - event.accept() - elif self.allow_internal_dnd: - event.accept() - else: - event.ignore() - - def dragMoveEvent(self, event): - """ - Receive drag move event, check if it is a file or internal object and allow it if it is. - - :param event: The event that occurred - """ - QtWidgets.QTreeWidget.dragMoveEvent(self, event) - if event.mimeData().hasUrls(): - event.setDropAction(QtCore.Qt.CopyAction) - event.accept() - elif self.allow_internal_dnd: - event.setDropAction(QtCore.Qt.CopyAction) - event.accept() - else: - event.ignore() - - def dropEvent(self, event): - """ - Receive drop event, check if it is a file or internal object and process it if it is. - - :param event: Handle of the event pint passed - """ - # If we are on Windows, OpenLP window will not be set on top. For example, user can drag images to Library and - # the folder stays on top of the group creation box. This piece of code fixes this issue. - if is_win(): - self.setWindowState(self.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) - self.setWindowState(QtCore.Qt.WindowNoState) - if event.mimeData().hasUrls(): - event.setDropAction(QtCore.Qt.CopyAction) - event.accept() - files = [] - for url in event.mimeData().urls(): - local_file = url.toLocalFile() - if os.path.isfile(local_file): - files.append(local_file) - elif os.path.isdir(local_file): - listing = os.listdir(local_file) - for file_name in listing: - files.append(os.path.join(local_file, file_name)) - Registry().execute('%s_dnd' % self.mime_data_text, {'files': files, 'target': self.itemAt(event.pos())}) - elif self.allow_internal_dnd: - event.setDropAction(QtCore.Qt.CopyAction) - event.accept() - Registry().execute('%s_dnd_internal' % self.mime_data_text, self.itemAt(event.pos())) - else: - event.ignore() - - # Convenience methods for emulating a QListWidget. This helps keeping MediaManagerItem simple. - def addItem(self, item): - self.addTopLevelItem(item) - - def count(self): - return self.topLevelItemCount() - - def item(self, index): - return self.topLevelItem(index) diff --git a/openlp/core/ui/maindisplay.py b/openlp/core/ui/maindisplay.py index d7f2264ee..ca634910e 100644 --- a/openlp/core/ui/maindisplay.py +++ b/openlp/core/ui/maindisplay.py @@ -36,9 +36,9 @@ from PyQt5 import QtCore, QtWidgets, QtWebKit, QtWebKitWidgets, QtGui, QtMultime from openlp.core.common import is_macosx, is_win from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import translate -from openlp.core.common.mixins import OpenLPMixin +from openlp.core.common.mixins import LogMixin, RegistryProperties from openlp.core.common.path import path_to_str -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.display.screens import ScreenList from openlp.core.lib import ServiceItem, ImageSource, build_html, expand_tags, image_to_byte @@ -131,7 +131,7 @@ class Display(QtWidgets.QGraphicsView): self.web_loaded = True -class MainDisplay(OpenLPMixin, Display, RegistryProperties): +class MainDisplay(Display, LogMixin, RegistryProperties): """ This is the display screen as a specialized class from the Display class """ @@ -603,7 +603,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): self.web_view.setGeometry(0, 0, self.width(), self.height()) -class AudioPlayer(OpenLPMixin, QtCore.QObject): +class AudioPlayer(LogMixin, QtCore.QObject): """ This Class will play audio only allowing components to work with a soundtrack independent of the user interface. """ diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 26a8b921a..5fa9b2a2f 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -41,7 +41,8 @@ from openlp.core.common.actions import ActionList, CategoryOrder from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import LanguageManager, UiStrings, translate from openlp.core.common.path import Path, copyfile, create_paths, path_to_str, str_to_path -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.display.screens import ScreenList from openlp.core.display.renderer import Renderer @@ -50,9 +51,8 @@ from openlp.core.lib.ui import create_action from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \ ShortcutListForm, FormattingTagForm, PreviewController from openlp.core.ui.firsttimeform import FirstTimeForm -from openlp.core.ui.lib.dockwidget import OpenLPDockWidget -from openlp.core.ui.lib.filedialog import FileDialog -from openlp.core.ui.lib.mediadockmanager import MediaDockManager +from openlp.core.widgets.dialogs import FileDialog +from openlp.core.widgets.docks import OpenLPDockWidget, MediaDockManager from openlp.core.ui.media import MediaController from openlp.core.ui.printserviceform import PrintServiceForm from openlp.core.ui.projector.manager import ProjectorManager diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index bdc315556..c72d6669d 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -31,8 +31,8 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.api.http import register_endpoint from openlp.core.common import extension_loader from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.mixins import OpenLPMixin, RegistryMixin -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import LogMixin, RegistryProperties +from openlp.core.common.registry import Registry, RegistryBase from openlp.core.common.settings import Settings from openlp.core.lib import ItemCapabilities from openlp.core.lib.ui import critical_error_message_box @@ -42,7 +42,7 @@ from openlp.core.ui.media.vendor.mediainfoWrapper import MediaInfoWrapper from openlp.core.ui.media.mediaplayer import MediaPlayer from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players,\ parse_optical_path -from openlp.core.ui.lib.toolbar import OpenLPToolbar +from openlp.core.widgets.toolbar import OpenLPToolbar log = logging.getLogger(__name__) @@ -91,7 +91,7 @@ class MediaSlider(QtWidgets.QSlider): QtWidgets.QSlider.mouseReleaseEvent(self, event) -class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): +class MediaController(RegistryBase, LogMixin, RegistryProperties): """ The implementation of the Media Controller. The Media Controller adds an own class for every Player. Currently these are QtWebkit, Phonon and Vlc. display_controllers are an array of controllers keyed on the diff --git a/openlp/core/ui/media/mediaplayer.py b/openlp/core/ui/media/mediaplayer.py index e4d210513..77d089c89 100644 --- a/openlp/core/ui/media/mediaplayer.py +++ b/openlp/core/ui/media/mediaplayer.py @@ -22,7 +22,7 @@ """ The :mod:`~openlp.core.ui.media.mediaplayer` module contains the MediaPlayer class. """ -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.core.ui.media import MediaState diff --git a/openlp/core/ui/media/playertab.py b/openlp/core/ui/media/playertab.py index f719a167b..28d7798ee 100644 --- a/openlp/core/ui/media/playertab.py +++ b/openlp/core/ui/media/playertab.py @@ -23,6 +23,7 @@ The :mod:`~openlp.core.ui.media.playertab` module holds the configuration tab for the media stuff. """ import platform + from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import UiStrings, translate @@ -31,7 +32,7 @@ from openlp.core.common.settings import Settings from openlp.core.lib import SettingsTab from openlp.core.lib.ui import create_button from openlp.core.ui.media import get_media_players, set_media_players -from openlp.core.ui.lib.colorbutton import ColorButton +from openlp.core.widgets.buttons import ColorButton class MediaQCheckBox(QtWidgets.QCheckBox): diff --git a/openlp/core/ui/pluginform.py b/openlp/core/ui/pluginform.py index b34d79714..43fa5e68d 100644 --- a/openlp/core/ui/pluginform.py +++ b/openlp/core/ui/pluginform.py @@ -27,7 +27,7 @@ import logging from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import translate -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.core.lib import PluginStatus from openlp.core.ui.plugindialog import Ui_PluginViewDialog diff --git a/openlp/core/ui/printservicedialog.py b/openlp/core/ui/printservicedialog.py index d71b42cdb..ee3b07080 100644 --- a/openlp/core/ui/printservicedialog.py +++ b/openlp/core/ui/printservicedialog.py @@ -26,7 +26,7 @@ from PyQt5 import QtCore, QtWidgets, QtPrintSupport from openlp.core.common.i18n import UiStrings, translate from openlp.core.lib import build_icon -from openlp.core.ui.lib import SpellTextEdit +from openlp.core.widgets.edits import SpellTextEdit class ZoomSize(object): diff --git a/openlp/core/ui/printserviceform.py b/openlp/core/ui/printserviceform.py index 482d0f084..07ab24496 100644 --- a/openlp/core/ui/printserviceform.py +++ b/openlp/core/ui/printserviceform.py @@ -30,7 +30,8 @@ from PyQt5 import QtCore, QtGui, QtWidgets, QtPrintSupport from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.lib import get_text_file_string from openlp.core.ui.printservicedialog import Ui_PrintServiceDialog, ZoomSize diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 47bbb832e..0770886e4 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -20,9 +20,9 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ - :mod: openlp.core.ui.projector.manager` module +:mod: openlp.core.ui.projector.manager` module - Provides the functions for the display/control of Projectors. +Provides the functions for the display/control of Projectors. """ import logging @@ -30,8 +30,8 @@ import logging from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common.i18n import translate -from openlp.core.common.mixins import OpenLPMixin, RegistryMixin -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import LogMixin, RegistryProperties +from openlp.core.common.registry import RegistryBase from openlp.core.common.settings import Settings from openlp.core.lib.ui import create_widget_action from openlp.core.lib.projector import DialogSourceStyle @@ -40,9 +40,9 @@ from openlp.core.lib.projector.constants import ERROR_MSG, ERROR_STRING, E_AUTHE S_INITIALIZE, S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP from openlp.core.lib.projector.db import ProjectorDB from openlp.core.lib.projector.pjlink import PJLink, PJLinkUDP -from openlp.core.ui.lib import OpenLPToolbar from openlp.core.ui.projector.editform import ProjectorEditForm from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle +from openlp.core.widgets.toolbar import OpenLPToolbar log = logging.getLogger(__name__) log.debug('projectormanager loaded') @@ -276,7 +276,7 @@ class UiProjectorManager(object): self.update_icons() -class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjectorManager, RegistryProperties): +class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogMixin, RegistryProperties): """ Manage the projectors. """ @@ -288,7 +288,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto :param projectordb: Database session inherited from superclass. """ log.debug('__init__()') - super().__init__(parent) + super(ProjectorManager, self).__init__(parent) self.settings_section = 'projector' self.projectordb = projectordb self.projector_list = [] diff --git a/openlp/core/ui/serviceitemeditform.py b/openlp/core/ui/serviceitemeditform.py index 17f648ecc..0a4b7cab6 100644 --- a/openlp/core/ui/serviceitemeditform.py +++ b/openlp/core/ui/serviceitemeditform.py @@ -24,7 +24,8 @@ The service item edit dialog """ from PyQt5 import QtCore, QtWidgets -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.ui.serviceitemeditdialog import Ui_ServiceItemEditDialog diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index ff6ab9a47..d13d7879d 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -36,15 +36,15 @@ from openlp.core.common import ThemeLevel, split_filename, delete_file from openlp.core.common.actions import ActionList, CategoryOrder from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import UiStrings, format_time, translate -from openlp.core.common.mixins import OpenLPMixin, RegistryMixin +from openlp.core.common.mixins import LogMixin, RegistryProperties from openlp.core.common.path import Path, create_paths, path_to_str, str_to_path -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.registry import Registry, RegistryBase from openlp.core.common.settings import Settings from openlp.core.lib import ServiceItem, ItemCapabilities, PluginStatus, build_icon from openlp.core.lib.ui import critical_error_message_box, create_widget_action, find_and_set_in_combo_box from openlp.core.ui import ServiceNoteForm, ServiceItemEditForm, StartTimeForm -from openlp.core.ui.lib import OpenLPToolbar -from openlp.core.ui.lib.filedialog import FileDialog +from openlp.core.widgets.dialogs import FileDialog +from openlp.core.widgets.toolbar import OpenLPToolbar class ServiceManagerList(QtWidgets.QTreeWidget): @@ -56,8 +56,24 @@ class ServiceManagerList(QtWidgets.QTreeWidget): Constructor """ super(ServiceManagerList, self).__init__(parent) + self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) + self.setAlternatingRowColors(True) + self.setHeaderHidden(True) + self.setExpandsOnDoubleClick(False) self.service_manager = service_manager + def dragEnterEvent(self, event): + """ + React to a drag enter event + """ + event.accept() + + def dragMoveEvent(self, event): + """ + React to a drage move event + """ + event.accept() + def keyPressEvent(self, event): """ Capture Key press and respond accordingly. @@ -117,7 +133,7 @@ class Ui_ServiceManager(object): self.layout.setSpacing(0) self.layout.setContentsMargins(0, 0, 0, 0) # Create the top toolbar - self.toolbar = OpenLPToolbar(widget) + self.toolbar = OpenLPToolbar(self) self.toolbar.add_toolbar_action('newService', text=UiStrings().NewService, icon=':/general/general_new.png', tooltip=UiStrings().CreateService, triggers=self.on_new_service_clicked) self.toolbar.add_toolbar_action('openService', text=UiStrings().OpenService, @@ -147,73 +163,67 @@ class Ui_ServiceManager(object): QtWidgets.QAbstractItemView.CurrentChanged | QtWidgets.QAbstractItemView.DoubleClicked | QtWidgets.QAbstractItemView.EditKeyPressed) - self.service_manager_list.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) - self.service_manager_list.setAlternatingRowColors(True) - self.service_manager_list.setHeaderHidden(True) - self.service_manager_list.setExpandsOnDoubleClick(False) self.service_manager_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.service_manager_list.customContextMenuRequested.connect(self.context_menu) self.service_manager_list.setObjectName('service_manager_list') # enable drop - self.service_manager_list.__class__.dragEnterEvent = lambda x, event: event.accept() - self.service_manager_list.__class__.dragMoveEvent = lambda x, event: event.accept() - self.service_manager_list.__class__.dropEvent = self.drop_event + self.service_manager_list.dropEvent = self.drop_event self.layout.addWidget(self.service_manager_list) # Add the bottom toolbar self.order_toolbar = OpenLPToolbar(widget) action_list = ActionList.get_instance() action_list.add_category(UiStrings().Service, CategoryOrder.standard_toolbar) - self.service_manager_list.move_top = self.order_toolbar.add_toolbar_action( + self.move_top_action = self.order_toolbar.add_toolbar_action( 'moveTop', text=translate('OpenLP.ServiceManager', 'Move to &top'), icon=':/services/service_top.png', tooltip=translate('OpenLP.ServiceManager', 'Move item to the top of the service.'), can_shortcuts=True, category=UiStrings().Service, triggers=self.on_service_top) - self.service_manager_list.move_up = self.order_toolbar.add_toolbar_action( + self.move_up_action = self.order_toolbar.add_toolbar_action( 'moveUp', text=translate('OpenLP.ServiceManager', 'Move &up'), icon=':/services/service_up.png', tooltip=translate('OpenLP.ServiceManager', 'Move item up one position in the service.'), can_shortcuts=True, category=UiStrings().Service, triggers=self.on_service_up) - self.service_manager_list.move_down = self.order_toolbar.add_toolbar_action( + self.move_down_action = self.order_toolbar.add_toolbar_action( 'moveDown', text=translate('OpenLP.ServiceManager', 'Move &down'), icon=':/services/service_down.png', tooltip=translate('OpenLP.ServiceManager', 'Move item down one position in the service.'), can_shortcuts=True, category=UiStrings().Service, triggers=self.on_service_down) - self.service_manager_list.move_bottom = self.order_toolbar.add_toolbar_action( + self.move_bottom_action = self.order_toolbar.add_toolbar_action( 'moveBottom', text=translate('OpenLP.ServiceManager', 'Move to &bottom'), icon=':/services/service_bottom.png', tooltip=translate('OpenLP.ServiceManager', 'Move item to the end of the service.'), can_shortcuts=True, category=UiStrings().Service, triggers=self.on_service_end) - self.service_manager_list.down = self.order_toolbar.add_toolbar_action( + self.down_action = self.order_toolbar.add_toolbar_action( 'down', text=translate('OpenLP.ServiceManager', 'Move &down'), can_shortcuts=True, tooltip=translate('OpenLP.ServiceManager', 'Moves the selection down the window.'), visible=False, triggers=self.on_move_selection_down) - action_list.add_action(self.service_manager_list.down) - self.service_manager_list.up = self.order_toolbar.add_toolbar_action( + action_list.add_action(self.down_action) + self.up_action = self.order_toolbar.add_toolbar_action( 'up', text=translate('OpenLP.ServiceManager', 'Move up'), can_shortcuts=True, tooltip=translate('OpenLP.ServiceManager', 'Moves the selection up the window.'), visible=False, triggers=self.on_move_selection_up) - action_list.add_action(self.service_manager_list.up) + action_list.add_action(self.up_action) self.order_toolbar.addSeparator() - self.service_manager_list.delete = self.order_toolbar.add_toolbar_action( + self.delete_action = self.order_toolbar.add_toolbar_action( 'delete', can_shortcuts=True, text=translate('OpenLP.ServiceManager', '&Delete From Service'), icon=':/general/general_delete.png', tooltip=translate('OpenLP.ServiceManager', 'Delete the selected item from the service.'), triggers=self.on_delete_from_service) self.order_toolbar.addSeparator() - self.service_manager_list.expand = self.order_toolbar.add_toolbar_action( + self.expand_action = self.order_toolbar.add_toolbar_action( 'expand', can_shortcuts=True, text=translate('OpenLP.ServiceManager', '&Expand all'), icon=':/services/service_expand_all.png', tooltip=translate('OpenLP.ServiceManager', 'Expand all the service items.'), category=UiStrings().Service, triggers=self.on_expand_all) - self.service_manager_list.collapse = self.order_toolbar.add_toolbar_action( + self.collapse_action = self.order_toolbar.add_toolbar_action( 'collapse', can_shortcuts=True, text=translate('OpenLP.ServiceManager', '&Collapse all'), icon=':/services/service_collapse_all.png', tooltip=translate('OpenLP.ServiceManager', 'Collapse all the service items.'), category=UiStrings().Service, triggers=self.on_collapse_all) self.order_toolbar.addSeparator() - self.service_manager_list.make_live = self.order_toolbar.add_toolbar_action( + self.make_live_action = self.order_toolbar.add_toolbar_action( 'make_live', can_shortcuts=True, text=translate('OpenLP.ServiceManager', 'Go Live'), icon=':/general/general_live.png', tooltip=translate('OpenLP.ServiceManager', 'Send the selected item to Live.'), @@ -254,7 +264,7 @@ class Ui_ServiceManager(object): icon=':/media/auto-start_active.png', triggers=self.on_auto_start) # Add already existing delete action to the menu. - self.menu.addAction(self.service_manager_list.delete) + self.menu.addAction(self.delete_action) self.create_custom_action = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', 'Create New &Custom ' 'Slide'), @@ -285,28 +295,20 @@ class Ui_ServiceManager(object): self.preview_action = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', 'Show &Preview'), icon=':/general/general_preview.png', triggers=self.make_preview) # Add already existing make live action to the menu. - self.menu.addAction(self.service_manager_list.make_live) + self.menu.addAction(self.make_live_action) self.menu.addSeparator() self.theme_menu = QtWidgets.QMenu(translate('OpenLP.ServiceManager', '&Change Item Theme')) self.menu.addMenu(self.theme_menu) - self.service_manager_list.addActions( - [self.service_manager_list.move_down, - self.service_manager_list.move_up, - self.service_manager_list.make_live, - self.service_manager_list.move_top, - self.service_manager_list.move_bottom, - self.service_manager_list.up, - self.service_manager_list.down, - self.service_manager_list.expand, - self.service_manager_list.collapse - ]) + self.service_manager_list.addActions([self.move_down_action, self.move_up_action, self.make_live_action, + self.move_top_action, self.move_bottom_action, self.up_action, + self.down_action, self.expand_action, self.collapse_action]) Registry().register_function('theme_update_list', self.update_theme_list) Registry().register_function('config_screen_changed', self.regenerate_service_items) Registry().register_function('theme_update_global', self.theme_change) Registry().register_function('mediaitem_suffix_reset', self.reset_supported_suffixes) -class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceManager, RegistryProperties): +class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixin, RegistryProperties): """ Manages the services. This involves taking text strings from plugins and adding them to the service. This service can then be zipped up with all the resources used into one OSZ or oszl file for use on any OpenLP v2 installation. @@ -320,7 +322,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa """ Sets up the service manager, toolbars, list view, et al. """ - super(ServiceManager, self).__init__(parent) + super().__init__(parent) self.active = build_icon(':/media/auto-start_active.png') self.inactive = build_icon(':/media/auto-start_inactive.png') self.service_items = [] diff --git a/openlp/core/ui/servicenoteform.py b/openlp/core/ui/servicenoteform.py index 998431636..179248570 100644 --- a/openlp/core/ui/servicenoteform.py +++ b/openlp/core/ui/servicenoteform.py @@ -25,9 +25,10 @@ The :mod:`~openlp.core.ui.servicenoteform` module contains the `ServiceNoteForm` from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import translate -from openlp.core.common.registry import Registry, RegistryProperties -from openlp.core.ui.lib import SpellTextEdit +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.lib.ui import create_button_box +from openlp.core.widgets.edits import SpellTextEdit class ServiceNoteForm(QtWidgets.QDialog, RegistryProperties): diff --git a/openlp/core/ui/settingsform.py b/openlp/core/ui/settingsform.py index 5524b89ad..e7a86bac2 100644 --- a/openlp/core/ui/settingsform.py +++ b/openlp/core/ui/settingsform.py @@ -27,7 +27,8 @@ import logging from PyQt5 import QtCore, QtWidgets from openlp.core.api import ApiTab -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.lib import build_icon from openlp.core.ui import AdvancedTab, GeneralTab, ThemesTab from openlp.core.ui.media import PlayerTab diff --git a/openlp/core/ui/shortcutlistform.py b/openlp/core/ui/shortcutlistform.py index 3db127d75..92a0e789f 100644 --- a/openlp/core/ui/shortcutlistform.py +++ b/openlp/core/ui/shortcutlistform.py @@ -29,7 +29,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common.actions import ActionList from openlp.core.common.i18n import translate -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.core.common.settings import Settings from openlp.core.ui.shortcutlistdialog import Ui_ShortcutListDialog diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 52e200886..80eb155e0 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -22,7 +22,6 @@ """ The :mod:`slidecontroller` module contains the most important part of OpenLP - the slide controller """ - import copy import os from collections import deque @@ -33,15 +32,15 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import SlideLimits from openlp.core.common.actions import ActionList, CategoryOrder from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.mixins import OpenLPMixin, RegistryMixin -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import LogMixin, RegistryProperties +from openlp.core.common.registry import Registry, RegistryBase from openlp.core.common.settings import Settings from openlp.core.display.screens import ScreenList from openlp.core.lib import ItemCapabilities, ServiceItem, ImageSource, ServiceItemAction, build_icon, build_html from openlp.core.lib.ui import create_action -from openlp.core.ui.lib.toolbar import OpenLPToolbar -from openlp.core.ui.lib.listpreviewwidget import ListPreviewWidget from openlp.core.ui import HideMode, MainDisplay, Display, DisplayControllerType +from openlp.core.widgets.toolbar import OpenLPToolbar +from openlp.core.widgets.views import ListPreviewWidget # Threshold which has to be trespassed to toggle. @@ -82,11 +81,11 @@ class DisplayController(QtWidgets.QWidget): """ Controller is a general display controller widget. """ - def __init__(self, parent): + def __init__(self, *args, **kwargs): """ Set up the general Controller. """ - super(DisplayController, self).__init__(parent) + super().__init__(*args, **kwargs) self.is_live = False self.display = None self.controller_type = None @@ -133,16 +132,16 @@ class InfoLabel(QtWidgets.QLabel): super().setText(text) -class SlideController(DisplayController, RegistryProperties): +class SlideController(DisplayController, LogMixin, RegistryProperties): """ SlideController is the slide controller widget. This widget is what the user uses to control the displaying of verses/slides/etc on the screen. """ - def __init__(self, parent): + def __init__(self, *args, **kwargs): """ Set up the Slide Controller. """ - super(SlideController, self).__init__(parent) + super().__init__(*args, **kwargs) def post_set_up(self): """ @@ -1505,7 +1504,7 @@ class SlideController(DisplayController, RegistryProperties): self.display.audio_player.go_to(action.data()) -class PreviewController(RegistryMixin, OpenLPMixin, SlideController): +class PreviewController(RegistryBase, SlideController): """ Set up the Preview Controller. """ @@ -1513,11 +1512,12 @@ class PreviewController(RegistryMixin, OpenLPMixin, SlideController): slidecontroller_preview_next = QtCore.pyqtSignal() slidecontroller_preview_previous = QtCore.pyqtSignal() - def __init__(self, parent): + def __init__(self, *args, **kwargs): """ Set up the base Controller as a preview. """ - super(PreviewController, self).__init__(parent) + self.__registry_name = 'preview_slidecontroller' + super().__init__(*args, **kwargs) self.split = 0 self.type_prefix = 'preview' self.category = 'Preview Toolbar' @@ -1529,7 +1529,7 @@ class PreviewController(RegistryMixin, OpenLPMixin, SlideController): self.post_set_up() -class LiveController(RegistryMixin, OpenLPMixin, SlideController): +class LiveController(RegistryBase, SlideController): """ Set up the Live Controller. """ @@ -1541,11 +1541,11 @@ class LiveController(RegistryMixin, OpenLPMixin, SlideController): mediacontroller_live_pause = QtCore.pyqtSignal() mediacontroller_live_stop = QtCore.pyqtSignal() - def __init__(self, parent): + def __init__(self, *args, **kwargs): """ Set up the base Controller as a live. """ - super(LiveController, self).__init__(parent) + super().__init__(*args, **kwargs) self.is_live = True self.split = 1 self.type_prefix = 'live' diff --git a/openlp/core/ui/starttimeform.py b/openlp/core/ui/starttimeform.py index e1ad9d9a6..00d0bff40 100644 --- a/openlp/core/ui/starttimeform.py +++ b/openlp/core/ui/starttimeform.py @@ -25,7 +25,8 @@ The actual start time form. from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui.starttimedialog import Ui_StartTimeDialog diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index c15110569..f8449986a 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -28,7 +28,8 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import get_images_filter, is_not_image_file from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.lib.theme import BackgroundType, BackgroundGradientType from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui import ThemeLayoutForm diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index f27eb6abb..5b4c5cbb9 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -31,17 +31,17 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import delete_file from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import UiStrings, translate, get_locale_key -from openlp.core.common.mixins import OpenLPMixin, RegistryMixin +from openlp.core.common.mixins import LogMixin, RegistryProperties from openlp.core.common.path import Path, copyfile, create_paths, path_to_str, rmtree -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.registry import Registry, RegistryBase from openlp.core.common.settings import Settings from openlp.core.lib import ImageSource, ValidationError, get_text_file_string, build_icon, \ check_item_selected, create_thumb, validate_thumb from openlp.core.lib.theme import Theme, BackgroundType from openlp.core.lib.ui import critical_error_message_box, create_widget_action from openlp.core.ui import FileRenameForm, ThemeForm -from openlp.core.ui.lib import OpenLPToolbar -from openlp.core.ui.lib.filedialog import FileDialog +from openlp.core.widgets.dialogs import FileDialog +from openlp.core.widgets.toolbar import OpenLPToolbar class Ui_ThemeManager(object): @@ -125,7 +125,7 @@ class Ui_ThemeManager(object): self.theme_list_widget.currentItemChanged.connect(self.check_list_state) -class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManager, RegistryProperties): +class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, RegistryProperties): """ Manages the orders of Theme. """ diff --git a/openlp/core/ui/themewizard.py b/openlp/core/ui/themewizard.py index c50cd1a85..d7bc5f822 100644 --- a/openlp/core/ui/themewizard.py +++ b/openlp/core/ui/themewizard.py @@ -29,7 +29,8 @@ from openlp.core.common.i18n import UiStrings, translate from openlp.core.lib import build_icon from openlp.core.lib.theme import HorizontalType, BackgroundType, BackgroundGradientType from openlp.core.lib.ui import add_welcome_page, create_valign_selection_widgets -from openlp.core.ui.lib import ColorButton, PathEdit +from openlp.core.widgets.buttons import ColorButton +from openlp.core.widgets.edits import PathEdit class Ui_ThemeWizard(object): diff --git a/tests/interfaces/openlp_core_common/__init__.py b/openlp/core/widgets/__init__.py similarity index 100% rename from tests/interfaces/openlp_core_common/__init__.py rename to openlp/core/widgets/__init__.py diff --git a/openlp/core/ui/lib/colorbutton.py b/openlp/core/widgets/buttons.py similarity index 100% rename from openlp/core/ui/lib/colorbutton.py rename to openlp/core/widgets/buttons.py diff --git a/openlp/core/ui/lib/filedialog.py b/openlp/core/widgets/dialogs.py similarity index 100% rename from openlp/core/ui/lib/filedialog.py rename to openlp/core/widgets/dialogs.py diff --git a/openlp/core/ui/lib/mediadockmanager.py b/openlp/core/widgets/docks.py similarity index 76% rename from openlp/core/ui/lib/mediadockmanager.py rename to openlp/core/widgets/docks.py index 8fee1d55d..a1b4e9789 100644 --- a/openlp/core/ui/lib/mediadockmanager.py +++ b/openlp/core/widgets/docks.py @@ -20,15 +20,41 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -The media manager dock. +The :mod:`~openlp.core.widgets.docks` module contains a customised base dock widget and dock widgets """ import logging -from openlp.core.lib import StringContent +from PyQt5 import QtWidgets + +from openlp.core.display.screens import ScreenList +from openlp.core.lib import StringContent, build_icon log = logging.getLogger(__name__) +class OpenLPDockWidget(QtWidgets.QDockWidget): + """ + Custom DockWidget class to handle events + """ + def __init__(self, parent=None, name=None, icon=None): + """ + Initialise the DockWidget + """ + log.debug('Initialise the %s widget' % name) + super(OpenLPDockWidget, self).__init__(parent) + if name: + self.setObjectName(name) + if icon: + self.setWindowIcon(build_icon(icon)) + # Sort out the minimum width. + screens = ScreenList() + main_window_docbars = screens.current['size'].width() // 5 + if main_window_docbars > 300: + self.setMinimumWidth(300) + else: + self.setMinimumWidth(main_window_docbars) + + class MediaDockManager(object): """ Provide a repository for MediaManagerItems diff --git a/openlp/core/widgets/edits.py b/openlp/core/widgets/edits.py new file mode 100644 index 000000000..c2396810f --- /dev/null +++ b/openlp/core/widgets/edits.py @@ -0,0 +1,573 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`~openlp.core.widgets.edits` module contains all the customised edit widgets used in OpenLP +""" +import logging +import re + +from PyQt5 import QtCore, QtGui, QtWidgets + +from openlp.core.common.i18n import UiStrings, translate +from openlp.core.common.path import Path, path_to_str, str_to_path +from openlp.core.common.settings import Settings +from openlp.core.lib import FormattingTags, build_icon +from openlp.core.lib.ui import create_widget_action, create_action +from openlp.core.widgets.dialogs import FileDialog +from openlp.core.widgets.enums import PathEditType + +try: + import enchant + from enchant import DictNotFoundError + from enchant.errors import Error + ENCHANT_AVAILABLE = True +except ImportError: + ENCHANT_AVAILABLE = False + +log = logging.getLogger(__name__) + + +class SearchEdit(QtWidgets.QLineEdit): + """ + This is a specialised QLineEdit with a "clear" button inside for searches. + """ + searchTypeChanged = QtCore.pyqtSignal(QtCore.QVariant) + cleared = QtCore.pyqtSignal() + + def __init__(self, parent, settings_section): + """ + Constructor. + """ + super().__init__(parent) + self.settings_section = settings_section + self._current_search_type = -1 + self.clear_button = QtWidgets.QToolButton(self) + self.clear_button.setIcon(build_icon(':/system/clear_shortcut.png')) + self.clear_button.setCursor(QtCore.Qt.ArrowCursor) + self.clear_button.setStyleSheet('QToolButton { border: none; padding: 0px; }') + self.clear_button.resize(18, 18) + self.clear_button.hide() + self.clear_button.clicked.connect(self._on_clear_button_clicked) + self.textChanged.connect(self._on_search_edit_text_changed) + self._update_style_sheet() + self.setAcceptDrops(False) + + def _update_style_sheet(self): + """ + Internal method to update the stylesheet depending on which widgets are available and visible. + """ + frame_width = self.style().pixelMetric(QtWidgets.QStyle.PM_DefaultFrameWidth) + right_padding = self.clear_button.width() + frame_width + if hasattr(self, 'menu_button'): + left_padding = self.menu_button.width() + stylesheet = 'QLineEdit {{ padding-left:{left}px; padding-right: {right}px; }} '.format(left=left_padding, + right=right_padding) + else: + stylesheet = 'QLineEdit {{ padding-right: {right}px; }} '.format(right=right_padding) + self.setStyleSheet(stylesheet) + msz = self.minimumSizeHint() + self.setMinimumSize(max(msz.width(), self.clear_button.width() + (frame_width * 2) + 2), + max(msz.height(), self.clear_button.height() + (frame_width * 2) + 2)) + + def resizeEvent(self, event): + """ + Reimplemented method to react to resizing of the widget. + + :param event: The event that happened. + """ + size = self.clear_button.size() + frame_width = self.style().pixelMetric(QtWidgets.QStyle.PM_DefaultFrameWidth) + self.clear_button.move(self.rect().right() - frame_width - size.width(), + (self.rect().bottom() + 1 - size.height()) // 2) + if hasattr(self, 'menu_button'): + size = self.menu_button.size() + self.menu_button.move(self.rect().left() + frame_width + 2, (self.rect().bottom() + 1 - size.height()) // 2) + + def current_search_type(self): + """ + Readonly property to return the current search type. + """ + return self._current_search_type + + def set_current_search_type(self, identifier): + """ + Set a new current search type. + + :param identifier: The search type identifier (int). + """ + menu = self.menu_button.menu() + for action in menu.actions(): + if identifier == action.data(): + self.setPlaceholderText(action.placeholder_text) + self.menu_button.setDefaultAction(action) + self._current_search_type = identifier + Settings().setValue('{section}/last used search type'.format(section=self.settings_section), identifier) + self.searchTypeChanged.emit(identifier) + return True + + def set_search_types(self, items): + """ + A list of tuples to be used in the search type menu. The first item in the list will be preselected as the + default. + + :param items: The list of tuples to use. The tuples should contain an integer identifier, an icon (QIcon + instance or string) and a title for the item in the menu. In short, they should look like this:: + + (<identifier>, <icon>, <title>, <place holder text>) + + For instance:: + + (1, <QIcon instance>, "Titles", "Search Song Titles...") + + Or:: + + (2, ":/songs/authors.png", "Authors", "Search Authors...") + """ + menu = QtWidgets.QMenu(self) + for identifier, icon, title, placeholder in items: + action = create_widget_action( + menu, text=title, icon=icon, data=identifier, triggers=self._on_menu_action_triggered) + action.placeholder_text = placeholder + if not hasattr(self, 'menu_button'): + self.menu_button = QtWidgets.QToolButton(self) + self.menu_button.setIcon(build_icon(':/system/clear_shortcut.png')) + self.menu_button.setCursor(QtCore.Qt.ArrowCursor) + self.menu_button.setPopupMode(QtWidgets.QToolButton.InstantPopup) + self.menu_button.setStyleSheet('QToolButton { border: none; padding: 0px 10px 0px 0px; }') + self.menu_button.resize(QtCore.QSize(28, 18)) + self.menu_button.setMenu(menu) + self.set_current_search_type( + Settings().value('{section}/last used search type'.format(section=self.settings_section))) + self.menu_button.show() + self._update_style_sheet() + + def _on_search_edit_text_changed(self, text): + """ + Internally implemented slot to react to when the text in the line edit has changed so that we can show or hide + the clear button. + + :param text: A :class:`~PyQt5.QtCore.QString` instance which represents the text in the line edit. + """ + self.clear_button.setVisible(bool(text)) + + def _on_clear_button_clicked(self): + """ + Internally implemented slot to react to the clear button being clicked to clear the line edit. Once it has + cleared the line edit, it emits the ``cleared()`` signal so that an application can react to the clearing of the + line edit. + """ + self.clear() + self.cleared.emit() + + def _on_menu_action_triggered(self): + """ + Internally implemented slot to react to the select of one of the search types in the menu. Once it has set the + correct action on the button, and set the current search type (using the list of identifiers provided by the + developer), the ``searchTypeChanged(int)`` signal is emitted with the identifier. + """ + for action in self.menu_button.menu().actions(): + # Why is this needed? + action.setChecked(False) + self.set_current_search_type(self.sender().data()) + + +class PathEdit(QtWidgets.QWidget): + """ + The :class:`~openlp.core.widgets.edits.PathEdit` class subclasses QWidget to create a custom widget for use when + a file or directory needs to be selected. + """ + pathChanged = QtCore.pyqtSignal(Path) + + def __init__(self, parent=None, path_type=PathEditType.Files, default_path=None, dialog_caption=None, + show_revert=True): + """ + Initialise the PathEdit widget + + :param QtWidget.QWidget | None: The parent of the widget. This is just passed to the super method. + :param str dialog_caption: Used to customise the caption in the QFileDialog. + :param openlp.core.common.path.Path default_path: The default path. This is set as the path when the revert + button is clicked + :param bool show_revert: Used to determine if the 'revert button' should be visible. + :rtype: None + """ + super().__init__(parent) + self.default_path = default_path + self.dialog_caption = dialog_caption + self._path_type = path_type + self._path = None + self.filters = '{all_files} (*)'.format(all_files=UiStrings().AllFiles) + self._setup(show_revert) + + def _setup(self, show_revert): + """ + Set up the widget + :param bool show_revert: Show or hide the revert button + :rtype: None + """ + widget_layout = QtWidgets.QHBoxLayout() + widget_layout.setContentsMargins(0, 0, 0, 0) + self.line_edit = QtWidgets.QLineEdit(self) + widget_layout.addWidget(self.line_edit) + self.browse_button = QtWidgets.QToolButton(self) + self.browse_button.setIcon(build_icon(':/general/general_open.png')) + widget_layout.addWidget(self.browse_button) + self.revert_button = QtWidgets.QToolButton(self) + self.revert_button.setIcon(build_icon(':/general/general_revert.png')) + self.revert_button.setVisible(show_revert) + widget_layout.addWidget(self.revert_button) + self.setLayout(widget_layout) + # Signals and Slots + self.browse_button.clicked.connect(self.on_browse_button_clicked) + self.revert_button.clicked.connect(self.on_revert_button_clicked) + self.line_edit.editingFinished.connect(self.on_line_edit_editing_finished) + self.update_button_tool_tips() + + @property + def path(self): + """ + A property getter method to return the selected path. + + :return: The selected path + :rtype: openlp.core.common.path.Path + """ + return self._path + + @path.setter + def path(self, path): + """ + A Property setter method to set the selected path + + :param openlp.core.common.path.Path path: The path to set the widget to + :rtype: None + """ + self._path = path + text = path_to_str(path) + self.line_edit.setText(text) + self.line_edit.setToolTip(text) + + @property + def path_type(self): + """ + A property getter method to return the path_type. Path type allows you to sepecify if the user is restricted to + selecting a file or directory. + + :return: The type selected + :rtype: PathType + """ + return self._path_type + + @path_type.setter + def path_type(self, path_type): + """ + A Property setter method to set the path type + + :param PathType path_type: The type of path to select + :rtype: None + """ + self._path_type = path_type + self.update_button_tool_tips() + + def update_button_tool_tips(self): + """ + Called to update the tooltips on the buttons. This is changing path types, and when the widget is initalised + + :rtype: None + """ + if self._path_type == PathEditType.Directories: + self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for directory.')) + self.revert_button.setToolTip(translate('OpenLP.PathEdit', 'Revert to default directory.')) + else: + self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for file.')) + self.revert_button.setToolTip(translate('OpenLP.PathEdit', 'Revert to default file.')) + + def on_browse_button_clicked(self): + """ + A handler to handle a click on the browse button. + + Show the QFileDialog and process the input from the user + + :rtype: None + """ + caption = self.dialog_caption + path = None + if self._path_type == PathEditType.Directories: + if not caption: + caption = translate('OpenLP.PathEdit', 'Select Directory') + path = FileDialog.getExistingDirectory(self, caption, self._path, FileDialog.ShowDirsOnly) + elif self._path_type == PathEditType.Files: + if not caption: + caption = self.dialog_caption = translate('OpenLP.PathEdit', 'Select File') + path, filter_used = FileDialog.getOpenFileName(self, caption, self._path, self.filters) + if path: + self.on_new_path(path) + + def on_revert_button_clicked(self): + """ + A handler to handle a click on the revert button. + + Set the new path to the value of the default_path instance variable. + + :rtype: None + """ + self.on_new_path(self.default_path) + + def on_line_edit_editing_finished(self): + """ + A handler to handle when the line edit has finished being edited. + + :rtype: None + """ + path = str_to_path(self.line_edit.text()) + self.on_new_path(path) + + def on_new_path(self, path): + """ + A method called to validate and set a new path. + + Emits the pathChanged Signal + + :param openlp.core.common.path.Path path: The new path + :rtype: None + """ + if self._path != path: + self.path = path + self.pathChanged.emit(path) + + +class SpellTextEdit(QtWidgets.QPlainTextEdit): + """ + Spell checking widget based on QPlanTextEdit. + + Based on code from http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check + """ + def __init__(self, parent=None, formatting_tags_allowed=True): + """ + Constructor. + """ + global ENCHANT_AVAILABLE + super(SpellTextEdit, self).__init__(parent) + self.formatting_tags_allowed = formatting_tags_allowed + # Default dictionary based on the current locale. + if ENCHANT_AVAILABLE: + try: + self.dictionary = enchant.Dict() + self.highlighter = Highlighter(self.document()) + self.highlighter.spelling_dictionary = self.dictionary + except (Error, DictNotFoundError): + ENCHANT_AVAILABLE = False + log.debug('Could not load default dictionary') + + def mousePressEvent(self, event): + """ + Handle mouse clicks within the text edit region. + """ + if event.button() == QtCore.Qt.RightButton: + # Rewrite the mouse event to a left button event so the cursor is moved to the location of the pointer. + event = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, + event.pos(), QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier) + QtWidgets.QPlainTextEdit.mousePressEvent(self, event) + + def contextMenuEvent(self, event): + """ + Provide the context menu for the text edit region. + """ + popup_menu = self.createStandardContextMenu() + # Select the word under the cursor. + cursor = self.textCursor() + # only select text if not already selected + if not cursor.hasSelection(): + cursor.select(QtGui.QTextCursor.WordUnderCursor) + self.setTextCursor(cursor) + # Add menu with available languages. + if ENCHANT_AVAILABLE: + lang_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Language:')) + for lang in enchant.list_languages(): + action = create_action(lang_menu, lang, text=lang, checked=lang == self.dictionary.tag) + lang_menu.addAction(action) + popup_menu.insertSeparator(popup_menu.actions()[0]) + popup_menu.insertMenu(popup_menu.actions()[0], lang_menu) + lang_menu.triggered.connect(self.set_language) + # Check if the selected word is misspelled and offer spelling suggestions if it is. + if ENCHANT_AVAILABLE and self.textCursor().hasSelection(): + text = self.textCursor().selectedText() + if not self.dictionary.check(text): + spell_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Spelling Suggestions')) + for word in self.dictionary.suggest(text): + action = SpellAction(word, spell_menu) + action.correct.connect(self.correct_word) + spell_menu.addAction(action) + # Only add the spelling suggests to the menu if there are suggestions. + if spell_menu.actions(): + popup_menu.insertMenu(popup_menu.actions()[0], spell_menu) + tag_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Formatting Tags')) + if self.formatting_tags_allowed: + for html in FormattingTags.get_html_tags(): + action = SpellAction(html['desc'], tag_menu) + action.correct.connect(self.html_tag) + tag_menu.addAction(action) + popup_menu.insertSeparator(popup_menu.actions()[0]) + popup_menu.insertMenu(popup_menu.actions()[0], tag_menu) + popup_menu.exec(event.globalPos()) + + def set_language(self, action): + """ + Changes the language for this spelltextedit. + + :param action: The action. + """ + self.dictionary = enchant.Dict(action.text()) + self.highlighter.spelling_dictionary = self.dictionary + self.highlighter.highlightBlock(self.toPlainText()) + self.highlighter.rehighlight() + + def correct_word(self, word): + """ + Replaces the selected text with word. + """ + cursor = self.textCursor() + cursor.beginEditBlock() + cursor.removeSelectedText() + cursor.insertText(word) + cursor.endEditBlock() + + def html_tag(self, tag): + """ + Replaces the selected text with word. + """ + tag = tag.replace('&', '') + for html in FormattingTags.get_html_tags(): + if tag == html['desc']: + cursor = self.textCursor() + if self.textCursor().hasSelection(): + text = cursor.selectedText() + cursor.beginEditBlock() + cursor.removeSelectedText() + cursor.insertText(html['start tag']) + cursor.insertText(text) + cursor.insertText(html['end tag']) + cursor.endEditBlock() + else: + cursor = self.textCursor() + cursor.insertText(html['start tag']) + cursor.insertText(html['end tag']) + + +class Highlighter(QtGui.QSyntaxHighlighter): + """ + Provides a text highlighter for pointing out spelling errors in text. + """ + WORDS = r'(?iu)[\w\']+' + + def __init__(self, *args): + """ + Constructor + """ + super(Highlighter, self).__init__(*args) + self.spelling_dictionary = None + + def highlightBlock(self, text): + """ + Highlight mis spelt words in a block of text. + + Note, this is a Qt hook. + """ + if not self.spelling_dictionary: + return + text = str(text) + char_format = QtGui.QTextCharFormat() + char_format.setUnderlineColor(QtCore.Qt.red) + char_format.setUnderlineStyle(QtGui.QTextCharFormat.SpellCheckUnderline) + for word_object in re.finditer(self.WORDS, text): + if not self.spelling_dictionary.check(word_object.group()): + self.setFormat(word_object.start(), word_object.end() - word_object.start(), char_format) + + +class SpellAction(QtWidgets.QAction): + """ + A special QAction that returns the text in a signal. + """ + correct = QtCore.pyqtSignal(str) + + def __init__(self, *args): + """ + Constructor + """ + super(SpellAction, self).__init__(*args) + self.triggered.connect(lambda x: self.correct.emit(self.text())) + + +class HistoryComboBox(QtWidgets.QComboBox): + """ + The :class:`~openlp.core.common.historycombobox.HistoryComboBox` widget emulates the QLineEdit ``returnPressed`` + signal for when the :kbd:`Enter` or :kbd:`Return` keys are pressed, and saves anything that is typed into the edit + box into its list. + """ + returnPressed = QtCore.pyqtSignal() + + def __init__(self, parent=None): + """ + Initialise the combo box, setting duplicates to False and the insert policy to insert items at the top. + + :param parent: The parent widget + """ + super().__init__(parent) + self.setDuplicatesEnabled(False) + self.setEditable(True) + self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) + self.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop) + + def keyPressEvent(self, event): + """ + Override the inherited keyPressEvent method to emit the ``returnPressed`` signal and to save the current text to + the dropdown list. + + :param event: The keyboard event + """ + # Handle Enter and Return ourselves + if event.key() == QtCore.Qt.Key_Enter or event.key() == QtCore.Qt.Key_Return: + # Emit the returnPressed signal + self.returnPressed.emit() + # Save the current text to the dropdown list + if self.currentText() and self.findText(self.currentText()) == -1: + self.insertItem(0, self.currentText()) + # Let the parent handle any keypress events + super().keyPressEvent(event) + + def focusOutEvent(self, event): + """ + Override the inherited focusOutEvent to save the current text to the dropdown list. + + :param event: The focus event + """ + # Save the current text to the dropdown list + if self.currentText() and self.findText(self.currentText()) == -1: + self.insertItem(0, self.currentText()) + # Let the parent handle any keypress events + super().focusOutEvent(event) + + def getItems(self): + """ + Get all the items from the history + + :return: A list of strings + """ + return [self.itemText(i) for i in range(self.count())] diff --git a/openlp/core/ui/lib/__init__.py b/openlp/core/widgets/enums.py similarity index 69% rename from openlp/core/ui/lib/__init__.py rename to openlp/core/widgets/enums.py index cf55b9d20..f79dd775c 100644 --- a/openlp/core/ui/lib/__init__.py +++ b/openlp/core/widgets/enums.py @@ -19,18 +19,12 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +""" +The :mod:`~openlp.core.widgets.enums` module contains enumerations used by the widgets +""" +from enum import Enum -from .colorbutton import ColorButton -from .listpreviewwidget import ListPreviewWidget -from .listwidgetwithdnd import ListWidgetWithDnD -from .mediadockmanager import MediaDockManager -from .dockwidget import OpenLPDockWidget -from .toolbar import OpenLPToolbar -from .wizard import OpenLPWizard, WizardStrings -from .pathedit import PathEdit, PathType -from .spelltextedit import SpellTextEdit -from .treewidgetwithdnd import TreeWidgetWithDnD -__all__ = ['ColorButton', 'ListPreviewWidget', 'ListWidgetWithDnD', 'MediaDockManager', 'OpenLPDockWidget', - 'OpenLPToolbar', 'OpenLPWizard', 'PathEdit', 'PathType', 'SpellTextEdit', 'TreeWidgetWithDnD', - 'WizardStrings'] +class PathEditType(Enum): + Files = 1 + Directories = 2 diff --git a/openlp/core/ui/lib/toolbar.py b/openlp/core/widgets/toolbar.py similarity index 98% rename from openlp/core/ui/lib/toolbar.py rename to openlp/core/widgets/toolbar.py index 68343889f..8c76ce50e 100644 --- a/openlp/core/ui/lib/toolbar.py +++ b/openlp/core/widgets/toolbar.py @@ -40,7 +40,7 @@ class OpenLPToolbar(QtWidgets.QToolBar): """ Initialise the toolbar. """ - super(OpenLPToolbar, self).__init__(parent) + super().__init__(parent) # useful to be able to reuse button icons... self.setIconSize(QtCore.QSize(20, 20)) self.actions = {} diff --git a/openlp/core/ui/lib/listpreviewwidget.py b/openlp/core/widgets/views.py similarity index 56% rename from openlp/core/ui/lib/listpreviewwidget.py rename to openlp/core/widgets/views.py index d03261e8c..f2a7d4498 100644 --- a/openlp/core/ui/lib/listpreviewwidget.py +++ b/openlp/core/widgets/views.py @@ -23,10 +23,14 @@ The :mod:`listpreviewwidget` is a widget that lists the slides in the slide controller. It is based on a QTableWidget but represents its contents in list form. """ +import os from PyQt5 import QtCore, QtGui, QtWidgets -from openlp.core.common.registry import RegistryProperties +from openlp.core.common import is_win +from openlp.core.common.i18n import UiStrings +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.lib import ImageSource, ItemCapabilities, ServiceItem @@ -238,3 +242,241 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): Returns the number of slides this widget holds. """ return super(ListPreviewWidget, self).rowCount() + + +class ListWidgetWithDnD(QtWidgets.QListWidget): + """ + Provide a list widget to store objects and handle drag and drop events + """ + def __init__(self, parent=None, name=''): + """ + Initialise the list widget + """ + super().__init__(parent) + self.mime_data_text = name + self.no_results_text = UiStrings().NoResults + self.setSpacing(1) + self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + self.setAlternatingRowColors(True) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + + def activateDnD(self): + """ + Activate DnD of widget + """ + self.setAcceptDrops(True) + self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) + Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file) + + def clear(self, search_while_typing=False): + """ + Re-implement clear, so that we can customise feedback when using 'Search as you type' + + :param search_while_typing: True if we want to display the customised message + :return: None + """ + if search_while_typing: + self.no_results_text = UiStrings().ShortResults + else: + self.no_results_text = UiStrings().NoResults + super().clear() + + def mouseMoveEvent(self, event): + """ + Drag and drop event does not care what data is selected as the recipient will use events to request the data + move just tell it what plugin to call + """ + if event.buttons() != QtCore.Qt.LeftButton: + event.ignore() + return + if not self.selectedItems(): + event.ignore() + return + drag = QtGui.QDrag(self) + mime_data = QtCore.QMimeData() + drag.setMimeData(mime_data) + mime_data.setText(self.mime_data_text) + drag.exec(QtCore.Qt.CopyAction) + + def dragEnterEvent(self, event): + """ + When something is dragged into this object, check if you should be able to drop it in here. + """ + if event.mimeData().hasUrls(): + event.accept() + else: + event.ignore() + + def dragMoveEvent(self, event): + """ + Make an object droppable, and set it to copy the contents of the object, not move it. + """ + if event.mimeData().hasUrls(): + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + else: + event.ignore() + + def dropEvent(self, event): + """ + Receive drop event check if it is a file and process it if it is. + + :param event: Handle of the event pint passed + """ + if event.mimeData().hasUrls(): + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + files = [] + for url in event.mimeData().urls(): + local_file = os.path.normpath(url.toLocalFile()) + if os.path.isfile(local_file): + files.append(local_file) + elif os.path.isdir(local_file): + listing = os.listdir(local_file) + for file in listing: + files.append(os.path.join(local_file, file)) + Registry().execute('{mime_data}_dnd'.format(mime_data=self.mime_data_text), + {'files': files, 'target': self.itemAt(event.pos())}) + else: + event.ignore() + + def allItems(self): + """ + An generator to list all the items in the widget + + :return: a generator + """ + for row in range(self.count()): + yield self.item(row) + + def paintEvent(self, event): + """ + Re-implement paintEvent so that we can add 'No Results' text when the listWidget is empty. + + :param event: A QPaintEvent + :return: None + """ + super().paintEvent(event) + if not self.count(): + viewport = self.viewport() + painter = QtGui.QPainter(viewport) + font = QtGui.QFont() + font.setItalic(True) + painter.setFont(font) + painter.drawText(QtCore.QRect(0, 0, viewport.width(), viewport.height()), + (QtCore.Qt.AlignHCenter | QtCore.Qt.TextWordWrap), self.no_results_text) + + +class TreeWidgetWithDnD(QtWidgets.QTreeWidget): + """ + Provide a tree widget to store objects and handle drag and drop events + """ + def __init__(self, parent=None, name=''): + """ + Initialise the tree widget + """ + super(TreeWidgetWithDnD, self).__init__(parent) + self.mime_data_text = name + self.allow_internal_dnd = False + self.header().close() + self.default_indentation = self.indentation() + self.setIndentation(0) + self.setAnimated(True) + + def activateDnD(self): + """ + Activate DnD of widget + """ + self.setAcceptDrops(True) + self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) + Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file) + Registry().register_function(('%s_dnd_internal' % self.mime_data_text), self.parent().dnd_move_internal) + + def mouseMoveEvent(self, event): + """ + Drag and drop event does not care what data is selected as the recipient will use events to request the data + move just tell it what plugin to call + + :param event: The event that occurred + """ + if event.buttons() != QtCore.Qt.LeftButton: + event.ignore() + return + if not self.selectedItems(): + event.ignore() + return + drag = QtGui.QDrag(self) + mime_data = QtCore.QMimeData() + drag.setMimeData(mime_data) + mime_data.setText(self.mime_data_text) + drag.exec(QtCore.Qt.CopyAction) + + def dragEnterEvent(self, event): + """ + Receive drag enter event, check if it is a file or internal object and allow it if it is. + + :param event: The event that occurred + """ + if event.mimeData().hasUrls(): + event.accept() + elif self.allow_internal_dnd: + event.accept() + else: + event.ignore() + + def dragMoveEvent(self, event): + """ + Receive drag move event, check if it is a file or internal object and allow it if it is. + + :param event: The event that occurred + """ + QtWidgets.QTreeWidget.dragMoveEvent(self, event) + if event.mimeData().hasUrls(): + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + elif self.allow_internal_dnd: + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + else: + event.ignore() + + def dropEvent(self, event): + """ + Receive drop event, check if it is a file or internal object and process it if it is. + + :param event: Handle of the event pint passed + """ + # If we are on Windows, OpenLP window will not be set on top. For example, user can drag images to Library and + # the folder stays on top of the group creation box. This piece of code fixes this issue. + if is_win(): + self.setWindowState(self.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) + self.setWindowState(QtCore.Qt.WindowNoState) + if event.mimeData().hasUrls(): + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + files = [] + for url in event.mimeData().urls(): + local_file = url.toLocalFile() + if os.path.isfile(local_file): + files.append(local_file) + elif os.path.isdir(local_file): + listing = os.listdir(local_file) + for file_name in listing: + files.append(os.path.join(local_file, file_name)) + Registry().execute('%s_dnd' % self.mime_data_text, {'files': files, 'target': self.itemAt(event.pos())}) + elif self.allow_internal_dnd: + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + Registry().execute('%s_dnd_internal' % self.mime_data_text, self.itemAt(event.pos())) + else: + event.ignore() + + # Convenience methods for emulating a QListWidget. This helps keeping MediaManagerItem simple. + def addItem(self, item): + self.addTopLevelItem(item) + + def count(self): + return self.topLevelItemCount() + + def item(self, index): + return self.topLevelItem(index) diff --git a/openlp/core/ui/lib/wizard.py b/openlp/core/widgets/wizard.py similarity index 98% rename from openlp/core/ui/lib/wizard.py rename to openlp/core/widgets/wizard.py index 1da24eab3..dc8288bc9 100644 --- a/openlp/core/ui/lib/wizard.py +++ b/openlp/core/widgets/wizard.py @@ -28,11 +28,12 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import is_macosx from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.lib import build_icon from openlp.core.lib.ui import add_welcome_page -from openlp.core.ui.lib.filedialog import FileDialog +from openlp.core.widgets.dialogs import FileDialog log = logging.getLogger(__name__) diff --git a/openlp/plugins/alerts/lib/alertsmanager.py b/openlp/plugins/alerts/lib/alertsmanager.py index 6eb26ff57..e5ae93093 100644 --- a/openlp/plugins/alerts/lib/alertsmanager.py +++ b/openlp/plugins/alerts/lib/alertsmanager.py @@ -26,12 +26,12 @@ displaying of alerts. from PyQt5 import QtCore from openlp.core.common.i18n import translate -from openlp.core.common.mixins import OpenLPMixin, RegistryMixin -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import LogMixin, RegistryProperties +from openlp.core.common.registry import Registry, RegistryBase from openlp.core.common.settings import Settings -class AlertsManager(OpenLPMixin, RegistryMixin, QtCore.QObject, RegistryProperties): +class AlertsManager(QtCore.QObject, RegistryBase, LogMixin, RegistryProperties): """ AlertsManager manages the settings of Alerts. """ diff --git a/openlp/plugins/alerts/lib/alertstab.py b/openlp/plugins/alerts/lib/alertstab.py index 406e35607..51d92cf06 100644 --- a/openlp/plugins/alerts/lib/alertstab.py +++ b/openlp/plugins/alerts/lib/alertstab.py @@ -26,7 +26,7 @@ from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.settings import Settings from openlp.core.lib import SettingsTab from openlp.core.lib.ui import create_valign_selection_widgets -from openlp.core.ui.lib.colorbutton import ColorButton +from openlp.core.widgets.buttons import ColorButton class AlertsTab(SettingsTab): diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index 4cb9ee453..111e361f8 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -41,7 +41,7 @@ from openlp.core.common.settings import Settings from openlp.core.lib.db import delete_database from openlp.core.lib.exceptions import ValidationError from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings +from openlp.core.widgets.wizard import OpenLPWizard, WizardStrings from openlp.plugins.bibles.lib.db import clean_filename from openlp.plugins.bibles.lib.importers.http import CWExtract, BGExtract, BSExtract from openlp.plugins.bibles.lib.manager import BibleFormat diff --git a/openlp/plugins/bibles/forms/editbibleform.py b/openlp/plugins/bibles/forms/editbibleform.py index 365d98a4e..8899087ba 100644 --- a/openlp/plugins/bibles/forms/editbibleform.py +++ b/openlp/plugins/bibles/forms/editbibleform.py @@ -26,7 +26,7 @@ import re from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.core.lib.ui import critical_error_message_box from .editbibledialog import Ui_EditBibleDialog from openlp.plugins.bibles.lib import BibleStrings diff --git a/openlp/plugins/bibles/lib/bibleimport.py b/openlp/plugins/bibles/lib/bibleimport.py index 61aa35a3e..5083d7f6b 100644 --- a/openlp/plugins/bibles/lib/bibleimport.py +++ b/openlp/plugins/bibles/lib/bibleimport.py @@ -23,15 +23,15 @@ from lxml import etree, objectify from zipfile import is_zipfile -from openlp.core.common.mixins import OpenLPMixin -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import LogMixin, RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.common.i18n import get_language, translate from openlp.core.lib import ValidationError from openlp.core.lib.ui import critical_error_message_box from openlp.plugins.bibles.lib.db import AlternativeBookNamesDB, BibleDB, BiblesResourcesDB -class BibleImport(OpenLPMixin, RegistryProperties, BibleDB): +class BibleImport(BibleDB, LogMixin, RegistryProperties): """ Helper class to import bibles from a third party source into OpenLP """ diff --git a/openlp/plugins/bibles/lib/db.py b/openlp/plugins/bibles/lib/db.py index 5d82fe613..bc8ce4150 100644 --- a/openlp/plugins/bibles/lib/db.py +++ b/openlp/plugins/bibles/lib/db.py @@ -19,7 +19,6 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - import chardet import logging import os diff --git a/tests/interfaces/openlp_core_lib/__init__.py b/openlp/plugins/bibles/lib/importers/__init__.py similarity index 100% rename from tests/interfaces/openlp_core_lib/__init__.py rename to openlp/plugins/bibles/lib/importers/__init__.py diff --git a/openlp/plugins/bibles/lib/importers/http.py b/openlp/plugins/bibles/lib/importers/http.py index 5d3098b4d..b88dbe7a9 100644 --- a/openlp/plugins/bibles/lib/importers/http.py +++ b/openlp/plugins/bibles/lib/importers/http.py @@ -32,7 +32,8 @@ from bs4 import BeautifulSoup, NavigableString, Tag from openlp.core.common.httputils import get_web_page from openlp.core.common.i18n import translate -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.lib.ui import critical_error_message_box from openlp.plugins.bibles.lib import SearchResults from openlp.plugins.bibles.lib.bibleimport import BibleImport diff --git a/openlp/plugins/bibles/lib/manager.py b/openlp/plugins/bibles/lib/manager.py index 55444f080..7290ca862 100644 --- a/openlp/plugins/bibles/lib/manager.py +++ b/openlp/plugins/bibles/lib/manager.py @@ -25,9 +25,8 @@ import logging from openlp.core.common import delete_file from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.mixins import OpenLPMixin +from openlp.core.common.mixins import LogMixin, RegistryProperties from openlp.core.common.path import Path -from openlp.core.common.registry import RegistryProperties from openlp.core.common.settings import Settings from openlp.plugins.bibles.lib import LanguageSelection, parse_reference from openlp.plugins.bibles.lib.db import BibleDB, BibleMeta @@ -98,7 +97,7 @@ class BibleFormat(object): ] -class BibleManager(OpenLPMixin, RegistryProperties): +class BibleManager(LogMixin, RegistryProperties): """ The Bible manager which holds and manages all the Bibles. """ diff --git a/openlp/plugins/bibles/lib/mediaitem.py b/openlp/plugins/bibles/lib/mediaitem.py index 10bb8d6ec..fdfc4b51d 100755 --- a/openlp/plugins/bibles/lib/mediaitem.py +++ b/openlp/plugins/bibles/lib/mediaitem.py @@ -30,9 +30,9 @@ from openlp.core.common.i18n import UiStrings, translate, get_locale_key from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.lib import MediaManagerItem, ItemCapabilities, ServiceItemContext -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.widgets.edits import SearchEdit from openlp.plugins.bibles.forms.bibleimportform import BibleImportForm from openlp.plugins.bibles.forms.editbibleform import EditBibleForm from openlp.plugins.bibles.lib import DisplayStyle, LayoutStyle, VerseReferenceList, \ diff --git a/openlp/plugins/custom/forms/editcustomslidedialog.py b/openlp/plugins/custom/forms/editcustomslidedialog.py index 452146feb..5ccc94cc6 100644 --- a/openlp/plugins/custom/forms/editcustomslidedialog.py +++ b/openlp/plugins/custom/forms/editcustomslidedialog.py @@ -25,7 +25,7 @@ from PyQt5 import QtWidgets from openlp.core.common.i18n import UiStrings, translate from openlp.core.lib import build_icon from openlp.core.lib.ui import create_button, create_button_box -from openlp.core.ui.lib import SpellTextEdit +from openlp.core.widgets.edits import SpellTextEdit class Ui_CustomSlideEditDialog(object): diff --git a/openlp/plugins/images/lib/imagetab.py b/openlp/plugins/images/lib/imagetab.py index 23b742bfb..e2b64a6cc 100644 --- a/openlp/plugins/images/lib/imagetab.py +++ b/openlp/plugins/images/lib/imagetab.py @@ -25,7 +25,7 @@ from PyQt5 import QtWidgets from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.settings import Settings from openlp.core.lib import SettingsTab -from openlp.core.ui.lib.colorbutton import ColorButton +from openlp.core.widgets.buttons import ColorButton class ImageTab(SettingsTab): diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index 134dbe2eb..b030d26fd 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -33,7 +33,7 @@ from openlp.core.common.settings import Settings from openlp.core.lib import ItemCapabilities, MediaManagerItem, ServiceItemContext, StringContent, build_icon, \ check_item_selected, create_thumb, validate_thumb from openlp.core.lib.ui import create_widget_action, critical_error_message_box -from openlp.core.ui.lib.treewidgetwithdnd import TreeWidgetWithDnD +from openlp.core.widgets.views import TreeWidgetWithDnD from openlp.plugins.images.forms import AddGroupForm, ChooseGroupForm from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index f48e9b09e..af08870df 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -29,7 +29,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import is_win, is_linux, is_macosx from openlp.core.common.i18n import translate -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.plugins.media.forms.mediaclipselectordialog import Ui_MediaClipSelector from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui.media.vlcplayer import get_vlc diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index ab0cf4968..ed787166b 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -28,7 +28,8 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import UiStrings, translate, get_locale_key from openlp.core.common.path import Path, path_to_str, create_paths -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.lib import ItemCapabilities, MediaManagerItem, MediaType, ServiceItem, ServiceItemContext, \ build_icon, check_item_selected diff --git a/openlp/plugins/presentations/lib/presentationtab.py b/openlp/plugins/presentations/lib/presentationtab.py index 96fcc573a..d6a4f5190 100644 --- a/openlp/plugins/presentations/lib/presentationtab.py +++ b/openlp/plugins/presentations/lib/presentationtab.py @@ -26,7 +26,7 @@ from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.settings import Settings from openlp.core.lib import SettingsTab from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.lib import PathEdit +from openlp.core.widgets.edits import PathEdit from openlp.plugins.presentations.lib.pdfcontroller import PdfController diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index d2238d4b7..8e7724808 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -29,8 +29,9 @@ import multiprocessing from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import translate -from openlp.core.common.registry import Registry, RegistryProperties -from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry +from openlp.core.widgets.wizard import OpenLPWizard, WizardStrings from openlp.plugins.songs.lib import delete_song from openlp.plugins.songs.lib.db import Song from openlp.plugins.songs.forms.songreviewwidget import SongReviewWidget diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index bdb1a9353..fa475a63f 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -31,10 +31,11 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import UiStrings, translate, get_natural_key from openlp.core.common.path import create_paths, copyfile -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.lib import PluginStatus, MediaType, create_separated_list from openlp.core.lib.ui import set_case_insensitive_completer, critical_error_message_box, find_and_set_in_combo_box -from openlp.core.ui.lib.filedialog import FileDialog +from openlp.core.widgets.dialogs import FileDialog from openlp.plugins.songs.forms.editsongdialog import Ui_EditSongDialog from openlp.plugins.songs.forms.editverseform import EditVerseForm from openlp.plugins.songs.forms.mediafilesform import MediaFilesForm diff --git a/openlp/plugins/songs/forms/editversedialog.py b/openlp/plugins/songs/forms/editversedialog.py index 63ca0cf27..76dc70c17 100644 --- a/openlp/plugins/songs/forms/editversedialog.py +++ b/openlp/plugins/songs/forms/editversedialog.py @@ -26,7 +26,7 @@ from openlp.core.common.settings import Settings from openlp.core.common.i18n import UiStrings, translate from openlp.core.lib import build_icon from openlp.core.lib.ui import create_button_box -from openlp.core.ui.lib import SpellTextEdit +from openlp.core.widgets.edits import SpellTextEdit from openlp.plugins.songs.lib import VerseType diff --git a/openlp/plugins/songs/forms/songexportform.py b/openlp/plugins/songs/forms/songexportform.py index 28ca8054c..c6446d74c 100644 --- a/openlp/plugins/songs/forms/songexportform.py +++ b/openlp/plugins/songs/forms/songexportform.py @@ -32,8 +32,9 @@ from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.lib import create_separated_list from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.lib import PathEdit, PathType -from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings +from openlp.core.widgets.edits import PathEdit +from openlp.core.widgets.enums import PathEditType +from openlp.core.widgets.wizard import OpenLPWizard, WizardStrings from openlp.plugins.songs.lib.db import Song from openlp.plugins.songs.lib.openlyricsexport import OpenLyricsExport @@ -124,7 +125,7 @@ class SongExportForm(OpenLPWizard): self.selected_list_widget.setObjectName('selected_list_widget') self.grid_layout.addWidget(self.selected_list_widget, 1, 0, 1, 2) self.output_directory_path_edit = PathEdit( - self.export_song_page, PathType.Directories, + self.export_song_page, PathEditType.Directories, dialog_caption=translate('SongsPlugin.ExportWizardForm', 'Select Destination Folder'), show_revert=False) self.output_directory_path_edit.path = Settings().value('songs/last directory export') self.directory_label = QtWidgets.QLabel(self.export_song_page) diff --git a/openlp/plugins/songs/forms/songimportform.py b/openlp/plugins/songs/forms/songimportform.py index acfa4b5b8..af88bfb35 100644 --- a/openlp/plugins/songs/forms/songimportform.py +++ b/openlp/plugins/songs/forms/songimportform.py @@ -27,12 +27,13 @@ import logging from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.core.common.settings import Settings from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.lib import PathEdit, PathType -from openlp.core.ui.lib.filedialog import FileDialog -from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings +from openlp.core.widgets.dialogs import FileDialog +from openlp.core.widgets.edits import PathEdit +from openlp.core.widgets.enums import PathEditType +from openlp.core.widgets.wizard import OpenLPWizard, WizardStrings from openlp.plugins.songs.lib.importer import SongFormat, SongFormatSelect log = logging.getLogger(__name__) @@ -383,10 +384,10 @@ class SongImportForm(OpenLPWizard, RegistryProperties): file_path_label = QtWidgets.QLabel(import_widget) file_path_layout.addWidget(file_path_label) if select_mode == SongFormatSelect.SingleFile: - path_type = PathType.Files + path_type = PathEditType.Files dialog_caption = WizardStrings.OpenTypeFile.format(file_type=format_name) else: - path_type = PathType.Directories + path_type = PathEditType.Directories dialog_caption = WizardStrings.OpenTypeFolder.format(folder_name=format_name) path_edit = PathEdit( parent=import_widget, path_type=path_type, dialog_caption=dialog_caption, show_revert=False) diff --git a/openlp/plugins/songs/forms/songmaintenanceform.py b/openlp/plugins/songs/forms/songmaintenanceform.py index 4dc485e24..7f0236d24 100644 --- a/openlp/plugins/songs/forms/songmaintenanceform.py +++ b/openlp/plugins/songs/forms/songmaintenanceform.py @@ -25,7 +25,8 @@ from PyQt5 import QtCore, QtWidgets from sqlalchemy.sql import and_ from openlp.core.common.i18n import UiStrings, translate, get_natural_key -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.mixins import RegistryProperties +from openlp.core.common.registry import Registry from openlp.core.lib.ui import critical_error_message_box from openlp.plugins.songs.forms.authorsform import AuthorsForm from openlp.plugins.songs.forms.topicsform import TopicsForm diff --git a/openlp/plugins/songs/forms/songselectdialog.py b/openlp/plugins/songs/forms/songselectdialog.py index b784071bd..bc0bd7415 100644 --- a/openlp/plugins/songs/forms/songselectdialog.py +++ b/openlp/plugins/songs/forms/songselectdialog.py @@ -28,7 +28,7 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import translate from openlp.core.lib import build_icon from openlp.core.ui import SingleColumnTableWidget -from openlp.core.ui.lib.historycombobox import HistoryComboBox +from openlp.core.widgets.edits import HistoryComboBox class Ui_SongSelectDialog(object): diff --git a/openlp/plugins/songs/lib/importer.py b/openlp/plugins/songs/lib/importer.py index 3b8081999..e306dae87 100644 --- a/openlp/plugins/songs/lib/importer.py +++ b/openlp/plugins/songs/lib/importer.py @@ -26,7 +26,7 @@ import logging from openlp.core.common import is_win from openlp.core.common.i18n import UiStrings, translate -from openlp.core.ui.lib.wizard import WizardStrings +from openlp.core.widgets.wizard import WizardStrings from .importers.opensong import OpenSongImport from .importers.easyslides import EasySlidesImport from .importers.openlp import OpenLPSongImport diff --git a/openlp/plugins/songs/lib/importers/easyworship.py b/openlp/plugins/songs/lib/importers/easyworship.py index c82a7e6ad..8f096099f 100644 --- a/openlp/plugins/songs/lib/importers/easyworship.py +++ b/openlp/plugins/songs/lib/importers/easyworship.py @@ -44,7 +44,7 @@ NOTE_REGEX = re.compile(r'\(.*?\)') log = logging.getLogger(__name__) -class FieldDescEntry: +class FieldDescEntry(object): def __init__(self, name, field_type, size): self.name = name self.field_type = field_type diff --git a/openlp/plugins/songs/lib/importers/foilpresenter.py b/openlp/plugins/songs/lib/importers/foilpresenter.py index 860177172..f164dfda2 100644 --- a/openlp/plugins/songs/lib/importers/foilpresenter.py +++ b/openlp/plugins/songs/lib/importers/foilpresenter.py @@ -88,7 +88,7 @@ import re from lxml import etree, objectify from openlp.core.common.i18n import translate -from openlp.core.ui.lib.wizard import WizardStrings +from openlp.core.widgets.wizard import WizardStrings from openlp.plugins.songs.lib import clean_song, VerseType from openlp.plugins.songs.lib.importers.songimport import SongImport from openlp.plugins.songs.lib.db import Author, Book, Song, Topic diff --git a/openlp/plugins/songs/lib/importers/openlp.py b/openlp/plugins/songs/lib/importers/openlp.py index 252b8fd8b..80a981d02 100644 --- a/openlp/plugins/songs/lib/importers/openlp.py +++ b/openlp/plugins/songs/lib/importers/openlp.py @@ -31,7 +31,7 @@ from sqlalchemy.orm.exc import UnmappedClassError from openlp.core.common.i18n import translate from openlp.core.lib.db import BaseModel -from openlp.core.ui.lib.wizard import WizardStrings +from openlp.core.widgets.wizard import WizardStrings from openlp.plugins.songs.lib import clean_song from openlp.plugins.songs.lib.db import Author, Book, Song, Topic, MediaFile from .songimport import SongImport diff --git a/openlp/plugins/songs/lib/importers/openlyrics.py b/openlp/plugins/songs/lib/importers/openlyrics.py index 44f5f96bf..f443cfb34 100644 --- a/openlp/plugins/songs/lib/importers/openlyrics.py +++ b/openlp/plugins/songs/lib/importers/openlyrics.py @@ -27,7 +27,7 @@ import logging from lxml import etree -from openlp.core.ui.lib.wizard import WizardStrings +from openlp.core.widgets.wizard import WizardStrings from openlp.plugins.songs.lib.importers.songimport import SongImport from openlp.plugins.songs.lib.ui import SongStrings from openlp.plugins.songs.lib.openlyricsxml import OpenLyrics, OpenLyricsError diff --git a/openlp/plugins/songs/lib/importers/powerpraise.py b/openlp/plugins/songs/lib/importers/powerpraise.py index a08652e3f..fd6f64360 100644 --- a/openlp/plugins/songs/lib/importers/powerpraise.py +++ b/openlp/plugins/songs/lib/importers/powerpraise.py @@ -25,8 +25,8 @@ Powerpraise song files into the current database. """ from lxml import objectify -from openlp.core.ui.lib.wizard import WizardStrings -from .songimport import SongImport +from openlp.core.widgets.wizard import WizardStrings +from openlp.plugins.songs.lib.importers.songimport import SongImport class PowerPraiseImport(SongImport): diff --git a/openlp/plugins/songs/lib/importers/presentationmanager.py b/openlp/plugins/songs/lib/importers/presentationmanager.py index e7fec2a6c..c8f9a16f9 100644 --- a/openlp/plugins/songs/lib/importers/presentationmanager.py +++ b/openlp/plugins/songs/lib/importers/presentationmanager.py @@ -29,8 +29,8 @@ from lxml import objectify, etree from openlp.core.common.i18n import translate from openlp.core.common import get_file_encoding -from openlp.core.ui.lib.wizard import WizardStrings -from .songimport import SongImport +from openlp.core.widgets.wizard import WizardStrings +from openlp.plugins.songs.lib.importers.songimport import SongImport class PresentationManagerImport(SongImport): diff --git a/openlp/plugins/songs/lib/importers/propresenter.py b/openlp/plugins/songs/lib/importers/propresenter.py index 582b1a6ee..0de0e5300 100644 --- a/openlp/plugins/songs/lib/importers/propresenter.py +++ b/openlp/plugins/songs/lib/importers/propresenter.py @@ -27,9 +27,9 @@ import base64 import logging from lxml import objectify -from openlp.core.ui.lib.wizard import WizardStrings +from openlp.core.widgets.wizard import WizardStrings from openlp.plugins.songs.lib import strip_rtf -from .songimport import SongImport +from openlp.plugins.songs.lib.importers.songimport import SongImport log = logging.getLogger(__name__) diff --git a/openlp/plugins/songs/lib/importers/songimport.py b/openlp/plugins/songs/lib/importers/songimport.py index e0cc5220e..a67c17fe7 100644 --- a/openlp/plugins/songs/lib/importers/songimport.py +++ b/openlp/plugins/songs/lib/importers/songimport.py @@ -29,7 +29,7 @@ from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import translate from openlp.core.common.path import copyfile, create_paths from openlp.core.common.registry import Registry -from openlp.core.ui.lib.wizard import WizardStrings +from openlp.core.widgets.wizard import WizardStrings from openlp.plugins.songs.lib import clean_song, VerseType from openlp.plugins.songs.lib.db import Song, Author, Topic, Book, MediaFile from openlp.plugins.songs.lib.ui import SongStrings diff --git a/openlp/plugins/songs/lib/importers/songshowplus.py b/openlp/plugins/songs/lib/importers/songshowplus.py index 2fcf414dd..3109fae1d 100644 --- a/openlp/plugins/songs/lib/importers/songshowplus.py +++ b/openlp/plugins/songs/lib/importers/songshowplus.py @@ -27,7 +27,7 @@ import logging import re import struct -from openlp.core.ui.lib.wizard import WizardStrings +from openlp.core.widgets.wizard import WizardStrings from openlp.plugins.songs.lib import VerseType, retrieve_windows_encoding from openlp.plugins.songs.lib.importers.songimport import SongImport diff --git a/openlp/plugins/songs/lib/openlyricsexport.py b/openlp/plugins/songs/lib/openlyricsexport.py index ca7fe0bc7..b07298253 100644 --- a/openlp/plugins/songs/lib/openlyricsexport.py +++ b/openlp/plugins/songs/lib/openlyricsexport.py @@ -30,7 +30,7 @@ from lxml import etree from openlp.core.common import clean_filename from openlp.core.common.i18n import translate from openlp.core.common.path import create_paths -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.plugins.songs.lib.openlyricsxml import OpenLyrics log = logging.getLogger(__name__) diff --git a/openlp/plugins/songs/reporting.py b/openlp/plugins/songs/reporting.py index 01fb0af6c..808c05b57 100644 --- a/openlp/plugins/songs/reporting.py +++ b/openlp/plugins/songs/reporting.py @@ -29,7 +29,7 @@ from openlp.core.common.i18n import translate from openlp.core.common.path import Path from openlp.core.common.registry import Registry from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.lib.filedialog import FileDialog +from openlp.core.widgets.dialogs import FileDialog from openlp.plugins.songs.lib.db import Song log = logging.getLogger(__name__) diff --git a/openlp/plugins/songusage/forms/songusagedeleteform.py b/openlp/plugins/songusage/forms/songusagedeleteform.py index 4a619e64a..9042756d6 100644 --- a/openlp/plugins/songusage/forms/songusagedeleteform.py +++ b/openlp/plugins/songusage/forms/songusagedeleteform.py @@ -23,7 +23,7 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import translate -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.plugins.songusage.lib.db import SongUsageItem from openlp.plugins.songusage.forms.songusagedeletedialog import Ui_SongUsageDeleteDialog diff --git a/openlp/plugins/songusage/forms/songusagedetaildialog.py b/openlp/plugins/songusage/forms/songusagedetaildialog.py index 9d51b041a..7f6d1c16b 100644 --- a/openlp/plugins/songusage/forms/songusagedetaildialog.py +++ b/openlp/plugins/songusage/forms/songusagedetaildialog.py @@ -24,7 +24,8 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import translate from openlp.core.lib import build_icon from openlp.core.lib.ui import create_button_box -from openlp.core.ui.lib import PathEdit, PathType +from openlp.core.widgets.edits import PathEdit +from openlp.core.widgets.enums import PathEditType class Ui_SongUsageDetailDialog(object): @@ -68,7 +69,7 @@ class Ui_SongUsageDetailDialog(object): self.file_horizontal_layout.setSpacing(8) self.file_horizontal_layout.setContentsMargins(8, 8, 8, 8) self.file_horizontal_layout.setObjectName('file_horizontal_layout') - self.report_path_edit = PathEdit(self.file_group_box, path_type=PathType.Directories, show_revert=False) + self.report_path_edit = PathEdit(self.file_group_box, path_type=PathEditType.Directories, show_revert=False) self.file_horizontal_layout.addWidget(self.report_path_edit) self.vertical_layout.addWidget(self.file_group_box) self.button_box = create_button_box(song_usage_detail_dialog, 'button_box', ['cancel', 'ok']) diff --git a/openlp/plugins/songusage/forms/songusagedetailform.py b/openlp/plugins/songusage/forms/songusagedetailform.py index afc013bbd..147e26d10 100644 --- a/openlp/plugins/songusage/forms/songusagedetailform.py +++ b/openlp/plugins/songusage/forms/songusagedetailform.py @@ -25,7 +25,7 @@ from PyQt5 import QtCore, QtWidgets from sqlalchemy.sql import and_ from openlp.core.common.i18n import translate -from openlp.core.common.registry import RegistryProperties +from openlp.core.common.mixins import RegistryProperties from openlp.core.common.settings import Settings from openlp.core.common.path import create_paths from openlp.core.lib.ui import critical_error_message_box diff --git a/tests/functional/openlp_core/common/test_mixins.py b/tests/functional/openlp_core/common/test_mixins.py index 7cb8604af..8eee60c6f 100644 --- a/tests/functional/openlp_core/common/test_mixins.py +++ b/tests/functional/openlp_core/common/test_mixins.py @@ -23,45 +23,76 @@ Package to test the openlp.core.common package. """ from unittest import TestCase +from unittest.mock import MagicMock, patch -from openlp.core.common.mixins import RegistryMixin +from openlp.core.common.mixins import RegistryProperties from openlp.core.common.registry import Registry -class PlainStub(object): - def __init__(self): - pass - - -class MixinStub(RegistryMixin): - def __init__(self): - super().__init__(None) - - -class TestRegistryMixin(TestCase): - - def test_registry_mixin_missing(self): +class TestRegistryProperties(TestCase, RegistryProperties): + """ + Test the functions in the ThemeManager module + """ + def setUp(self): """ - Test the registry creation and its usage + Create the Register """ - # GIVEN: A new registry Registry.create() - # WHEN: I create an instance of a class that doesn't inherit from RegistryMixin - PlainStub() - - # THEN: Nothing is registered with the registry - self.assertEqual(len(Registry().functions_list), 0), 'The function should not be in the dict anymore.' - - def test_registry_mixin_present(self): + def test_no_application(self): """ - Test the registry creation and its usage + Test property if no registry value assigned """ - # GIVEN: A new registry - Registry.create() + # GIVEN an Empty Registry + # WHEN there is no Application + # THEN the application should be none + self.assertEqual(self.application, None, 'The application value should be None') - # WHEN: I create an instance of a class that inherits from RegistryMixin - MixinStub() + def test_application(self): + """ + Test property if registry value assigned + """ + # GIVEN an Empty Registry + application = MagicMock() - # THEN: The bootstrap methods should be registered - self.assertEqual(len(Registry().functions_list), 2), 'The bootstrap functions should be in the dict.' + # WHEN the application is registered + Registry().register('application', application) + + # THEN the application should be none + self.assertEqual(self.application, application, 'The application value should match') + + @patch('openlp.core.common.mixins.is_win') + def test_application_on_windows(self, mocked_is_win): + """ + Test property if registry value assigned on Windows + """ + # GIVEN an Empty Registry and we're on Windows + application = MagicMock() + mocked_is_win.return_value = True + + # WHEN the application is registered + Registry().register('application', application) + + # THEN the application should be none + self.assertEqual(self.application, application, 'The application value should match') + + @patch('openlp.core.common.mixins.is_win') + def test_get_application_on_windows(self, mocked_is_win): + """ + Set that getting the application object on Windows happens dynamically + """ + # GIVEN an Empty Registry and we're on Windows + mocked_is_win.return_value = True + mock_application = MagicMock() + reg_props = RegistryProperties() + registry = Registry() + + # WHEN the application is accessed + with patch.object(registry, 'get') as mocked_get: + mocked_get.return_value = mock_application + actual_application = reg_props.application + + # THEN the application should be the mock object, and the correct function should have been called + self.assertEqual(mock_application, actual_application, 'The application value should match') + mocked_is_win.assert_called_with() + mocked_get.assert_called_with('application') diff --git a/tests/functional/openlp_core/common/test_registry.py b/tests/functional/openlp_core/common/test_registry.py index 6428a27c3..a274243bd 100644 --- a/tests/functional/openlp_core/common/test_registry.py +++ b/tests/functional/openlp_core/common/test_registry.py @@ -24,9 +24,9 @@ Package to test the openlp.core.lib package. """ import os from unittest import TestCase -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock -from openlp.core.common.registry import Registry, RegistryProperties +from openlp.core.common.registry import Registry, RegistryBase TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '../', '..', 'resources')) @@ -151,70 +151,40 @@ class TestRegistry(TestCase): return "function_2" -class TestRegistryProperties(TestCase, RegistryProperties): - """ - Test the functions in the ThemeManager module - """ - def setUp(self): +class PlainStub(object): + def __init__(self): + pass + + +class RegistryStub(RegistryBase): + def __init__(self): + super().__init__() + + +class TestRegistryBase(TestCase): + + def test_registry_mixin_missing(self): """ - Create the Register + Test the registry creation and its usage """ + # GIVEN: A new registry Registry.create() - def test_no_application(self): + # WHEN: I create an instance of a class that doesn't inherit from RegistryMixin + PlainStub() + + # THEN: Nothing is registered with the registry + self.assertEqual(len(Registry().functions_list), 0), 'The function should not be in the dict anymore.' + + def test_registry_mixin_present(self): """ - Test property if no registry value assigned + Test the registry creation and its usage """ - # GIVEN an Empty Registry - # WHEN there is no Application - # THEN the application should be none - self.assertEqual(self.application, None, 'The application value should be None') + # GIVEN: A new registry + Registry.create() - def test_application(self): - """ - Test property if registry value assigned - """ - # GIVEN an Empty Registry - application = MagicMock() + # WHEN: I create an instance of a class that inherits from RegistryMixin + RegistryStub() - # WHEN the application is registered - Registry().register('application', application) - - # THEN the application should be none - self.assertEqual(self.application, application, 'The application value should match') - - @patch('openlp.core.common.registry.is_win') - def test_application_on_windows(self, mocked_is_win): - """ - Test property if registry value assigned on Windows - """ - # GIVEN an Empty Registry and we're on Windows - application = MagicMock() - mocked_is_win.return_value = True - - # WHEN the application is registered - Registry().register('application', application) - - # THEN the application should be none - self.assertEqual(self.application, application, 'The application value should match') - - @patch('openlp.core.common.registry.is_win') - def test_get_application_on_windows(self, mocked_is_win): - """ - Set that getting the application object on Windows happens dynamically - """ - # GIVEN an Empty Registry and we're on Windows - mocked_is_win.return_value = True - mock_application = MagicMock() - reg_props = RegistryProperties() - registry = Registry() - - # WHEN the application is accessed - with patch.object(registry, 'get') as mocked_get: - mocked_get.return_value = mock_application - actual_application = reg_props.application - - # THEN the application should be the mock object, and the correct function should have been called - self.assertEqual(mock_application, actual_application, 'The application value should match') - mocked_is_win.assert_called_with() - mocked_get.assert_called_with('application') + # THEN: The bootstrap methods should be registered + self.assertEqual(len(Registry().functions_list), 2), 'The bootstrap functions should be in the dict.' diff --git a/tests/functional/openlp_core/test_app.py b/tests/functional/openlp_core/test_app.py index 0f5633c3e..eccb81447 100644 --- a/tests/functional/openlp_core/test_app.py +++ b/tests/functional/openlp_core/test_app.py @@ -29,8 +29,6 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.app import OpenLP, parse_options from openlp.core.common.settings import Settings -from tests.helpers.testmixin import TestMixin - TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'resources')) @@ -141,23 +139,33 @@ class TestOpenLP(TestCase): """ Test the OpenLP app class """ - @patch('openlp.core.app.QtWidgets.QApplication.exec') - def test_exec(self, mocked_exec): + def setUp(self): + self.build_settings() + self.qapplication_patcher = patch('openlp.core.app.QtGui.QApplication') + self.mocked_qapplication = self.qapplication_patcher.start() + self.openlp = OpenLP([]) + + def tearDown(self): + self.qapplication_patcher.stop() + self.destroy_settings() + del self.openlp + self.openlp = None + + def test_exec(self): """ Test the exec method """ # GIVEN: An app - app = OpenLP([]) - app.shared_memory = MagicMock() - mocked_exec.return_value = False + self.openlp.shared_memory = MagicMock() + self.mocked_qapplication.exec.return_value = False # WHEN: exec() is called - result = app.exec() + result = self.openlp.exec() # THEN: The right things should be called - assert app.is_event_loop_active is True - mocked_exec.assert_called_once_with() - app.shared_memory.detach.assert_called_once_with() + assert self.openlp.is_event_loop_active is True + self.mocked_qapplication.exec.assert_called_once_with() + self.openlp.shared_memory.detach.assert_called_once_with() assert result is False @patch('openlp.core.app.QtCore.QSharedMemory') @@ -169,10 +177,9 @@ class TestOpenLP(TestCase): mocked_shared_memory = MagicMock() mocked_shared_memory.attach.return_value = False MockedSharedMemory.return_value = mocked_shared_memory - app = OpenLP([]) # WHEN: is_already_running() is called - result = app.is_already_running() + result = self.openlp.is_already_running() # THEN: The result should be false MockedSharedMemory.assert_called_once_with('OpenLP') @@ -193,10 +200,9 @@ class TestOpenLP(TestCase): MockedSharedMemory.return_value = mocked_shared_memory MockedStandardButtons.return_value = 0 mocked_critical.return_value = QtWidgets.QMessageBox.Yes - app = OpenLP([]) # WHEN: is_already_running() is called - result = app.is_already_running() + result = self.openlp.is_already_running() # THEN: The result should be false MockedSharedMemory.assert_called_once_with('OpenLP') @@ -218,10 +224,9 @@ class TestOpenLP(TestCase): MockedSharedMemory.return_value = mocked_shared_memory MockedStandardButtons.return_value = 0 mocked_critical.return_value = QtWidgets.QMessageBox.No - app = OpenLP([]) # WHEN: is_already_running() is called - result = app.is_already_running() + result = self.openlp.is_already_running() # THEN: The result should be false MockedSharedMemory.assert_called_once_with('OpenLP') @@ -235,11 +240,9 @@ class TestOpenLP(TestCase): Test that the app.process_events() method simply calls the Qt method """ # GIVEN: An app - app = OpenLP([]) - # WHEN: process_events() is called - with patch.object(app, 'processEvents') as mocked_processEvents: - app.process_events() + with patch.object(self.openlp, 'processEvents') as mocked_processEvents: + self.openlp.process_events() # THEN: processEvents was called mocked_processEvents.assert_called_once_with() @@ -249,12 +252,10 @@ class TestOpenLP(TestCase): Test that the set_busy_cursor() method sets the cursor """ # GIVEN: An app - app = OpenLP([]) - # WHEN: set_busy_cursor() is called - with patch.object(app, 'setOverrideCursor') as mocked_setOverrideCursor, \ - patch.object(app, 'processEvents') as mocked_processEvents: - app.set_busy_cursor() + with patch.object(self.openlp, 'setOverrideCursor') as mocked_setOverrideCursor, \ + patch.object(self.openlp, 'processEvents') as mocked_processEvents: + self.openlp.set_busy_cursor() # THEN: The cursor should have been set mocked_setOverrideCursor.assert_called_once_with(QtCore.Qt.BusyCursor) @@ -265,29 +266,15 @@ class TestOpenLP(TestCase): Test that the set_normal_cursor() method resets the cursor """ # GIVEN: An app - app = OpenLP([]) - # WHEN: set_normal_cursor() is called - with patch.object(app, 'restoreOverrideCursor') as mocked_restoreOverrideCursor, \ - patch.object(app, 'processEvents') as mocked_processEvents: - app.set_normal_cursor() + with patch.object(self.openlp, 'restoreOverrideCursor') as mocked_restoreOverrideCursor, \ + patch.object(self.openlp, 'processEvents') as mocked_processEvents: + self.openlp.set_normal_cursor() # THEN: The cursor should have been set mocked_restoreOverrideCursor.assert_called_once_with() mocked_processEvents.assert_called_once_with() - -class TestInit(TestCase, TestMixin): - def setUp(self): - self.build_settings() - with patch('openlp.core.app.OpenLPMixin.__init__') as constructor: - constructor.return_value = None - self.openlp = OpenLP(list()) - - def tearDown(self): - self.destroy_settings() - del self.openlp - def test_event(self): """ Test the reimplemented event method diff --git a/tests/functional/openlp_core/ui/lib/test_listwidgetwithdnd.py b/tests/functional/openlp_core/ui/lib/test_listwidgetwithdnd.py deleted file mode 100755 index 2a4e039fa..000000000 --- a/tests/functional/openlp_core/ui/lib/test_listwidgetwithdnd.py +++ /dev/null @@ -1,137 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2017 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### -""" -This module contains tests for the openlp.core.lib.listwidgetwithdnd module -""" -from unittest import TestCase -from unittest.mock import MagicMock, patch -from types import GeneratorType - -from openlp.core.common.i18n import UiStrings -from openlp.core.ui.lib.listwidgetwithdnd import ListWidgetWithDnD - - -class TestListWidgetWithDnD(TestCase): - """ - Test the :class:`~openlp.core.lib.listwidgetwithdnd.ListWidgetWithDnD` class - """ - def test_clear(self): - """ - Test the clear method when called without any arguments. - """ - # GIVEN: An instance of ListWidgetWithDnD - widget = ListWidgetWithDnD() - - # WHEN: Calling clear with out any arguments - widget.clear() - - # THEN: The results text should be the standard 'no results' text. - self.assertEqual(widget.no_results_text, UiStrings().NoResults) - - def test_clear_search_while_typing(self): - """ - Test the clear method when called with the search_while_typing argument set to True - """ - # GIVEN: An instance of ListWidgetWithDnD - widget = ListWidgetWithDnD() - - # WHEN: Calling clear with search_while_typing set to True - widget.clear(search_while_typing=True) - - # THEN: The results text should be the 'short results' text. - self.assertEqual(widget.no_results_text, UiStrings().ShortResults) - - def test_all_items_no_list_items(self): - """ - Test allItems when there are no items in the list widget - """ - # GIVEN: An instance of ListWidgetWithDnD - widget = ListWidgetWithDnD() - with patch.object(widget, 'count', return_value=0), \ - patch.object(widget, 'item', side_effect=lambda x: [][x]): - - # WHEN: Calling allItems - result = widget.allItems() - - # THEN: An instance of a Generator object should be returned. The generator should not yeild any results - self.assertIsInstance(result, GeneratorType) - self.assertEqual(list(result), []) - - def test_all_items_list_items(self): - """ - Test allItems when the list widget contains some items. - """ - # GIVEN: An instance of ListWidgetWithDnD - widget = ListWidgetWithDnD() - with patch.object(widget, 'count', return_value=2), \ - patch.object(widget, 'item', side_effect=lambda x: [5, 3][x]): - - # WHEN: Calling allItems - result = widget.allItems() - - # THEN: An instance of a Generator object should be returned. The generator should not yeild any results - self.assertIsInstance(result, GeneratorType) - self.assertEqual(list(result), [5, 3]) - - def test_paint_event(self): - """ - Test the paintEvent method when the list is not empty - """ - # GIVEN: An instance of ListWidgetWithDnD with a mocked out count methode which returns 1 - # (i.e the list has an item) - widget = ListWidgetWithDnD() - with patch('openlp.core.ui.lib.listwidgetwithdnd.QtWidgets.QListWidget.paintEvent') as mocked_paint_event, \ - patch.object(widget, 'count', return_value=1), \ - patch.object(widget, 'viewport') as mocked_viewport: - mocked_event = MagicMock() - - # WHEN: Calling paintEvent - widget.paintEvent(mocked_event) - - # THEN: The overridden paintEvnet should have been called - mocked_paint_event.assert_called_once_with(mocked_event) - self.assertFalse(mocked_viewport.called) - - def test_paint_event_no_items(self): - """ - Test the paintEvent method when the list is empty - """ - # GIVEN: An instance of ListWidgetWithDnD with a mocked out count methode which returns 0 - # (i.e the list is empty) - widget = ListWidgetWithDnD() - mocked_painter_instance = MagicMock() - mocked_qrect = MagicMock() - with patch('openlp.core.ui.lib.listwidgetwithdnd.QtWidgets.QListWidget.paintEvent') as mocked_paint_event, \ - patch.object(widget, 'count', return_value=0), \ - patch.object(widget, 'viewport'), \ - patch('openlp.core.ui.lib.listwidgetwithdnd.QtGui.QPainter', - return_value=mocked_painter_instance) as mocked_qpainter, \ - patch('openlp.core.ui.lib.listwidgetwithdnd.QtCore.QRect', return_value=mocked_qrect): - mocked_event = MagicMock() - - # WHEN: Calling paintEvent - widget.paintEvent(mocked_event) - - # THEN: The overridden paintEvnet should have been called, and some text should be drawn. - mocked_paint_event.assert_called_once_with(mocked_event) - mocked_qpainter.assert_called_once_with(widget.viewport()) - mocked_painter_instance.drawText.assert_called_once_with(mocked_qrect, 4100, 'No Search Results') diff --git a/tests/functional/openlp_core/ui/test_mainwindow.py b/tests/functional/openlp_core/ui/test_mainwindow.py index 4bb1cee37..5e1a69cbc 100644 --- a/tests/functional/openlp_core/ui/test_mainwindow.py +++ b/tests/functional/openlp_core/ui/test_mainwindow.py @@ -26,10 +26,11 @@ import os from unittest import TestCase from unittest.mock import MagicMock, patch -from PyQt5 import QtWidgets +from PyQt5 import QtCore, QtWidgets from openlp.core.common.i18n import UiStrings from openlp.core.common.registry import Registry +from openlp.core.display.screens import ScreenList from openlp.core.ui.mainwindow import MainWindow from tests.helpers.testmixin import TestMixin @@ -37,8 +38,23 @@ from tests.utils.constants import TEST_RESOURCES_PATH class TestMainWindow(TestCase, TestMixin): + """ + Test the main window + """ + def _create_mock_action(self, parent, name, **kwargs): + """ + Create a fake action with some "real" attributes + """ + action = QtWidgets.QAction(parent) + action.setObjectName(name) + if kwargs.get('triggers'): + action.triggered.connect(kwargs.pop('triggers')) + return action def setUp(self): + """ + Set up the objects we need for all of the tests + """ Registry.create() self.registry = Registry() self.setup_application() @@ -48,30 +64,18 @@ class TestMainWindow(TestCase, TestMixin): self.app.args = [] Registry().register('application', self.app) Registry().set_flag('no_web_server', False) - # Mock classes and methods used by mainwindow. - with patch('openlp.core.ui.mainwindow.SettingsForm') as mocked_settings_form, \ - patch('openlp.core.ui.mainwindow.ImageManager') as mocked_image_manager, \ - patch('openlp.core.ui.mainwindow.LiveController') as mocked_live_controller, \ - patch('openlp.core.ui.mainwindow.PreviewController') as mocked_preview_controller, \ - patch('openlp.core.ui.mainwindow.OpenLPDockWidget') as mocked_dock_widget, \ - patch('openlp.core.ui.mainwindow.QtWidgets.QToolBox') as mocked_q_tool_box_class, \ - patch('openlp.core.ui.mainwindow.QtWidgets.QMainWindow.addDockWidget') as mocked_add_dock_method, \ - patch('openlp.core.ui.mainwindow.ThemeManager') as mocked_theme_manager, \ - patch('openlp.core.ui.mainwindow.Renderer') as mocked_renderer, \ - patch('openlp.core.ui.mainwindow.websockets.WebSocketServer'), \ - patch('openlp.core.ui.mainwindow.server.HttpServer'): - self.mocked_settings_form = mocked_settings_form - self.mocked_image_manager = mocked_image_manager - self.mocked_live_controller = mocked_live_controller - self.mocked_preview_controller = mocked_preview_controller - self.mocked_dock_widget = mocked_dock_widget - self.mocked_q_tool_box_class = mocked_q_tool_box_class - self.mocked_add_dock_method = mocked_add_dock_method - self.mocked_theme_manager = mocked_theme_manager - self.mocked_renderer = mocked_renderer + self.add_toolbar_action_patcher = patch('openlp.core.ui.mainwindow.create_action') + self.mocked_add_toolbar_action = self.add_toolbar_action_patcher.start() + self.mocked_add_toolbar_action.side_effect = self._create_mock_action + with patch('openlp.core.display.screens.ScreenList.__instance__', spec=ScreenList) as mocked_screen_list: + mocked_screen_list.current = {'number': 0, 'size': QtCore.QSize(600, 800), 'primary': True} self.main_window = MainWindow() def tearDown(self): + """ + Delete all the C++ objects and stop all the patchers + """ + self.add_toolbar_action_patcher.stop() del self.main_window def test_cmd_line_file(self): @@ -81,13 +85,13 @@ class TestMainWindow(TestCase, TestMixin): # GIVEN a service as an argument to openlp service = os.path.join(TEST_RESOURCES_PATH, 'service', 'test.osz') self.main_window.arguments = [service] - with patch('openlp.core.ui.servicemanager.ServiceManager.load_file') as mocked_load_path: - # WHEN the argument is processed + # WHEN the argument is processed + with patch.object(self.main_window.service_manager, 'load_file') as mocked_load_file: self.main_window.open_cmd_line_files(service) - # THEN the service from the arguments is loaded - mocked_load_path.assert_called_with(service), 'load_path should have been called with the service\'s path' + # THEN the service from the arguments is loaded + mocked_load_file.assert_called_with(service) def test_cmd_line_arg(self): """ @@ -96,13 +100,13 @@ class TestMainWindow(TestCase, TestMixin): # GIVEN a non service file as an argument to openlp service = os.path.join('openlp.py') self.main_window.arguments = [service] - with patch('openlp.core.ui.servicemanager.ServiceManager.load_file') as mocked_load_path: + with patch('openlp.core.ui.servicemanager.ServiceManager.load_file') as mocked_load_file: # WHEN the argument is processed self.main_window.open_cmd_line_files("") # THEN the file should not be opened - assert not mocked_load_path.called, 'load_path should not have been called' + assert not mocked_load_file.called, 'load_file should not have been called' def test_main_window_title(self): """ @@ -151,14 +155,14 @@ class TestMainWindow(TestCase, TestMixin): # WHEN: you check the started functions # THEN: the following registry functions should have been registered - self.assertEqual(len(self.registry.service_list), 6, 'The registry should have 6 services.') - self.assertEqual(len(self.registry.functions_list), 18, 'The registry should have 18 functions') - self.assertTrue('application' in self.registry.service_list, 'The application should have been registered.') - self.assertTrue('main_window' in self.registry.service_list, 'The main_window should have been registered.') - self.assertTrue('media_controller' in self.registry.service_list, 'The media_controller should have been ' - 'registered.') - self.assertTrue('plugin_manager' in self.registry.service_list, - 'The plugin_manager should have been registered.') + assert len(self.registry.service_list) == 12, \ + 'The registry should have 12 services, got {}'.format(self.registry.service_list.keys()) + assert len(self.registry.functions_list) == 19, \ + 'The registry should have 19 functions, got {}'.format(self.registry.functions_list.keys()) + assert 'application' in self.registry.service_list, 'The application should have been registered.' + assert 'main_window' in self.registry.service_list, 'The main_window should have been registered.' + assert 'media_controller' in self.registry.service_list, 'The media_controller should have been registered.' + assert 'plugin_manager' in self.registry.service_list, 'The plugin_manager should have been registered.' def test_projector_manager_hidden_on_startup(self): """ @@ -167,7 +171,7 @@ class TestMainWindow(TestCase, TestMixin): # GIVEN: A built main window # WHEN: OpenLP is started # THEN: The projector manager should be hidden - self.main_window.projector_manager_dock.setVisible.assert_called_once_with(False) + assert self.main_window.projector_manager_dock.isVisible() is False def test_on_search_shortcut_triggered_shows_media_manager(self): """ @@ -203,56 +207,38 @@ class TestMainWindow(TestCase, TestMixin): self.assertEqual(0, mocked_media_manager_dock.setVisible.call_count) mocked_widget.on_focus.assert_called_with() - @patch('openlp.core.ui.mainwindow.MainWindow.plugin_manager') - @patch('openlp.core.ui.mainwindow.MainWindow.first_time') - @patch('openlp.core.ui.mainwindow.MainWindow.application') @patch('openlp.core.ui.mainwindow.FirstTimeForm') @patch('openlp.core.ui.mainwindow.QtWidgets.QMessageBox.warning') @patch('openlp.core.ui.mainwindow.Settings') - def test_on_first_time_wizard_clicked_show_projectors_after(self, mocked_Settings, mocked_warning, - mocked_FirstTimeForm, mocked_application, - mocked_first_time, - mocked_plugin_manager): + def test_on_first_time_wizard_clicked_show_projectors_after(self, MockSettings, mocked_warning, MockWizard): + """Test that the projector manager is shown after the FTW is run""" # GIVEN: Main_window, patched things, patched "Yes" as confirmation to re-run wizard, settings to True. - mocked_Settings_obj = MagicMock() - mocked_Settings_obj.value.return_value = True - mocked_Settings.return_value = mocked_Settings_obj + MockSettings.return_value.value.return_value = True mocked_warning.return_value = QtWidgets.QMessageBox.Yes - mocked_FirstTimeForm_obj = MagicMock() - mocked_FirstTimeForm_obj.was_cancelled = False - mocked_FirstTimeForm.return_value = mocked_FirstTimeForm_obj - mocked_plugin_manager.plugins = [] - self.main_window.projector_manager_dock = MagicMock() + MockWizard.return_value.was_cancelled = False - # WHEN: on_first_time_wizard_clicked is called - self.main_window.on_first_time_wizard_clicked() + with patch.object(self.main_window, 'projector_manager_dock') as mocked_dock, \ + patch.object(self.registry, 'execute'), patch.object(self.main_window, 'theme_manager_contents'): + # WHEN: on_first_time_wizard_clicked is called + self.main_window.on_first_time_wizard_clicked() # THEN: projector_manager_dock.setVisible should had been called once - self.main_window.projector_manager_dock.setVisible.assert_called_once_with(True) + mocked_dock.setVisible.assert_called_once_with(True) - @patch('openlp.core.ui.mainwindow.MainWindow.plugin_manager') - @patch('openlp.core.ui.mainwindow.MainWindow.first_time') - @patch('openlp.core.ui.mainwindow.MainWindow.application') @patch('openlp.core.ui.mainwindow.FirstTimeForm') @patch('openlp.core.ui.mainwindow.QtWidgets.QMessageBox.warning') @patch('openlp.core.ui.mainwindow.Settings') - def test_on_first_time_wizard_clicked_hide_projectors_after(self, mocked_Settings, mocked_warning, - mocked_FirstTimeForm, mocked_application, - mocked_first_time, - mocked_plugin_manager): + def test_on_first_time_wizard_clicked_hide_projectors_after(self, MockSettings, mocked_warning, MockWizard): + """Test that the projector manager is hidden after the FTW is run""" # GIVEN: Main_window, patched things, patched "Yes" as confirmation to re-run wizard, settings to False. - mocked_Settings_obj = MagicMock() - mocked_Settings_obj.value.return_value = False - mocked_Settings.return_value = mocked_Settings_obj + MockSettings.return_value.value.return_value = False mocked_warning.return_value = QtWidgets.QMessageBox.Yes - mocked_FirstTimeForm_obj = MagicMock() - mocked_FirstTimeForm_obj.was_cancelled = False - mocked_FirstTimeForm.return_value = mocked_FirstTimeForm_obj - mocked_plugin_manager.plugins = [] - self.main_window.projector_manager_dock = MagicMock() + MockWizard.return_value.was_cancelled = False # WHEN: on_first_time_wizard_clicked is called - self.main_window.on_first_time_wizard_clicked() + with patch.object(self.main_window, 'projector_manager_dock') as mocked_dock, \ + patch.object(self.registry, 'execute'), patch.object(self.main_window, 'theme_manager_contents'): + self.main_window.on_first_time_wizard_clicked() # THEN: projector_manager_dock.setVisible should had been called once - self.main_window.projector_manager_dock.setVisible.assert_called_once_with(False) + mocked_dock.setVisible.assert_called_once_with(False) diff --git a/tests/functional/openlp_core/ui/test_servicemanager.py b/tests/functional/openlp_core/ui/test_servicemanager.py index 487f56362..adf241d35 100644 --- a/tests/functional/openlp_core/ui/test_servicemanager.py +++ b/tests/functional/openlp_core/ui/test_servicemanager.py @@ -32,7 +32,7 @@ from openlp.core.common import ThemeLevel from openlp.core.common.registry import Registry from openlp.core.lib import ServiceItem, ServiceItemType, ItemCapabilities from openlp.core.ui import ServiceManager -from openlp.core.ui.lib.toolbar import OpenLPToolbar +from openlp.core.widgets.toolbar import OpenLPToolbar class TestServiceManager(TestCase): diff --git a/tests/functional/openlp_core/ui/test_thememanager.py b/tests/functional/openlp_core/ui/test_thememanager.py index c1ba80f1d..62bf980b4 100644 --- a/tests/functional/openlp_core/ui/test_thememanager.py +++ b/tests/functional/openlp_core/ui/test_thememanager.py @@ -52,25 +52,24 @@ class TestThemeManager(TestCase): """ shutil.rmtree(self.temp_folder) - def test_export_theme(self): + @patch('openlp.core.ui.thememanager.zipfile.ZipFile.__init__') + @patch('openlp.core.ui.thememanager.zipfile.ZipFile.write') + def test_export_theme(self, mocked_zipfile_write, mocked_zipfile_init): """ Test exporting a theme . """ # GIVEN: A new ThemeManager instance. theme_manager = ThemeManager() theme_manager.theme_path = Path(TEST_RESOURCES_PATH, 'themes') - with patch('zipfile.ZipFile.__init__') as mocked_zipfile_init, \ - patch('zipfile.ZipFile.write') as mocked_zipfile_write: - mocked_zipfile_init.return_value = None + mocked_zipfile_init.return_value = None - # WHEN: The theme is exported - theme_manager._export_theme(Path('some', 'path', 'Default.otz'), 'Default') + # WHEN: The theme is exported + theme_manager._export_theme(Path('some', 'path', 'Default.otz'), 'Default') - # THEN: The zipfile should be created at the given path - mocked_zipfile_init.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w') - mocked_zipfile_write.assert_called_with(os.path.join(TEST_RESOURCES_PATH, 'themes', - 'Default', 'Default.xml'), - os.path.join('Default', 'Default.xml')) + # THEN: The zipfile should be created at the given path + mocked_zipfile_init.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w') + mocked_zipfile_write.assert_called_with(os.path.join(TEST_RESOURCES_PATH, 'themes', 'Default', 'Default.xml'), + os.path.join('Default', 'Default.xml')) def test_initial_theme_manager(self): """ @@ -83,53 +82,53 @@ class TestThemeManager(TestCase): # THEN: The the controller should be registered in the registry. self.assertIsNotNone(Registry().get('theme_manager'), 'The base theme manager should be registered') - def test_write_theme_same_image(self): + @patch('openlp.core.ui.thememanager.copyfile') + @patch('openlp.core.ui.thememanager.create_paths') + def test_write_theme_same_image(self, mocked_create_paths, mocked_copyfile): """ Test that we don't try to overwrite a theme background image with itself """ # GIVEN: A new theme manager instance, with mocked builtins.open, copyfile, # theme, create_paths and thememanager-attributes. - with patch('openlp.core.ui.thememanager.copyfile') as mocked_copyfile, \ - patch('openlp.core.ui.thememanager.create_paths'): - theme_manager = ThemeManager(None) - theme_manager.old_background_image = None - theme_manager.generate_and_save_image = MagicMock() - theme_manager.theme_path = MagicMock() - mocked_theme = MagicMock() - mocked_theme.theme_name = 'themename' - mocked_theme.extract_formatted_xml = MagicMock() - mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode() + theme_manager = ThemeManager(None) + theme_manager.old_background_image = None + theme_manager.generate_and_save_image = MagicMock() + theme_manager.theme_path = MagicMock() + mocked_theme = MagicMock() + mocked_theme.theme_name = 'themename' + mocked_theme.extract_formatted_xml = MagicMock() + mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode() - # WHEN: Calling _write_theme with path to the same image, but the path written slightly different - file_name1 = Path(TEST_RESOURCES_PATH, 'church.jpg') - theme_manager._write_theme(mocked_theme, file_name1, file_name1) + # WHEN: Calling _write_theme with path to the same image, but the path written slightly different + file_name1 = Path(TEST_RESOURCES_PATH, 'church.jpg') + theme_manager._write_theme(mocked_theme, file_name1, file_name1) - # THEN: The mocked_copyfile should not have been called - self.assertFalse(mocked_copyfile.called, 'copyfile should not be called') + # THEN: The mocked_copyfile should not have been called + assert mocked_copyfile.called is False, 'copyfile should not be called' - def test_write_theme_diff_images(self): + @patch('openlp.core.ui.thememanager.copyfile') + @patch('openlp.core.ui.thememanager.create_paths') + def test_write_theme_diff_images(self, mocked_create_paths, mocked_copyfile): """ Test that we do overwrite a theme background image when a new is submitted """ # GIVEN: A new theme manager instance, with mocked builtins.open, copyfile, # theme, create_paths and thememanager-attributes. - with patch('openlp.core.ui.thememanager.copyfile') as mocked_copyfile, \ - patch('openlp.core.ui.thememanager.create_paths'): - theme_manager = ThemeManager(None) - theme_manager.old_background_image = None - theme_manager.generate_and_save_image = MagicMock() - theme_manager.theme_path = MagicMock() - mocked_theme = MagicMock() - mocked_theme.theme_name = 'themename' - mocked_theme.filename = "filename" + theme_manager = ThemeManager(None) + theme_manager.old_background_image = None + theme_manager.generate_and_save_image = MagicMock() + theme_manager.theme_path = MagicMock() + mocked_theme = MagicMock() + mocked_theme.theme_name = 'themename' + mocked_theme.filename = "filename" - # WHEN: Calling _write_theme with path to different images - file_name1 = Path(TEST_RESOURCES_PATH, 'church.jpg') - file_name2 = Path(TEST_RESOURCES_PATH, 'church2.jpg') - theme_manager._write_theme(mocked_theme, file_name1, file_name2) + # WHEN: Calling _write_theme with path to different images + file_name1 = Path(TEST_RESOURCES_PATH, 'church.jpg') + file_name2 = Path(TEST_RESOURCES_PATH, 'church2.jpg') + theme_manager._write_theme(mocked_theme, file_name1, file_name2) - # THEN: The mocked_copyfile should not have been called - self.assertTrue(mocked_copyfile.called, 'copyfile should be called') + # THEN: The mocked_copyfile should not have been called + assert mocked_copyfile.called is True, 'copyfile should be called' def test_write_theme_special_char_name(self): """ @@ -151,45 +150,43 @@ class TestThemeManager(TestCase): self.assertTrue(os.path.exists(os.path.join(self.temp_folder, 'theme æ„› name', 'theme æ„› name.json')), 'Theme with special characters should have been created!') - def test_over_write_message_box_yes(self): + @patch('openlp.core.ui.thememanager.QtWidgets.QMessageBox.question', return_value=QtWidgets.QMessageBox.Yes) + @patch('openlp.core.ui.thememanager.translate') + def test_over_write_message_box_yes(self, mocked_translate, mocked_qmessagebox_question): """ Test that theme_manager.over_write_message_box returns True when the user clicks yes. """ # GIVEN: A patched QMessageBox.question and an instance of ThemeManager - with patch('openlp.core.ui.thememanager.QtWidgets.QMessageBox.question', - return_value=QtWidgets.QMessageBox.Yes) as mocked_qmessagebox_question,\ - patch('openlp.core.ui.thememanager.translate') as mocked_translate: - mocked_translate.side_effect = lambda context, text: text - theme_manager = ThemeManager(None) + mocked_translate.side_effect = lambda context, text: text + theme_manager = ThemeManager(None) - # WHEN: Calling over_write_message_box with 'Theme Name' - result = theme_manager.over_write_message_box('Theme Name') + # WHEN: Calling over_write_message_box with 'Theme Name' + result = theme_manager.over_write_message_box('Theme Name') - # THEN: over_write_message_box should return True and the message box should contain the theme name - self.assertTrue(result) - mocked_qmessagebox_question.assert_called_once_with( - theme_manager, 'Theme Already Exists', 'Theme Theme Name already exists. Do you want to replace it?', - defaultButton=ANY) + # THEN: over_write_message_box should return True and the message box should contain the theme name + assert result is True + mocked_qmessagebox_question.assert_called_once_with( + theme_manager, 'Theme Already Exists', 'Theme Theme Name already exists. Do you want to replace it?', + defaultButton=ANY) - def test_over_write_message_box_no(self): + @patch('openlp.core.ui.thememanager.QtWidgets.QMessageBox.question', return_value=QtWidgets.QMessageBox.No) + @patch('openlp.core.ui.thememanager.translate') + def test_over_write_message_box_no(self, mocked_translate, mocked_qmessagebox_question): """ Test that theme_manager.over_write_message_box returns False when the user clicks no. """ # GIVEN: A patched QMessageBox.question and an instance of ThemeManager - with patch('openlp.core.ui.thememanager.QtWidgets.QMessageBox.question', return_value=QtWidgets.QMessageBox.No)\ - as mocked_qmessagebox_question,\ - patch('openlp.core.ui.thememanager.translate') as mocked_translate: - mocked_translate.side_effect = lambda context, text: text - theme_manager = ThemeManager(None) + mocked_translate.side_effect = lambda context, text: text + theme_manager = ThemeManager(None) - # WHEN: Calling over_write_message_box with 'Theme Name' - result = theme_manager.over_write_message_box('Theme Name') + # WHEN: Calling over_write_message_box with 'Theme Name' + result = theme_manager.over_write_message_box('Theme Name') - # THEN: over_write_message_box should return False and the message box should contain the theme name - self.assertFalse(result) - mocked_qmessagebox_question.assert_called_once_with( - theme_manager, 'Theme Already Exists', 'Theme Theme Name already exists. Do you want to replace it?', - defaultButton=ANY) + # THEN: over_write_message_box should return False and the message box should contain the theme name + assert result is False + mocked_qmessagebox_question.assert_called_once_with( + theme_manager, 'Theme Already Exists', 'Theme Theme Name already exists. Do you want to replace it?', + defaultButton=ANY) def test_unzip_theme(self): """ diff --git a/tests/interfaces/openlp_core_ui_lib/__init__.py b/tests/functional/openlp_core/widgets/__init__.py similarity index 100% rename from tests/interfaces/openlp_core_ui_lib/__init__.py rename to tests/functional/openlp_core/widgets/__init__.py diff --git a/tests/functional/openlp_core/ui/lib/test_colorbutton.py b/tests/functional/openlp_core/widgets/test_buttons.py similarity index 70% rename from tests/functional/openlp_core/ui/lib/test_colorbutton.py rename to tests/functional/openlp_core/widgets/test_buttons.py index b010cbbdc..4abd38da4 100644 --- a/tests/functional/openlp_core/ui/lib/test_colorbutton.py +++ b/tests/functional/openlp_core/widgets/test_buttons.py @@ -20,12 +20,12 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -This module contains tests for the openlp.core.ui.lib.colorbutton module +This module contains tests for the openlp.core.widgets.buttons module """ from unittest import TestCase from unittest.mock import MagicMock, call, patch -from openlp.core.ui.lib import ColorButton +from openlp.core.widgets.buttons import ColorButton class TestColorDialog(TestCase): @@ -33,11 +33,11 @@ class TestColorDialog(TestCase): Test the :class:`~openlp.core.lib.colorbutton.ColorButton` class """ def setUp(self): - self.change_color_patcher = patch('openlp.core.ui.lib.colorbutton.ColorButton.change_color') - self.clicked_patcher = patch('openlp.core.ui.lib.colorbutton.ColorButton.clicked') - self.color_changed_patcher = patch('openlp.core.ui.lib.colorbutton.ColorButton.colorChanged') - self.qt_gui_patcher = patch('openlp.core.ui.lib.colorbutton.QtWidgets') - self.translate_patcher = patch('openlp.core.ui.lib.colorbutton.translate', **{'return_value': 'Tool Tip Text'}) + self.change_color_patcher = patch('openlp.core.widgets.buttons.ColorButton.change_color') + self.clicked_patcher = patch('openlp.core.widgets.buttons.ColorButton.clicked') + self.color_changed_patcher = patch('openlp.core.widgets.buttons.ColorButton.colorChanged') + self.qt_gui_patcher = patch('openlp.core.widgets.buttons.QtWidgets') + self.translate_patcher = patch('openlp.core.widgets.buttons.translate', **{'return_value': 'Tool Tip Text'}) self.addCleanup(self.change_color_patcher.stop) self.addCleanup(self.clicked_patcher.stop) self.addCleanup(self.color_changed_patcher.stop) @@ -49,41 +49,40 @@ class TestColorDialog(TestCase): self.mocked_qt_widgets = self.qt_gui_patcher.start() self.mocked_translate = self.translate_patcher.start() - def test_constructor(self): + @patch('openlp.core.widgets.buttons.ColorButton.setToolTip') + def test_constructor(self, mocked_set_tool_tip): """ Test that constructing a ColorButton object works correctly """ # GIVEN: The ColorButton class, a mocked change_color, setToolTip methods and clicked signal - with patch('openlp.core.ui.lib.colorbutton.ColorButton.setToolTip') as mocked_set_tool_tip: + # WHEN: The ColorButton object is instantiated + widget = ColorButton() - # WHEN: The ColorButton object is instantiated - widget = ColorButton() + # THEN: The widget __init__ method should have the correct properties and methods called + self.assertEqual(widget.parent, None, + 'The parent should be the same as the one that the class was instianted with') + self.mocked_change_color.assert_called_once_with('#ffffff') + mocked_set_tool_tip.assert_called_once_with('Tool Tip Text') + self.mocked_clicked.connect.assert_called_once_with(widget.on_clicked) - # THEN: The widget __init__ method should have the correct properties and methods called - self.assertEqual(widget.parent, None, - 'The parent should be the same as the one that the class was instianted with') - self.mocked_change_color.assert_called_once_with('#ffffff') - mocked_set_tool_tip.assert_called_once_with('Tool Tip Text') - self.mocked_clicked.connect.assert_called_once_with(widget.on_clicked) - - def test_change_color(self): + @patch('openlp.core.widgets.buttons.ColorButton.setStyleSheet') + def test_change_color(self, mocked_set_style_sheet): """ Test that change_color sets the new color and the stylesheet """ self.change_color_patcher.stop() # GIVEN: An instance of the ColorButton object, and a mocked out setStyleSheet - with patch('openlp.core.ui.lib.colorbutton.ColorButton.setStyleSheet') as mocked_set_style_sheet: - widget = ColorButton() + widget = ColorButton() - # WHEN: Changing the color - widget.change_color('#000000') + # WHEN: Changing the color + widget.change_color('#000000') - # THEN: The _color attribute should be set to #000000 and setStyleSheet should have been called twice - self.assertEqual(widget._color, '#000000', '_color should have been set to #000000') - mocked_set_style_sheet.assert_has_calls( - [call('background-color: #ffffff'), call('background-color: #000000')]) + # THEN: The _color attribute should be set to #000000 and setStyleSheet should have been called twice + self.assertEqual(widget._color, '#000000', '_color should have been set to #000000') + mocked_set_style_sheet.assert_has_calls( + [call('background-color: #ffffff'), call('background-color: #000000')]) self.mocked_change_color = self.change_color_patcher.start() @@ -91,22 +90,6 @@ class TestColorDialog(TestCase): """ Test that the color property method returns the set color """ - - # GIVEN: An instance of ColorButton, with a set _color attribute - widget = ColorButton() - widget._color = '#000000' - - # WHEN: Accesing the color property - value = widget.color - - # THEN: The value set in _color should be returned - self.assertEqual(value, '#000000', 'The value returned should be equal to the one we set') - - def test_color(self): - """ - Test that the color property method returns the set color - """ - # GIVEN: An instance of ColorButton, with a set _color attribute widget = ColorButton() widget._color = '#000000' @@ -117,20 +100,19 @@ class TestColorDialog(TestCase): # THEN: The value set in _color should be returned self.assertEqual(value, '#000000', 'The value returned should be equal to the one we set') + # @patch('openlp.core.widgets.buttons.ColorButton.__init__', **{'return_value': None}) def test_color_setter(self): """ Test that the color property setter method sets the color """ - # GIVEN: An instance of ColorButton, with a mocked __init__ - with patch('openlp.core.ui.lib.colorbutton.ColorButton.__init__', **{'return_value': None}): - widget = ColorButton() + widget = ColorButton() - # WHEN: Setting the color property - widget.color = '#000000' + # WHEN: Setting the color property + widget.color = '#000000' - # THEN: Then change_color should have been called with the value we set - self.mocked_change_color.assert_called_once_with('#000000') + # THEN: Then change_color should have been called with the value we set + self.mocked_change_color.assert_called_with('#000000') def test_on_clicked_invalid_color(self): """ diff --git a/tests/functional/openlp_core/ui/lib/test_filedialog.py b/tests/functional/openlp_core/widgets/test_dialogs.py similarity index 95% rename from tests/functional/openlp_core/ui/lib/test_filedialog.py rename to tests/functional/openlp_core/widgets/test_dialogs.py index 777ff65ec..fd65de33e 100755 --- a/tests/functional/openlp_core/ui/lib/test_filedialog.py +++ b/tests/functional/openlp_core/widgets/test_dialogs.py @@ -5,12 +5,12 @@ from unittest.mock import patch from PyQt5 import QtWidgets from openlp.core.common.path import Path -from openlp.core.ui.lib.filedialog import FileDialog +from openlp.core.widgets.dialogs import FileDialog class TestFileDialogPatches(TestCase): """ - Tests for the :mod:`openlp.core.ui.lib.filedialogpatches` module + Tests for the :mod:`openlp.core.widgets.dialogs` module """ def test_file_dialog(self): @@ -55,7 +55,7 @@ class TestFileDialogPatches(TestCase): order """ # GIVEN: FileDialog - with patch('openlp.core.ui.lib.filedialog.QtWidgets.QFileDialog.getExistingDirectory', return_value='') \ + with patch('openlp.core.widgets.dialogs.QtWidgets.QFileDialog.getExistingDirectory', return_value='') \ as mocked_get_existing_directory: # WHEN: Calling the getExistingDirectory method with all parameters set diff --git a/tests/functional/openlp_core/ui/lib/test_pathedit.py b/tests/functional/openlp_core/widgets/test_edits.py similarity index 81% rename from tests/functional/openlp_core/ui/lib/test_pathedit.py rename to tests/functional/openlp_core/widgets/test_edits.py index 227a4317a..5ce6dc9df 100755 --- a/tests/functional/openlp_core/ui/lib/test_pathedit.py +++ b/tests/functional/openlp_core/widgets/test_edits.py @@ -20,23 +20,24 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -This module contains tests for the openlp.core.ui.lib.pathedit module +This module contains tests for the openlp.core.widgets.edits module """ import os from unittest import TestCase from unittest.mock import MagicMock, PropertyMock, patch from openlp.core.common.path import Path -from openlp.core.ui.lib import PathEdit, PathType -from openlp.core.ui.lib.filedialog import FileDialog +from openlp.core.widgets.edits import PathEdit +from openlp.core.widgets.enums import PathEditType +from openlp.core.widgets.dialogs import FileDialog class TestPathEdit(TestCase): """ - Test the :class:`~openlp.core.lib.pathedit.PathEdit` class + Test the :class:`~openlp.core.widgets.edits.PathEdit` class """ def setUp(self): - with patch('openlp.core.ui.lib.pathedit.PathEdit._setup'): + with patch('openlp.core.widgets.edits.PathEdit._setup'): self.widget = PathEdit() def test_path_getter(self): @@ -73,7 +74,7 @@ class TestPathEdit(TestCase): # GIVEN: An instance of PathEdit # WHEN: Reading the `path` property # THEN: The default value should be returned - self.assertEqual(self.widget.path_type, PathType.Files) + self.assertEqual(self.widget.path_type, PathEditType.Files) def test_path_type_setter(self): """ @@ -83,11 +84,11 @@ class TestPathEdit(TestCase): with patch.object(self.widget, 'update_button_tool_tips') as mocked_update_button_tool_tips: # WHEN: Writing to a different value than default to the `path_type` property - self.widget.path_type = PathType.Directories + self.widget.path_type = PathEditType.Directories # THEN: The `_path_type` instance variable should be set with the test data and not the default. The # update_button_tool_tips should have been called. - self.assertEqual(self.widget._path_type, PathType.Directories) + self.assertEqual(self.widget._path_type, PathEditType.Directories) mocked_update_button_tool_tips.assert_called_once_with() def test_update_button_tool_tips_directories(self): @@ -97,7 +98,7 @@ class TestPathEdit(TestCase): # GIVEN: An instance of PathEdit with the `path_type` set to `Directories` self.widget.browse_button = MagicMock() self.widget.revert_button = MagicMock() - self.widget._path_type = PathType.Directories + self.widget._path_type = PathEditType.Directories # WHEN: Calling update_button_tool_tips self.widget.update_button_tool_tips() @@ -112,7 +113,7 @@ class TestPathEdit(TestCase): # GIVEN: An instance of PathEdit with the `path_type` set to `Files` self.widget.browse_button = MagicMock() self.widget.revert_button = MagicMock() - self.widget._path_type = PathType.Files + self.widget._path_type = PathEditType.Files # WHEN: Calling update_button_tool_tips self.widget.update_button_tool_tips() @@ -120,26 +121,25 @@ class TestPathEdit(TestCase): self.widget.browse_button.setToolTip.assert_called_once_with('Browse for file.') self.widget.revert_button.setToolTip.assert_called_once_with('Revert to default file.') - def test_on_browse_button_clicked_directory(self): + @patch('openlp.core.widgets.edits.FileDialog.getExistingDirectory', return_value=None) + @patch('openlp.core.widgets.edits.FileDialog.getOpenFileName') + def test_on_browse_button_clicked_directory(self, mocked_get_open_file_name, mocked_get_existing_directory): """ Test the `browse_button` `clicked` handler on_browse_button_clicked when the `path_type` is set to Directories. """ # GIVEN: An instance of PathEdit with the `path_type` set to `Directories` and a mocked # QFileDialog.getExistingDirectory - with patch('openlp.core.ui.lib.pathedit.FileDialog.getExistingDirectory', return_value=None) as \ - mocked_get_existing_directory, \ - patch('openlp.core.ui.lib.pathedit.FileDialog.getOpenFileName') as mocked_get_open_file_name: - self.widget._path_type = PathType.Directories - self.widget._path = Path('test', 'path') + self.widget._path_type = PathEditType.Directories + self.widget._path = Path('test', 'path') - # WHEN: Calling on_browse_button_clicked - self.widget.on_browse_button_clicked() + # WHEN: Calling on_browse_button_clicked + self.widget.on_browse_button_clicked() - # THEN: The FileDialog.getExistingDirectory should have been called with the default caption - mocked_get_existing_directory.assert_called_once_with(self.widget, 'Select Directory', - Path('test', 'path'), - FileDialog.ShowDirsOnly) - self.assertFalse(mocked_get_open_file_name.called) + # THEN: The FileDialog.getExistingDirectory should have been called with the default caption + mocked_get_existing_directory.assert_called_once_with(self.widget, 'Select Directory', + Path('test', 'path'), + FileDialog.ShowDirsOnly) + self.assertFalse(mocked_get_open_file_name.called) def test_on_browse_button_clicked_directory_custom_caption(self): """ @@ -148,10 +148,10 @@ class TestPathEdit(TestCase): """ # GIVEN: An instance of PathEdit with the `path_type` set to `Directories` and a mocked # QFileDialog.getExistingDirectory with `default_caption` set. - with patch('openlp.core.ui.lib.pathedit.FileDialog.getExistingDirectory', return_value=None) as \ + with patch('openlp.core.widgets.edits.FileDialog.getExistingDirectory', return_value=None) as \ mocked_get_existing_directory, \ - patch('openlp.core.ui.lib.pathedit.FileDialog.getOpenFileName') as mocked_get_open_file_name: - self.widget._path_type = PathType.Directories + patch('openlp.core.widgets.edits.FileDialog.getOpenFileName') as mocked_get_open_file_name: + self.widget._path_type = PathEditType.Directories self.widget._path = Path('test', 'path') self.widget.dialog_caption = 'Directory Caption' @@ -169,10 +169,10 @@ class TestPathEdit(TestCase): Test the `browse_button` `clicked` handler on_browse_button_clicked when the `path_type` is set to Files. """ # GIVEN: An instance of PathEdit with the `path_type` set to `Files` and a mocked QFileDialog.getOpenFileName - with patch('openlp.core.ui.lib.pathedit.FileDialog.getExistingDirectory') as mocked_get_existing_directory, \ - patch('openlp.core.ui.lib.pathedit.FileDialog.getOpenFileName', return_value=(None, '')) as \ + with patch('openlp.core.widgets.edits.FileDialog.getExistingDirectory') as mocked_get_existing_directory, \ + patch('openlp.core.widgets.edits.FileDialog.getOpenFileName', return_value=(None, '')) as \ mocked_get_open_file_name: - self.widget._path_type = PathType.Files + self.widget._path_type = PathEditType.Files self.widget._path = Path('test', 'pat.h') # WHEN: Calling on_browse_button_clicked @@ -190,10 +190,10 @@ class TestPathEdit(TestCase): """ # GIVEN: An instance of PathEdit with the `path_type` set to `Files` and a mocked QFileDialog.getOpenFileName # with `default_caption` set. - with patch('openlp.core.ui.lib.pathedit.FileDialog.getExistingDirectory') as mocked_get_existing_directory, \ - patch('openlp.core.ui.lib.pathedit.FileDialog.getOpenFileName', return_value=(None, '')) as \ + with patch('openlp.core.widgets.edits.FileDialog.getExistingDirectory') as mocked_get_existing_directory, \ + patch('openlp.core.widgets.edits.FileDialog.getOpenFileName', return_value=(None, '')) as \ mocked_get_open_file_name: - self.widget._path_type = PathType.Files + self.widget._path_type = PathEditType.Files self.widget._path = Path('test', 'pat.h') self.widget.dialog_caption = 'File Caption' @@ -212,7 +212,7 @@ class TestPathEdit(TestCase): """ # GIVEN: An instance of PathEdit with a mocked QFileDialog.getOpenFileName which returns an empty str for the # file path. - with patch('openlp.core.ui.lib.pathedit.FileDialog.getOpenFileName', return_value=(None, '')) as \ + with patch('openlp.core.widgets.edits.FileDialog.getOpenFileName', return_value=(None, '')) as \ mocked_get_open_file_name: # WHEN: Calling on_browse_button_clicked @@ -228,7 +228,7 @@ class TestPathEdit(TestCase): """ # GIVEN: An instance of PathEdit with a mocked QFileDialog.getOpenFileName which returns a str for the file # path. - with patch('openlp.core.ui.lib.pathedit.FileDialog.getOpenFileName', + with patch('openlp.core.widgets.edits.FileDialog.getOpenFileName', return_value=(Path('test', 'pat.h'), '')) as mocked_get_open_file_name, \ patch.object(self.widget, 'on_new_path'): @@ -272,7 +272,7 @@ class TestPathEdit(TestCase): Test `on_new_path` when called with a path that is the same as the existing path. """ # GIVEN: An instance of PathEdit with a test path and mocked `pathChanged` signal - with patch('openlp.core.ui.lib.pathedit.PathEdit.path', new_callable=PropertyMock): + with patch('openlp.core.widgets.edits.PathEdit.path', new_callable=PropertyMock): self.widget._path = Path('/old', 'test', 'pat.h') self.widget.pathChanged = MagicMock() @@ -287,7 +287,7 @@ class TestPathEdit(TestCase): Test `on_new_path` when called with a path that is the different to the existing path. """ # GIVEN: An instance of PathEdit with a test path and mocked `pathChanged` signal - with patch('openlp.core.ui.lib.pathedit.PathEdit.path', new_callable=PropertyMock): + with patch('openlp.core.widgets.edits.PathEdit.path', new_callable=PropertyMock): self.widget._path = Path('/old', 'test', 'pat.h') self.widget.pathChanged = MagicMock() diff --git a/tests/functional/openlp_core/ui/lib/test_listpreviewwidget.py b/tests/functional/openlp_core/widgets/test_views.py similarity index 75% rename from tests/functional/openlp_core/ui/lib/test_listpreviewwidget.py rename to tests/functional/openlp_core/widgets/test_views.py index a64ae5e0e..d931a5ef5 100644 --- a/tests/functional/openlp_core/ui/lib/test_listpreviewwidget.py +++ b/tests/functional/openlp_core/widgets/test_views.py @@ -20,15 +20,17 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -Package to test the openlp.core.ui.lib.listpreviewwidget package. +Package to test the openlp.core.widgets.views package. """ +from types import GeneratorType from unittest import TestCase from unittest.mock import MagicMock, patch, call from PyQt5 import QtGui -from openlp.core.ui.lib.listpreviewwidget import ListPreviewWidget +from openlp.core.common.i18n import UiStrings from openlp.core.lib import ImageSource +from openlp.core.widgets.views import ListPreviewWidget, ListWidgetWithDnD, TreeWidgetWithDnD class TestListPreviewWidget(TestCase): @@ -38,13 +40,13 @@ class TestListPreviewWidget(TestCase): Mock out stuff for all the tests """ # Mock self.parent().width() - self.parent_patcher = patch('openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.parent') + self.parent_patcher = patch('openlp.core.widgets.views.ListPreviewWidget.parent') self.mocked_parent = self.parent_patcher.start() self.mocked_parent.width.return_value = 100 self.addCleanup(self.parent_patcher.stop) # Mock Settings().value() - self.Settings_patcher = patch('openlp.core.ui.lib.listpreviewwidget.Settings') + self.Settings_patcher = patch('openlp.core.widgets.views.Settings') self.mocked_Settings = self.Settings_patcher.start() self.mocked_Settings_obj = MagicMock() self.mocked_Settings_obj.value.return_value = None @@ -52,7 +54,7 @@ class TestListPreviewWidget(TestCase): self.addCleanup(self.Settings_patcher.stop) # Mock self.viewport().width() - self.viewport_patcher = patch('openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.viewport') + self.viewport_patcher = patch('openlp.core.widgets.views.ListPreviewWidget.viewport') self.mocked_viewport = self.viewport_patcher.start() self.mocked_viewport_obj = MagicMock() self.mocked_viewport_obj.width.return_value = 200 @@ -72,9 +74,9 @@ class TestListPreviewWidget(TestCase): self.assertIsNotNone(list_preview_widget, 'The ListPreviewWidget object should not be None') self.assertEquals(list_preview_widget.screen_ratio, 1, 'Should not be called') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.image_manager') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.image_manager') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.setRowHeight') def test_replace_service_item_thumbs(self, mocked_setRowHeight, mocked_resizeRowsToContents, mocked_image_manager): """ @@ -119,8 +121,8 @@ class TestListPreviewWidget(TestCase): call('TEST3', ImageSource.CommandPlugins), call('TEST4', ImageSource.CommandPlugins)] mocked_image_manager.get_image.assert_has_calls(calls) - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.setRowHeight') def test_replace_recalculate_layout_text(self, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." enabled, txt slides unchanged in replace_service_item & __recalc... @@ -151,8 +153,8 @@ class TestListPreviewWidget(TestCase): self.assertEquals(mocked_resizeRowsToContents.call_count, 2, 'Should be called') self.assertEquals(mocked_setRowHeight.call_count, 0, 'Should not be called') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.setRowHeight') def test_replace_recalculate_layout_img(self, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." disabled, img slides unchanged in replace_service_item & __recalc... @@ -188,8 +190,8 @@ class TestListPreviewWidget(TestCase): calls = [call(0, 200), call(1, 200), call(0, 400), call(1, 400), call(0, 400), call(1, 400)] mocked_setRowHeight.assert_has_calls(calls) - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.setRowHeight') def test_replace_recalculate_layout_img_max(self, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." enabled, img slides resized in replace_service_item & __recalc... @@ -223,8 +225,8 @@ class TestListPreviewWidget(TestCase): calls = [call(0, 100), call(1, 100), call(0, 100), call(1, 100)] mocked_setRowHeight.assert_has_calls(calls) - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.setRowHeight') def test_replace_recalculate_layout_img_auto(self, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." auto, img slides resized in replace_service_item & __recalc... @@ -261,9 +263,9 @@ class TestListPreviewWidget(TestCase): calls = [call(0, 100), call(1, 100), call(0, 150), call(1, 150), call(0, 100), call(1, 100)] mocked_setRowHeight.assert_has_calls(calls) - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.cellWidget') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.cellWidget') def test_row_resized_text(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." enabled, text-based slides not affected in row_resized. @@ -295,9 +297,9 @@ class TestListPreviewWidget(TestCase): # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should not be called self.assertEquals(mocked_cellWidget_child.setMaximumWidth.call_count, 0, 'Should not be called') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.cellWidget') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.cellWidget') def test_row_resized_img(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." disabled, image-based slides not affected in row_resized. @@ -332,9 +334,9 @@ class TestListPreviewWidget(TestCase): # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should not be called self.assertEquals(mocked_cellWidget_child.setMaximumWidth.call_count, 0, 'Should not be called') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.cellWidget') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.cellWidget') def test_row_resized_img_max(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." enabled, image-based slides are scaled in row_resized. @@ -367,9 +369,9 @@ class TestListPreviewWidget(TestCase): # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should be called mocked_cellWidget_child.setMaximumWidth.assert_called_once_with(150) - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.setRowHeight') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.cellWidget') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.cellWidget') def test_row_resized_setting_changed(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." enabled while item live, program doesn't crash on row_resized. @@ -402,10 +404,10 @@ class TestListPreviewWidget(TestCase): # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should fail self.assertRaises(Exception) - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.selectRow') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.scrollToItem') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.item') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.slide_count') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.selectRow') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.scrollToItem') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.item') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.slide_count') def test_autoscroll_setting_invalid(self, mocked_slide_count, mocked_item, mocked_scrollToItem, mocked_selectRow): """ Test if 'advanced/autoscrolling' setting None or invalid, that no autoscrolling occurs on change_slide(). @@ -438,10 +440,10 @@ class TestListPreviewWidget(TestCase): self.assertEquals(mocked_selectRow.call_count, 0, 'Should not be called') self.assertEquals(mocked_item.call_count, 0, 'Should not be called') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.selectRow') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.scrollToItem') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.item') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.slide_count') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.selectRow') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.scrollToItem') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.item') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.slide_count') def test_autoscroll_dist_bounds(self, mocked_slide_count, mocked_item, mocked_scrollToItem, mocked_selectRow): """ Test if 'advanced/autoscrolling' setting asks to scroll beyond list bounds, that it does not beyond. @@ -468,10 +470,10 @@ class TestListPreviewWidget(TestCase): calls = [call(0, 0), call(0, 0)] mocked_item.assert_has_calls(calls) - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.selectRow') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.scrollToItem') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.item') - @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.slide_count') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.selectRow') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.scrollToItem') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.item') + @patch(u'openlp.core.widgets.views.ListPreviewWidget.slide_count') def test_autoscroll_normal(self, mocked_slide_count, mocked_item, mocked_scrollToItem, mocked_selectRow): """ Test if 'advanced/autoscrolling' setting valid, autoscrolling called as expected. @@ -499,3 +501,130 @@ class TestListPreviewWidget(TestCase): self.assertEquals(mocked_item.call_count, 3, 'Should be called') calls = [call(0, 0), call(1, 0), call(2, 0)] mocked_item.assert_has_calls(calls) + + +class TestListWidgetWithDnD(TestCase): + """ + Test the :class:`~openlp.core.widgets.views.ListWidgetWithDnD` class + """ + def test_clear(self): + """ + Test the clear method when called without any arguments. + """ + # GIVEN: An instance of ListWidgetWithDnD + widget = ListWidgetWithDnD() + + # WHEN: Calling clear with out any arguments + widget.clear() + + # THEN: The results text should be the standard 'no results' text. + self.assertEqual(widget.no_results_text, UiStrings().NoResults) + + def test_clear_search_while_typing(self): + """ + Test the clear method when called with the search_while_typing argument set to True + """ + # GIVEN: An instance of ListWidgetWithDnD + widget = ListWidgetWithDnD() + + # WHEN: Calling clear with search_while_typing set to True + widget.clear(search_while_typing=True) + + # THEN: The results text should be the 'short results' text. + self.assertEqual(widget.no_results_text, UiStrings().ShortResults) + + def test_all_items_no_list_items(self): + """ + Test allItems when there are no items in the list widget + """ + # GIVEN: An instance of ListWidgetWithDnD + widget = ListWidgetWithDnD() + with patch.object(widget, 'count', return_value=0), \ + patch.object(widget, 'item', side_effect=lambda x: [][x]): + + # WHEN: Calling allItems + result = widget.allItems() + + # THEN: An instance of a Generator object should be returned. The generator should not yeild any results + self.assertIsInstance(result, GeneratorType) + self.assertEqual(list(result), []) + + def test_all_items_list_items(self): + """ + Test allItems when the list widget contains some items. + """ + # GIVEN: An instance of ListWidgetWithDnD + widget = ListWidgetWithDnD() + with patch.object(widget, 'count', return_value=2), \ + patch.object(widget, 'item', side_effect=lambda x: [5, 3][x]): + + # WHEN: Calling allItems + result = widget.allItems() + + # THEN: An instance of a Generator object should be returned. The generator should not yeild any results + self.assertIsInstance(result, GeneratorType) + self.assertEqual(list(result), [5, 3]) + + def test_paint_event(self): + """ + Test the paintEvent method when the list is not empty + """ + # GIVEN: An instance of ListWidgetWithDnD with a mocked out count methode which returns 1 + # (i.e the list has an item) + widget = ListWidgetWithDnD() + with patch('openlp.core.widgets.views.QtWidgets.QListWidget.paintEvent') as mocked_paint_event, \ + patch.object(widget, 'count', return_value=1), \ + patch.object(widget, 'viewport') as mocked_viewport: + mocked_event = MagicMock() + + # WHEN: Calling paintEvent + widget.paintEvent(mocked_event) + + # THEN: The overridden paintEvnet should have been called + mocked_paint_event.assert_called_once_with(mocked_event) + self.assertFalse(mocked_viewport.called) + + def test_paint_event_no_items(self): + """ + Test the paintEvent method when the list is empty + """ + # GIVEN: An instance of ListWidgetWithDnD with a mocked out count methode which returns 0 + # (i.e the list is empty) + widget = ListWidgetWithDnD() + mocked_painter_instance = MagicMock() + mocked_qrect = MagicMock() + with patch('openlp.core.widgets.views.QtWidgets.QListWidget.paintEvent') as mocked_paint_event, \ + patch.object(widget, 'count', return_value=0), \ + patch.object(widget, 'viewport'), \ + patch('openlp.core.widgets.views.QtGui.QPainter', + return_value=mocked_painter_instance) as mocked_qpainter, \ + patch('openlp.core.widgets.views.QtCore.QRect', return_value=mocked_qrect): + mocked_event = MagicMock() + + # WHEN: Calling paintEvent + widget.paintEvent(mocked_event) + + # THEN: The overridden paintEvnet should have been called, and some text should be drawn. + mocked_paint_event.assert_called_once_with(mocked_event) + mocked_qpainter.assert_called_once_with(widget.viewport()) + mocked_painter_instance.drawText.assert_called_once_with(mocked_qrect, 4100, 'No Search Results') + + +class TestTreeWidgetWithDnD(TestCase): + """ + Test the :class:`~openlp.core.widgets.views.TreeWidgetWithDnD` class + """ + def test_constructor(self): + """ + Test the constructor + """ + # GIVEN: A TreeWidgetWithDnD + # WHEN: An instance is created + widget = TreeWidgetWithDnD(name='Test') + + # THEN: It should be initialised correctly + assert widget.mime_data_text == 'Test' + assert widget.allow_internal_dnd is False + assert widget.indentation() == 0 + assert widget.isAnimated() is True + diff --git a/tests/interfaces/openlp_core_ul_media_vendor/__init__.py b/tests/interfaces/openlp_core/__init__.py similarity index 100% rename from tests/interfaces/openlp_core_ul_media_vendor/__init__.py rename to tests/interfaces/openlp_core/__init__.py diff --git a/openlp/core/ui/lib/dockwidget.py b/tests/interfaces/openlp_core/api/__init__.py similarity index 60% rename from openlp/core/ui/lib/dockwidget.py rename to tests/interfaces/openlp_core/api/__init__.py index 398d1e674..ea62548f4 100644 --- a/openlp/core/ui/lib/dockwidget.py +++ b/tests/interfaces/openlp_core/api/__init__.py @@ -19,39 +19,3 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - -""" -Provide additional functionality required by OpenLP from the inherited QDockWidget. -""" - -import logging - -from PyQt5 import QtWidgets - -from openlp.core.display.screens import ScreenList -from openlp.core.lib import build_icon - -log = logging.getLogger(__name__) - - -class OpenLPDockWidget(QtWidgets.QDockWidget): - """ - Custom DockWidget class to handle events - """ - def __init__(self, parent=None, name=None, icon=None): - """ - Initialise the DockWidget - """ - log.debug('Initialise the %s widget' % name) - super(OpenLPDockWidget, self).__init__(parent) - if name: - self.setObjectName(name) - if icon: - self.setWindowIcon(build_icon(icon)) - # Sort out the minimum width. - screens = ScreenList() - main_window_docbars = screens.current['size'].width() // 5 - if main_window_docbars > 300: - self.setMinimumWidth(300) - else: - self.setMinimumWidth(main_window_docbars) diff --git a/tests/interfaces/openlp_core/common/__init__.py b/tests/interfaces/openlp_core/common/__init__.py new file mode 100644 index 000000000..ea62548f4 --- /dev/null +++ b/tests/interfaces/openlp_core/common/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### diff --git a/tests/interfaces/openlp_core_common/test_utils.py b/tests/interfaces/openlp_core/common/test_utils.py similarity index 100% rename from tests/interfaces/openlp_core_common/test_utils.py rename to tests/interfaces/openlp_core/common/test_utils.py diff --git a/tests/interfaces/openlp_core/lib/__init__.py b/tests/interfaces/openlp_core/lib/__init__.py new file mode 100644 index 000000000..ea62548f4 --- /dev/null +++ b/tests/interfaces/openlp_core/lib/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### diff --git a/tests/interfaces/openlp_core_lib/test_pluginmanager.py b/tests/interfaces/openlp_core/lib/test_pluginmanager.py similarity index 100% rename from tests/interfaces/openlp_core_lib/test_pluginmanager.py rename to tests/interfaces/openlp_core/lib/test_pluginmanager.py diff --git a/tests/interfaces/openlp_core_ui/__init__.py b/tests/interfaces/openlp_core/ui/__init__.py similarity index 100% rename from tests/interfaces/openlp_core_ui/__init__.py rename to tests/interfaces/openlp_core/ui/__init__.py diff --git a/tests/interfaces/openlp_core/ui/lib/__init__.py b/tests/interfaces/openlp_core/ui/lib/__init__.py new file mode 100644 index 000000000..ea62548f4 --- /dev/null +++ b/tests/interfaces/openlp_core/ui/lib/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### diff --git a/tests/interfaces/openlp_core_ui_lib/test_historycombobox.py b/tests/interfaces/openlp_core/ui/lib/test_historycombobox.py similarity index 100% rename from tests/interfaces/openlp_core_ui_lib/test_historycombobox.py rename to tests/interfaces/openlp_core/ui/lib/test_historycombobox.py diff --git a/tests/interfaces/openlp_core/ui/media/__init__.py b/tests/interfaces/openlp_core/ui/media/__init__.py new file mode 100644 index 000000000..ea62548f4 --- /dev/null +++ b/tests/interfaces/openlp_core/ui/media/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### diff --git a/tests/interfaces/openlp_core/ui/media/vendor/__init__.py b/tests/interfaces/openlp_core/ui/media/vendor/__init__.py new file mode 100644 index 000000000..ea62548f4 --- /dev/null +++ b/tests/interfaces/openlp_core/ui/media/vendor/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### diff --git a/tests/interfaces/openlp_core_ul_media_vendor/test_mediainfoWrapper.py b/tests/interfaces/openlp_core/ui/media/vendor/test_mediainfoWrapper.py similarity index 97% rename from tests/interfaces/openlp_core_ul_media_vendor/test_mediainfoWrapper.py rename to tests/interfaces/openlp_core/ui/media/vendor/test_mediainfoWrapper.py index d4c1891b1..f8fee253d 100644 --- a/tests/interfaces/openlp_core_ul_media_vendor/test_mediainfoWrapper.py +++ b/tests/interfaces/openlp_core/ui/media/vendor/test_mediainfoWrapper.py @@ -28,7 +28,7 @@ from unittest import TestCase from openlp.core.ui.media.vendor.mediainfoWrapper import MediaInfoWrapper -TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources', 'media')) +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..', '..', 'resources', 'media')) TEST_MEDIA = [['avi_file.avi', 61495], ['mp3_file.mp3', 134426], ['mpg_file.mpg', 9404], ['mp4_file.mp4', 188336]] diff --git a/tests/interfaces/openlp_core_ui/test_filerenamedialog.py b/tests/interfaces/openlp_core/ui/test_filerenamedialog.py similarity index 100% rename from tests/interfaces/openlp_core_ui/test_filerenamedialog.py rename to tests/interfaces/openlp_core/ui/test_filerenamedialog.py diff --git a/tests/interfaces/openlp_core_ui/test_mainwindow.py b/tests/interfaces/openlp_core/ui/test_mainwindow.py similarity index 100% rename from tests/interfaces/openlp_core_ui/test_mainwindow.py rename to tests/interfaces/openlp_core/ui/test_mainwindow.py diff --git a/tests/interfaces/openlp_core_ui/test_projectoreditform.py b/tests/interfaces/openlp_core/ui/test_projectoreditform.py similarity index 100% rename from tests/interfaces/openlp_core_ui/test_projectoreditform.py rename to tests/interfaces/openlp_core/ui/test_projectoreditform.py diff --git a/tests/interfaces/openlp_core_ui/test_projectormanager.py b/tests/interfaces/openlp_core/ui/test_projectormanager.py similarity index 100% rename from tests/interfaces/openlp_core_ui/test_projectormanager.py rename to tests/interfaces/openlp_core/ui/test_projectormanager.py diff --git a/tests/interfaces/openlp_core_ui/test_projectorsourceform.py b/tests/interfaces/openlp_core/ui/test_projectorsourceform.py similarity index 100% rename from tests/interfaces/openlp_core_ui/test_projectorsourceform.py rename to tests/interfaces/openlp_core/ui/test_projectorsourceform.py diff --git a/tests/interfaces/openlp_core_ui/test_servicemanager.py b/tests/interfaces/openlp_core/ui/test_servicemanager.py similarity index 87% rename from tests/interfaces/openlp_core_ui/test_servicemanager.py rename to tests/interfaces/openlp_core/ui/test_servicemanager.py index 1deb1a97f..3426fffcb 100644 --- a/tests/interfaces/openlp_core_ui/test_servicemanager.py +++ b/tests/interfaces/openlp_core/ui/test_servicemanager.py @@ -25,102 +25,103 @@ from unittest import TestCase from unittest.mock import MagicMock, patch +from PyQt5 import QtCore, QtGui, QtWidgets + from openlp.core.common.registry import Registry -from openlp.core.display.screens import ScreenList from openlp.core.lib import ServiceItem, ItemCapabilities -from openlp.core.ui.mainwindow import MainWindow +from openlp.core.ui.servicemanager import ServiceManager from tests.helpers.testmixin import TestMixin -from PyQt5 import QtCore, QtGui, QtWidgets - class TestServiceManager(TestCase, TestMixin): + """ + Test the service manager + """ + + def _create_mock_action(self, name, **kwargs): + """ + Create a fake action with some "real" attributes + """ + action = QtWidgets.QAction(self.service_manager) + action.setObjectName(name) + if kwargs.get('triggers'): + action.triggered.connect(kwargs.pop('triggers')) + self.service_manager.toolbar.actions[name] = action + return action def setUp(self): """ Create the UI """ Registry.create() - Registry().set_flag('no_web_server', False) self.setup_application() - ScreenList.create(self.app.desktop()) Registry().register('application', MagicMock()) - # Mock classes and methods used by mainwindow. - with patch('openlp.core.ui.mainwindow.SettingsForm'), \ - patch('openlp.core.ui.mainwindow.ImageManager'), \ - patch('openlp.core.ui.mainwindow.LiveController'), \ - patch('openlp.core.ui.mainwindow.PreviewController'), \ - patch('openlp.core.ui.mainwindow.OpenLPDockWidget'), \ - patch('openlp.core.ui.mainwindow.QtWidgets.QToolBox'), \ - patch('openlp.core.ui.mainwindow.QtWidgets.QMainWindow.addDockWidget'), \ - patch('openlp.core.ui.mainwindow.ThemeManager'), \ - patch('openlp.core.ui.mainwindow.ProjectorManager'), \ - patch('openlp.core.ui.mainwindow.Renderer'), \ - patch('openlp.core.ui.mainwindow.websockets.WebSocketServer'), \ - patch('openlp.core.ui.mainwindow.server.HttpServer'): - self.main_window = MainWindow() - self.service_manager = Registry().get('service_manager') + Registry().register('main_window', MagicMock(service_manager_settings_section='servicemanager')) + self.service_manager = ServiceManager() + self.add_toolbar_action_patcher = patch('openlp.core.ui.servicemanager.OpenLPToolbar.add_toolbar_action') + self.mocked_add_toolbar_action = self.add_toolbar_action_patcher.start() + self.mocked_add_toolbar_action.side_effect = self._create_mock_action def tearDown(self): """ Delete all the C++ objects at the end so that we don't have a segfault """ - del self.main_window + self.add_toolbar_action_patcher.stop() + del self.service_manager def test_basic_service_manager(self): """ Test the Service Manager UI Functionality """ # GIVEN: A New Service Manager instance - # WHEN I have set up the display self.service_manager.setup_ui(self.service_manager) + # THEN the count of items should be zero self.assertEqual(self.service_manager.service_manager_list.topLevelItemCount(), 0, 'The service manager list should be empty ') - def test_default_context_menu(self): + @patch('openlp.core.ui.servicemanager.QtWidgets.QTreeWidget.itemAt') + @patch('openlp.core.ui.servicemanager.QtWidgets.QWidget.mapToGlobal') + @patch('openlp.core.ui.servicemanager.QtWidgets.QMenu.exec') + def test_default_context_menu(self, mocked_exec, mocked_mapToGlobal, mocked_item_at_method): """ Test the context_menu() method with a default service item """ # GIVEN: A service item added + mocked_item = MagicMock() + mocked_item.parent.return_value = None + mocked_item_at_method.return_value = mocked_item + mocked_item.data.return_value = 1 self.service_manager.setup_ui(self.service_manager) - with patch('PyQt5.QtWidgets.QTreeWidget.itemAt') as mocked_item_at_method, \ - patch('PyQt5.QtWidgets.QWidget.mapToGlobal'), \ - patch('PyQt5.QtWidgets.QMenu.exec'): - mocked_item = MagicMock() - mocked_item.parent.return_value = None - mocked_item_at_method.return_value = mocked_item - # We want 1 to be returned for the position - mocked_item.data.return_value = 1 - # A service item without capabilities. - service_item = ServiceItem() - self.service_manager.service_items = [{'service_item': service_item}] - q_point = None - # Mocked actions. - self.service_manager.edit_action.setVisible = MagicMock() - self.service_manager.create_custom_action.setVisible = MagicMock() - self.service_manager.maintain_action.setVisible = MagicMock() - self.service_manager.notes_action.setVisible = MagicMock() - self.service_manager.time_action.setVisible = MagicMock() - self.service_manager.auto_start_action.setVisible = MagicMock() + # A service item without capabilities. + service_item = ServiceItem() + self.service_manager.service_items = [{'service_item': service_item}] + q_point = None + # Mocked actions. + self.service_manager.edit_action.setVisible = MagicMock() + self.service_manager.create_custom_action.setVisible = MagicMock() + self.service_manager.maintain_action.setVisible = MagicMock() + self.service_manager.notes_action.setVisible = MagicMock() + self.service_manager.time_action.setVisible = MagicMock() + self.service_manager.auto_start_action.setVisible = MagicMock() - # WHEN: Show the context menu. - self.service_manager.context_menu(q_point) + # WHEN: Show the context menu. + self.service_manager.context_menu(q_point) - # THEN: The following actions should be not visible. - self.service_manager.edit_action.setVisible.assert_called_once_with(False), \ - 'The action should be set invisible.' - self.service_manager.create_custom_action.setVisible.assert_called_once_with(False), \ - 'The action should be set invisible.' - self.service_manager.maintain_action.setVisible.assert_called_once_with(False), \ - 'The action should be set invisible.' - self.service_manager.notes_action.setVisible.assert_called_with(True), 'The action should be set visible.' - self.service_manager.time_action.setVisible.assert_called_once_with(False), \ - 'The action should be set invisible.' - self.service_manager.auto_start_action.setVisible.assert_called_once_with(False), \ - 'The action should be set invisible.' + # THEN: The following actions should be not visible. + self.service_manager.edit_action.setVisible.assert_called_once_with(False), \ + 'The action should be set invisible.' + self.service_manager.create_custom_action.setVisible.assert_called_once_with(False), \ + 'The action should be set invisible.' + self.service_manager.maintain_action.setVisible.assert_called_once_with(False), \ + 'The action should be set invisible.' + self.service_manager.notes_action.setVisible.assert_called_with(True), 'The action should be set visible.' + self.service_manager.time_action.setVisible.assert_called_once_with(False), \ + 'The action should be set invisible.' + self.service_manager.auto_start_action.setVisible.assert_called_once_with(False), \ + 'The action should be set invisible.' def test_edit_context_menu(self): """ diff --git a/tests/interfaces/openlp_core_ui/test_servicenotedialog.py b/tests/interfaces/openlp_core/ui/test_servicenotedialog.py similarity index 100% rename from tests/interfaces/openlp_core_ui/test_servicenotedialog.py rename to tests/interfaces/openlp_core/ui/test_servicenotedialog.py diff --git a/tests/interfaces/openlp_core_ui/test_settings_form.py b/tests/interfaces/openlp_core/ui/test_settings_form.py similarity index 100% rename from tests/interfaces/openlp_core_ui/test_settings_form.py rename to tests/interfaces/openlp_core/ui/test_settings_form.py diff --git a/tests/interfaces/openlp_core_ui/test_shortcutlistform.py b/tests/interfaces/openlp_core/ui/test_shortcutlistform.py similarity index 100% rename from tests/interfaces/openlp_core_ui/test_shortcutlistform.py rename to tests/interfaces/openlp_core/ui/test_shortcutlistform.py diff --git a/tests/interfaces/openlp_core_ui/test_starttimedialog.py b/tests/interfaces/openlp_core/ui/test_starttimedialog.py similarity index 100% rename from tests/interfaces/openlp_core_ui/test_starttimedialog.py rename to tests/interfaces/openlp_core/ui/test_starttimedialog.py diff --git a/tests/interfaces/openlp_core_ui/test_thememanager.py b/tests/interfaces/openlp_core/ui/test_thememanager.py similarity index 100% rename from tests/interfaces/openlp_core_ui/test_thememanager.py rename to tests/interfaces/openlp_core/ui/test_thememanager.py diff --git a/tests/interfaces/openlp_core/widgets/__init__.py b/tests/interfaces/openlp_core/widgets/__init__.py new file mode 100644 index 000000000..ea62548f4 --- /dev/null +++ b/tests/interfaces/openlp_core/widgets/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### diff --git a/tests/interfaces/openlp_core_lib/test_searchedit.py b/tests/interfaces/openlp_core/widgets/test_edits.py similarity index 84% rename from tests/interfaces/openlp_core_lib/test_searchedit.py rename to tests/interfaces/openlp_core/widgets/test_edits.py index d4751ddf0..3951e5f80 100644 --- a/tests/interfaces/openlp_core_lib/test_searchedit.py +++ b/tests/interfaces/openlp_core/widgets/test_edits.py @@ -28,7 +28,7 @@ from unittest.mock import MagicMock, call, patch from PyQt5 import QtCore, QtGui, QtTest, QtWidgets from openlp.core.common.registry import Registry -from openlp.core.lib.searchedit import SearchEdit +from openlp.core.widgets.edits import SearchEdit, HistoryComboBox from tests.helpers.testmixin import TestMixin @@ -60,7 +60,7 @@ class TestSearchEdit(TestCase, TestMixin): Registry().register('main_window', self.main_window) settings_patcher = patch( - 'openlp.core.lib.searchedit.Settings', return_value=MagicMock(**{'value.return_value': SearchTypes.First})) + 'openlp.core.widgets.edits.Settings', return_value=MagicMock(**{'value.return_value': SearchTypes.First})) self.addCleanup(settings_patcher.stop) self.mocked_settings = settings_patcher.start() @@ -135,3 +135,35 @@ class TestSearchEdit(TestCase, TestMixin): # THEN: The search edit text should be cleared and the button be hidden. assert not self.search_edit.text(), "The search edit should not have any text." assert self.search_edit.clear_button.isHidden(), "The clear button should be hidden." + + +class TestHistoryComboBox(TestCase, TestMixin): + def setUp(self): + """ + Some pre-test setup required. + """ + Registry.create() + self.setup_application() + self.main_window = QtWidgets.QMainWindow() + Registry().register('main_window', self.main_window) + self.combo = HistoryComboBox(self.main_window) + + def tearDown(self): + """ + Delete all the C++ objects at the end so that we don't have a segfault + """ + del self.combo + del self.main_window + + def test_get_items(self): + """ + Test the getItems() method + """ + # GIVEN: The combo. + + # WHEN: Add two items. + self.combo.addItem('test1') + self.combo.addItem('test2') + + # THEN: The list of items should contain both strings. + self.assertEqual(self.combo.getItems(), ['test1', 'test2']) diff --git a/tests/interfaces/openlp_core_ui_lib/test_listpreviewwidget.py b/tests/interfaces/openlp_core/widgets/test_views.py similarity index 97% rename from tests/interfaces/openlp_core_ui_lib/test_listpreviewwidget.py rename to tests/interfaces/openlp_core/widgets/test_views.py index e22f9e9a4..f4a493f4d 100644 --- a/tests/interfaces/openlp_core_ui_lib/test_listpreviewwidget.py +++ b/tests/interfaces/openlp_core/widgets/test_views.py @@ -20,7 +20,7 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ - Package to test the openlp.core.ui.lib.listpreviewwidget. + Package to test the openlp.core.widgets.views. """ from unittest import TestCase from unittest.mock import MagicMock, patch @@ -29,7 +29,7 @@ from PyQt5 import QtGui, QtWidgets from openlp.core.common.registry import Registry from openlp.core.lib import ServiceItem -from openlp.core.ui.lib import ListWidgetWithDnD, ListPreviewWidget +from openlp.core.widgets.views import ListPreviewWidget from tests.utils.osdinteraction import read_service_from_file from tests.helpers.testmixin import TestMixin