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