class ButtonLineEdit(QLineEdit): buttonClicked = pyqtSignal() def __init__(self, parent=None): super(ButtonLineEdit, self).__init__(parent) self.setPlaceholderText("Search") self.btnSearch = QToolButton(self) self.btnSearch.setIcon(QIcon(os.path.join(pluginPath, "icons", "search.svg"))) self.btnSearch.setStyleSheet("QToolButton { padding: 0px; }") self.btnSearch.setCursor(Qt.ArrowCursor) self.btnSearch.clicked.connect(self.buttonClicked.emit) frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth) buttonSize = self.btnSearch.sizeHint() self.setStyleSheet("QLineEdit {{padding-right: {}px; }}".format(buttonSize.width() + frameWidth + 1)) self.setMinimumSize(max(self.minimumSizeHint().width(), buttonSize.width() + frameWidth * 2 + 2), max(self.minimumSizeHint().height(), buttonSize.height() + frameWidth * 2 + 2)) def resizeEvent(self, event): buttonSize = self.btnSearch.sizeHint() frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth) self.btnSearch.move(self.rect().right() - frameWidth - buttonSize.width(), (self.rect().bottom() - buttonSize.height() + 1) / 2) super(ButtonLineEdit, self).resizeEvent(event)
class PasswordLineEdit(QLineEdit): def __init__(self, parent=None): QLineEdit.__init__(self, parent) self.setPlaceholderText(self.tr("Password")) self.setEchoMode(QLineEdit.Password) self.btnIcon = QToolButton(self) self.btnIcon.setIcon(QIcon(os.path.join(iconsPath, "lock.svg"))) self.btnIcon.setEnabled(False) self.btnIcon.setStyleSheet( "QToolButton { border: none; padding: 0px; }") self.btnToggle = QToolButton(self) self.btnToggle.setIcon(QIcon(os.path.join(iconsPath, "eye-slash.svg"))) self.btnToggle.setCheckable(True) self.btnToggle.setToolTip(self.tr("Toggle password visibility")) self.btnToggle.setCursor(Qt.ArrowCursor) self.btnToggle.setStyleSheet( "QToolButton { border: none; padding: 0px; }") self.btnToggle.toggled.connect(self.togglePassword) frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth) self.setStyleSheet( "QLineEdit {{ padding-right: {}px; padding-left: {}px }} ".format( self.btnToggle.sizeHint().width() + frameWidth + 1, self.btnIcon.sizeHint().width() + frameWidth + 1)) msz = self.minimumSizeHint() self.setMinimumSize( max(msz.width(), self.btnToggle.sizeHint().height() + frameWidth * 2 + 2), max(msz.height(), self.btnToggle.sizeHint().height() + frameWidth * 2 + 2)) def resizeEvent(self, event): frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth) sz = self.btnIcon.sizeHint() self.btnIcon.move(frameWidth + 1, (self.rect().bottom() + 1 - sz.height()) / 2) sz = self.btnToggle.sizeHint() self.btnToggle.move(self.rect().right() - frameWidth - sz.width(), (self.rect().bottom() + 1 - sz.height()) / 2) def togglePassword(self, toggled): if toggled: self.setEchoMode(QLineEdit.Normal) self.btnToggle.setIcon(QIcon(os.path.join(iconsPath, "eye.svg"))) else: self.setEchoMode(QLineEdit.Password) self.btnToggle.setIcon( QIcon(os.path.join(iconsPath, "eye-slash.svg")))
class AlgorithmDialogBase(BASE, WIDGET): def __init__(self, alg): super(AlgorithmDialogBase, self).__init__(iface.mainWindow() if iface else None) self.setupUi(self) # don't collapse parameters panel self.splitter.setCollapsible(0, False) # add collapse button to splitter splitterHandle = self.splitter.handle(1) handleLayout = QVBoxLayout() handleLayout.setContentsMargins(0, 0, 0, 0) self.btnCollapse = QToolButton(splitterHandle) self.btnCollapse.setAutoRaise(True) self.btnCollapse.setFixedSize(12, 12) self.btnCollapse.setCursor(Qt.ArrowCursor) handleLayout.addWidget(self.btnCollapse) handleLayout.addStretch() splitterHandle.setLayout(handleLayout) self.settings = QgsSettings() self.splitter.restoreState( self.settings.value("/Processing/dialogBaseSplitter", QByteArray())) self.restoreGeometry( self.settings.value("/Processing/dialogBase", QByteArray())) self.splitterState = self.splitter.saveState() self.splitterChanged(0, 0) self.executed = False self.mainWidget = None self.alg = alg self.setWindowTitle(self.alg.displayName()) self.buttonBox.rejected.connect(self.reject) self.buttonBox.accepted.connect(self.accept) # Rename OK button to Run self.btnRun = self.buttonBox.button(QDialogButtonBox.Ok) self.btnRun.setText(self.tr('Run')) self.buttonCancel.setEnabled(False) self.btnClose = self.buttonBox.button(QDialogButtonBox.Close) self.buttonBox.helpRequested.connect(self.openHelp) self.btnCollapse.clicked.connect(self.toggleCollapsed) self.splitter.splitterMoved.connect(self.splitterChanged) # desktop = QDesktopWidget() # if desktop.physicalDpiX() > 96: # self.txtHelp.setZoomFactor(desktop.physicalDpiX() / 96) algHelp = self.formatHelp(self.alg) if algHelp is None: self.textShortHelp.hide() else: self.textShortHelp.document().setDefaultStyleSheet( '''.summary { margin-left: 10px; margin-right: 10px; } h2 { color: #555555; padding-bottom: 15px; } a { text-decoration: none; color: #3498db; font-weight: bold; } p { color: #666666; } b { color: #333333; } dl dd { margin-bottom: 5px; }''' ) self.textShortHelp.setHtml(algHelp) def linkClicked(url): webbrowser.open(url.toString()) self.textShortHelp.anchorClicked.connect(linkClicked) self.showDebug = ProcessingConfig.getSetting( ProcessingConfig.SHOW_DEBUG_IN_DIALOG) def createFeedback(self): feedback = AlgorithmDialogFeedback(self) feedback.progressChanged.connect(self.setPercentage) feedback.error.connect(self.error) feedback.progress_text.connect(self.setText) feedback.info.connect(self.setInfo) feedback.command_info.connect(self.setCommand) feedback.debug_info.connect(self.setDebugInfo) feedback.console_info.connect(self.setConsoleInfo) self.buttonCancel.clicked.connect(feedback.cancel) return feedback def formatHelp(self, alg): text = alg.shortHelpString() if not text: return None return "<h2>%s</h2>%s" % (alg.displayName(), "".join( ["<p>%s</p>" % s for s in text.split("\n")])) def closeEvent(self, event): self._saveGeometry() super(AlgorithmDialogBase, self).closeEvent(event) def setMainWidget(self, widget): if self.mainWidget is not None: QgsProject.instance().layerWasAdded.disconnect( self.mainWidget.layerRegistryChanged) QgsProject.instance().layersWillBeRemoved.disconnect( self.mainWidget.layerRegistryChanged) self.mainWidget = widget self.tabWidget.widget(0).layout().addWidget(self.mainWidget) QgsProject.instance().layerWasAdded.connect( self.mainWidget.layerRegistryChanged) QgsProject.instance().layersWillBeRemoved.connect( self.mainWidget.layerRegistryChanged) def error(self, msg): self.setInfo(msg, True) self.resetGUI() self.tabWidget.setCurrentIndex(1) def resetGUI(self): self.lblProgress.setText('') self.progressBar.setMaximum(100) self.progressBar.setValue(0) self.btnRun.setEnabled(True) self.btnClose.setEnabled(True) def setInfo(self, msg, error=False, escape_html=True): if error: self.txtLog.append( '<span style="color:red">{}</span><br />'.format(msg, quote=False)) elif escape_html: self.txtLog.append(html.escape(msg)) else: self.txtLog.append(msg) def setCommand(self, cmd): if self.showDebug: self.txtLog.append('<code>{}<code>'.format( html.escape(cmd, quote=False))) def setDebugInfo(self, msg): if self.showDebug: self.txtLog.append('<span style="color:blue">{}</span>'.format( html.escape(msg, quote=False))) def setConsoleInfo(self, msg): if self.showDebug: self.txtLog.append( '<code><span style="color:darkgray">{}</span></code>'.format( html.escape(msg, quote=False))) def setPercentage(self, value): if self.progressBar.maximum() == 0: self.progressBar.setMaximum(100) self.progressBar.setValue(value) def setText(self, text): self.lblProgress.setText(text) self.setInfo(text, False) def getParamValues(self): return {} def accept(self): pass def reject(self): self._saveGeometry() super(AlgorithmDialogBase, self).reject() def finish(self, successful, result, context, feedback): pass def toggleCollapsed(self): if self.helpCollapsed: self.splitter.restoreState(self.splitterState) self.btnCollapse.setArrowType(Qt.RightArrow) else: self.splitterState = self.splitter.saveState() self.splitter.setSizes([1, 0]) self.btnCollapse.setArrowType(Qt.LeftArrow) self.helpCollapsed = not self.helpCollapsed def splitterChanged(self, pos, index): if self.splitter.sizes()[1] == 0: self.helpCollapsed = True self.btnCollapse.setArrowType(Qt.LeftArrow) else: self.helpCollapsed = False self.btnCollapse.setArrowType(Qt.RightArrow) def openHelp(self): algHelp = self.alg.helpUrl() if not algHelp: algHelp = QgsHelp.helpUrl("processing_algs/{}/{}".format( self.alg.provider().id(), self.alg.id())).toString() if algHelp not in [None, ""]: webbrowser.open(algHelp) def _saveGeometry(self): self.settings.setValue("/Processing/dialogBaseSplitter", self.splitter.saveState()) self.settings.setValue("/Processing/dialogBase", self.saveGeometry()) class InvalidParameterValue(Exception): def __init__(self, param, widget): (self.parameter, self.widget) = (param, widget)
class AlgorithmDialogBase(BASE, WIDGET): def __init__(self, alg): super(AlgorithmDialogBase, self).__init__(iface.mainWindow() if iface else None) self.setupUi(self) # don't collapse parameters panel self.splitter.setCollapsible(0, False) # add collapse button to splitter splitterHandle = self.splitter.handle(1) handleLayout = QVBoxLayout() handleLayout.setContentsMargins(0, 0, 0, 0) self.btnCollapse = QToolButton(splitterHandle) self.btnCollapse.setAutoRaise(True) self.btnCollapse.setFixedSize(12, 12) self.btnCollapse.setCursor(Qt.ArrowCursor) handleLayout.addWidget(self.btnCollapse) handleLayout.addStretch() splitterHandle.setLayout(handleLayout) self.settings = QgsSettings() self.splitter.restoreState(self.settings.value("/Processing/dialogBaseSplitter", QByteArray())) self.restoreGeometry(self.settings.value("/Processing/dialogBase", QByteArray())) self.splitterState = self.splitter.saveState() self.splitterChanged(0, 0) self.executed = False self.mainWidget = None self.alg = alg self.setWindowTitle(self.alg.displayName()) self.buttonBox.rejected.connect(self.reject) self.buttonBox.accepted.connect(self.accept) # Rename OK button to Run self.btnRun = self.buttonBox.button(QDialogButtonBox.Ok) self.btnRun.setText(self.tr('Run')) self.buttonCancel.setEnabled(False) self.btnClose = self.buttonBox.button(QDialogButtonBox.Close) self.buttonBox.helpRequested.connect(self.openHelp) self.btnCollapse.clicked.connect(self.toggleCollapsed) self.splitter.splitterMoved.connect(self.splitterChanged) # desktop = QDesktopWidget() # if desktop.physicalDpiX() > 96: # self.txtHelp.setZoomFactor(desktop.physicalDpiX() / 96) algHelp = self.formatHelp(self.alg) if algHelp is None: self.textShortHelp.hide() else: self.textShortHelp.document().setDefaultStyleSheet('''.summary { margin-left: 10px; margin-right: 10px; } h2 { color: #555555; padding-bottom: 15px; } a { text-decoration: none; color: #3498db; font-weight: bold; } p { color: #666666; } b { color: #333333; } dl dd { margin-bottom: 5px; }''') self.textShortHelp.setHtml(algHelp) def linkClicked(url): webbrowser.open(url.toString()) self.textShortHelp.anchorClicked.connect(linkClicked) self.showDebug = ProcessingConfig.getSetting( ProcessingConfig.SHOW_DEBUG_IN_DIALOG) def createFeedback(self): feedback = AlgorithmDialogFeedback(self) feedback.progressChanged.connect(self.setPercentage) feedback.error.connect(self.error) feedback.progress_text.connect(self.setText) feedback.info.connect(self.setInfo) feedback.command_info.connect(self.setCommand) feedback.debug_info.connect(self.setDebugInfo) feedback.console_info.connect(self.setConsoleInfo) self.buttonCancel.clicked.connect(feedback.cancel) return feedback def formatHelp(self, alg): text = alg.shortHelpString() if not text: return None return "<h2>%s</h2>%s" % (alg.displayName(), "".join(["<p>%s</p>" % s for s in text.split("\n")])) def closeEvent(self, event): self._saveGeometry() super(AlgorithmDialogBase, self).closeEvent(event) def setMainWidget(self, widget): if self.mainWidget is not None: QgsProject.instance().layerWasAdded.disconnect(self.mainWidget.layerRegistryChanged) QgsProject.instance().layersWillBeRemoved.disconnect(self.mainWidget.layerRegistryChanged) self.mainWidget = widget self.tabWidget.widget(0).layout().addWidget(self.mainWidget) QgsProject.instance().layerWasAdded.connect(self.mainWidget.layerRegistryChanged) QgsProject.instance().layersWillBeRemoved.connect(self.mainWidget.layerRegistryChanged) def error(self, msg): self.setInfo(msg, True) self.resetGUI() self.tabWidget.setCurrentIndex(1) def resetGUI(self): self.lblProgress.setText('') self.progressBar.setMaximum(100) self.progressBar.setValue(0) self.btnRun.setEnabled(True) self.btnClose.setEnabled(True) def setInfo(self, msg, error=False, escape_html=True): if error: self.txtLog.append('<span style="color:red">{}</span><br />'.format(msg, quote=False)) elif escape_html: self.txtLog.append(html.escape(msg)) else: self.txtLog.append(msg) def setCommand(self, cmd): if self.showDebug: self.txtLog.append('<code>{}<code>'.format(html.escape(cmd, quote=False))) def setDebugInfo(self, msg): if self.showDebug: self.txtLog.append('<span style="color:blue">{}</span>'.format(html.escape(msg, quote=False))) def setConsoleInfo(self, msg): if self.showDebug: self.txtLog.append('<code><span style="color:darkgray">{}</span></code>'.format(html.escape(msg, quote=False))) def setPercentage(self, value): if self.progressBar.maximum() == 0: self.progressBar.setMaximum(100) self.progressBar.setValue(value) def setText(self, text): self.lblProgress.setText(text) self.setInfo(text, False) def getParamValues(self): return {} def accept(self): pass def reject(self): self._saveGeometry() super(AlgorithmDialogBase, self).reject() def finish(self, successful, result, context, feedback): pass def toggleCollapsed(self): if self.helpCollapsed: self.splitter.restoreState(self.splitterState) self.btnCollapse.setArrowType(Qt.RightArrow) else: self.splitterState = self.splitter.saveState() self.splitter.setSizes([1, 0]) self.btnCollapse.setArrowType(Qt.LeftArrow) self.helpCollapsed = not self.helpCollapsed def splitterChanged(self, pos, index): if self.splitter.sizes()[1] == 0: self.helpCollapsed = True self.btnCollapse.setArrowType(Qt.LeftArrow) else: self.helpCollapsed = False self.btnCollapse.setArrowType(Qt.RightArrow) def openHelp(self): algHelp = self.alg.helpUrl() if not algHelp: algHelp = QgsHelp.helpUrl("processing_algs/{}/{}".format( self.alg.provider().id(), self.alg.id())).toString() if algHelp not in [None, ""]: webbrowser.open(algHelp) def _saveGeometry(self): self.settings.setValue("/Processing/dialogBaseSplitter", self.splitter.saveState()) self.settings.setValue("/Processing/dialogBase", self.saveGeometry()) class InvalidParameterValue(Exception): def __init__(self, param, widget): (self.parameter, self.widget) = (param, widget)
class ExpressionLineEdit(QLineEdit): def __init__(self, column, host=None, parent=None): # Use a different pixmap self._current_profile = current_profile() QLineEdit.__init__(self, parent) self.column = column self._entity = self.column.entity self.layer = self.create_layer() self.host = host # Configure load button self.btn_load = QToolButton(parent) self.btn_load.setCursor(Qt.PointingHandCursor) self.btn_load.setFocusPolicy(Qt.NoFocus) px = GuiUtils.get_icon_pixmap('expression.png') self.btn_load.setIcon(QIcon(px)) self.btn_load.setIconSize(px.size()) self.btn_load.setStyleSheet('background: transparent; padding: 0px; ' 'border: none;') frame_width = self.set_button_minimum_size(self.btn_load) # Ensure that text does not overlay button padding = self.btn_load.sizeHint().width() + frame_width + 1 self.setStyleSheet('padding-right: ' + str(padding * 2) + 'px;') # Set layout self.button_layout = QHBoxLayout(self) self.button_layout.addWidget(self.btn_load, 0, Qt.AlignRight) self.button_layout.setSpacing(0) self.button_layout.setMargin(5) # Readonly as text generated automatically self.setReadOnly(True) # Current model object self._current_item = None def create_layer(self): srid = None column = '' if self.entity.has_geometry_column(): geom_cols = [col.name for col in self.entity.columns.values() if col.TYPE_INFO == 'GEOMETRY'] column = geom_cols[0] geom_col_obj = self.entity.columns[column] if geom_col_obj.srid >= 100000: srid = geom_col_obj.srid layer = vector_layer(self.entity.name, geom_column=column, proj_wkt=srid) return layer def get_feature_value(self, model=None): self.layer.startEditing() feature = None request = QgsFeatureRequest() if model is None: model = self.host.model() request.setFilterFid(model.id) feature_itr = self.layer.getFeatures(request) for feat in feature_itr: feature = feat break exp = QgsExpression(self.column.expression) if exp.hasParserError(): raise Exception(exp.parserErrorString()) exp.prepare(self.layer.fields()) if feature is not None: value = exp.evaluate(feature) return value else: return None def set_button_minimum_size(self, button): """ Sets the minimum button size. :param button: The button to be set. :type button: QToolButton :return: Returns the frame width of the button :rtype: Integer """ frame_width = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth) msz = self.minimumSizeHint() self.setMinimumSize( max( msz.width(), button.sizeHint().height() + frame_width * 2 + 2 ), max( msz.height(), button.sizeHint().height() + frame_width * 2 + 2 ) ) return frame_width @property def entity(self): """ :return: Returns the entity object corresponding to this widget. :rtype: Entity """ return self._entity def clear_line_edit(self): """ Clears the text in the line edit. """ self.clear() def on_expression_triggered(self, model=None): """ Slot raised to load browser for selecting foreign key entities. To be implemented by subclasses. """ self.format_display(self.get_feature_value(model)) return self.get_feature_value() def format_display(self, value): """ Extract object values to show in the line edit based on the specified display columns. """ if value is not None: self.setText(str(value))
class ForeignKeyLineEdit(QLineEdit): """ Line edit that enables the browsing of related entities defined through foreign key constraint. """ def __init__(self, column, parent=None, pixmap=None, host=None): """ Class constructor. :param column: Column object containing foreign key information. :type column: BaseColumn :param parent: Parent widget for the control. :type parent: QWidget :param pixmap: Pixmap to use for the line edit button. :type pixmap: QPixmap """ QLineEdit.__init__(self, parent) self.column = column self._entity = self.column.entity self.entity_dialog = host # Configure load button self.btn_load = QToolButton(parent) self.btn_load.setCursor(Qt.PointingHandCursor) self.btn_load.setFocusPolicy(Qt.NoFocus) px = GuiUtils.get_icon_pixmap('select_record.png') if not pixmap is None: px = pixmap self.btn_load.setIcon(QIcon(px)) self.btn_load.setIconSize(px.size()) self.btn_load.setStyleSheet('background: transparent; padding: 0px; ' 'border: none;') self.btn_load.clicked.connect(self.on_load_foreign_key_browser) clear_px = GuiUtils.get_icon_pixmap('clear.png') self.btn_clear = QToolButton(parent) self.btn_clear.setCursor(Qt.PointingHandCursor) self.btn_clear.setFocusPolicy(Qt.NoFocus) self.btn_clear.setIcon(QIcon(clear_px)) self.btn_clear.setIconSize(clear_px.size()) self.btn_clear.setStyleSheet('background: transparent; padding: 0px; ' 'border: none;') self.btn_clear.clicked.connect(self.clear_line_edit) frame_width = self.set_button_minimum_size(self.btn_load) self.set_button_minimum_size(self.btn_clear) # Ensure that text does not overlay button padding = self.btn_load.sizeHint().width() + frame_width + 1 self.setStyleSheet('padding-right: ' + str(padding * 2) + 'px;') # Set layout self.button_layout = QHBoxLayout(self) self.button_layout.addWidget(self.btn_clear, 0, Qt.AlignRight) self.button_layout.addWidget(self.btn_load, 0, Qt.AlignRight) self.button_layout.setSpacing(0) self.button_layout.setMargin(5) self.btn_clear.setVisible(False) # Readonly as text is loaded from the related entity self.setReadOnly(True) # Current model object self._current_item = None def set_button_minimum_size(self, button): """ Sets the minimum button size. :param button: The button to be set. :type button: QToolButton :return: Returns the frame width of the button :rtype: Integer """ frame_width = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth) msz = self.minimumSizeHint() self.setMinimumSize( max( msz.width(), button.sizeHint().height() + frame_width * 2 + 2 ), max( msz.height(), button.sizeHint().height() + frame_width * 2 + 2 ) ) return frame_width @property def current_item(self): return self._current_item @current_item.setter def current_item(self, value): # Update display every time the current item is changed. self._current_item = value self.format_display() @property def entity(self): """ :return: Returns the entity object corresponding to this widget. :rtype: Entity """ return self._entity def clear_line_edit(self): """ Clears the text in the line edit. """ self.clear() self.hide_clear_button() def hide_clear_button(self): """ Hides the clear button. """ self.btn_clear.setVisible(False) self.button_layout.setStretch(0, 0) def show_clear_button(self): """ Shows the clear button if a text exists. """ if len(self.text()) > 0: self.btn_clear.setVisible(True) self.button_layout.setStretch(0, 5) def on_load_foreign_key_browser(self): """ Slot raised to load browser for selecting foreign key entities. To be implemented by subclasses. """ raise NotImplementedError def format_display(self): """ Extract object values to show in the line edit based on the specified display columns. """ raise NotImplementedError def parent_entity_model(self): """ :return: Returns the database model corresponding to the parent table of the relation defined by this column. Please note that the database model will not contain relationship configurations in its attributes. :rtype: object """ entity = self.column.entity_relation.parent return entity_model(entity, entity_only=True) def load_current_item_from_id(self, id): """ Loads the current item from the id corresponding to the primary key. :param id: Primary key of the referenced entity. :type id: int """ QApplication.processEvents() model = self.parent_entity_model() if model is None: return model_obj = model() res = model_obj.queryObject().filter(model.id == id).first() if not res is None: self.current_item = res