1# -*- coding: utf-8 -*-
2
3"""
4***************************************************************************
5    ConfigDialog.py
6    ---------------------
7    Date                 : August 2012
8    Copyright            : (C) 2012 by Victor Olaya
9    Email                : volayaf at gmail dot com
10***************************************************************************
11*                                                                         *
12*   This program is free software; you can redistribute it and/or modify  *
13*   it under the terms of the GNU General Public License as published by  *
14*   the Free Software Foundation; either version 2 of the License, or     *
15*   (at your option) any later version.                                   *
16*                                                                         *
17***************************************************************************
18"""
19
20__author__ = 'Victor Olaya'
21__date__ = 'August 2012'
22__copyright__ = '(C) 2012, Victor Olaya'
23
24import os
25import warnings
26
27from qgis.PyQt import uic
28from qgis.PyQt.QtCore import Qt, QEvent
29from qgis.PyQt.QtWidgets import (QFileDialog,
30                                 QStyle,
31                                 QMessageBox,
32                                 QStyledItemDelegate,
33                                 QLineEdit,
34                                 QWidget,
35                                 QToolButton,
36                                 QHBoxLayout,
37                                 QComboBox,
38                                 QPushButton,
39                                 QApplication)
40from qgis.PyQt.QtGui import (QIcon,
41                             QStandardItemModel,
42                             QStandardItem,
43                             QCursor)
44
45from qgis.gui import (QgsDoubleSpinBox,
46                      QgsSpinBox,
47                      QgsOptionsPageWidget,
48                      QgsOptionsDialogHighlightWidget)
49from qgis.core import NULL, QgsApplication, QgsSettings
50from qgis.utils import OverrideCursor
51
52from processing.core.ProcessingConfig import (ProcessingConfig,
53                                              settingsWatcher,
54                                              Setting)
55from processing.core.Processing import Processing
56from processing.gui.DirectorySelectorDialog import DirectorySelectorDialog
57from processing.gui.menus import defaultMenuEntries, menusSettingsGroup
58
59pluginPath = os.path.split(os.path.dirname(__file__))[0]
60with warnings.catch_warnings():
61    warnings.filterwarnings("ignore", category=DeprecationWarning)
62    WIDGET, BASE = uic.loadUiType(
63        os.path.join(pluginPath, 'ui', 'DlgConfig.ui'))
64
65
66class ConfigOptionsPage(QgsOptionsPageWidget):
67
68    def __init__(self, parent):
69        super(ConfigOptionsPage, self).__init__(parent)
70        self.config_widget = ConfigDialog(False)
71        layout = QHBoxLayout()
72        layout.setContentsMargins(0, 0, 0, 0)
73        layout.setMargin(0)
74        self.setLayout(layout)
75        layout.addWidget(self.config_widget)
76        self.setObjectName('processingOptions')
77        self.highlightWidget = ProcessingTreeHighlight(self.config_widget)
78        self.registerHighlightWidget(self.highlightWidget)
79
80    def apply(self):
81        self.config_widget.accept()
82
83    def helpKey(self):
84        return 'processing/index.html'
85
86
87class ProcessingTreeHighlight(QgsOptionsDialogHighlightWidget):
88
89    def __init__(self, config_dialog):
90        super(ProcessingTreeHighlight, self).__init__(config_dialog.tree)
91        self.config_dialog = config_dialog
92
93    def highlightText(self, text):
94        return self.config_dialog.textChanged(text)
95
96    def searchText(self, text):
97        return self.config_dialog.textChanged(text)
98
99    def reset(self):
100        self.config_dialog.textChanged('')
101
102
103class ConfigDialog(BASE, WIDGET):
104
105    def __init__(self, showSearch=True):
106        super(ConfigDialog, self).__init__(None)
107        self.setupUi(self)
108
109        self.groupIcon = QgsApplication.getThemeIcon('mIconFolder.svg')
110
111        self.model = QStandardItemModel()
112        self.tree.setModel(self.model)
113
114        self.delegate = SettingDelegate()
115        self.tree.setItemDelegateForColumn(1, self.delegate)
116
117        if showSearch:
118            if hasattr(self.searchBox, 'setPlaceholderText'):
119                self.searchBox.setPlaceholderText(QApplication.translate('ConfigDialog', 'Search…'))
120            self.searchBox.textChanged.connect(self.textChanged)
121        else:
122            self.searchBox.hide()
123
124        self.fillTree()
125
126        self.saveMenus = False
127        self.tree.expanded.connect(self.itemExpanded)
128        self.auto_adjust_columns = True
129
130    def textChanged(self, text=None):
131        if text is not None:
132            text = str(text.lower())
133        else:
134            text = str(self.searchBox.text().lower())
135        found = self._filterItem(self.model.invisibleRootItem(), text)
136
137        self.auto_adjust_columns = False
138        if text:
139            self.tree.expandAll()
140        else:
141            self.tree.collapseAll()
142
143        self.adjustColumns()
144        self.auto_adjust_columns = True
145
146        if text:
147            return found
148        else:
149            self.tree.collapseAll()
150            return False
151
152    def _filterItem(self, item, text, forceShow=False):
153        if item.hasChildren():
154            show = forceShow or isinstance(item, QStandardItem) and bool(text) and (text in item.text().lower())
155            for i in range(item.rowCount()):
156                child = item.child(i)
157                show = self._filterItem(child, text, forceShow) or show
158            self.tree.setRowHidden(item.row(), item.index().parent(), not show)
159            return show
160
161        elif isinstance(item, QStandardItem):
162            show = forceShow or bool(text) and (text in item.text().lower())
163            self.tree.setRowHidden(item.row(), item.index().parent(), not show)
164            return show
165
166    def fillTree(self):
167        self.fillTreeUsingProviders()
168
169    def fillTreeUsingProviders(self):
170        self.items = {}
171        self.model.clear()
172        self.model.setHorizontalHeaderLabels([self.tr('Setting'),
173                                              self.tr('Value')])
174
175        settings = ProcessingConfig.getSettings()
176
177        rootItem = self.model.invisibleRootItem()
178
179        """
180        Filter 'General', 'Models' and 'Scripts' items
181        """
182        priorityKeys = [self.tr('General'), self.tr('Models'), self.tr('Scripts')]
183        for group in priorityKeys:
184            groupItem = QStandardItem(group)
185            icon = ProcessingConfig.getGroupIcon(group)
186            groupItem.setIcon(icon)
187            groupItem.setEditable(False)
188            emptyItem = QStandardItem()
189            emptyItem.setEditable(False)
190
191            rootItem.insertRow(0, [groupItem, emptyItem])
192            if group not in settings:
193                continue
194
195            # add menu item only if it has any search matches
196            for setting in settings[group]:
197                if setting.hidden or setting.name.startswith("MENU_"):
198                    continue
199
200                labelItem = QStandardItem(setting.description)
201                labelItem.setIcon(icon)
202                labelItem.setEditable(False)
203                self.items[setting] = SettingItem(setting)
204                groupItem.insertRow(0, [labelItem, self.items[setting]])
205
206        """
207        Filter 'Providers' items
208        """
209        providersItem = QStandardItem(self.tr('Providers'))
210        icon = QgsApplication.getThemeIcon("/processingAlgorithm.svg")
211        providersItem.setIcon(icon)
212        providersItem.setEditable(False)
213        emptyItem = QStandardItem()
214        emptyItem.setEditable(False)
215
216        rootItem.insertRow(0, [providersItem, emptyItem])
217        for group in list(settings.keys()):
218            if group in priorityKeys or group == menusSettingsGroup:
219                continue
220
221            groupItem = QStandardItem(group)
222            icon = ProcessingConfig.getGroupIcon(group)
223            groupItem.setIcon(icon)
224            groupItem.setEditable(False)
225
226            for setting in settings[group]:
227                if setting.hidden:
228                    continue
229
230                labelItem = QStandardItem(setting.description)
231                labelItem.setIcon(icon)
232                labelItem.setEditable(False)
233                self.items[setting] = SettingItem(setting)
234                groupItem.insertRow(0, [labelItem, self.items[setting]])
235
236            emptyItem = QStandardItem()
237            emptyItem.setEditable(False)
238            providersItem.appendRow([groupItem, emptyItem])
239
240        """
241        Filter 'Menus' items
242        """
243        self.menusItem = QStandardItem(self.tr('Menus'))
244        icon = QIcon(os.path.join(pluginPath, 'images', 'menu.png'))
245        self.menusItem.setIcon(icon)
246        self.menusItem.setEditable(False)
247        emptyItem = QStandardItem()
248        emptyItem.setEditable(False)
249
250        rootItem.insertRow(0, [self.menusItem, emptyItem])
251
252        button = QPushButton(self.tr('Reset to defaults'))
253        button.clicked.connect(self.resetMenusToDefaults)
254        layout = QHBoxLayout()
255        layout.setContentsMargins(0, 0, 0, 0)
256        layout.addWidget(button)
257        layout.addStretch()
258        widget = QWidget()
259        widget.setLayout(layout)
260        self.tree.setIndexWidget(emptyItem.index(), widget)
261
262        for provider in QgsApplication.processingRegistry().providers():
263            providerDescription = provider.name()
264            groupItem = QStandardItem(providerDescription)
265            icon = provider.icon()
266            groupItem.setIcon(icon)
267            groupItem.setEditable(False)
268
269            for alg in provider.algorithms():
270                algItem = QStandardItem(alg.displayName())
271                algItem.setIcon(icon)
272                algItem.setEditable(False)
273                try:
274                    settingMenu = ProcessingConfig.settings["MENU_" + alg.id()]
275                    settingButton = ProcessingConfig.settings["BUTTON_" + alg.id()]
276                    settingIcon = ProcessingConfig.settings["ICON_" + alg.id()]
277                except:
278                    continue
279                self.items[settingMenu] = SettingItem(settingMenu)
280                self.items[settingButton] = SettingItem(settingButton)
281                self.items[settingIcon] = SettingItem(settingIcon)
282                menuLabelItem = QStandardItem("Menu path")
283                menuLabelItem.setEditable(False)
284                buttonLabelItem = QStandardItem("Add button in toolbar")
285                buttonLabelItem.setEditable(False)
286                iconLabelItem = QStandardItem("Icon")
287                iconLabelItem.setEditable(False)
288                emptyItem = QStandardItem()
289                emptyItem.setEditable(False)
290                algItem.insertRow(0, [menuLabelItem, self.items[settingMenu]])
291                algItem.insertRow(0, [buttonLabelItem, self.items[settingButton]])
292                algItem.insertRow(0, [iconLabelItem, self.items[settingIcon]])
293                groupItem.insertRow(0, [algItem, emptyItem])
294
295            emptyItem = QStandardItem()
296            emptyItem.setEditable(False)
297
298            self.menusItem.appendRow([groupItem, emptyItem])
299
300        self.tree.sortByColumn(0, Qt.AscendingOrder)
301        self.adjustColumns()
302
303    def resetMenusToDefaults(self):
304        for provider in QgsApplication.processingRegistry().providers():
305            for alg in provider.algorithms():
306                d = defaultMenuEntries.get(alg.id(), "")
307                setting = ProcessingConfig.settings["MENU_" + alg.id()]
308                item = self.items[setting]
309                item.setData(d, Qt.EditRole)
310        self.saveMenus = True
311
312    def accept(self):
313        qsettings = QgsSettings()
314        for setting in list(self.items.keys()):
315            if setting.group != menusSettingsGroup or self.saveMenus:
316                if isinstance(setting.value, bool):
317                    setting.setValue(self.items[setting].checkState() == Qt.Checked)
318                else:
319                    try:
320                        setting.setValue(str(self.items[setting].text()))
321                    except ValueError as e:
322                        QMessageBox.warning(self, self.tr('Wrong value'),
323                                            self.tr('Wrong value for parameter "{0}":\n\n{1}').format(setting.description, str(e)))
324                        return
325                setting.save(qsettings)
326
327        with OverrideCursor(Qt.WaitCursor):
328            for p in QgsApplication.processingRegistry().providers():
329                p.refreshAlgorithms()
330
331        settingsWatcher.settingsChanged.emit()
332
333    def itemExpanded(self, idx):
334        if idx == self.menusItem.index():
335            self.saveMenus = True
336        if self.auto_adjust_columns:
337            self.adjustColumns()
338
339    def adjustColumns(self):
340        self.tree.resizeColumnToContents(0)
341        self.tree.resizeColumnToContents(1)
342
343
344class SettingItem(QStandardItem):
345
346    def __init__(self, setting):
347        QStandardItem.__init__(self)
348        self.setting = setting
349        self.setData(setting, Qt.UserRole)
350        if isinstance(setting.value, bool):
351            self.setCheckable(True)
352            self.setEditable(False)
353            if setting.value:
354                self.setCheckState(Qt.Checked)
355            else:
356                self.setCheckState(Qt.Unchecked)
357        else:
358            self.setData(setting.value, Qt.EditRole)
359
360
361class SettingDelegate(QStyledItemDelegate):
362
363    def __init__(self, parent=None):
364        QStyledItemDelegate.__init__(self, parent)
365
366    def createEditor(self, parent, options, index):
367        setting = index.model().data(index, Qt.UserRole)
368        if setting.valuetype == Setting.FOLDER:
369            return FileDirectorySelector(parent, placeholder=setting.placeholder)
370        elif setting.valuetype == Setting.FILE:
371            return FileDirectorySelector(parent, True, setting.placeholder)
372        elif setting.valuetype == Setting.SELECTION:
373            combo = QComboBox(parent)
374            combo.addItems(setting.options)
375            return combo
376        elif setting.valuetype == Setting.MULTIPLE_FOLDERS:
377            return MultipleDirectorySelector(parent, setting.placeholder)
378        else:
379            value = self.convertValue(index.model().data(index, Qt.EditRole))
380            if isinstance(value, int):
381                spnBox = QgsSpinBox(parent)
382                spnBox.setRange(-999999999, 999999999)
383                return spnBox
384            elif isinstance(value, float):
385                spnBox = QgsDoubleSpinBox(parent)
386                spnBox.setRange(-999999999.999999, 999999999.999999)
387                spnBox.setDecimals(6)
388                return spnBox
389            elif isinstance(value, str):
390                lineEdit = QLineEdit(parent)
391                lineEdit.setPlaceholderText(setting.placeholder)
392                return lineEdit
393
394    def setEditorData(self, editor, index):
395        value = self.convertValue(index.model().data(index, Qt.EditRole))
396        setting = index.model().data(index, Qt.UserRole)
397        if setting.valuetype == Setting.SELECTION:
398            editor.setCurrentIndex(editor.findText(value))
399        elif setting.valuetype in (Setting.FLOAT, Setting.INT):
400            editor.setValue(value)
401        else:
402            editor.setText(value)
403
404    def setModelData(self, editor, model, index):
405        value = self.convertValue(index.model().data(index, Qt.EditRole))
406        setting = index.model().data(index, Qt.UserRole)
407        if setting.valuetype == Setting.SELECTION:
408            model.setData(index, editor.currentText(), Qt.EditRole)
409        else:
410            if isinstance(value, str):
411                model.setData(index, editor.text(), Qt.EditRole)
412            else:
413                model.setData(index, editor.value(), Qt.EditRole)
414
415    def sizeHint(self, option, index):
416        return QgsSpinBox().sizeHint()
417
418    def eventFilter(self, editor, event):
419        if event.type() == QEvent.FocusOut and hasattr(editor, 'canFocusOut'):
420            if not editor.canFocusOut:
421                return False
422        return QStyledItemDelegate.eventFilter(self, editor, event)
423
424    def convertValue(self, value):
425        if value is None or value == NULL:
426            return ""
427        try:
428            return int(value)
429        except:
430            try:
431                return float(value)
432            except:
433                return str(value)
434
435
436class FileDirectorySelector(QWidget):
437
438    def __init__(self, parent=None, selectFile=False, placeholder=""):
439        QWidget.__init__(self, parent)
440
441        # create gui
442        self.btnSelect = QToolButton()
443        self.btnSelect.setText('…')
444        self.lineEdit = QLineEdit()
445        self.lineEdit.setPlaceholderText(placeholder)
446        self.hbl = QHBoxLayout()
447        self.hbl.setMargin(0)
448        self.hbl.setSpacing(0)
449        self.hbl.addWidget(self.lineEdit)
450        self.hbl.addWidget(self.btnSelect)
451
452        self.setLayout(self.hbl)
453
454        self.canFocusOut = False
455        self.selectFile = selectFile
456
457        self.setFocusPolicy(Qt.StrongFocus)
458        self.btnSelect.clicked.connect(self.select)
459
460    def select(self):
461        lastDir = ''
462        if not self.selectFile:
463            selectedPath = QFileDialog.getExistingDirectory(None,
464                                                            self.tr('Select directory'), lastDir,
465                                                            QFileDialog.ShowDirsOnly)
466        else:
467            selectedPath, selected_filter = QFileDialog.getOpenFileName(None,
468                                                                        self.tr('Select file'), lastDir, self.tr('All files (*)')
469                                                                        )
470
471        if not selectedPath:
472            return
473
474        self.lineEdit.setText(selectedPath)
475        self.canFocusOut = True
476
477    def text(self):
478        return self.lineEdit.text()
479
480    def setText(self, value):
481        self.lineEdit.setText(value)
482
483
484class MultipleDirectorySelector(QWidget):
485
486    def __init__(self, parent=None, placeholder=""):
487        QWidget.__init__(self, parent)
488
489        # create gui
490        self.btnSelect = QToolButton()
491        self.btnSelect.setText('…')
492        self.lineEdit = QLineEdit()
493        self.lineEdit.setPlaceholderText(placeholder)
494        self.hbl = QHBoxLayout()
495        self.hbl.setMargin(0)
496        self.hbl.setSpacing(0)
497        self.hbl.addWidget(self.lineEdit)
498        self.hbl.addWidget(self.btnSelect)
499
500        self.setLayout(self.hbl)
501
502        self.canFocusOut = False
503
504        self.setFocusPolicy(Qt.StrongFocus)
505        self.btnSelect.clicked.connect(self.select)
506
507    def select(self):
508        text = self.lineEdit.text()
509        if text != '':
510            items = text.split(';')
511        else:
512            items = []
513
514        dlg = DirectorySelectorDialog(None, items)
515        if dlg.exec_():
516            text = dlg.value()
517            self.lineEdit.setText(text)
518
519        self.canFocusOut = True
520
521    def text(self):
522        return self.lineEdit.text()
523
524    def setText(self, value):
525        self.lineEdit.setText(value)
526