#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (C) 2005-2007 Carabos Coop. V. All rights reserved # Copyright (C) 2008-2019 Vicent Mas. All rights reserved # # 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, either version 3 of the License, or # (at your option) any later version. # # 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, see . # # Author: Vicent Mas - vmas@vitables.org """ This module provides a dialog for changing ``ViTables`` settings at runtime. The dialog has 3 pages managed via QtGui.QStackedWidget: General settings page, Look&Feel settings page and Plugins settings page. """ import os from qtpy import QtCore from qtpy import QtGui from qtpy import QtWidgets from qtpy.uic import loadUiType from vitables.vtsite import ICONDIR import vitables.utils __docformat__ = 'restructuredtext' translate = QtWidgets.QApplication.translate # This method of the PyQt5.uic module allows for dynamically loading user # interfaces created by QtDesigner. See the PyQt5 Reference Guide for more # info. Ui_SettingsDialog = \ loadUiType(os.path.join(os.path.dirname(__file__), 'settings_dlg.ui'))[0] class Preferences(QtWidgets.QDialog, Ui_SettingsDialog): """ Create the Settings dialog. By loading UI files at runtime we can: - create user interfaces at runtime (without using pyuic) - use multiple inheritance, MyParentClass(BaseClass, FormClass) """ def __init__(self): """ Initialize the preferences dialog. * initializes the dialog appearance according to current preferences * connects dialog widgets to slots that provide them functionality """ self.vtapp = vitables.utils.getVTApp() self.vtgui = self.vtapp.gui # Create the Settings dialog and customize it super(Preferences, self).__init__(self.vtgui) self.setupUi(self) self.config = self.vtapp.config self.pg_loader = self.vtapp.plugins_mgr self.all_plugins = \ dict(item for item in self.pg_loader.all_plugins.items()) self.enabled_plugins = self.pg_loader.enabled_plugins[:] # Setup the Plugins page self.setupPluginsPage() # Setup the page selector widget self.setupSelector() # Display the General Settings page self.stackedPages.setCurrentIndex(0) # Style names can be retrieved with qt.QStyleFactory.keys() styles = QtWidgets.QStyleFactory.keys() self.stylesCB.insertItems(0, styles) # The dictionary of current ViTables preferences self.initial_prefs = {} style_sheet = self.vtgui.logger.styleSheet() paper = style_sheet[-7:] self.initial_prefs['Logger/Paper'] = QtGui.QColor(paper) self.initial_prefs['Logger/Text'] = self.vtgui.logger.textColor() self.initial_prefs['Logger/Font'] = self.vtgui.logger.font() self.initial_prefs['Workspace/Background'] = \ self.vtgui.workspace.background() self.initial_prefs['Look/currentStyle'] = self.config.current_style self.initial_prefs['Session/startupWorkingDir'] = \ self.config.initial_working_directory self.initial_prefs['Session/restoreLastSession'] = \ self.config.restore_last_session # The dictionary used to update the preferences self.new_prefs = {} # Apply the current ViTables configuration to the Preferences dialog self.resetPreferences() # Connect SIGNALS to SLOTS self.buttonsBox.helpRequested.connect( QtWidgets.QWhatsThis.enterWhatsThisMode) def setupPluginsPage(self): """Populate the tree of plugins. """ nrows = len(self.all_plugins) self.plugins_model = QtGui.QStandardItemModel(nrows, 2, self) self.pluginsTV.setModel(self.plugins_model) header = QtWidgets.QHeaderView(QtCore.Qt.Horizontal, self.pluginsTV) header.setStretchLastSection(True) self.pluginsTV.setHeader(header) self.plugins_model.setHorizontalHeaderLabels(['Name', 'Comment']) # Populate the model row = 0 for UID, desc in self.all_plugins.items(): name = desc['name'] comment = desc['comment'] nitem = QtGui.QStandardItem(name) nitem.setData(UID) nitem.setCheckable(True) if UID in self.enabled_plugins: nitem.setCheckState(QtCore.Qt.Checked) citem = QtGui.QStandardItem(comment) self.plugins_model.setItem(row, 0, nitem) self.plugins_model.setItem(row, 1, citem) row = row + 1 def setupSelector(self): """Setup the page selector widget of the Preferences dialog. """ iconsdir = os.path.join(ICONDIR, '64x64') self.selector_model = QtGui.QStandardItemModel(self) self.pageSelector.setModel(self.selector_model) # Populate the model with top level items alignment = QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled general_item = QtGui.QStandardItem() general_item.setIcon(QtGui.QIcon(os.path.join( iconsdir, 'preferences-other.png'))) general_item.setText(translate('Preferences', " General ", "Text for page selector icon")) general_item.setTextAlignment(alignment) general_item.setFlags(flags) style_item = QtGui.QStandardItem() style_item.setIcon(QtGui.QIcon(os.path.join( iconsdir, 'preferences-desktop-theme.png'))) style_item.setText(translate('Preferences', "Look & Feel", "Text for page selector icon")) style_item.setTextAlignment(alignment) style_item.setFlags(flags) self.plugins_item = QtGui.QStandardItem() self.plugins_item.setIcon(QtGui.QIcon(os.path.join( iconsdir, 'preferences-plugin.png'))) self.plugins_item.setText(translate('Preferences', " Plugins ", "Text for page selector icon")) self.plugins_item.setTextAlignment(alignment) self.plugins_item.setFlags(flags) for item in (general_item, style_item, self.plugins_item): self.selector_model.appendRow(item) # Add items for *loaded* plugins to the Plugins item index = self.selector_model.indexFromItem(self.plugins_item) self.pageSelector.setExpanded(index, True) for UID in self.vtapp.plugins_mgr.loaded_plugins.keys(): name = UID.split('#@#')[0] item = QtGui.QStandardItem(name) item.setData(UID) self.plugins_item.appendRow(item) @QtCore.Slot("QModelIndex", name="on_pageSelector_clicked") def changeSettingsPage(self, index): """Slot for changing the selected page in the Settings dialog. :Parameter index: the index clicked by the user """ # If top level item is clicked if not index.parent().isValid(): self.stackedPages.setCurrentIndex(index.row()) # If a plugin item is clicked elif index.parent() == self.plugins_item.index(): pluginID = self.selector_model.itemFromIndex(index).data() self.aboutPluginPage(pluginID) @QtCore.Slot("QAbstractButton *", name="on_buttonsBox_clicked") def executeButtonAction(self, button): """Slot that manages button box clicks in the Preferences dialog. Whenever one of the `Help`, `Reset`, `Cancel` or `OK` buttons is clicked in the Preferences dialog this slot is called. :Parameter button: the clicked button. """ if button == self.buttonsBox.button(QtWidgets.QDialogButtonBox.Reset): self.resetPreferences() elif button == self.buttonsBox.button(QtWidgets.QDialogButtonBox.Help): pass elif button == self.buttonsBox.button( QtWidgets.QDialogButtonBox.Cancel): self.reject() else: self.applySettings() def resetPreferences(self): """ Apply the current ``ViTables`` configuration to the Preferences dialog. """ # Startup page if self.initial_prefs['Session/startupWorkingDir'] == 'last': self.lastDirCB.setChecked(True) else: self.lastDirCB.setChecked(False) self.restoreCB.setChecked( self.initial_prefs['Session/restoreLastSession']) # Style page self.sampleTE.selectAll() self.sampleTE.setCurrentFont(self.initial_prefs['Logger/Font']) self.sampleTE.setTextColor(self.initial_prefs['Logger/Text']) self.sampleTE.moveCursor(QtGui.QTextCursor.End) # Unselect text self.sampleTE.setStyleSheet("background-color: {0}".format( self.initial_prefs['Logger/Paper'].name())) self.workspaceLabel.setStyleSheet('background-color: {0}'.format( self.initial_prefs['Workspace/Background'].color().name())) index = self.stylesCB.findText(self.initial_prefs['Look/currentStyle']) self.stylesCB.setCurrentIndex(index) # The visual update done above is not enough, we must reset the # new preferences dictionary and the list of enabled plugins self.new_prefs.clear() self.new_prefs.update(self.initial_prefs) self.enabled_plugins = self.pg_loader.enabled_plugins[:] self.all_plugins = \ dict(item for item in self.pg_loader.all_plugins.items()) # UIDs = self.all_plugins.keys() for row in range(0, self.plugins_model.rowCount()): item = self.plugins_model.item(row, 0) if item.data() in self.enabled_plugins: item.setCheckState(2) else: item.setCheckState(0) def applySettings(self): """ Apply the current preferences to the application and close the dialog. This method is a slot connected to the `accepted` signal. See ctor for details. """ # Update the plugins manager self.updatePluginsManager() # Update the rest of settings for key, value in self.new_prefs.items(): self.new_prefs[key] = value self.accept() @QtCore.Slot("bool", name="on_lastDirCB_toggled") def setInitialWorkingDirectory(self, cb_on): """ Configure startup behavior of the application. If the `Start in last opened directory` check box is checked then when the user opens a file *for the very first time* the current directory of the file selector dialog (CDFSD) will be the last directory accessed in the previous ``ViTables session``. If it is not checked then ``ViTables`` follows the standard behavior: if it has been started from a console session then the CDFSD will be the current working directory of the session, if it has been started from a menu/desktop-icon/run-command-applet the CDFSD will be the users' home. This is a slot method. :Parameter cb_on: a boolean indicator of the checkbox state. """ if cb_on: self.new_prefs['Session/startupWorkingDir'] = 'last' else: self.new_prefs['Session/startupWorkingDir'] = 'home' @QtCore.Slot("bool", name="on_restoreCB_toggled") def setRestoreSession(self, cb_on): """ Configure startup behavior of the application. If the `Restore last session` checkbox is checked then, at the next startup, the application will atempt to restore the last working session. This is a slot method. :Parameter cb_on: a boolean indicator of the checkbox state. """ if cb_on: self.new_prefs['Session/restoreLastSession'] = True else: self.new_prefs['Session/restoreLastSession'] = False @QtCore.Slot(name="on_fontPB_clicked") def setLoggerFont(self): """Slot for setting the logger font.""" new_font, is_ok = \ QtWidgets.QFontDialog.getFont(self.sampleTE.currentFont()) # The selected font is applied to the sample text if is_ok: self.new_prefs['Logger/Font'] = new_font self.sampleTE.selectAll() self.sampleTE.setCurrentFont(new_font) self.sampleTE.moveCursor(QtGui.QTextCursor.End) # Unselect text @QtCore.Slot(name="on_foregroundPB_clicked") def setLoggerTextColor(self): """Slot for setting the logger foreground color.""" text_color = self.sampleTE.textColor() color = QtWidgets.QColorDialog.getColor(text_color) # The selected text color is applied to the sample text if color.isValid(): self.new_prefs['Logger/Text'] = color self.sampleTE.selectAll() self.sampleTE.setTextColor(color) self.sampleTE.moveCursor(QtGui.QTextCursor.End) @QtCore.Slot(name="on_backgroundPB_clicked") def setLoggerBackgroundColor(self): """Slot for setting the logger background color.""" stylesheet = self.sampleTE.styleSheet() background = stylesheet[-7:] color = QtWidgets.QColorDialog.getColor(QtGui.QColor(background)) # The selected paper color is applied to the sample text window if color.isValid(): self.new_prefs['Logger/Paper'] = color new_stylesheet = stylesheet.replace(background, color.name()) self.sampleTE.setStyleSheet(new_stylesheet) @QtCore.Slot(name="on_workspacePB_clicked") def setWorkspaceColor(self): """Slot for setting the workspace background color.""" stylesheet = self.workspaceLabel.styleSheet() background = stylesheet[-7:] color = QtWidgets.QColorDialog.getColor(QtGui.QColor(background)) # The selected color is applied to the sample label besides the button if color.isValid(): self.new_prefs['Workspace/Background'] = QtGui.QBrush(color) new_stylesheet = stylesheet.replace(background, color.name()) self.workspaceLabel.setStyleSheet(new_stylesheet) @QtCore.Slot("QString", name="on_stylesCB_activated") def setGlobalStyle(self, style_name): """ Slot for setting the application style. :Parameter style_name: the style to be applied """ self.new_prefs['Look/currentStyle'] = style_name def updatePluginsManager(self): """Update the plugins manager before closing the dialog. When the Apply button is clicked the list of enabled plugins is refreshed. """ self.enabled_plugins = [] for row in range(self.plugins_model.rowCount()): item = self.plugins_model.item(row, 0) if item.checkState() == 2: self.enabled_plugins.append(item.data()) self.pg_loader.enabled_plugins = self.enabled_plugins[:] def aboutPluginPage(self, pluginID): """A page with info about the plugin clicked in the selector widget. :Parameter pluginID: a unique ID for getting the proper plugin """ # Refresh the Preferences dialog pages. There is at most one # About Plugin page at any given time while self.stackedPages.count() > 3: about_page = self.stackedPages.widget(3) self.stackedPages.removeWidget(about_page) del about_page pg_instance = self.vtapp.plugins_mgr.loaded_plugins[pluginID] try: about_page = pg_instance.helpAbout(self.stackedPages) except AttributeError: about_page = QtWidgets.QWidget(self.stackedPages) label = QtWidgets.QLabel(translate( 'Preferences', 'Sorry, there are no info available for this plugin', 'A text label'), about_page) layout = QtWidgets.QVBoxLayout(about_page) layout.addWidget(label) self.stackedPages.addWidget(about_page) self.stackedPages.setCurrentIndex(3)