Beispiel #1
0
    def saveSettings(self):

        settings = QSettings()

        settings.remove("Preferences")
        settings.beginGroup("Preferences")

        # General: Geometry & State
        settings.setValue("RestoreApplicationGeometry",
                          self._restoreApplicationGeometry)
        settings.setValue("RestoreApplicationState",
                          self._restoreApplicationState)

        # General: Recently Opened Documents
        settings.setValue("MaximumRecentDocuments",
                          self._maximumRecentDocuments)
        settings.setValue("RestoreRecentDocuments",
                          self._restoreRecentDocuments)

        # Document Presets: Header Labels
        settings.setValue("DefaultHeaderLabelHorizontal",
                          self._defaultHeaderLabelHorizontal.value)
        settings.setValue("DefaultHeaderLabelVertical",
                          self._defaultHeaderLabelVertical.value)

        # Document Presets: Cell Counts
        settings.setValue("DefaultCellCountColumn",
                          self._defaultCellCountColumn)
        settings.setValue("DefaultCellCountRow", self._defaultCellCountRow)

        settings.endGroup()
Beispiel #2
0
    def delete_app_from_folder(self, appid: str, folder: str):
        assert "/" not in folder
        settings = QSettings()
        settings.beginGroup(f"apps/_folder/{folder}")

        for key in settings.childKeys():
            if appid == settings.value(key):
                settings.remove(key)
                self.app_change.emit()
                return
Beispiel #3
0
 def save(self):
     settings = QSettings()
     settings.beginGroup('profiles')
     settings.remove('')
     for p in self.profiles:
         settings.setValue(f'{p.name}/path', p.path)
         settings.setValue(f'{p.name}/mask', p.mask)
         settings.setValue(f'{p.name}/pattern', p.pattern)
     settings.endGroup()
     if self.active_profile is not None:
         settings.setValue('active_profile', self.active_profile.name)
Beispiel #4
0
    def accept(self):
        super().accept()

        settings = QSettings()

        cykit_address = self.cykit_address_field.text()
        if cykit_address:
            settings.setValue('CyKitAddress', cykit_address)
        else:
            settings.remove('CyKitAddress')

        cykit_port = self.cykit_port_field.text()
        if cykit_port:
            settings.setValue('CyKitPort', cykit_port)
        else:
            settings.remove('CyKitPort')
Beispiel #5
0
    def rmdir(self, folder: str):
        assert folder
        assert "/" not in folder

        settings = QSettings()
        settings.beginGroup(f"apps/_folder/{folder}")

        for key in settings.childKeys():
            if "_" in key:
                continue

            try:
                int(key)
            except ValueError:
                continue

            self.add_app_to_folder(settings.value(key), "", _emit=False)

        settings.remove("")
        self.app_change.emit()
Beispiel #6
0
    def _saveSettings(self):

        settings = QSettings()

        # Recent documents
        if not self._preferences.restoreRecentDocuments():
            self._recentDocuments.clear()
        settings.remove("RecentDocuments")
        settings.beginWriteArray("RecentDocuments")
        for idx in range(len(self._recentDocuments)):
            settings.setArrayIndex(idx)
            settings.setValue("Document", self._recentDocuments[idx])
        settings.endArray()

        # Application properties: Geometry
        geometry = self.saveGeometry() if self._preferences.restoreApplicationGeometry() else QByteArray()
        settings.setValue("Application/Geometry", geometry)

        # Application properties: State
        state = self.saveState() if self._preferences.restoreApplicationState() else QByteArray()
        settings.setValue("Application/State", state)
Beispiel #7
0
class AppStore(QObject):
    app_change = Signal()

    def __init__(self):
        super().__init__()
        self.settings = QSettings(self)

        qt_write_base = Path(
            QStandardPaths.writableLocation(QStandardPaths.AppConfigLocation))
        qt_write_base.mkdir(parents=True, exist_ok=True)

        self.icon_write_dir = qt_write_base / "app_icons"
        self.icon_write_dir.mkdir(exist_ok=True)

    def has_default_apps(self) -> bool:
        return self.settings.value("apps/_meta/isDefaultLoaded")

    def load_default_apps(self):
        def on_progress(value):
            if value != self.default_app_progress.maximum():
                return

            self.settings.setValue("apps/_meta/isDefaultLoaded", True)
            logger.info("Default apps loaded")
            self.default_app_thread.progress.disconnect(on_progress)
            del self.default_app_progress
            del self.default_app_thread

        self.default_app_progress = QProgressDialog("Loading default apps",
                                                    "Cancel", 0, 100)

        self.default_app_thread = _FetchRegistryThread(self)
        self.default_app_thread.progress.connect(
            self.default_app_progress.setValue)
        self.default_app_thread.progress.connect(on_progress)
        self.default_app_thread.items.connect(
            self.default_app_progress.setMaximum)
        self.default_app_thread.label.connect(
            self.default_app_progress.setLabelText)
        self.default_app_progress.canceled.connect(
            self.default_app_thread.cancel)
        self.default_app_thread.start()

        self.default_app_progress.show()

    def add_app(self, manifest_url: str, manifest: AppManifest):
        appid = app_id(manifest_url)

        try:
            manifest["appUrl"] = urljoin(manifest_url, manifest["appUrl"])
            manifest["iconUrl"] = (urljoin(manifest_url, manifest["iconUrl"])
                                   if manifest["iconUrl"] else "")
            manifest["configUrl"] = urljoin(manifest_url,
                                            manifest["configUrl"])
        except KeyError:
            raise AddAppError(manifest_url)

        if manifest["iconUrl"]:
            self.download_app_icon(appid, manifest["iconUrl"])

        self.settings.setValue(f"apps/{appid}", json.dumps(manifest))
        self.app_change.emit()
        logger.info("Application %s (%s:%s) installed", manifest["appName"],
                    appid, manifest_url)

    def mkdir(self, folder: str):
        assert folder
        assert "/" not in folder

        settings = QSettings()
        settings.beginGroup(f"apps/_folder/{folder}")
        settings.setValue("_dir", "true")
        self.app_change.emit()

    def add_app_to_folder(self, appid: str, folder: str = "", _emit=True):
        assert "/" not in folder
        settings = QSettings()
        settings.beginGroup(f"apps/_folder/{folder}")

        last_id = 0
        for key in settings.childKeys():
            if "_" in key:
                continue

            last_id = max(int(key), last_id)
            if appid == settings.value(key):
                settings.endGroup()
                return

        keys = [int(x) for x in settings.childKeys() if "_" not in x]
        last_id = max(keys) if keys else 0
        settings.endGroup()

        settings.setValue(f"apps/_folder/{folder}/{last_id + 1}", appid)

        if _emit:
            self.app_change.emit()

    def delete_app_from_folder(self, appid: str, folder: str):
        assert "/" not in folder
        settings = QSettings()
        settings.beginGroup(f"apps/_folder/{folder}")

        for key in settings.childKeys():
            if appid == settings.value(key):
                settings.remove(key)
                self.app_change.emit()
                return

    def remove_app(self, appid: str):
        settings = QSettings()
        settings.beginGroup("apps/_folder/")

        def recurse():
            for key in settings.childKeys():
                if settings.value(key) == appid:
                    settings.remove(key)

            for childGroup in settings.childGroups():
                settings.beginGroup(childGroup)
                recurse()
                settings.endGroup()

        recurse()

        self.settings.remove(f"apps/{appid}")

        icon_file = self.icon_write_dir / (appid + ".png")
        icon_file.unlink(True)

        self.app_change.emit()

    def rmdir(self, folder: str):
        assert folder
        assert "/" not in folder

        settings = QSettings()
        settings.beginGroup(f"apps/_folder/{folder}")

        for key in settings.childKeys():
            if "_" in key:
                continue

            try:
                int(key)
            except ValueError:
                continue

            self.add_app_to_folder(settings.value(key), "", _emit=False)

        settings.remove("")
        self.app_change.emit()

    def download_app_icon(self, appid: str, url: str):
        dest = self.icon_write_dir / (appid + ".png")
        req = requests.get(url)
        with dest.open("wb") as fp:
            fp.write(req.content)

        logger.info("App icon %s wrote to %s", appid, str(dest))

    def icon(self, appid: str) -> Optional[QIcon]:
        fn = QStandardPaths.locate(QStandardPaths.AppConfigLocation,
                                   "app_icons/" + appid + ".png")
        if fn == "":
            return None

        return QIcon(QPixmap(fn))

    def all_apps(self) -> Iterator[Tuple[str, AppManifest]]:
        settings = QSettings()
        settings.beginGroup("apps")
        for appid in settings.childKeys():
            if appid.startswith("_"):
                continue

            manifest = json.loads(settings.value(appid))
            yield appid, manifest

        settings.endGroup()

    def list_app(self,
                 root: str) -> Iterator[Tuple[str, Union[AppManifest, None]]]:
        settings = QSettings()

        settings.beginGroup(f"apps/_folder/" + root)
        for key in settings.childGroups():
            yield key, None
        for key in settings.childKeys():
            try:
                int(key)
            except ValueError:
                continue

            appid = settings.value(key)
            yield appid, self[appid]

    def __iter__(self):
        yield from self.all_apps()

    def __getitem__(self, item: str) -> AppManifest:
        assert not item.startswith("_")
        return json.loads(self.settings.value(f"apps/{item}"))
class MainWindow(QMainWindow):
    """Main window of Cutevariant"""
    def __init__(self, parent=None):

        super().__init__(parent)

        self.setWindowTitle("Cutevariant")
        self.toolbar = self.addToolBar("maintoolbar")
        self.toolbar.setObjectName("maintoolbar")  # For window saveState
        self.setWindowIcon(QIcon(DIR_ICONS + "app.png"))
        self.setWindowFlags(Qt.WindowContextHelpButtonHint
                            | self.windowFlags())

        # Keep sqlite connection
        self.conn = None

        # App settings
        self.app_settings = QSettings()

        # State variable of application
        # store fields, source, filters, group_by, having data
        # Often changed by plugins
        self.state = State()

        # Central workspace
        self.central_tab = QTabWidget()
        self.footer_tab = QTabWidget()
        self.vsplit = QSplitter(Qt.Vertical)
        self.vsplit.addWidget(self.central_tab)
        self.vsplit.addWidget(self.footer_tab)
        self.setCentralWidget(self.vsplit)

        # Status Bar
        self.status_bar = QStatusBar()
        self.setStatusBar(self.status_bar)

        # Setup menubar
        self.setup_menubar()
        # Setup toobar under the menu bar
        self.setup_toolbar()

        # Register plugins
        self.plugins = {
        }  # dict of names (not titles) as keys and widgets as values
        self.dialog_plugins = {
        }  # dict of actions as keys and classes as values
        self.register_plugins()

        # Window geometry
        self.resize(600, 400)
        self.setGeometry(QApplication.instance().desktop().rect().adjusted(
            100, 100, -100, -100))

        self.setTabPosition(Qt.AllDockWidgetAreas, QTabWidget.North)

        # If True, the GUI settings are deleted when the app is closed
        self.requested_reset_ui = False
        # Restores the state of this mainwindow's toolbars and dockwidgets
        self.read_settings()

        # Auto open recent projects
        recent = self.get_recent_projects()
        if recent and os.path.isfile(recent[0]):
            self.open(recent[0])

    def add_panel(self, widget, area=Qt.LeftDockWidgetArea):
        """Add given widget to a new QDockWidget and to view menu in menubar"""
        dock = QDockWidget()
        dock.setWindowTitle(widget.windowTitle())
        dock.setWidget(widget)
        dock.setStyleSheet("QDockWidget { font: bold }")
        # Keep the attached dock to allow further clean deletion
        widget.dock = dock
        # Set the objectName for a correct restoration after saveState
        dock.setObjectName(str(widget.__class__))

        self.addDockWidget(area, dock)
        self.view_menu.addAction(dock.toggleViewAction())

    def register_plugins(self):
        """Dynamically load plugins to the window

        Wrapper of :meth:`register_plugin`

        Two types of plugins can be registered:
        - widget: Added to the GUI,
        - dialog: DialogBox accessible from the tool menu.

        `setting` type is handled separately in :meth:`cutevariant.gui.settings.load_plugins`
        """
        LOGGER.info("MainWindow:: Registering plugins...")

        # Get classes of plugins
        # Don't forget to skip disabled plugins
        for extension in plugin.find_plugins():
            self.register_plugin(extension)

    def register_plugin(self, extension):
        """Dynamically load plugins to the window

        Two types of plugins can be registered:
        - widget: Added to the GUI,
        - dialog: DialogBox accessible from the tool menu.

        `setting` type is handled separately in :meth:`cutevariant.gui.settings.load_plugins`

        Args:
            extension (dict): Extension dict. See :meth:`cutevariant.gui.plugin.find_plugins`
        """
        LOGGER.debug("Extension: %s", extension)

        name = extension["name"]
        title = extension["title"]
        displayed_title = name if LOGGER.getEffectiveLevel(
        ) == DEBUG else title

        if "widget" in extension and extension["widget"].ENABLE:
            # New GUI widget
            plugin_widget_class = extension["widget"]

            # Setup new widget
            widget = plugin_widget_class(parent=self)
            if not widget.objectName():
                LOGGER.debug(
                    "widget '%s' has no objectName attribute; "
                    "=> fallback to extension name",
                    displayed_title,
                )
                widget.setObjectName(name)

            # Set title
            widget.setWindowTitle(displayed_title)

            # WhatsThis content
            long_description = extension.get("long_description")
            if not long_description:
                long_description = extension.get("description")
            widget.setWhatsThis(long_description)
            # Register (launch first init on some of them)
            widget.on_register(self)
            if self.conn:
                # If register occurs after a project is loaded we must set its
                # connection attribute
                widget.on_open_project(self.conn)

            # Init mainwindow via the constructor or on_register
            if widget.mainwindow != self:
                LOGGER.error(
                    "Bad plugin implementation, <mainwindow> plugin attribute is not set."
                )
                widget.on_close()
            else:
                # Add new plugin to plugins already registered
                self.plugins[name] = widget

                # Set position on the GUI
                if plugin_widget_class.LOCATION == plugin.DOCK_LOCATION:
                    self.add_panel(widget)

                if plugin_widget_class.LOCATION == plugin.CENTRAL_LOCATION:
                    self.central_tab.addTab(widget, widget.windowTitle())

                if plugin_widget_class.LOCATION == plugin.FOOTER_LOCATION:
                    self.footer_tab.addTab(widget, widget.windowTitle())

        if "dialog" in extension and extension["dialog"].ENABLE:
            # New tool menu entry
            plugin_dialog_class = extension["dialog"]

            # Add plugin to Tools menu
            dialog_action = self.tool_menu.addAction(displayed_title + "...")
            self.dialog_plugins[dialog_action] = plugin_dialog_class
            dialog_action.triggered.connect(self.show_dialog)

    def refresh_plugins(self, sender: plugin.PluginWidget = None):
        """Refresh all widget plugins

        It doesn't refresh the sender plugin, and not visible plugins.

        Args:
            sender (PluginWidget): from a plugin, you can pass "self" as argument
        """
        for plugin_obj in self.plugins.values():
            if plugin_obj is not sender and (
                    plugin_obj.isVisible()
                    or plugin_obj.REFRESH_ONLY_VISIBLE is False):
                try:
                    plugin_obj.on_refresh()
                except Exception as e:
                    LOGGER.exception(e)

    def refresh_plugin(self, plugin_name: str):
        """Refresh a widget plugin identified by plugin_name

        It doesn't refresh the sender plugin

        Args:
            plugin_name (str): Name of the plugin to be refreshed
        """
        if plugin_name in self.plugins:
            plugin_obj = self.plugins[plugin_name]
            plugin_obj.on_refresh()

    def setup_menubar(self):
        """Menu bar setup: items and actions

        .. note:: Setup tools menu that could be dynamically augmented by plugins.
        """
        ## File Menu
        self.file_menu = self.menuBar().addMenu(self.tr("&File"))
        self.new_project_action = self.file_menu.addAction(
            FIcon(0xF01BA), self.tr("&New project"), self.new_project,
            QKeySequence.New)
        self.open_project_action = self.file_menu.addAction(
            FIcon(0xF095D),
            self.tr("&Open project..."),
            self.open_project,
            QKeySequence.Open,
        )
        ### Recent projects
        self.recent_files_menu = self.file_menu.addMenu(self.tr("Open recent"))
        self.setup_recent_menu()

        ### Export projects as
        self.export_csv_action = self.file_menu.addAction(
            self.tr("Export as csv"), self.export_as_csv)

        self.export_ped_action = self.file_menu.addAction(
            self.tr("Export pedigree PED/PLINK"), self.export_ped)

        self.file_menu.addSeparator()
        ### Misc
        self.file_menu.addAction(FIcon(0xF0493), self.tr("Settings..."),
                                 self.show_settings)
        self.file_menu.addSeparator()
        self.file_menu.addAction(self.tr("&Quit"), self.close,
                                 QKeySequence.Quit)

        ## Edit
        # TODO: if variant_view plugin is not loaded, disable this menu entries...
        self.edit_menu = self.menuBar().addMenu(self.tr("&Edit"))
        self.edit_menu.addAction(
            FIcon(0xF018F),
            self.tr("&Copy selected variants"),
            self.copy_variants_to_clipboard,
            "ctrl+shift+c",
        )
        self.edit_menu.addSeparator()
        self.edit_menu.addAction(
            FIcon(0xF0486),
            self.tr("&Select all variants"),
            self.select_all_variants,
            QKeySequence.SelectAll,
        )

        ## View
        self.view_menu = self.menuBar().addMenu(self.tr("&View"))
        self.view_menu.addAction(self.tr("Reset widgets positions"),
                                 self.reset_ui)
        # Set toggle footer visibility action
        show_action = self.view_menu.addAction(FIcon(0xF018D),
                                               self.tr("Show bottom toolbar"))
        show_action.setCheckable(True)
        show_action.setChecked(True)
        show_action.toggled.connect(self.toggle_footer_visibility)
        self.view_menu.addSeparator()

        ## Tools
        self.tool_menu = self.menuBar().addMenu(self.tr("&Tools"))

        ## Help
        self.help_menu = self.menuBar().addMenu(self.tr("Help"))

        self.help_menu.addAction(
            FIcon(0xF02D6),
            self.tr("What's this"),
            QWhatsThis.enterWhatsThisMode,
            QKeySequence.WhatsThis,
        )
        self.help_menu.addAction(
            FIcon(0xF059F),
            self.tr("Documentation..."),
            partial(QDesktopServices.openUrl,
                    QUrl(cm.WIKI_URL, QUrl.TolerantMode)),
        )
        self.help_menu.addAction(
            FIcon(0xF0A30),
            self.tr("Report a bug..."),
            partial(QDesktopServices.openUrl,
                    QUrl(cm.REPORT_BUG_URL, QUrl.TolerantMode)),
        )

        self.help_menu.addSeparator()
        self.help_menu.addAction(self.tr("About Qt..."),
                                 QApplication.instance().aboutQt)
        self.help_menu.addAction(
            QIcon(DIR_ICONS + "app.png"),
            self.tr("About Cutevariant..."),
            self.about_cutevariant,
        )

    def setup_toolbar(self):
        """Top tool bar setup: items and actions

        .. note:: Require selection_widget and some actions of Menubar
        """
        # Tool bar
        self.toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
        self.toolbar.addAction(self.new_project_action)
        self.toolbar.addAction(self.open_project_action)
        self.toolbar.addAction(FIcon(0xF02D7), self.tr("Help"),
                               QWhatsThis.enterWhatsThisMode)
        self.toolbar.addSeparator()

    def open(self, filepath):
        """Open the given db/project file

        .. note:: Called at the end of a project creation by the Wizard,
            and by Open/Open recent projects slots.

        :param filepath: Path of project file.
        :type filepath: <str>
        """
        if not os.path.isfile(filepath):
            return

        # Save directory
        self.app_settings.setValue("last_directory", os.path.dirname(filepath))

        # Create connection
        self.conn = get_sql_connection(filepath)

        try:
            # DB version filter
            db_version = get_metadatas(self.conn).get("cutevariant_version")
            if db_version and parse_version(db_version) < parse_version(
                    MIN_AUTHORIZED_DB_VERSION):
                # Refuse to open blacklisted DB versions
                # Unversioned files are still accepted
                QMessageBox.critical(
                    self,
                    self.tr("Error while opening project"),
                    self.tr("File: {} is too old; please create a new project."
                            ).format(filepath),
                )
                return

            self.open_database(self.conn)
            self.save_recent_project(filepath)

        except sqlite3.OperationalError as e:
            LOGGER.exception(e)
            QMessageBox.critical(
                self,
                self.tr("Error while opening project"),
                self.tr(
                    "File: {}\nThe following exception occurred:\n{}").format(
                        filepath, e),
            )
            return

        # Show the project name in title and in status bar
        self.setWindowTitle("Cutevariant - %s" % os.path.basename(filepath))
        self.status_bar.showMessage(self.tr("{} opened").format(filepath))

    def open_database(self, conn):
        """Open the project file and populate widgets

        Args:
            conn (sqlite3.Connection): Sqlite3 Connection
        """
        self.conn = conn

        # Clear memoization cache for count_cmd
        command.clear_cache_cmd()
        # Clear State variable of application
        # store fields, source, filters, group_by, having data
        self.state = State()

        for plugin_obj in self.plugins.values():
            plugin_obj.on_open_project(self.conn)

    def save_recent_project(self, path):
        """Save current project into QSettings

        Args:
            path (str): path of project
        """
        paths = self.get_recent_projects()
        if path in paths:
            paths.remove(path)
        paths.insert(0, path)

        self.app_settings.setValue("recent_projects",
                                   paths[:MAX_RECENT_PROJECTS])

    def get_recent_projects(self):
        """Return the list of recent projects stored in settings

        Returns:
            list: Recent project paths
        """
        # Reload last projects opened
        recent_projects = self.app_settings.value("recent_projects", list())

        # Check if recent_projects is a list() (as expected)
        if isinstance(recent_projects, str):
            recent_projects = [recent_projects]

        # Return only existing project files
        return [p for p in recent_projects if os.path.exists(p)]

    def clear_recent_projects(self):
        """Slot to clear the list of recent projects"""
        self.app_settings.remove("recent_projects")
        self.setup_recent_menu()

    def setup_recent_menu(self):
        """Setup recent submenu with previously opened projects"""
        self.recent_files_menu.clear()
        for path in self.get_recent_projects():
            self.recent_files_menu.addAction(path,
                                             self.on_recent_project_clicked)

        self.recent_files_menu.addSeparator()
        self.recent_files_menu.addAction(self.tr("Clear"),
                                         self.clear_recent_projects)

    def on_recent_project_clicked(self):
        """Slot to load a recent project"""
        action = self.sender()
        LOGGER.debug(action.text())
        self.open(action.text())

    def new_project(self):
        """Slot to allow creation of a project with the Wizard"""
        wizard = ProjectWizard()
        if not wizard.exec_():
            return

        db_filename = (wizard.field("project_path") + QDir.separator() +
                       wizard.field("project_name") + ".db")
        try:
            self.open(db_filename)
        except Exception as e:
            self.status_bar.showMessage(e.__class__.__name__ + ": " + str(e))
            raise

    def open_project(self):
        """Slot to open an already existing project from a QFileDialog"""
        # Reload last directory used
        last_directory = self.app_settings.value("last_directory",
                                                 QDir.homePath())

        filepath, _ = QFileDialog.getOpenFileName(
            self,
            self.tr("Open project"),
            last_directory,
            self.tr("Cutevariant project (*.db)"),
        )
        if filepath:
            try:
                self.open(filepath)
            except Exception as e:
                self.status_bar.showMessage(e.__class__.__name__ + ": " +
                                            str(e))
                raise

    def export_as_csv(self):
        """Export variants into CSV file"""
        # Reload last directory used
        last_directory = self.app_settings.value("last_directory",
                                                 QDir.homePath())

        filepath, _ = QFileDialog.getSaveFileName(self,
                                                  self.tr("Save project"),
                                                  last_directory,
                                                  self.tr("(*.csv)"))

        if filepath:
            with open(filepath, "w") as file:
                writer = CsvWriter(file)
                writer.save(self.conn)

    def export_ped(self):
        """Export samples into PED/PLINK file"""
        # Reload last directory used
        last_directory = self.app_settings.value("last_directory",
                                                 QDir.homePath())

        # noinspection PyCallByClass
        filepath, _ = QFileDialog.getSaveFileName(self,
                                                  self.tr("Save project"),
                                                  last_directory, "(*.tfam)")

        if filepath:
            filepath = filepath if filepath.endswith(
                ".tfam") else filepath + ".tfam"

            with open(filepath, "w") as file:
                writer = PedWriter(file)
                writer.save(self.conn)

    def show_settings(self):
        """Slot to show settings window"""
        widget = SettingsWidget(self)
        widget.uiSettingsChanged.connect(self.reload_ui)
        widget.exec_()

    def show_dialog(self):
        """Show Plugin dialog box after a click on the tool menu"""
        action = self.sender()
        if action in self.dialog_plugins:
            # Get class object and instantiate it
            dialog_class = self.dialog_plugins[action]
            # Send SQL connection
            dialog = dialog_class(conn=self.conn)
            dialog.mainwindow = self
            dialog.exec_()

    def about_cutevariant(self):
        """Slot to show about window"""
        dialog_window = AboutCutevariant(self)
        dialog_window.exec_()

    def reload_ui(self):
        """Reload *without* reset the positions of the widgets"""
        geometry = self.saveGeometry()
        ui_state = self.saveState()
        self.reset_ui()
        # Reload positions
        self.restoreGeometry(geometry)
        self.restoreState(ui_state)

    def reset_ui(self):
        """Reset the positions of docks (and their widgets) to the default state

        All the widgets are deleted and reinstantiated on the GUI.
        GUI settings via QSettings are also reset.
        """
        # Set reset ui boolean (used by closeEvent)
        self.requested_reset_ui = True
        # Reset settings
        self.write_settings()

        # Remove widgets in QTabWidgets
        for index in range(self.central_tab.count()):
            self.central_tab.removeTab(index)

        for index in range(self.footer_tab.count()):
            self.footer_tab.removeTab(index)

        # Remove tool menu actions linked to the dialog plugins
        for action in self.dialog_plugins:
            # LOGGER.debug("Remove action <%s>", action.text())
            self.tool_menu.removeAction(action)
            action.deleteLater()

        # Purge widgets (central/footer and others) and related docks
        for plugin_obj in self.plugins.values():
            # LOGGER.debug("Remove plugin <%s>", plugin_obj)
            self.removeDockWidget(plugin_obj.dock)
            plugin_obj.on_close()

        # Clean registered plugins
        self.plugins = {}
        self.dialog_plugins = {}
        # Register new plugins
        self.register_plugins()
        self.open_database(self.conn)
        # Allow a user to save further modifications
        self.requested_reset_ui = False

    def deregister_plugin(self, extension):
        """Delete plugin from the UI; Called from app settings when a plugin is disabled.

        - dialogs plugins: Remove action from tool menu
        - widgets plugins: Delete widget and its dock if available via its
            on_close() method.

        Args:
            extension (dict): Extension dict. See :meth:`cutevariant.gui.plugin.find_plugins`
        """
        name = extension["name"]
        title = extension["title"]
        displayed_title = name if LOGGER.getEffectiveLevel(
        ) == DEBUG else title

        # Remove tool menu actions linked to the dialog plugins
        for action in self.dialog_plugins:
            if action.text() == displayed_title:
                # LOGGER.debug("Remove action <%s>", action.text())
                self.tool_menu.removeAction(action)
                action.deleteLater()
                del self.dialog_plugins[action]
                break

        # Purge widgets and related docks
        if name in self.plugins:
            plugin_obj = self.plugins[name]
            # LOGGER.debug("Remove plugin <%s>", plugin_obj)
            self.removeDockWidget(plugin_obj.dock)
            plugin_obj.on_close()
            del self.plugins[name]

    def copy_variants_to_clipboard(self):
        if "variant_view" in self.plugins:
            self.plugins["variant_view"].copy()

    def select_all_variants(self):
        """Select all elements in the current tab's view"""
        if "variant_view" in self.plugins:
            self.plugins["variant_view"].select_all()

    def closeEvent(self, event):
        """Save the current state of this mainwindow's toolbars and dockwidgets

        .. warning:: Make sure that the property objectName is unique for each
            QToolBar and QDockWidget added to the QMainWindow.

        .. note:: Reset windowState if asked.
        """
        self.write_settings()
        super().closeEvent(event)

    def write_settings(self):
        """Store the state of this mainwindow.

        .. note:: This methods is called by closeEvent
        """
        if self.requested_reset_ui:
            # Delete window state setting
            self.app_settings.remove("windowState")
        else:
            self.app_settings.setValue("geometry", self.saveGeometry())
            #  TODO: handle UI changes by passing UI_VERSION to saveState()
            self.app_settings.setValue("windowState", self.saveState())

    def read_settings(self):
        """Restore the state of this mainwindow's toolbars and dockwidgets

        .. note:: If windowState is not stored, current state is written.
        """
        # Init reset ui boolean
        self.requested_reset_ui = False

        self.restoreGeometry(QByteArray(self.app_settings.value("geometry")))
        #  TODO: handle UI changes by passing UI_VERSION to saveState()
        window_state = self.app_settings.value("windowState")
        if window_state:
            self.restoreState(QByteArray(window_state))
        else:
            # Setting has been deleted: set the current default state
            #  TODO: handle UI changes by passing UI_VERSION to saveState()
            self.app_settings.setValue("windowState", self.saveState())

    def toggle_footer_visibility(self, visibility):
        """Toggle visibility of the bottom toolbar and all its plugins"""
        self.footer_tab.setVisible(visibility)
Beispiel #9
0
    def _unregister_url_scheme_windows(self):

        reg_path = self.WIN_REG_PATH.format(self.URL_SCHEME)
        reg = QSettings(reg_path, QSettings.NativeFormat)

        reg.remove("")
Beispiel #10
0
class PluginManager():
    """Creates and manages the plugins.

    Serves as a bridge between the editor and the plugins.

    """
    def __init__(self, parent):
        self.parent = parent
        self.settings = QSettings(c.SETTINGS_PATH, QSettings.IniFormat)
        self.keyboard_settings = QSettings(c.KEYBOARD_SETTINGS_PATH,
                                           QSettings.IniFormat)
        self.theme = self.settings.value(c.THEME, defaultValue=c.THEME_D)
        self.get_keyboard_shortcuts()
        self.get_plugins()
        self.do_not_delete = [
            c.SAVE_KEY, c.WORD_BY_WORD_KEY_NEXT, c.FORWARD_KEY,
            c.PLAY_PAUSE_KEY, c.BACKWARDS_KEY, c.HELP_KEY,
            c.WORD_BY_WORD_KEY_PREV
        ]

    def get_keyboard_shortcuts(self):
        """Saves the current occupied shortcuts in a list"""

        self.occupied_keys = []
        for key in self.keyboard_settings.allKeys():
            keyboard_value = self.keyboard_settings.value(key)
            if len(keyboard_value) != 0:
                self.occupied_keys.append(keyboard_value)

    def get_plugins(self):
        """Creates all Plugins and saves them in the plugin-list."""

        self.plugin_list = []
        for (dirpath, dirname, filenames) in walk(c.PLUGIN_PATH):
            for file in filenames:
                if c.PLUGIN_POST not in file or "__pycache__" in dirpath:
                    continue
                try:
                    module_path = os.path.join(dirpath, file)
                    temp_spec = ilu.spec_from_file_location(
                        file.split(".")[0], module_path)
                    temp_module = ilu.module_from_spec(temp_spec)
                    temp_spec.loader.exec_module(temp_module)
                    temp_plugin = temp_module.Plugin(self)
                    if issubclass(type(temp_plugin), IPlugin):
                        self.plugin_list.append(temp_plugin)
                        print("Imported {} as a Plugin".format(file))
                except BaseException as e:
                    print("Error while importing {} with Exception {}".format(
                        file, e))

    def get_plugin_licences(self):
        """Gets all used plugin-licences."""

        licences = {}
        for (dirpath, dirname, filenames) in walk(c.PLUGIN_PATH):
            if dirpath.lower().endswith("licences"):
                for filename in filenames:
                    licences[filename.split(".")[0]] = dirpath

        return licences

    def get_toolbar_actions(self, parent):
        """Queries all toolbar actions from the plugins.

        Handles the Keyboard-Shortcuts from this toolbar-actions and prevents overlapping.

        Args:
          parent: The Parent-Window.

        Returns:
          Returns list of the QActions from all plugins.
        """

        actions = []
        for plugin in self.plugin_list:
            plugin_actions = plugin.get_toolbar_action(parent)
            plugin_name = plugin.get_name()
            for action in plugin_actions:
                if action is None or plugin_name is None or not plugin_name:
                    continue
                key_name = plugin_name.upper().replace(" ", "_") + "_KEY"
                saved_key = self.keyboard_settings.value(key_name,
                                                         defaultValue=None)
                plugin_shortcut = action.shortcut()
                if saved_key is None and not plugin_shortcut.isEmpty():
                    plugin_shortcut_upper = plugin_shortcut.toString().upper()
                    if plugin_shortcut_upper not in self.occupied_keys:
                        self.keyboard_settings.setValue(
                            key_name, plugin_shortcut_upper)
                        self.occupied_keys.append(saved_key)
                    else:
                        self.keyboard_settings.setValue(key_name, "")
                    self.do_not_delete.append(key_name)
                if saved_key is not None:
                    action.setShortcut(QKeySequence(saved_key))
                actions.append(action)
        self.clear_keyboard_settings()
        return actions

    def clear_keyboard_settings(self):
        """Deletes all unused shortcuts from the keyboard settings."""

        all_keys = set(self.keyboard_settings.allKeys())
        to_delete = all_keys.difference(set(self.do_not_delete))
        for key in to_delete:
            print("removed", key)
            self.keyboard_settings.remove(key)

    def get_word_by_word_actions(self, word, meta_data, pos):
        """Queries all QPushButtons of the plugins.

        Args:
          word: str: The current word
          word_meta_data: List[dict]: The meta_data from the word.
          word_pos: int:  The current word position

        """

        for plugin in self.plugin_list:
            plugin.get_word_action(word, meta_data, pos)

    def project_loaded(self):
        """Calls the project_loaded method of all plugins."""

        for plugin in self.plugin_list:
            plugin.project_loaded()

    def add_new_word_by_word_action(self, btns, name, word, word_pos):
        """Adds a new Word-Action (QPushButton) to the editor word by word list.

        Args:
          btns: The QPushButtons from the plugin.
          name: The Name of the Buttons.
          word: The word for which these buttons are.
          word_pos: The position of the word.

        """

        btns = btns
        for btn in btns:
            btn.setShortcut(None)
        self.parent.add_new_word_by_word_action(btns, name, word, word_pos)

    def get_selection(self):
        """Gets the current selected word(s) in the editor.

        Returns:
          Selected word(s)

        """

        return self.parent.get_selection()

    def replace_selection(self, new_word):
        """Replaces the current selection in the editor.

        Args:
          new_word: the word(s) which should replace the current selection

        """

        self.parent.replace_selection(new_word)

    def get_text(self):
        """Returns the full text from the editor.

        Returns:
          The full text from the editor.

        """

        return self.parent.get_text()

    def set_text(self, new_text):
        """Sets the text in the editor with the given new_text.

        Args:
          new_text: The new text.

        """

        self.parent.set_text(new_text)

    def get_word_at(self, pos):
        """Gets a the word at the given position.

        Args:
          pos: Position of the desired word.

        Returns:
          The word at the position.
        """

        return self.parent.get_word_at(pos)

    def set_word_at(self, word, pos, replace_old):
        """Sets the word at a given position.

        Args:
          word: The word which should be set.
          pos: The position on which the word should be set.
          replace_old: true: replace the old word on the positon, false: set it before.

        """

        self.parent.set_word_at(word, pos, replace_old)

    def get_setting(self, key, default):
        """Get a specific setting.

        Args:
          key: The settings-key
          default: The default value if there is no setting for the key.

        Returns:
          The settings value for the given key or the default if nothing is there.
        """

        return self.settings.value(key, defaultValue=default)

    def get_language(self):
        """Returns the language of the current project.

        Returns:
            The language of the current project.

        """

        return self.parent.get_language()

    def set_hint_text(self, text):
        """Sets the hint text (left bottom corner) in the editor.

        Args:
          text: The Hint text.

        """

        self.parent.set_hint_text(text)

    def set_text_with_line_breaks(self, new_text):
        """Sets the Text in the editor but tries to restore the given linebreaks.

        Args:
          new_text: The new text.

        """

        self.parent.set_text_with_line_breaks(new_text)

    def get_project_folder_path(self):
        """Get the current project folder path

        Returns:
          Project folder path.

        """

        return self.parent.get_project_folder_path()
class SettingsDialog(QWidget):

    def __init__(self, appctxt, config, config_file):
        super().__init__()

        ui_path = appctxt.get_resource('SettingsDialog.ui')
        self.ui = loader.load(ui_path, self)
        self.ui.show()

        self.config = config
        self.config_file = config_file

        # Set values
        self.ui.line_edit_api_token.setText(self.config.get('api_token'))
        self.ui.checkbox_run_at_startup.setChecked(self.config.get('run_at_startup'))
        self.ui.line_edit_upload_directory.setText(self.config.get('watch_dir'))
        self.ui.line_edit_account_id.setText(self.config.get('account_id'))

        # Connect
        self.ui.button_select_file.clicked.connect(self.file_dialog)
        self.ui.pushButton.clicked.connect(self.save_config)

        # PE not sure how to test this with unit tests
        RUN_PATH = "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"
        self.run_at_startup_settings = QSettings(RUN_PATH, QSettings.NativeFormat)

    def file_dialog(self):
        logging.info('file_dialog')
        watch_dir = str(QFileDialog.getExistingDirectory(self, "Select Upload Directory"))
        self.config.set('watch_dir', watch_dir)
        self.ui.line_edit_upload_directory.setText(self.config.get('watch_dir'))

    def save_config(self):
        logging.info(f'accept')

        self.config.set('api_token', self.ui.line_edit_api_token.text())
        self.config.set('run_at_startup', self.ui.checkbox_run_at_startup.isChecked())
        self.config.set('account_id', self.ui.line_edit_account_id.text())

        # FIXME should restart watchdog thread

        # Run at startup
        if self.config.get('run_at_startup'):
            startup_loc = 'C:\\Program Files (x86)\\PWUploader\\PWUploader.exe'
            logging.debug(f'Set run at startup: {startup_loc}')
            self.run_at_startup_settings.setValue('PWUploader', startup_loc)
        else:
            self.run_at_startup_settings.remove('PWUploader')

        # App guardian
        # FIXME
        # if self.config.get('always_running'):
        #     start_guardian_detached()
        # else:
        #     kill_guardian()

        with open(self.config_file, 'w') as f:
            f.write(json.dumps(self.config.as_dict(), indent=2))
        logging.info('config saved')

        self.ui.accept()
Beispiel #12
0
class Settings:
    def __init__(self):

        # Standard constructor stuff
        super().__init__()
        self.settings = QSettings()

        if not pygame.get_init():
            pygame.init()
        # Instance variables
        self.exercises = []
        self.instrument = int(self.settings.value('instrument'))

        # Load exercises from stored settings
        self.settings.beginReadArray('exercises')
        for ex in self.settings.allKeys():
            if ex == 'size':
                continue
            self.exercises.append([ex, self.settings.value(ex)])
        self.settings.endArray()

    def remove_exercise(self, exercise_name):
        # Replace self.exercises with a copy without the selected exercise
        new_exercises = []
        for ex in self.exercises:
            if ex[0] != exercise_name:
                new_exercises.append(ex)
        self.exercises = new_exercises

    def reload_exercise(self, exercise_name, exercise_text):
        # Load all exercise names
        exercise_names = []
        for ex in self.exercises:
            exercise_names.append(ex[0])
        new_exercises = []
        # If the reloaded exercise is existing then update it in memory,
        #   otherwise just add it
        exercise_contents = exercise_text.split()
        if exercise_name in exercise_names:
            for ex in self.exercises:
                if ex[0] == exercise_name:
                    new_exercises.append([ex[0], exercise_contents])
                else:
                    new_exercises.append([ex[0], ex[1]])
            self.exercises = new_exercises
        else:
            self.exercises.append([exercise_name, exercise_contents])

    def set_instrument(self, instrument_id):
        self.instrument = instrument_id

    def save_settings(self):
        self.settings.beginWriteArray('exercises')
        for key in self.settings.allKeys():
            self.settings.remove(key) if key != 'size' else None
        for ex in self.exercises:
            self.settings.setValue(ex[0], ex[1])
        self.settings.endArray()
        self.settings.setValue('instrument', self.instrument)
        self.settings.sync()

    def preview(self):
        pygame.mixer.music.stop()
        midi_file = tempfile.NamedTemporaryFile(delete=False)
        mid = MidiFile()
        track = MidiTrack()
        mid.tracks.append(track)
        instrument = self.instrument
        track.append(Message('program_change', program=instrument, time=0))
        note = 60
        track.append(Message('note_on', note=note, velocity=100, time=0))
        track.append(Message('note_off', note=note, time=2000))
        mid.save(file=midi_file)
        midi_file.flush()
        midi_file.close()
        pygame.mixer.music.load(midi_file.name)
        pygame.mixer.music.play()
        if 'WARMUPPY_KEEP_MIDI' in os.environ:
            copyfile(midi_file.name, os.environ['WARMUPPY_KEEP_MIDI'])
        os.remove(midi_file.name)