def validate(self): """ Verifies contents displayed on mapping table in order to infer its validity as datasource conversion map. :return: (bool) map validity status. """ # validate map msg = self.invalidatedReason() if msg: # if an invalidation reason was given, warn user and nothing else. msgBar = QgsMessageBar(self) # if window is resized, msgBar stays, not ideal, but works for now # maybe we should connect to some parent resizing signal or something... msgBar.resize( QSize(self.geometry().size().width(), msgBar.geometry().height())) msgBar.pushMessage(self.tr('Warning!'), msg, level=Qgis.Warning, duration=5) QgsMessageLog.logMessage(msg, 'DSGTools Plugin', Qgis.Critical) return msg == ''
class FMEManagerWidget(QWidget, FORM_CLASS): def __init__(self, parent=None): """Constructor.""" super(FMEManagerWidget, self).__init__(parent=parent) self.setupUi(self) self._workspaceList = list() self.paramWidgetMap = dict() self.versionComboBox.addItems(["v1", "v2"]) self.messageBar = QgsMessageBar(self) def clearLayout(self): """ Removes all inserted widgets from workspace's parameters from GUI. """ for i in range(self.verticalLayout_2.count()-1, -1, -1): self.verticalLayout_2.itemAt(i).widget().setParent(None) @pyqtSlot(int) def on_workspaceComboBox_currentIndexChanged(self): """ Fetch necessary parameters from selected workspace and insert a QLineEdit for each parameter. """ self.clearLayout() workspace = self.getCurrentWorkspace() key = "parameters" if self.version() == "v1" else "parametros" _parameters = workspace.get(key, list()) parameters = filter(lambda x: x != "LOG_FILE", _parameters) for parameter in parameters: newLabel = QLabel(parameter) self.verticalLayout_2.addWidget(newLabel) newLineEdit = QLineEdit() self.paramWidgetMap[parameter] = newLineEdit self.verticalLayout_2.addWidget(newLineEdit) def resizeEvent(self, e): """ Resize QgsMessageBar to widget's width """ self.messageBar.resize( QSize( self.geometry().size().width(), 30 ) ) def getProxyInfo(self): """ Reads Proxy settings as registered on QGIS settings. :return: (tuple) the QGIS proxy mapping per schema and its authentication object. """ if self.useProxy(): return Utils().get_proxy_config() else: return (None, None) def version(self): """ Identifies the selected FME Manager from GUI. :return: (str) FME Manager selected version. """ return self.versionComboBox.currentText() def server(self): """ Identifies server provided by user from GUI. :return: (str) server from which FME routines are read from. """ return self.serverLineEdit.text() def useSsl(self): """ Identifies whether user intends to use SSL to request routines from FME Manager server. :return: (bool) whether SSL is going to be used as from GUI. """ return self.sslCheckBox.isChecked() def useProxy(self): """ Identifies whether user intends to connect to FME Manager server behind a proxy. :return: (bool) whether Proxy is set. Proxy setup is read from QSettings (QGIS settings). """ return self.sslCheckBox.isChecked() def getWorkspacesFromServer(self): """ Reads all available workspaces from a filled server. :return: (list-of-dict) list of available workspaces and its metadata. """ if self.version() == "v1": url = "{server}/versions?last=true".format(server=self.server()) jsonKey = "data" else: url = "{server}/api/rotinas".format(server=self.server()) jsonKey = "dados" try: if not self.useProxy(): workspaceList = requests.get( url, timeout=8 ).json()[jsonKey] else: proxyInfo, proxyAuth = self.getProxyInfo() workspaceList = requests.get( url, proxies=proxyInfo, auth=proxyAuth, timeout=8 ).json()[jsonKey] except ReadTimeout: self.messageBar.pushMessage( self.tr("Connection timed out."), level=Qgis.Warning) workspaceList = list() except ConnectTimeout: self.messageBar.pushMessage( self.tr("Connection timed out (max attempts)."), level=Qgis.Warning ) workspaceList = list() except InvalidSchema: self.messageBar.pushMessage( self.tr("Missing connection schema (e.g.'http', etc)."), level=Qgis.Warning ) workspaceList = list() except BaseException as e: self.messageBar.pushMessage( self.tr("Unexpected error while trying to reach server. " "Check your parameters. Error message: {}".format(e)), level=Qgis.Warning ) workspaceList = list() return workspaceList def setWorkspaces(self, workspaces): """ Sets a list of workspaces to the GUI. :param workspaces: (list-of-str) """ self.workspaceComboBox.clear() if workspaces: self.workspaceComboBox.addItems(workspaces) def getCurrentWorkspace(self): """ Reads currently selected workspace from GUI. :return: (dict) selected workspace's metadata map. """ idx = self.workspaceComboBox.currentIndex() try: return self._workspaceList[idx] except IndexError: return dict() @pyqtSlot(bool) def on_loadPushButton_clicked(self): """ Sync available workspaces from server and display these workspaces on workspaceComboBox """ self.workspaceComboBox.clear() self._workspaceList = self.getWorkspacesFromServer() if self.version() == "v1": workspaceNameKey = "workspace_name" workspaceDescKey = "workspace_description" else: workspaceNameKey = "rotina" workspaceDescKey = "descricao" for workspace in self._workspaceList: self.workspaceComboBox.addItem( "{name} ({description})".format( name=workspace[workspaceNameKey], description=workspace[workspaceDescKey] ) ) def validate(self, pushAlert=False): """ Validates fields. Returns True if all information are filled correctly. :param pushAlert: (bool) whether invalidation reason should be displayed on the widget. :return: (bool) whether set of filled parameters if valid. """ proxyInfo, _ = self.getProxyInfo() if self.useProxy() and not proxyInfo: if pushAlert: self.messageBar.pushMessage( self.tr("Proxy usage is set but no QGIS proxy settings " "was found."), level=Qgis.Warning ) return False if self.server() == "": if pushAlert: self.messageBar.pushMessage( self.tr("URL to FME Manager server was not provided."), level=Qgis.Warning ) return False return True def getParameters(self): """ Returns necessary parameters for running the algorithm """ workspace = self.getCurrentWorkspace() workspaceId = workspace.get("id", None) version = self.version() parameters = { "parameters" if version == "v1" else "parametros": { key: value.text() for key, value in self.paramWidgetMap.items() } } proxyInfo, proxyAuth = self.getProxyInfo() return { "version": version, "server": self.server(), "workspace_id": workspaceId, "parameters": parameters, "auth": proxyAuth, "proxy_dict": proxyInfo, "use_ssl": self.useSsl(), "use_proxy": self.useProxy() }
class ButtonSetupWidget(QDialog, FORM_CLASS): def __init__(self, parent=None, buttonSetup=None): """ Class constructor. :param parent: (QtWidgets.*) any widget that 'contains' this tool. :param buttonSetup: (CustomButtonSetup) object that handles all buttons displayed and configured through this GUI. """ super(ButtonSetupWidget, self).__init__(parent) self.setupUi(self) self.messageBar = QgsMessageBar(self) self.setup = CustomButtonSetup() if buttonSetup: self.setSetup(buttonSetup) self.buttonComboBox.addItem(self.tr("No button selected")) self.tableWidget.verticalHeader().sectionDoubleClicked.connect( self.setButtonFromRow) bEnabled = self.buttonComboBox.currentIndex() > 0 for w in ("savePushButton", "undoPushButton", "removePushButton", "buttonPropWidget"): getattr(self, w).setEnabled(bEnabled) # making the button selection to stand out a little bit if "Night Mapping" in app.activeThemePath(): ss = """QHeaderView::section:checked { color:black; background-color:white; }""" else: ss = """QHeaderView::section:checked { color:gray; background-color:black; }""" self.tableWidget.setStyleSheet(ss) def raiseWarning(self, msg, title=None, lvl=None, duration=None): """ Raises a warning message to the user on a message bar and logs it to QGIS logger. :param msg: (str) message to be displayed. :param title: (str) pre-message on the warning bar. :param lvl: (int) warning level enumerator as from Qgis module. :param duration: (int) warning message display time. """ self.messageBar.pushMessage(title or self.tr('Invalid workflow'), msg, level=lvl or Qgis.Warning, duration=duration or 5) # msg = self.tr("Buttons setup definion invalid: {m}").format(m=msg) QgsMessageLog.logMessage(msg, 'DSGTools Plugin', Qgis.Warning) def resizeEvent(self, e): """ Reimplementation in order to use this window's resize event. On this object, this method makes sure that message bar is always the same size as the window. :param e: (QResizeEvent) resize event. """ self.messageBar.resize( QSize( self.geometry().size().width(), 40 # this felt nicer than the original height (30) )) def clear(self): """ Clears all data filled into GUI. """ self.buttonComboBox.clear() for button in self.registeredButtonNames(): self.removeButtonFromTable(self.getButtonByName(button)) self.setup.removeButton(button) self.buttonComboBox.removeItem( self.buttonComboBox.findText(button)) def setSetupName(self, name): """ Defines setup's name on GUI. :param name: (str) name to be set. """ self.setupNameLineEdit.setText(name) def setupName(self): """ Retrieves button's setup name read from GUI. :return: (str) name for button's setup. """ return self.setupNameLineEdit.text() def setCurrentSetupName(self, name): """ Defines current button's setup name. :param name: (str) name for button's setup. """ return self.setup.setName(name) def currentSetupName(self): """ Retrieves current button's setup name. :param name: (str) name for button's setup. """ return self.setup.name() def setDescription(self, desc): """ Defines setup's description on GUI. :param desc: (str) description to be set. """ self.textEdit.setText(desc) def description(self): """ Reads button's setup description from GUI. :return: (str) description for button's setup. """ return self.textEdit.toPlainText() def setCurrentDescription(self, name): """ Defines current button's setup description. :param name: (str) description for button's setup. """ self.setup.setDescription(name) def currentDescription(self): """ Retrieves current button's description. :param name: (str) description for button's setup. """ return self.setup.description() def setDynamicShortcut(self, ds): """ Defines setup's dynamic shortcut option on GUI. :param ds: (bool) dynamic shortcut assignment option. """ self.dsCheckBox.setChecked(ds) def dynamicShortcut(self): """ Retrieves button's setup dynamic shortcut option read from GUI. :param ds: (bool) dynamic shortcut assignment option. """ return self.dsCheckBox.isChecked() def setCurrentDynamicShortcut(self, ds): """ Defines current button's setup dynamic shortcut option. :param ds: (bool) dynamic shortcut assignment option. """ self.setup.setDynamicShortcut(ds) def currentDynamicShortcut(self): """ Retrieves current button's description. :return: (bool) dynamic shortcut assignment option. :param name: (str) description for button's setup. """ return self.setup.dynamicShortcut() def readSetup(self): """ Reads setup from GUI. :return: (CustomButtonSetup) reads all data from GUI as a setup. """ s = CustomButtonSetup() s.setName(self.setupName()) s.setDescription(self.description()) for row in range(self.tableWidget.rowCount()): s.addButton(self.buttonFromRow(row).properties()) s.setDynamicShortcut(self.dynamicShortcut()) return s def setSetup(self, newSetup): """ Imports buttons setup definitions from another buttons setup. :param newSetup: (CustomButtonSetup) setup to be imported. """ self.buttonComboBox.blockSignals(True) self.setSetupName(newSetup.name()) self.setCurrentSetupName(newSetup.name()) self.setDescription(newSetup.description()) self.setCurrentDescription(newSetup.description()) self.setDynamicShortcut(newSetup.dynamicShortcut()) self.setCurrentDynamicShortcut(newSetup.dynamicShortcut()) for button in newSetup.buttons(): self.addButton(button) self.buttonComboBox.blockSignals(False) def getButtonByName(self, name): """ Retrieves a registered button from its name. :param name: (str) name for the requested button. """ return self.setup.button(name) def setButtonName(self, name): """ Sets button name to GUI. :param name: (str) name to be set to GUI. """ self.buttonPropWidget.setButtonName(name) def buttonName(self): """ Reads button name from GUI. :return: (str) button name read from GUI. """ return self.buttonPropWidget.buttonName() def registeredButtonNames(self): """ All names for registered buttons on current profile. :return: (list-of-str) list of button names. """ return self.setup.buttonNames() def setDigitizingTool(self, tool): """ Sets button's digitizing tool to GUI. :param tool: (str) a supported digitizing tool to be set. """ self.buttonPropWidget.setDigitizingTool(tool) def digitizingTool(self): """ Reads current digitizing tool from GUI. :return: (str) current digitizing tool. """ return self.buttonPropWidget.digitizingTool() def setUseColor(self, useColor): """ Sets button's digitizing tool to GUI. :param useColor: (bool) whether button should use a custom color palette. """ self.buttonPropWidget.setUseColor(useColor) def useColor(self): """ Reads whether button will have a custom color from GUI. :return: (bool) whether button should use a custom color palette. """ return self.buttonPropWidget.useColor() def setColor(self, color): """ Sets custom color to the color widget. :param color: (str/tuple) color to be set. """ self.buttonPropWidget.setColor(color) def color(self): """ Reads custom color to be set to widget as read from GUI. :return: (tuple) color to be used. """ return self.buttonPropWidget.color() def setUseToolTip(self, useToolTip): """ Sets button's digitizing tool to GUI. :param useToolTip: (bool) whether button will have a tool tip assigned. """ self.buttonPropWidget.setUseToolTip(useToolTip) def useToolTip(self): """ Reads if the button will have a tool tip assigned to it from GUI. :return: (bool) whether the button will have a tool tip assigned. """ return self.buttonPropWidget.useToolTip() def setToolTip(self, tooltip): """ Sets a tool tip for the active button widget. :param tooltip: (str) tool tip to be set. """ self.buttonPropWidget.setToolTip(tooltip) def toolTip(self): """ Reads the tool tip for the button from GUI. :param tooltip: (str) tool tip to be used. """ return self.buttonPropWidget.toolTip() def setUseCategory(self, useCat): """ Sets whether button should be assigned a category/group on GUI. :param useCat: (bool) whether button will have a category assigned. """ self.buttonPropWidget.setUseCategory(useCat) def useCategory(self): """ Reads button's category/group from GUI. :return: (bool) whether button will have a category assigned. """ return self.buttonPropWidget.useCategory() def setCategory(self, cat): """ Assigns a group to the active button. :param cat: (str) category to be set. """ self.buttonPropWidget.setCategory(cat) def category(self): """ Reads the assigned category/group to the active button from GUI. :return: (str) category to be used. """ return self.buttonPropWidget.category() def setUseKeywords(self, useKw): """ Sets whether active button should have keywords for button searching. :param useKw: (bool) whether button will have keywords assigned to it. """ self.buttonPropWidget.setUseKeywords(useKw) def useKeywords(self): """ Reads whether active button should have keywords for button searching from GUI. :return: (bool) whether button will have keywords assigned to it. """ return self.buttonPropWidget.useKeywords() def setKeywords(self, kws): """ Sets button's keywords for button searching. :param kws: (set-of-str) set of keywords to be assigned to the button. """ self.buttonPropWidget.setKeywords(kws) def keywords(self): """ Reads button's keywords for button searching from GUI. :return: (set-of-str) set of keywords to be assigned to the button. """ return self.buttonPropWidget.keywords() def setUseShortcut(self, useShortcut): """ Sets whether active button should have a shortcut assigned to it. :param useShortcut: (bool) whether button will have a shortcut assigned. """ self.buttonPropWidget.setUseShortcut(useShortcut) def useShortcut(self): """ Reads whether active button should have a shortcut assigned to it from GUI. :return: (bool) whether button will have a shortcut assigned. """ return self.buttonPropWidget.useShortcut() def setShortcurt(self, s, autoReplace): """ Assigns a shortcut to trigger active button's action. :param s: (str) new shortcut to be set. :param autoReplace: (bool) whether a confirmation from the user is necessary in order to replace existing shortcuts. """ self.buttonPropWidget.setShortcurt(s, autoReplace) def shortcut(self): """ Assigned shortcut read from GUI. :return: (str) shortcut to be used. """ return self.buttonPropWidget.shorcut() def setOpenForm(self, openForm): """ Defines whether (re)classification tool will open feature form while being used. :param openForm: (bool) whether feature form should be opened. """ self.buttonPropWidget.setOpenForm(openForm) def openForm(self): """ Defines whether (re)classification tool will open feature form while being used. :return: (bool) whether feature form should be opened. """ return self.buttonPropWidget.openForm() def setAttributeMap(self, attrMap): """ Sets the attribute value map for current button to GUI. :param attrMap: (dict) a map from each field and its value to be set. """ self.buttonPropWidget.setAttributeMap(attrMap) def attributeMap(self): """ Reads the field map data and set it to a button attribute map format. :return: (dict) read attribute map. """ return self.buttonPropWidget.attributeMap() def setLayer(self, layer): """ Sets current layer selection on GUI. :param layer: (str) name for the layer to be set. """ self.buttonPropWidget.setLayer(layer) def layer(self): """ Reads current layer selection from GUI. :return: (str) name for the selected layer. """ self.buttonPropWidget.layer() def currentButton(self): """ Retrives button active on GUI that has a saved state (on its last saved state). :return: (CustomFeatureButton) current button. """ return self.buttonPropWidget.currentButton() def readButton(self): """ Reads current data on GUI and gets a button with those properties. :return: (CustomFeatureButton) current button as from GUI. """ return self.buttonPropWidget.readButton() def buttonIsModified(self): """ Checks whether current button is modified. :return: (bool) whether button is modified. """ return self.currentButton() != self.readButton() def updateButton(self, buttonName, newProps): """ Updates a registered button with a new set of properties. :param button: (str) name for the button to be updated. :param newProps: (dict) new set of properties. """ self.setup.updateButton(buttonName, newProps) def confirmAction(self, msg, title=None, showNo=True): """ Raises a message box that asks for user confirmation. :param msg: (str) message requesting for confirmation to be shown. :param showNo: (bool) whether No button should be exposed. :return: (bool) whether action was confirmed. """ return self.buttonPropWidget.confirmAction(msg, title, showNo) def validate(self): """ Validates current input data, giving invalidation reason. :return: (str) invalidation reason. """ if self.setupName() == "": return self.tr("No name provided for current setup.") buttons = self.readSetup().buttons() if not buttons: return self.tr("Please register at least one button.") # for button in buttons: # # validate attribute map # pass return "" def isValid(self): """ Validates current input data, giving invalidation reason. :return: (bool) current input data validity. """ if self.setupName() == "": return False buttons = self.readSetup().buttons() if not buttons: return False # for button in buttons: # # validate attribute map # pass return True @pyqtSlot(int, name="on_buttonComboBox_currentIndexChanged") def setCurrentButton(self, button): """ Sets button properties to the GUI. :param button: (CustomFeatureButton) button to be set to the GUI. """ if isinstance(button, int): if button <= 0: button = CustomFeatureButton() button.setName("") else: # table row is less 1 due to "no button" option button = self.buttonFromRow(button - 1) if button.name() not in self.registeredButtonNames(): # create a new one with that button? pass self.buttonComboBox.setCurrentText(button.name()) bEnabled = self.buttonComboBox.currentIndex() > 0 for w in ("savePushButton", "undoPushButton", "removePushButton", "buttonPropWidget"): getattr(self, w).setEnabled(bEnabled) self.buttonPropWidget.setButton(button) @pyqtSlot(bool, name="on_savePushButton_clicked") def updateCurrentButton(self, props=None): """ Current data will be stored as current button's properties. :param props: (dict) a map to button's properties to be updated. """ if isinstance(props, bool) or props is None: # if button pressing was the triggering event, current data will be # store into current button props = self.readButton().properties() prevName = self.currentButton().name() button = self.getButtonByName(prevName) self.updateButton(prevName, props) newName = button.name() self.buttonPropWidget.button = button if prevName != newName: idx = self.buttonComboBox.findText(prevName) self.buttonComboBox.setItemText(idx, newName) self.setCurrentButton(button) @pyqtSlot(bool, name="on_undoPushButton_clicked") def undoButtonModifications(self): """ Restores stored data from current button and sets it to GUI. """ self.buttonPropWidget.setButton(self.currentButton()) @pyqtSlot(bool, name="on_addPushButton_clicked") def addButton(self, button=None): """ Adds a button to the setup. :param button: (CustomFeatureButton) a pre-existent button to be set. :return: (CustomFeatureButton) added button. """ if button is not None and not isinstance(button, bool): buttonName = button.name() if buttonName in self.registeredButtonNames(): msg = self.tr("Button {b} already exists. Would you like to " "replace it?").format(b=buttonName) cnf = self.confirmAction(msg, self.tr("Replace existing button")) if not cnf: return self.getButtonByName(buttonName) self.updateButton(buttonName, button.properties()) else: props = button.properties() button = self.setup.newButton() self.buttonComboBox.addItem(buttonName) self.updateButton(button.name(), props) # we want the button passed by reference from setup button = self.getButtonByName(buttonName) else: button = self.setup.newButton() self.buttonComboBox.addItem(button.name()) self.setCurrentButton(button) self.addButtonToTable(button) return button @pyqtSlot(bool, name="on_removePushButton_clicked") def removeButton(self): """ Removes the current button from setup. """ name = self.buttonName() txt = self.tr("Confirm button '{b}' removal?").format(b=name) if name == "": # ignore the "Select a button..." return self.removeButtonFromTable(self.currentButton()) self.setup.removeButton(name) self.buttonComboBox.removeItem(self.buttonComboBox.findText(name)) def buttonFromRow(self, row): """ Retrieves the button object from table row. :param row: (int) target row to get its button. :return: (CustomFeatureButton) retrieved button. """ # combo box includes "no button", table does not -> row + 1 return self.getButtonByName(self.buttonComboBox.itemText(row + 1)) def setButtonFromRow(self, row): """ Fills GUI with a button's properties from its row. :param row: (int) target row to get its button. """ b = self.getButtonByName(self.buttonComboBox.itemText(row + 1)) self.setCurrentButton(b) def addButtonToTable(self, button): """ Adds widget to table widget. :param button: (CustomFeatureButton) button to have its widget added. """ row = self.tableWidget.rowCount() self.tableWidget.insertRow(row) self.tableWidget.setCellWidget(row, 0, button.newWidget()) def removeButtonFromTable(self, button): """ Removes the button widget from buttons table. :param button: (CustomFeatureButton) button to be removed. """ name = button.displayName() for row in range(self.tableWidget.rowCount()): bName = self.tableWidget.cellWidget(row, 0).text().replace("&", "") if bName == name: self.tableWidget.removeRow(row) return def readButtonTable(self): """ Reads all registered buttons from button table and returns it, in order. :return: (list-of-CustomFeature) ordered buttons. """ buttons = list() count = self.tableWidget.rowCount() if count > 0: for row in range(count): buttons.append( self.tableWidget.cellWidget(row, 0).text()\ .rsplit(" [", 1)[0].replace("&", "") ) # Qt mnemonic shortcut for it widgets introduces "&"... return buttons def buttonsOrder(self): """ Retrieves button order to be used for button setup. :return: (dict) a map from button name to its position on GUI. """ buttons = dict() for row in range(self.tableWidget.rowCount()): button = self.tableWidget.cellWidget(row, 0).text()\ .rsplit(" [", 1)[0].replace("&", "") buttons[button] = row return buttons def selectedIndexes(self): """ :return: (list-of-QModelIndex) table's selected indexes. """ return self.tableWidget.selectedIndexes() def selectedRows(self, reverseOrder=False): """ List of all rows that have selected items on the table. :param reverOrder: (bool) indicates if the row order is reversed. :return: (list-of-int) ordered list of selected rows' indexes. """ rows = self.tableWidget.selectionModel().selectedRows() return sorted(set(i.row() for i in rows), reverse=reverseOrder) def selectedColumns(self, reverseOrder=False): """ List of all columns that have selected items on the table. :param reverOrder: (bool) indicates if the column order is reversed. :return: (list-of-int) ordered list of selected columns' indexes. """ return sorted(set(i.column() for i in self.selectedIndexes()), reverse=reverseOrder) def selectRow(self, row): """ Clears all selected rows and selects row. :param row: (int) index for the row to be select. """ self.clearRowSelection() self.addRowToSelection(row) def addRowToSelection(self, row): """ Adds a row to selection. :param row: (int) index for the row to be added to selection. """ if row not in self.selectedRows(): self.tableWidget.setSelectionMode(QAbstractItemView.MultiSelection) self.tableWidget.selectRow(row) self.tableWidget.setSelectionMode( QAbstractItemView.ExtendedSelection) def removeRowFromSelection(self, row): """ Removes a row from selection. :param row: (int) index for the row to be removed from selection. """ if row in self.selectedRows(): self.tableWidget.setSelectionMode(QAbstractItemView.MultiSelection) self.tableWidget.selectRow(row) self.tableWidget.setSelectionMode( QAbstractItemView.ExtendedSelection) def clearRowSelection(self): """ Removes all selected rows from selection. """ for row in self.selectedRows(): self.removeRowFromSelection(row) def moveRowUp(self, row): """ Moves a row one position up, if possible. :param row: (int) row be moved. """ if row <= 0: return button = self.buttonFromRow(row) upperButton = self.buttonFromRow(row - 1) self.tableWidget.setCellWidget(row - 1, 0, button.newWidget()) self.buttonComboBox.setItemText(row, button.name()) self.tableWidget.setCellWidget(row, 0, upperButton.newWidget()) self.buttonComboBox.setItemText(row + 1, upperButton.name()) self.addRowToSelection(row - 1) self.removeRowFromSelection(row) def moveRowDown(self, row): """ Moves a row one position up, if possible. :param row: (int) row be moved. """ if row >= self.tableWidget.rowCount() - 1: return button = self.buttonFromRow(row) lowerButton = self.buttonFromRow(row + 1) self.tableWidget.setCellWidget(row + 1, 0, button.newWidget()) self.buttonComboBox.setItemText(row + 1 + 1, button.name()) self.tableWidget.setCellWidget(row, 0, lowerButton.newWidget()) self.buttonComboBox.setItemText(row + 1, lowerButton.name()) self.addRowToSelection(row + 1) self.removeRowFromSelection(row) @pyqtSlot() def on_moveUpPushButton_clicked(self): """ Method triggered when move row up button is clicked. """ rows = self.selectedRows() if not rows: return for row in self.selectedRows(): if row - 1 in rows: # rows is a copy of selected rows that is updated after the # item is moved continue self.moveRowUp(row) if row != 0: # this row is never aftected, hence it is "fixed" rows.remove(row) row = max(self.selectedRows()) self.setCurrentButton(self.buttonFromRow(row)) @pyqtSlot() def on_moveDownPushButton_clicked(self): """ Method triggered when move row down button is clicked. """ rows = self.selectedRows(True) if not rows: return lastRow = self.tableWidget.rowCount() - 1 for row in self.selectedRows(True): if row + 1 in rows: continue self.moveRowDown(row) if row != lastRow: rows.remove(row) row = max(self.selectedRows()) self.setCurrentButton(self.buttonFromRow(row)) @pyqtSlot() def on_okPushButton_clicked(self): """ Closes setup dialog and returns a confirmation code. :return: (int) confirmation code. """ if not self.isValid(): msg = self.tr("Invalid input data: {r}").format(r=self.validate()) self.raiseWarning(msg) return msg = self.tr("Current button has been modified and not saved. Would " "you like to save it?") title = self.tr("Unsaved modifications") if self.buttonIsModified() and self.confirmAction(msg, title): self.updateCurrentButton() self.setCurrentSetupName(self.setupName()) self.setCurrentDescription(self.description()) self.setCurrentDynamicShortcut(self.dynamicShortcut()) self.done(1) @pyqtSlot() def on_cancelPushButton_clicked(self): """ Closes setup dialog and returns a refusal code. :return: (int) confirmation code. """ self.done(0) def state(self): """ Exports current setup's state as read from the GUI. :return: (dict) a map to tool's current state. """ state = self.readSetup().state() # buttons' keywords are stored as sets, which are not seriallizable for idx, props in enumerate(state["buttons"]): kws = props["keywords"] state["buttons"][idx]["keywords"] = tuple(kws) return {"state": state, "order": self.buttonsOrder()} def setState(self, state): """ Restores the GUI to a given state. :param state: (dict) a map to tool's state. """ self.setup.setState(state) @pyqtSlot(bool, name="on_importPushButton_clicked") def importSetup(self): """ Imports a setup from a file. """ fd = QFileDialog() filename = fd.getOpenFileName( caption=self.tr("Import a DSGTools Button Setup (set of buttons)"), filter=self.tr("DSGTools Buttons Setup (*.setup)")) filename = filename[0] if isinstance(filename, tuple) else filename if not filename: return with open(filename, "r", encoding="utf-8") as fp: state = json.load(fp) order = [b[0] for b in \ sorted(state["order"].items(), key=lambda i: i[1])] state = state["state"] # buttons' keywords are stored as tuple in order to be seriallizable for idx, props in enumerate(state["buttons"]): kws = props["keywords"] state["buttons"][idx]["keywords"] = set(kws) # tuples and list are misinterpreted when exported col = props["color"] state["buttons"][idx]["color"] = tuple(col) self.clear() self.setState(state) self.buttonComboBox.blockSignals(True) self.buttonComboBox.addItem(self.tr("No button selected")) self.buttonComboBox.addItems(order) for btn in order: self.addButtonToTable(self.setup.button(btn)) self.buttonComboBox.blockSignals(False) if order: self.setCurrentButton(self.setup.button(order[0])) else: self.buttonComboBox.setCurrentIndex(0) self.setCurrentButton(None) self.setSetupName(self.setup.name()) self.setDescription(self.setup.description()) self.setDynamicShortcut(self.setup.dynamicShortcut()) msg = self.tr('Setup "{0}" imported from "{1}"')\ .format(self.setup.name(), filename) self.raiseWarning(msg, title=self.tr("Imported workflow"), lvl=Qgis.Success) @pyqtSlot(bool, name="on_exportPushButton_clicked") def exportSetup(self): """ Exports current setup's saved state to a file. :return: (bool) whether setup was exported. """ if not self.isValid(): msg = self.tr("Invalid input data: {r}").format(r=self.validate()) self.raiseWarning(msg) return False # add check of modified button in here s = self.readSetup() fd = QFileDialog() filename = fd.getSaveFileName( caption=self.tr("Export setup - {0}").format(s.name()), filter=self.tr("DSGTools Buttons Setup (*.setup)")) filename = filename[0] if isinstance(filename, tuple) else filename if not filename: return False with open(filename, "w", encoding="utf-8") as fp: fp.write(json.dumps(self.state(), sort_keys=True, indent=4)) res = os.path.exists(filename) if res: msg = self.tr('Setup "{0}" exported to "{1}"')\ .format(s.name(), filename) self.raiseWarning(msg, title=self.tr("Exported workflow"), lvl=Qgis.Success) return res
class CustomFeatureForm(QDialog, FORM_CLASS): """ Customized form tailor made for reclassification mode of DSGTools: Custom Feature Tool Box. This form was copied from `Ferramentas de Produção` and modified for the DSGTools plugin. """ def __init__(self, layer, layerMap, attributeMap=None, valueMaps=None): """ Class constructor. :param layer: (QgsVectorLayer) layer that will receive the reclassified features. :param layerMap: (dict) a map from vector layer to feature list to be reclassified (allocated to another layer). :param attributeMap: (dict) a map from attribute name to its (reclassified) value. :param valueMaps: (dict) map of all value/relations maps set to layer's fields. These maps will be used for domain checking operations. """ super(CustomFeatureForm, self).__init__() self.setupUi(self) self._layer = layer self.layerMap = layerMap self.valueMaps = valueMaps or LayerHandler().valueMaps(layer) self.attributeMap = attributeMap or dict() self._layersWidgets = dict() self.setupReclassifiedLayers() self.widgetsLayout = QGridLayout(self.scrollAreaWidgetContents) self._fieldsWidgets = dict() self.setupFields() self.setWindowTitle(self.tr("DSGTools Feature Reclassification Form")) self.messageBar = QgsMessageBar(self) def resizeEvent(self, e): """ Just make sure if any alert box is being displayed, it matches the dialog size. Method reimplementation. :param e: (QResizeEvent) resize event related to this widget resizing. """ self.messageBar.resize( QSize( self.geometry().size().width(), 40 # this felt nicer than the original height (30) )) def setupReclassifiedLayers(self): """ Fills GUI with the data for reclassified data. Dialog should allow user to choose whether he wants to reclassify all identified layers or just part of it. """ layout = self.vLayout for l, fl in self.layerMap.items(): cb = QCheckBox() size = len(fl) fCount = self.tr("features") if size > 1 else self.tr("feature") cb.setText("{0} ({1} {2})".format(l.name(), size, fCount)) cb.setChecked(True) # just to avoid reading the layout when reading the form self._layersWidgets[l.name()] = cb layout.addWidget(cb) def fieldHasDomainMap(self, field): """ Identifies whether a given field has a value/relations map. :param field: (QgsField) field to be checked. :return: (bool) whether field has a value/relations map set. """ return field.name() in self.valueMaps def getFieldComboBox(self, field): """ Provides a combo box containing all values possible for a field set to have value/relations map value. This method also set a getValue and setValue proxy that handles value management through its 'real' value. E.G. a given domain {"Not available": 1, "Available": 2} shall have its values set and read as '1' and '2', instead of either combo box's text and index usually used. This also handles setting invalid value setting attempts. :param field: (QgsField) field to have its field's values exposed. :return: (QComboBox) combo box widget filled with all possible values. """ cb = QComboBox() # this methods assumes that if a field is used, than it has a value map domain = self.valueMaps.get(field.name(), None) inverseDomain = {v: k for k, v in domain.items()} if not domain: raise ValueError( self.tr("Field {0} does not have a value/relations map")\ .format(field.name()) ) cb.addItems(list(domain.keys())) def setValue(val): """val: field's real value""" if val not in domain.values(): return cb.setCurrentText(inverseDomain[val]) def value(): return domain[cb.currentText()] cb.setValue = setValue cb.value = value return cb def layer(self): """ Retrieves layer set to receive the reclassified features from the other layers. :return: (QgsVectorLayer) layer to get the newly reclassified features. """ return self._layer def setupFields(self): """ Sets up all fields and fill up with available data on the attribute map. """ utils = Utils() row = 0 # in case no fields are provided for row, f in enumerate(self.layer().fields()): fName = f.name() fMap = self.attributeMap.get(fName, None) if fName in self.attributeMap: fMap = self.attributeMap[fName] if fMap["ignored"]: w = QLineEdit() w.setText(self.tr("Field is set to be ignored")) value = None enabled = False else: value = fMap["value"] enabled = fMap["editable"] if fMap["isPk"]: # visually identify primary key attributes text = '<p>{0} <img src=":/plugins/DsgTools/icons/key.png" '\ 'width="16" height="16"></p>'.format(fName) else: text = fName else: value = None enabled = True text = fName if fName in self.attributeMap and self.attributeMap[fName][ "ignored"]: pass if self.fieldHasDomainMap(f): # this will provide the combo box already filled with the # possible values for the provided field w = self.getFieldComboBox(f) # proxy method added on customized qcombobox w.setValue(value) elif utils.fieldIsBool(f): w = QCheckBox() w.setChecked(False if value is None else value) elif utils.fieldIsFloat(f): w = QDoubleSpinBox() w.setValue(0 if value is None else value) elif utils.fieldIsInt(f): w = QSpinBox() w.setValue(0 if value is None else value) else: w = QLineEdit() w.setText("" if value is None else value) w.setEnabled(enabled) # also to make easier to read data self._fieldsWidgets[fName] = w label = QLabel(text) label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) w.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.widgetsLayout.addWidget(label, row, 0) self.widgetsLayout.addWidget(w, row, 1) self.widgetsLayout.addItem( QSpacerItem(20, 40, QSizePolicy.Expanding, QSizePolicy.Expanding), row + 1, 1, 1, 2) # row, col, rowSpan, colSpan def updateAttributeMap(self): """ Reads all filled data and updated attribute map with such values. """ for fName, w in self._fieldsWidgets.items(): if not fName in self.attributeMap: self.attributeMap[fName] = dict() self.attributeMap[fName]["ignored"] = False self.attributeMap[fName]["editable"] = True if self.attributeMap[fName]["ignored"]: continue w = self._fieldsWidgets[fName] if isinstance(w, QSpinBox) or isinstance(w, QDoubleSpinBox): self.attributeMap[fName]["value"] = w.value() elif isinstance(w, QCheckBox): self.attributeMap[fName]["value"] = w.isChecked() elif isinstance(w, QComboBox): self.attributeMap[fName]["value"] = w.value() else: self.attributeMap[fName]["value"] = w.text() def readSelectedLayers(self): """ Applies a filter over the layer/feature list map based on user selection. :return: (dict) filtered layer-feature list map. """ filtered = dict() for l, fl in self.layerMap.items(): if self._layersWidgets[l.name()].isChecked(): filtered[l] = fl return filtered def readFieldMap(self): """ Reads filled data into the form and sets it to a map from field name to field value to be set. Only fields allowed to be reclassified shall be exported in this method. :return: (dict) a map from field name to its output value. """ fMap = dict() for fName, w in self._fieldsWidgets.items(): if not fName in self.attributeMap: continue w = self._fieldsWidgets[fName] if isinstance(w, QSpinBox) or isinstance(w, QDoubleSpinBox): fMap[fName] = w.value() elif isinstance(w, QCheckBox): fMap[fName] = w.isChecked() elif isinstance(w, QComboBox): fMap[fName] = w.value() else: fMap[fName] = w.text() return fMap @pyqtSlot() def on_okPushButton_clicked(self): """ Verifies if at least one layer is selected and either warn user to select one, or closes with status 1 ("OK"). """ if len(self.readSelectedLayers()) > 0: self.updateAttributeMap() self.done(1) else: self.messageBar.pushMessage( self.tr('Invalid layer selection'), self.tr("select at least one layer for reclassification!"), level=Qgis.Warning, duration=5)
class CustomFeatureForm(QDialog, FORM_CLASS): """ Customized form tailor made for reclassification mode of DSGTools: Custom Feature Tool Box. This form was copied from `Ferramentas de Produção` and modified for the DSGTools plugin. """ def __init__(self, fields, layerMap, attributeMap=None): """ Class constructor. :param fields: (QgsFields) set of fields that will be applied to new feature(s). :param layerMap: (dict) a map from vector layer to feature list to be reclassified (allocated to another layer). :param attributeMap: (dict) a map from attribute name to its (reclassified) value. """ super(CustomFeatureForm, self).__init__() self.setupUi(self) self.fields = fields self.layerMap = layerMap self.attributeMap = attributeMap or dict() self._layersWidgets = dict() self.setupReclassifiedLayers() self.widgetsLayout = QGridLayout(self.scrollAreaWidgetContents) self._fieldsWidgets = dict() self.setupFields() self.setWindowTitle(self.tr("DSGTools Feature Reclassification Form")) self.messageBar = QgsMessageBar(self) def resizeEvent(self, e): """ Just make sure if any alert box is being displayed, it matches the dialog size. Method reimplementation. :param e: (QResizeEvent) resize event related to this widget resizing. """ self.messageBar.resize( QSize( self.geometry().size().width(), 40 # this felt nicer than the original height (30) )) def setupReclassifiedLayers(self): """ Fills GUI with the data for reclassified data. Dialog should allow user to choose whether he wants to reclassify all identified layers or just part of it. """ layout = self.vLayout for l, fl in self.layerMap.items(): cb = QCheckBox() size = len(fl) fCount = self.tr("features") if size > 1 else self.tr("feature") cb.setText("{0} ({1} {2})".format(l.name(), size, fCount)) cb.setChecked(True) # just to avoid reading the layout when reading the form self._layersWidgets[l.name()] = cb layout.addWidget(cb) def setupFields(self): """ Setups up all fields and fill up with available data on the attribute map. """ utils = Utils() row = 0 # in case no fields are provided for row, f in enumerate(self.fields): fName = f.name() fMap = self.attributeMap[fName] if fName in self.attributeMap \ else None if fName in self.attributeMap: fMap = self.attributeMap[fName] if fMap["ignored"]: w = QLineEdit() w.setText(self.tr("Field is set to be ignored")) value = None enabled = False else: value = fMap["value"] enabled = fMap["editable"] if fMap["isPk"]: # visually identify primary key attributes text = '<p>{0} <img src=":/plugins/DsgTools/icons/key.png" '\ 'width="16" height="16"></p>'.format(fName) else: text = fName else: value = None enabled = True text = fName if fName in self.attributeMap and self.attributeMap[fName][ "ignored"]: pass elif utils.fieldIsFloat(f): w = QDoubleSpinBox() w.setValue(0 if value is None else value) elif utils.fieldIsInt(f): w = QSpinBox() w.setValue(0 if value is None else value) else: w = QLineEdit() w.setText("" if value is None else value) w.setEnabled(enabled) # also to make easier to read data self._fieldsWidgets[fName] = w label = QLabel(text) label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) w.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.widgetsLayout.addWidget(label, row, 0) self.widgetsLayout.addWidget(w, row, 1) self.widgetsLayout.addItem( QSpacerItem(20, 40, QSizePolicy.Expanding, QSizePolicy.Expanding), row + 1, 1, 1, 2) # row, col, rowSpan, colSpan def updateAttributeMap(self): """ Reads all filled data and updated attribute map with such values. """ for fName, w in self._fieldsWidgets.items(): if not fName in self.attributeMap: self.attributeMap[fName] = dict() self.attributeMap[fName]["ignored"] = False self.attributeMap[fName]["editable"] = True if self.attributeMap[fName]["ignored"]: continue w = self._fieldsWidgets[fName] if isinstance(w, QSpinBox) or isinstance(w, QDoubleSpinBox): self.attributeMap[fName]["value"] = w.value() else: self.attributeMap[fName]["value"] = w.text() def readSelectedLayers(self): """ Applies a filter over the layer/feature list map based on user selection. :return: (dict) filtered layer-feature list map. """ filtered = dict() for l, fl in self.layerMap.items(): if self._layersWidgets[l.name()].isChecked(): filtered[l] = fl return filtered def readFieldMap(self): """ Reads filled data into the form and sets it to a map from field name to field value to be set. Only fields allowed to be reclassified shall be exported in this method. :return: (dict) a map from field name to its output value. """ fMap = dict() for fName, w in self._fieldsWidgets.items(): if not fName in self.attributeMap: continue w = self._fieldsWidgets[fName] if isinstance(w, QSpinBox) or isinstance(w, QDoubleSpinBox): fMap[fName] = w.value() else: fMap[fName] = w.text() return fMap @pyqtSlot() def on_okPushButton_clicked(self): """ Verifies if at least one layer is selected and either warn user to select one, or closes with status 1 ("OK"). """ if len(self.readSelectedLayers()) > 0: self.updateAttributeMap() self.done(1) else: self.messageBar.pushMessage( self.tr('Invalid layer selection'), self.tr("select at least one layer for reclassification!"), level=Qgis.Warning, duration=5)
class EnforceSpatialRuleWrapper(WidgetWrapper): __ATTRIBUTE_MAP_VERSION = 0.2 def __init__(self, *args, **kwargs): super(EnforceSpatialRuleWrapper, self).__init__(*args, **kwargs) self.messageBar = QgsMessageBar(self.panel) self.panel.resizeEvent = self.resizeEvent self._lastError = "" def resizeEvent(self, e): """ Resize QgsMessageBar to widget's width """ self.messageBar.resize( QSize(self.panel.parent().geometry().size().width(), 30)) def ruleNameWidget(self): """ Retrieves the widget for reading/setting rule name. :return: (QLineEdit) """ le = QLineEdit() le.setPlaceholderText(self.tr("Set a name for this spatial rule...")) return le def mapLayerComboBox(self): """ Retrieves the configured map layer selection combo box. :return: (QgsMapLayerComboBox) configured layer selection widget. """ cb = QgsMapLayerComboBox() cb.setFilters(QgsMapLayerProxyModel.VectorLayer) return cb def mapLayerModelDialog(self): """ Retrieves widget for map layer selection in a model dialog setup. :return: (QLineEdit) map layer setter widget for processing dialog mode. """ le = QLineEdit() le.setPlaceholderText(self.tr("Type a vector layer's name...")) return le def filterExpressionWidget(self): """ Retrieves a new widget for filtering expression setting. :return: (QgsFieldExpressionWidget) snap mode selection widget. """ fe = QgsFieldExpressionWidget() def setValueProxy(exp): layer = fe.layer() if layer and exp.strip() in layer.fields().names(): # if a layer is set and the expression is the name purely a # field, it will be ignore. single names are causing a weird # crash when running the algorithm. this seems to solve it. exp = "" fe.setExpression(exp) def getValueProxy(): layer = fe.layer() exp = fe.currentText() if layer and exp.strip() in layer.fields().names(): exp = "" return exp fe.setExpression_ = setValueProxy fe.currentText_ = getValueProxy return fe def predicateComboBox(self): """ Retrieves widget for spatial predicate selection. :return: (QComboBox) a combo box with all available predicates. """ cb = QComboBox() cb.addItems( list(SpatialRelationsHandler().availablePredicates().values())) return cb def de9imWidget(self): """ Creates a new widget to handle DE-9IM masks as input. :return: (QLineEdit) a line edit with a DE-9IM text validator. """ le = QLineEdit() regex = QRegExp("[FfTt012\*]{9}") le.setValidator(QRegExpValidator(regex, le)) le.setPlaceholderText(self.tr("Type in a DE-9IM as 'T*F0*F21*'...")) return le def cardinalityWidget(self): """ Retrieves a widget for cardinality setting. :return: (QLineEdit) cardinality widget with its content validation applied. """ le = QLineEdit() regex = QRegExp("[0-9\*]\.\.[0-9\*]") le.setValidator(QRegExpValidator(regex, le)) le.setPlaceholderText("1..*") return le def useDE9IM(self): """ Identifies whether user chose to input predicate as a DE-9IM mask. :return: (bool) whether GUI should handle the DE-9IM mask widget over the combo box selection. """ return self.panel.cb.isChecked() def _checkCardinalityAvailability(self, row): """ Checks if the cardinality for the rule at a given row is available. Cardinality is only handled when predicate is provided through the combo box options and are not available for the "NOT" options. :param row: (int) row to have its cardinality checked. :return: (bool) whether cardinality is available """ otw = self.panel.otw if self.useDE9IM(): # if user is using the DE-9IM input, cardinality won't be # managed otw.itemAt(row, 7).setEnabled(True) return True predicate = otw.getValue(row, 3) handler = SpatialRelationsHandler() noCardinality = predicate in (handler.DISJOINT, handler.NOTEQUALS, handler.NOTINTERSECTS, handler.NOTTOUCHES, handler.NOTCROSSES, handler.NOTWITHIN, handler.NOTOVERLAPS, handler.NOTCONTAINS) otw.itemAt(row, 7).setEnabled(not noCardinality) if noCardinality: otw.setValue(row, 7, "") return not noCardinality def postAddRowStandard(self, row): """ Sets up widgets to work as expected right after they are added to GUI. :param row: (int) row to have its widgets setup. """ # in standard GUI, the layer selectors are QgsMapLayerComboBox, and its # layer changed signal should be connected to the filter expression # widget setup otw = self.panel.otw for col in [1, 5]: mapLayerComboBox = otw.itemAt(row, col) filterWidget = otw.itemAt(row, col + 1) mapLayerComboBox.layerChanged.connect(filterWidget.setLayer) mapLayerComboBox.layerChanged.connect( partial(filterWidget.setExpression, "")) # first setup is manual though vl = mapLayerComboBox.currentLayer() if vl: filterWidget.setLayer(vl) predicateWidget = otw.itemAt(row, 3) predicateWidget.currentIndexChanged.connect( partial(self._checkCardinalityAvailability, row)) # also triggers the action for the first time it is open self._checkCardinalityAvailability(row) def postAddRowModeler(self, row): """ Sets up widgets to work as expected right after they are added to GUI. :param row: (int) row to have its widgets setup. """ otw = self.panel.otw def checkLayerBeforeConnect(le, filterExp): lName = le.text().strip() for layer in QgsProject.instance().mapLayersByName(lName): if isinstance(layer, QgsVectorLayer) and layer.name() == lName: filterExp.setLayer(layer) return filterExp.setLayer(None) for col in [1, 5]: le = otw.itemAt(row, col) filterWidget = otw.itemAt(row, col + 1) le.editingFinished.connect( partial(checkLayerBeforeConnect, le, filterWidget)) predicateWidget = otw.itemAt(row, 3) predicateWidget.currentIndexChanged.connect( partial(self._checkCardinalityAvailability, row)) self._checkCardinalityAvailability(row) def standardPanel(self): """ Returns the table prepared for the standard Processing GUI. :return: (OrderedTableWidget) DSGTools customized table widget. """ widget = QWidget() layout = QVBoxLayout() # added as an attribute in order to make it easier to be read widget.cb = QCheckBox() widget.cb.setText(self.tr("Use DE-9IM inputs")) layout.addWidget(widget.cb) widget.otw = OrderedTableWidget( headerMap={ 0: { "header": self.tr("Rule name"), "type": "widget", "widget": self.ruleNameWidget, "setter": "setText", "getter": "text" }, 1: { "header": self.tr("Layer A"), "type": "widget", "widget": self.mapLayerComboBox, "setter": "setCurrentText", "getter": "currentText" }, 2: { "header": self.tr("Filter A"), "type": "widget", "widget": self.filterExpressionWidget, "setter": "setExpression_", "getter": "currentText_" }, 3: { "header": self.tr("Predicate"), "type": "widget", "widget": self.predicateComboBox, "setter": "setCurrentIndex", "getter": "currentIndex" }, 4: { "header": self.tr("DE-9IM mask predicate"), "type": "widget", "widget": self.de9imWidget, "setter": "setText", "getter": "text" }, 5: { "header": self.tr("Layer B"), "type": "widget", "widget": self.mapLayerComboBox, "setter": "setCurrentText", "getter": "currentText" }, 6: { "header": self.tr("Filter B"), "type": "widget", "widget": self.filterExpressionWidget, "setter": "setExpression_", "getter": "currentText_" }, 7: { "header": self.tr("Cardinality"), "type": "widget", "widget": self.cardinalityWidget, "setter": "setText", "getter": "text" } }) def handlePredicateColumns(checked): """ Predicate input widgets are mutually exclusively: the user may only input data through either of them. This method manages hiding and showing correct columns in accord to the user selection. :param checked: (bool) whether the DE-9IM usage checkbox is ticked. """ widget.otw.tableWidget.hideColumn(3 if checked else 4) widget.otw.tableWidget.showColumn(4 if checked else 3) widget.cb.toggled.connect(handlePredicateColumns) widget.cb.toggled.emit(widget.cb.isChecked()) widget.otw.setHeaderDoubleClickBehaviour("replicate") widget.otw.rowAdded.connect(self.postAddRowStandard) layout.addWidget(widget.otw) widget.setLayout(layout) return widget def batchPanel(self): """ Returns the table prepared for the batch Processing GUI. :return: (OrderedTableWidget) DSGTools customized table widget. """ return self.standardPanel() def modelerPanel(self): """ Returns the table prepared for the modeler Processing GUI. :return: (OrderedTableWidget) DSGTools customized table widget. """ widget = QWidget() layout = QVBoxLayout() # added as an attribute in order to make it easier to be read widget.cb = QCheckBox() widget.cb.setText(self.tr("Use DE-9IM inputs")) layout.addWidget(widget.cb) widget.otw = OrderedTableWidget( headerMap={ 0: { "header": self.tr("Rule name"), "type": "widget", "widget": self.ruleNameWidget, "setter": "setText", "getter": "text" }, 1: { "header": self.tr("Layer A"), "type": "widget", "widget": self.mapLayerModelDialog, "setter": "setText", "getter": "text" }, 2: { "header": self.tr("Filter A"), "type": "widget", "widget": self.filterExpressionWidget, "setter": "setExpression_", "getter": "currentText_" }, 3: { "header": self.tr("Predicate"), "type": "widget", "widget": self.predicateComboBox, "setter": "setCurrentIndex", "getter": "currentIndex" }, 4: { "header": self.tr("DE-9IM mask predicate"), "type": "widget", "widget": self.de9imWidget, "setter": "setText", "getter": "text" }, 5: { "header": self.tr("Layer B"), "type": "widget", "widget": self.mapLayerModelDialog, "setter": "setText", "getter": "text" }, 6: { "header": self.tr("Filter B"), "type": "widget", "widget": self.filterExpressionWidget, "setter": "setExpression_", "getter": "currentText_" }, 7: { "header": self.tr("Cardinality"), "type": "widget", "widget": self.cardinalityWidget, "setter": "setText", "getter": "text" } }) def handlePredicateColumns(checked): """ Predicate input widgets are mutually exclusively: the user may only input data through either of them. This method manages hiding and showing correct columns in accord to the user selection. :param checked: (bool) whether the DE-9IM usage checkbox is ticked. """ widget.otw.tableWidget.hideColumn(3 if checked else 4) widget.otw.tableWidget.showColumn(4 if checked else 3) widget.cb.toggled.connect(handlePredicateColumns) widget.cb.toggled.emit(widget.cb.isChecked()) widget.otw.setHeaderDoubleClickBehaviour("replicate") widget.otw.rowAdded.connect(self.postAddRowModeler) layout.addWidget(widget.otw) widget.setLayout(layout) return widget def createPanel(self): return { DIALOG_MODELER: self.modelerPanel, DIALOG_STANDARD: self.standardPanel, DIALOG_BATCH: self.batchPanel }[self.dialogType]() def createWidget(self): self.panel = self.createPanel() self.panel.otw.showSaveLoadButtons(True) self.panel.otw.extension = ".rules" self.panel.otw.fileType = self.tr("Set of DSGTools Spatial Rules") self.panel.otw.setMetadata({"version": self.__ATTRIBUTE_MAP_VERSION}) return self.panel def parentLayerChanged(self, layer=None): pass def setLayer(self, layer): pass def showLoadingMsg(self, invalidRules=None, msgType=None): """ Shows a message box to user if successfully loaded data or not. If not, shows to user a list of not loaded layers and allows user to choice between ignore and continue or cancel the importation. :param lyrList: (list) a list of not loaded layers. :param msgType: (str) type of message box - warning or information. :return: (signal) value returned from the clicked button. """ msg = QMessageBox() msg.setWindowTitle(self.tr("DSGTools: importing spatial rules")) if invalidRules and msgType == "warning": msg.setIcon(QMessageBox.Warning) msg.setText(self.tr("Some rules have not been loaded")) msg.setInformativeText( self.tr("Do you want to ignore and continue or cancel?")) msgString = "\n".join((r.ruleName() for r in invalidRules)) formatedMsgString = self.tr( "The following layers have not been loaded:\n{0}").format( msgString) msg.setDetailedText(formatedMsgString) msg.setStandardButtons(QMessageBox.Ignore | QMessageBox.Cancel) msg.setDefaultButton(QMessageBox.Cancel) else: msg.setIcon(QMessageBox.Information) msg.setText(self.tr("Successfully loaded rules!")) choice = msg.exec_() return choice def setValue(self, value): """ Sets back parameters to the GUI. Method reimplementation. :param value: (list-of-SpatialRule) list of spatial rules to be set. """ if not value: return otw = self.panel.otw useDE9IM = value[0].get("useDE9IM", False) self.panel.cb.setChecked(useDE9IM) # signal must be triggered to adjust the correct column display self.panel.cb.toggled.emit(useDE9IM) isNotModeler = self.dialogType != DIALOG_MODELER invalids = list() for rule in value: # GUI was crashing when passing SpatialRule straight up rule = SpatialRule(**rule, checkLoadedLayer=False) # we want to check whether the layer is loaded as this does not # work properly with the map layer combobox. on the modeler it # won't matter as it is a line edit if not rule.isValid(checkLoaded=isNotModeler): invalids.append(rule) continue otw.addRow({ 0: rule.ruleName(), 1: rule.layerA(), 2: rule.filterA(), 3: rule.predicateEnum(), 4: rule.predicateDE9IM(), 5: rule.layerB(), 6: rule.filterB(), 7: rule.cardinality() }) choice = self.showLoadingMsg(invalids, "warning" if invalids else "") if choice == QMessageBox.Cancel: otw.clear() def readStandardPanel(self): """ Reads widget's contents when process' parameters are set from an algorithm call (e.g. Processing toolbox). """ ruleList = list() otw = self.panel.otw useDe9im = self.useDE9IM() for row in range(otw.rowCount()): ruleList.append( SpatialRule( name=otw.getValue(row, 0).strip(), # or \ # self.tr("Spatial Rule #{n}".format(n=row + 1)), layer_a=otw.getValue(row, 1), filter_a=otw.getValue(row, 2), predicate=otw.getValue(row, 3), de9im_predicate=otw.getValue(row, 4), layer_b=otw.getValue(row, 5), filter_b=otw.getValue(row, 6), cardinality=otw.getValue(row, 7) or "1..*", useDE9IM=useDe9im, checkLoadedLayer=False).asDict()) return ruleList def readModelerPanel(self): """ Reads widget's contents when process' parameters are set from a modeler instance. """ return self.readStandardPanel() def readBatchPanel(self): """ Reads widget's contents when process' parameters are set from a batch processing instance. """ return self.readStandardPanel() def validate(self, pushAlert=False): """ Validates fields. Returns True if all information are filled correctly. :param pushAlert: (bool) whether invalidation reason should be displayed on the widget. :return: (bool) whether set of filled parameters if valid. """ inputMap = { DIALOG_STANDARD: self.readStandardPanel, DIALOG_MODELER: self.readModelerPanel, DIALOG_BATCH: self.readBatchPanel }[self.dialogType]() if len(inputMap) == 0: if pushAlert: self.messageBar.pushMessage( self.tr("Please provide at least 1 spatial rule."), level=Qgis.Warning, duration=5) return False for row, rule in enumerate(inputMap): # GUI was crashing when passing SpatialRule straight up rule = SpatialRule(**rule) if not rule.isValid(): if pushAlert: self.messageBar.pushMessage( self.tr("{0} (row {1}).")\ .format(rule.validate(), row + 1), level=Qgis.Warning, duration=5 ) return False return True def value(self): """ Retrieves parameters from current widget. Method reimplementation. :return: (dict) value currently set to the GUI. """ if self.validate(pushAlert=True): return { DIALOG_STANDARD: self.readStandardPanel, DIALOG_MODELER: self.readModelerPanel, DIALOG_BATCH: self.readBatchPanel }[self.dialogType]() def postInitialize(self, wrappers): pass
class WorkflowSetupDialog(QDialog, FORM_CLASS): __qgisModelPath__ = ModelerUtils.modelsFolders()[0] ON_FLAGS_HALT, ON_FLAGS_WARN, ON_FLAGS_IGNORE = range(3) onFlagsDisplayNameMap = { ON_FLAGS_HALT: QCoreApplication.translate('WorkflowSetupDialog', "Halt"), ON_FLAGS_WARN: QCoreApplication.translate('WorkflowSetupDialog', "Warn"), ON_FLAGS_IGNORE: QCoreApplication.translate('WorkflowSetupDialog', "Ignore") } onFlagsValueMap = { ON_FLAGS_HALT: "halt", ON_FLAGS_WARN: "warn", ON_FLAGS_IGNORE: "ignore" } MODEL_NAME_HEADER, MODEL_SOURCE_HEADER, ON_FLAGS_HEADER, LOAD_OUT_HEADER = range( 4) def __init__(self, parent=None): """ Class constructor. :param headerMap: (dict) a map from each header to be shown and type of cell content (e.g. widget or item). :param parent: (QtWidgets.*) any widget parent to current instance. """ super(WorkflowSetupDialog, self).__init__(parent) self.parent = parent self.setupUi(self) self.messageBar = QgsMessageBar(self) self.orderedTableWidget.setHeaders({ self.MODEL_NAME_HEADER: { "header": self.tr("Model name"), "type": "widget", "widget": self.modelNameWidget, "setter": "setText", "getter": "text" }, self.MODEL_SOURCE_HEADER: { "header": self.tr("Model source"), "type": "widget", "widget": self.modelWidget, "setter": "setText", "getter": "text" }, self.ON_FLAGS_HEADER: { "header": self.tr("On flags"), "type": "widget", "widget": self.onFlagsWidget, "setter": "setCurrentIndex", "getter": "currentIndex" }, self.LOAD_OUT_HEADER: { "header": self.tr("Load output"), "type": "widget", "widget": self.loadOutputWidget, "setter": "setChecked", "getter": "isChecked" } }) self.orderedTableWidget.setHeaderDoubleClickBehaviour("replicate") def resizeTable(self): """ Adjusts table columns sizes. """ dSize = self.orderedTableWidget.geometry().width() - \ self.orderedTableWidget.horizontalHeader().geometry().width() onFlagsColSize = self.orderedTableWidget.sectionSize(2) loadOutColSize = self.orderedTableWidget.sectionSize(3) missingBarSize = self.geometry().size().width() - dSize\ - onFlagsColSize - loadOutColSize # the "-11" is empiric: it makes it fit header to table self.orderedTableWidget.tableWidget.horizontalHeader().resizeSection( 0, int(0.4 * missingBarSize) - 11) self.orderedTableWidget.tableWidget.horizontalHeader().resizeSection( 1, missingBarSize - int(0.4 * missingBarSize) - 11) def resizeEvent(self, e): """ Reimplementation in order to use this window's resize event. On this object, this method makes sure that message bar is always the same size as the window. :param e: (QResizeEvent) resize event. """ self.messageBar.resize( QSize( self.geometry().size().width(), 40 # this felt nicer than the original height (30) )) self.resizeTable() def confirmAction(self, msg, showCancel=True): """ Raises a message box for confirmation before executing an action. :param msg: (str) message to be exposed. :param showCancel: (bool) whether Cancel button should be exposed. :return: (bool) whether action was confirmed. """ if showCancel: return QMessageBox.question(self, self.tr('Confirm Action'), msg, QMessageBox.Ok | QMessageBox.Cancel) == QMessageBox.Ok else: return QMessageBox.question(self, self.tr('Confirm Action'), msg, QMessageBox.Ok) == QMessageBox.Ok def clear(self): """ Clears all input data from GUI. """ self.authorLineEdit.setText("") self.nameLineEdit.setText("") self.versionLineEdit.setText("") self.orderedTableWidget.clear() def modelNameWidget(self, name=None): """ Gets a new instance of model name's setter widget. :param name: (str) model name to be filled. :return: (QLineEdit) widget for model's name setting. """ # no need to pass parenthood as it will be set to the table when added # to a cell le = QLineEdit() # setPlace"h"older, with a lower case "h"... le.setPlaceholderText(self.tr("Set a name for the model...")) if name is not None: le.setText(name) le.setFrame(False) return le def modelWidget(self, filepath=None): """ Gets a new instance of model settter's widget. :parma filepath: (str) path to a model. :return: (SelectFileWidget) DSGTools custom file selection widget. """ widget = SelectFileWidget() widget.label.hide() widget.selectFilePushButton.setText("...") widget.selectFilePushButton.setMaximumWidth(32) widget.lineEdit.setPlaceholderText(self.tr("Select a model...")) widget.lineEdit.setFrame(False) widget.setCaption(self.tr("Select a QGIS Processing model file")) widget.setFilter( self.tr("Select a QGIS Processing model (*.model *.model3)")) # defining setter and getter methods for composed widgets into OTW widget.setText = widget.lineEdit.setText widget.text = widget.lineEdit.text if filepath is not None: widget.setText(filepath) return widget def onFlagsWidget(self, option=None): """ Gets a new instance for the widget that sets model's behaviour when flags are raised. :param option: (str) on flags raised behaviour (non translatable text). :return: (QComboBox) model's behaviour selection widget. """ combo = QComboBox() combo.addItems([ self.onFlagsDisplayNameMap[self.ON_FLAGS_HALT], self.onFlagsDisplayNameMap[self.ON_FLAGS_WARN], self.onFlagsDisplayNameMap[self.ON_FLAGS_IGNORE] ]) if option is not None: optIdx = None for idx, txt in self.onFlagsValueMap.items(): if option == txt: optIdx = idx break optIdx = optIdx if optIdx is not None else 0 combo.setCurrentIndex(optIdx) return combo def loadOutputWidget(self, option=None): """ Gets a new instance for the widget that sets output layer loading definitions. :param option: (bool) if output should be loaded. :return: (QWidget) widget for output layer loading behaviour definition. """ cb = QCheckBox() cb.setStyleSheet("margin:auto;") if option is not None: cb.setChecked(option) return cb def now(self): """ Gets time and date from the system. Format: "dd/mm/yyyy HH:MM:SS". :return: (str) current's date and time """ paddle = lambda n: str(n) if n > 9 else "0{0}".format(n) now = datetime.now() return "{day}/{month}/{year} {hour}:{minute}:{second}".format( year=now.year, month=paddle(now.month), day=paddle(now.day), hour=paddle(now.hour), minute=paddle(now.minute), second=paddle(now.second)) def workflowName(self): """ Reads filled workflow name from GUI. :return: (str) workflow's name. """ return self.nameLineEdit.text().strip() def setWorkflowName(self, name): """ Sets workflow name to GUI. :param name: (str) workflow's name. """ self.nameLineEdit.setText(name) def author(self): """ Reads filled workflow name from GUI. :return: (str) workflow's author. """ return self.authorLineEdit.text().strip() def setWorkflowAuthor(self, author): """ Sets workflow author name to GUI. :param author: (str) workflow's author name. """ self.authorLineEdit.setText(author) def version(self): """ Reads filled workflow name from GUI. :return: (str) workflow's version. """ return self.versionLineEdit.text().strip() def setWorkflowVersion(self, version): """ Sets workflow version to GUI. :param version: (str) workflow's version. """ self.versionLineEdit.setText(version) def modelCount(self): """ Reads the amount of models (rows added) the user intend to use. :return: (int) model count. """ return self.orderedTableWidget.rowCount() def readRow(self, row): """ Reads a row's contents and set it as a DsgToolsProcessingModel set of parameters. :return: (dict) parameters map. """ contents = self.orderedTableWidget.row(row) filepath = contents[self.MODEL_SOURCE_HEADER].strip() onFlagsIdx = contents[self.ON_FLAGS_HEADER] name = contents[self.MODEL_NAME_HEADER].strip() loadOutput = contents[self.LOAD_OUT_HEADER] if not os.path.exists(filepath): xml = "" else: with open(filepath, "r", encoding="utf-8") as f: xml = f.read() return { "displayName": name, "flags": { "onFlagsRaised": self.onFlagsValueMap[onFlagsIdx], "loadOutput": loadOutput }, "source": { "type": "xml", "data": xml }, "metadata": { "originalName": os.path.basename(filepath) } } def setModelToRow(self, row, model): """ Reads model's parameters from model parameters default map. :param row: (int) row to have its widgets filled with model's parameters. :param model: (DsgToolsProcessingModel) model object. """ # all model files handled by this tool are read/written on QGIS model dir data = model.data() if model.source() == "file" and os.path.exists(data): with open(data, "r", encoding="utf-8") as f: xml = f.read() originalName = os.path.basename(data) elif model.source() == "xml": xml = data meta = model.metadata() originalName = model.originalName() if model.originalName() \ else "temp_{0}.model3".format(hash(time())) else: return False path = os.path.join(self.__qgisModelPath__, originalName) msg = self.tr( "Model '{0}' is already imported would you like to overwrite it?" ).format(path) if os.path.exists(path) and self.confirmAction(msg): os.remove(path) if not os.path.exists(path): with open(path, "w") as f: f.write(xml) self.orderedTableWidget.addRow( contents={ self.MODEL_NAME_HEADER: model.displayName(), self.MODEL_SOURCE_HEADER: path, self.ON_FLAGS_HEADER: { "halt": self.ON_FLAGS_HALT, "warn": self.ON_FLAGS_WARN, "ignore": self.ON_FLAGS_IGNORE }[model.onFlagsRaised()], self.LOAD_OUT_HEADER: model.loadOutput() }) return True def validateRowContents(self, contents): """ Checks if all attributes read from a row are valid. :param contents: (dict) map to (already read) row contents. :return: (str) invalidation reason """ if contents["displayName"] == "": return self.tr("Missing model's name.") if contents["source"]["data"] == "": return self.tr("Model is empty or file was not found.") return "" def models(self): """ Reads all table contents and sets it as a DsgToolsProcessingAlgorithm's set of parameters. :return: (dict) map to each model's set of parameters. """ models = dict() for row in range(self.modelCount()): contents = self.readRow(row) models[contents["displayName"]] = contents return models def validateModels(self): """ Check if each row on table has a valid input. :return: (str) invalidation reason. """ for row in range(self.modelCount()): msg = self.validateRowContents(self.readRow(row)) if msg: return "Row {row}: '{error}'".format(row=row + 1, error=msg) if len(self.models()) != self.modelCount(): return self.tr("Check if no model name is repeated.") return "" def workflowParameterMap(self): """ Generates a Workflow map from input data. """ return { "displayName": self.workflowName(), "models": self.models(), "metadata": { "author": self.author(), "version": self.version(), "lastModified": self.now() } } def currentWorkflow(self): """ Returns current workflow object as read from GUI. :return: (QualityAssuranceWorkflow) current workflow object. """ try: return QualityAssuranceWorkflow(self.workflowParameterMap()) except: return None def validate(self): """ Checks if all filled data generates a valid Workflow object. :return: (bool) validation status. """ if self.workflowName() == "": return self.tr("Workflow's name needs to be filled.") if self.author() == "": return self.tr("Workflow's author needs to be filled.") if self.version() == "": return self.tr("Workflow's version needs to be filled.") msg = self.validateModels() if msg != "": return msg return "" def exportWorkflow(self, filepath): """ Exports current data to a JSON file. :param filepath: (str) output file directory. """ QualityAssuranceWorkflow(self.workflowParameterMap()).export(filepath) @pyqtSlot(bool, name="on_exportPushButton_clicked") def export(self): """ Exports current input data as a workflow JSON, IF input is valid. :return: (bool) operation success. """ msg = self.validate() if msg != "": self.messageBar.pushMessage(self.tr('Invalid workflow'), msg, level=Qgis.Warning, duration=5) return False fd = QFileDialog() filename = fd.getSaveFileName( caption=self.tr("Export DSGTools Workflow"), filter=self.tr("DSGTools Workflow (*.workflow)")) filename = filename[0] if isinstance(filename, tuple) else "" if filename == "": return False filename = filename if filename.lower().endswith(".workflow") \ else "{0}.workflow".format(filename) try: self.exportWorkflow(filename) except Exception as e: self.messageBar.pushMessage( self.tr('Invalid workflow'), self.tr( "Unable to export workflow to '{fp}' ({error}).").format( fp=filename, error=str(e)), level=Qgis.Warning, duration=5) return False result = os.path.exists(filename) msg = (self.tr("Workflow exported to {fp}") if result else \ self.tr("Unable to export workflow to '{fp}'")).format(fp=filename) lvl = Qgis.Success if result else Qgis.Warning self.messageBar.pushMessage(self.tr('Workflow exportation'), msg, level=lvl, duration=5) return result def importWorkflow(self, filepath): """ Sets workflow contents from an imported DSGTools Workflow dump file. :param filepath: (str) workflow file to be imported. """ with open(filepath, "r", encoding="utf-8") as f: xml = json.load(f) workflow = QualityAssuranceWorkflow(xml) self.clear() self.setWorkflowAuthor(workflow.author()) self.setWorkflowVersion(workflow.version()) self.setWorkflowName(workflow.displayName()) for row, modelParam in enumerate(xml["models"].values()): self.setModelToRow(row, DsgToolsProcessingModel(modelParam, "")) @pyqtSlot(bool, name="on_importPushButton_clicked") def import_(self): """ Request a file for Workflow importation and sets it to GUI. :return: (bool) operation status. """ fd = QFileDialog() filename = fd.getOpenFileName( caption=self.tr('Select a Workflow file'), filter=self.tr('DSGTools Workflow (*.workflow *.json)')) filename = filename[0] if isinstance(filename, tuple) else "" if not filename: return False try: self.importWorkflow(filename) except Exception as e: self.messageBar.pushMessage( self.tr('Invalid workflow'), self.tr( "Unable to export workflow to '{fp}' ({error}).").format( fp=filename, error=str(e)), level=Qgis.Critical, duration=5) return False self.messageBar.pushMessage( self.tr('Success'), self.tr("Workflow '{fp}' imported!").format(fp=filename), level=Qgis.Info, duration=5) return True @pyqtSlot(bool, name="on_okPushButton_clicked") def ok(self): """ Closes dialog and checks if current workflow is valid. """ msg = self.validate() if msg == "" and self.currentWorkflow(): self.done(1) else: self.messageBar.pushMessage(self.tr('Invalid workflow'), self.validate(), level=Qgis.Warning, duration=5) @pyqtSlot(bool, name="on_cancelPushButton_clicked") def cancel(self): """ Restores GUI to last state and closes it. """ self.done(0)