1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3 4# Copyright (C) 2005-2007 Carabos Coop. V. All rights reserved 5# Copyright (C) 2008-2019 Vicent Mas. All rights reserved 6# 7# This program is free software: you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation, either version 3 of the License, or 10# (at your option) any later version. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with this program. If not, see <http://www.gnu.org/licenses/>. 19# 20# Author: Vicent Mas - vmas@vitables.org 21 22""" 23This module provides a dialog for changing ``ViTables`` settings at runtime. 24 25The dialog has 3 pages managed via QtGui.QStackedWidget: General settings 26page, Look&Feel settings page and Plugins settings page. 27""" 28 29import os 30 31from qtpy import QtCore 32from qtpy import QtGui 33from qtpy import QtWidgets 34 35from qtpy.uic import loadUiType 36 37from vitables.vtsite import ICONDIR 38import vitables.utils 39 40 41__docformat__ = 'restructuredtext' 42 43translate = QtWidgets.QApplication.translate 44# This method of the PyQt5.uic module allows for dynamically loading user 45# interfaces created by QtDesigner. See the PyQt5 Reference Guide for more 46# info. 47Ui_SettingsDialog = \ 48 loadUiType(os.path.join(os.path.dirname(__file__), 'settings_dlg.ui'))[0] 49 50 51class Preferences(QtWidgets.QDialog, Ui_SettingsDialog): 52 """ 53 Create the Settings dialog. 54 55 By loading UI files at runtime we can: 56 57 - create user interfaces at runtime (without using pyuic) 58 - use multiple inheritance, MyParentClass(BaseClass, FormClass) 59 60 """ 61 62 def __init__(self): 63 """ 64 Initialize the preferences dialog. 65 66 * initializes the dialog appearance according to current preferences 67 * connects dialog widgets to slots that provide them functionality 68 """ 69 70 self.vtapp = vitables.utils.getVTApp() 71 self.vtgui = self.vtapp.gui 72 # Create the Settings dialog and customize it 73 super(Preferences, self).__init__(self.vtgui) 74 self.setupUi(self) 75 76 self.config = self.vtapp.config 77 self.pg_loader = self.vtapp.plugins_mgr 78 self.all_plugins = \ 79 dict(item for item in self.pg_loader.all_plugins.items()) 80 self.enabled_plugins = self.pg_loader.enabled_plugins[:] 81 82 # Setup the Plugins page 83 self.setupPluginsPage() 84 85 # Setup the page selector widget 86 self.setupSelector() 87 88 # Display the General Settings page 89 self.stackedPages.setCurrentIndex(0) 90 91 # Style names can be retrieved with qt.QStyleFactory.keys() 92 styles = QtWidgets.QStyleFactory.keys() 93 self.stylesCB.insertItems(0, styles) 94 95 # The dictionary of current ViTables preferences 96 self.initial_prefs = {} 97 style_sheet = self.vtgui.logger.styleSheet() 98 paper = style_sheet[-7:] 99 self.initial_prefs['Logger/Paper'] = QtGui.QColor(paper) 100 self.initial_prefs['Logger/Text'] = self.vtgui.logger.textColor() 101 self.initial_prefs['Logger/Font'] = self.vtgui.logger.font() 102 self.initial_prefs['Workspace/Background'] = \ 103 self.vtgui.workspace.background() 104 self.initial_prefs['Look/currentStyle'] = self.config.current_style 105 self.initial_prefs['Session/startupWorkingDir'] = \ 106 self.config.initial_working_directory 107 self.initial_prefs['Session/restoreLastSession'] = \ 108 self.config.restore_last_session 109 110 # The dictionary used to update the preferences 111 self.new_prefs = {} 112 113 # Apply the current ViTables configuration to the Preferences dialog 114 self.resetPreferences() 115 116 # Connect SIGNALS to SLOTS 117 self.buttonsBox.helpRequested.connect( 118 QtWidgets.QWhatsThis.enterWhatsThisMode) 119 120 def setupPluginsPage(self): 121 """Populate the tree of plugins. 122 """ 123 124 nrows = len(self.all_plugins) 125 self.plugins_model = QtGui.QStandardItemModel(nrows, 2, self) 126 self.pluginsTV.setModel(self.plugins_model) 127 header = QtWidgets.QHeaderView(QtCore.Qt.Horizontal, self.pluginsTV) 128 header.setStretchLastSection(True) 129 self.pluginsTV.setHeader(header) 130 self.plugins_model.setHorizontalHeaderLabels(['Name', 'Comment']) 131 132 # Populate the model 133 row = 0 134 for UID, desc in self.all_plugins.items(): 135 name = desc['name'] 136 comment = desc['comment'] 137 nitem = QtGui.QStandardItem(name) 138 nitem.setData(UID) 139 nitem.setCheckable(True) 140 if UID in self.enabled_plugins: 141 nitem.setCheckState(QtCore.Qt.Checked) 142 citem = QtGui.QStandardItem(comment) 143 self.plugins_model.setItem(row, 0, nitem) 144 self.plugins_model.setItem(row, 1, citem) 145 row = row + 1 146 147 def setupSelector(self): 148 """Setup the page selector widget of the Preferences dialog. 149 """ 150 151 iconsdir = os.path.join(ICONDIR, '64x64') 152 self.selector_model = QtGui.QStandardItemModel(self) 153 self.pageSelector.setModel(self.selector_model) 154 155 # Populate the model with top level items 156 alignment = QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter 157 flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled 158 general_item = QtGui.QStandardItem() 159 general_item.setIcon(QtGui.QIcon(os.path.join( 160 iconsdir, 'preferences-other.png'))) 161 general_item.setText(translate('Preferences', " General ", 162 "Text for page selector icon")) 163 general_item.setTextAlignment(alignment) 164 general_item.setFlags(flags) 165 166 style_item = QtGui.QStandardItem() 167 style_item.setIcon(QtGui.QIcon(os.path.join( 168 iconsdir, 'preferences-desktop-theme.png'))) 169 style_item.setText(translate('Preferences', "Look & Feel", 170 "Text for page selector icon")) 171 style_item.setTextAlignment(alignment) 172 style_item.setFlags(flags) 173 174 self.plugins_item = QtGui.QStandardItem() 175 self.plugins_item.setIcon(QtGui.QIcon(os.path.join( 176 iconsdir, 'preferences-plugin.png'))) 177 self.plugins_item.setText(translate('Preferences', " Plugins ", 178 "Text for page selector icon")) 179 self.plugins_item.setTextAlignment(alignment) 180 self.plugins_item.setFlags(flags) 181 182 for item in (general_item, style_item, self.plugins_item): 183 self.selector_model.appendRow(item) 184 185 # Add items for *loaded* plugins to the Plugins item 186 index = self.selector_model.indexFromItem(self.plugins_item) 187 self.pageSelector.setExpanded(index, True) 188 for UID in self.vtapp.plugins_mgr.loaded_plugins.keys(): 189 name = UID.split('#@#')[0] 190 item = QtGui.QStandardItem(name) 191 item.setData(UID) 192 self.plugins_item.appendRow(item) 193 194 @QtCore.Slot("QModelIndex", name="on_pageSelector_clicked") 195 def changeSettingsPage(self, index): 196 """Slot for changing the selected page in the Settings dialog. 197 198 :Parameter index: the index clicked by the user 199 """ 200 201 # If top level item is clicked 202 if not index.parent().isValid(): 203 self.stackedPages.setCurrentIndex(index.row()) 204 # If a plugin item is clicked 205 elif index.parent() == self.plugins_item.index(): 206 pluginID = self.selector_model.itemFromIndex(index).data() 207 self.aboutPluginPage(pluginID) 208 209 @QtCore.Slot("QAbstractButton *", name="on_buttonsBox_clicked") 210 def executeButtonAction(self, button): 211 """Slot that manages button box clicks in the Preferences dialog. 212 213 Whenever one of the `Help`, `Reset`, `Cancel` or `OK` buttons is 214 clicked in the Preferences dialog this slot is called. 215 216 :Parameter button: the clicked button. 217 """ 218 219 if button == self.buttonsBox.button(QtWidgets.QDialogButtonBox.Reset): 220 self.resetPreferences() 221 elif button == self.buttonsBox.button(QtWidgets.QDialogButtonBox.Help): 222 pass 223 elif button == self.buttonsBox.button( 224 QtWidgets.QDialogButtonBox.Cancel): 225 self.reject() 226 else: 227 self.applySettings() 228 229 def resetPreferences(self): 230 """ 231 Apply the current ``ViTables`` configuration to the Preferences dialog. 232 """ 233 234 # Startup page 235 if self.initial_prefs['Session/startupWorkingDir'] == 'last': 236 self.lastDirCB.setChecked(True) 237 else: 238 self.lastDirCB.setChecked(False) 239 240 self.restoreCB.setChecked( 241 self.initial_prefs['Session/restoreLastSession']) 242 243 # Style page 244 self.sampleTE.selectAll() 245 self.sampleTE.setCurrentFont(self.initial_prefs['Logger/Font']) 246 self.sampleTE.setTextColor(self.initial_prefs['Logger/Text']) 247 self.sampleTE.moveCursor(QtGui.QTextCursor.End) # Unselect text 248 self.sampleTE.setStyleSheet("background-color: {0}".format( 249 self.initial_prefs['Logger/Paper'].name())) 250 251 self.workspaceLabel.setStyleSheet('background-color: {0}'.format( 252 self.initial_prefs['Workspace/Background'].color().name())) 253 254 index = self.stylesCB.findText(self.initial_prefs['Look/currentStyle']) 255 self.stylesCB.setCurrentIndex(index) 256 257 # The visual update done above is not enough, we must reset the 258 # new preferences dictionary and the list of enabled plugins 259 self.new_prefs.clear() 260 self.new_prefs.update(self.initial_prefs) 261 self.enabled_plugins = self.pg_loader.enabled_plugins[:] 262 self.all_plugins = \ 263 dict(item for item in self.pg_loader.all_plugins.items()) 264# UIDs = self.all_plugins.keys() 265 for row in range(0, self.plugins_model.rowCount()): 266 item = self.plugins_model.item(row, 0) 267 if item.data() in self.enabled_plugins: 268 item.setCheckState(2) 269 else: 270 item.setCheckState(0) 271 272 def applySettings(self): 273 """ 274 Apply the current preferences to the application and close the dialog. 275 276 This method is a slot connected to the `accepted` signal. See 277 ctor for details. 278 """ 279 280 # Update the plugins manager 281 self.updatePluginsManager() 282 283 # Update the rest of settings 284 for key, value in self.new_prefs.items(): 285 self.new_prefs[key] = value 286 287 self.accept() 288 289 @QtCore.Slot("bool", name="on_lastDirCB_toggled") 290 def setInitialWorkingDirectory(self, cb_on): 291 """ 292 Configure startup behavior of the application. 293 294 If the `Start in last opened directory` check box is checked 295 then when the user opens a file *for the very first time* the 296 current directory of the file selector dialog (CDFSD) will be 297 the last directory accessed in the previous ``ViTables session``. If 298 it is not checked then ``ViTables`` follows the standard behavior: 299 if it has been started from a console session then the CDFSD 300 will be the current working directory of the session, if it has 301 been started from a menu/desktop-icon/run-command-applet the 302 CDFSD will be the users' home. 303 304 This is a slot method. 305 306 :Parameter cb_on: a boolean indicator of the checkbox state. 307 """ 308 309 if cb_on: 310 self.new_prefs['Session/startupWorkingDir'] = 'last' 311 else: 312 self.new_prefs['Session/startupWorkingDir'] = 'home' 313 314 @QtCore.Slot("bool", name="on_restoreCB_toggled") 315 def setRestoreSession(self, cb_on): 316 """ 317 Configure startup behavior of the application. 318 319 If the `Restore last session` checkbox is checked then, at the 320 next startup, the application will atempt to restore the last 321 working session. 322 323 This is a slot method. 324 325 :Parameter cb_on: a boolean indicator of the checkbox state. 326 """ 327 328 if cb_on: 329 self.new_prefs['Session/restoreLastSession'] = True 330 else: 331 self.new_prefs['Session/restoreLastSession'] = False 332 333 @QtCore.Slot(name="on_fontPB_clicked") 334 def setLoggerFont(self): 335 """Slot for setting the logger font.""" 336 337 new_font, is_ok = \ 338 QtWidgets.QFontDialog.getFont(self.sampleTE.currentFont()) 339 # The selected font is applied to the sample text 340 if is_ok: 341 self.new_prefs['Logger/Font'] = new_font 342 self.sampleTE.selectAll() 343 self.sampleTE.setCurrentFont(new_font) 344 self.sampleTE.moveCursor(QtGui.QTextCursor.End) # Unselect text 345 346 @QtCore.Slot(name="on_foregroundPB_clicked") 347 def setLoggerTextColor(self): 348 """Slot for setting the logger foreground color.""" 349 350 text_color = self.sampleTE.textColor() 351 color = QtWidgets.QColorDialog.getColor(text_color) 352 # The selected text color is applied to the sample text 353 if color.isValid(): 354 self.new_prefs['Logger/Text'] = color 355 self.sampleTE.selectAll() 356 self.sampleTE.setTextColor(color) 357 self.sampleTE.moveCursor(QtGui.QTextCursor.End) 358 359 @QtCore.Slot(name="on_backgroundPB_clicked") 360 def setLoggerBackgroundColor(self): 361 """Slot for setting the logger background color.""" 362 363 stylesheet = self.sampleTE.styleSheet() 364 background = stylesheet[-7:] 365 color = QtWidgets.QColorDialog.getColor(QtGui.QColor(background)) 366 # The selected paper color is applied to the sample text window 367 if color.isValid(): 368 self.new_prefs['Logger/Paper'] = color 369 new_stylesheet = stylesheet.replace(background, color.name()) 370 self.sampleTE.setStyleSheet(new_stylesheet) 371 372 @QtCore.Slot(name="on_workspacePB_clicked") 373 def setWorkspaceColor(self): 374 """Slot for setting the workspace background color.""" 375 376 stylesheet = self.workspaceLabel.styleSheet() 377 background = stylesheet[-7:] 378 color = QtWidgets.QColorDialog.getColor(QtGui.QColor(background)) 379 # The selected color is applied to the sample label besides the button 380 if color.isValid(): 381 self.new_prefs['Workspace/Background'] = QtGui.QBrush(color) 382 new_stylesheet = stylesheet.replace(background, color.name()) 383 self.workspaceLabel.setStyleSheet(new_stylesheet) 384 385 @QtCore.Slot("QString", name="on_stylesCB_activated") 386 def setGlobalStyle(self, style_name): 387 """ 388 Slot for setting the application style. 389 390 :Parameter style_name: the style to be applied 391 """ 392 self.new_prefs['Look/currentStyle'] = style_name 393 394 def updatePluginsManager(self): 395 """Update the plugins manager before closing the dialog. 396 397 When the Apply button is clicked the list of enabled plugins 398 is refreshed. 399 """ 400 401 self.enabled_plugins = [] 402 for row in range(self.plugins_model.rowCount()): 403 item = self.plugins_model.item(row, 0) 404 if item.checkState() == 2: 405 self.enabled_plugins.append(item.data()) 406 407 self.pg_loader.enabled_plugins = self.enabled_plugins[:] 408 409 def aboutPluginPage(self, pluginID): 410 """A page with info about the plugin clicked in the selector widget. 411 412 :Parameter pluginID: a unique ID for getting the proper plugin 413 """ 414 415 # Refresh the Preferences dialog pages. There is at most one 416 # About Plugin page at any given time 417 while self.stackedPages.count() > 3: 418 about_page = self.stackedPages.widget(3) 419 self.stackedPages.removeWidget(about_page) 420 del about_page 421 422 pg_instance = self.vtapp.plugins_mgr.loaded_plugins[pluginID] 423 try: 424 about_page = pg_instance.helpAbout(self.stackedPages) 425 except AttributeError: 426 about_page = QtWidgets.QWidget(self.stackedPages) 427 label = QtWidgets.QLabel(translate( 428 'Preferences', 429 'Sorry, there are no info available for this plugin', 430 'A text label'), about_page) 431 layout = QtWidgets.QVBoxLayout(about_page) 432 layout.addWidget(label) 433 434 self.stackedPages.addWidget(about_page) 435 self.stackedPages.setCurrentIndex(3) 436