class BigList(Ui_BigList, QWidget): itemselected = pyqtSignal(QModelIndex) closewidget = pyqtSignal() savewidget = pyqtSignal() def __init__(self, parent=None, centeronparent=False, showsave=True): super(BigList, self).__init__(parent) self.setupUi(self) self.centeronparent = centeronparent self.listView.clicked.connect(self.selected) self.saveButton.pressed.connect(self.savewidget.emit) self.closebutton.pressed.connect(self.closewidget.emit) self._index = None self.search.textEdited.connect(self.set_filter) self.filtermodel = QSortFilterProxyModel() self.filtermodel.setFilterCaseSensitivity(Qt.CaseInsensitive) self.listView.setModel(self.filtermodel) self.listView.setWordWrap(True) self.saveButton.setVisible(showsave) utils.install_touch_scroll(self.listView) def set_filter(self, text): self.filtermodel.setFilterRegExp(text + ".*") def selected(self, index): self._index = index self._index = self.filtermodel.mapToSource(index) self.itemselected.emit(self._index) def setmodel(self, model): self.filtermodel.setSourceModel(model) def setlabel(self, fieldname): self.fieldnameLabel.setText(fieldname) def currentindex(self): return self._index def setcurrentindex(self, index): if index is None: index = QModelIndex() if isinstance(index, int): index = self.listView.model().index(index, 0) self.listView.setCurrentIndex(index) def show(self): super(BigList, self).show() if self.centeronparent: width = self.parent().width() height = self.parent().height() self.move(width / 4, 0) self.resize(QSize(width / 2, height))
class ExtendedComboBox(QComboBox): def __init__(self, parent=None): super(ExtendedComboBox, self).__init__(parent) self.setFocusPolicy(Qt.StrongFocus) self.setEditable(True) self.pFilterModel = QSortFilterProxyModel(self) self.pFilterModel.setFilterCaseSensitivity(Qt.CaseInsensitive) self.pFilterModel.setSourceModel(self.model()) self.completer = QCompleter(self.pFilterModel, self) self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion) self.completer.popup().setStyleSheet("min-height: 150px") self.completer.popup().setAlternatingRowColors(True) self.setCompleter(self.completer) self.lineEdit().textEdited[str].connect(self.pFilterModel.setFilterFixedString)
class ExtendedComboBox(QComboBox): def __init__(self, parent=None): super(ExtendedComboBox, self).__init__(parent) self.setFocusPolicy(Qt.StrongFocus) self.setEditable(True) self.pFilterModel = QSortFilterProxyModel(self) self.pFilterModel.setFilterCaseSensitivity(Qt.CaseInsensitive) self.pFilterModel.setSourceModel(self.model()) self.completer = QCompleter(self.pFilterModel, self) self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion) self.completer.popup().setStyleSheet('min-height: 150px') self.completer.popup().setAlternatingRowColors(True) self.setCompleter(self.completer) self.lineEdit().textEdited[unicode].connect( self.pFilterModel.setFilterFixedString)
class ExtendedComboBox(QComboBox): """Extended class of QComboBox so we can perform a filtering of items. """ def __init__(self, parent=None): super(ExtendedComboBox, self).__init__(parent) self.setFocusPolicy(Qt.StrongFocus) self.setEditable(True) # add a filter model to filter matching items self.pFilterModel = QSortFilterProxyModel(self) self.pFilterModel.setFilterCaseSensitivity(Qt.CaseInsensitive) self.pFilterModel.setSourceModel(self.model()) # add a completer, which uses the filter model self.completer = QCompleter(self.pFilterModel, self) # always show all (filtered) completions self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion) self.setCompleter(self.completer) # connect signals self.lineEdit().textEdited[str].connect( self.pFilterModel.setFilterFixedString) self.completer.activated.connect(self.on_completer_activated) # on selection of an item from the completer, # select the corresponding item from combobox def on_completer_activated(self, text): if text: index = self.findText(text) self.setCurrentIndex(index) # on model change, update the models of the filter and completer as well def setModel(self, model): super(ExtendedComboBox, self).setModel(model) self.pFilterModel.setSourceModel(model) self.completer.setModel(self.pFilterModel) # on model column change, update the model column of # the filter and completer as well def setModelColumn(self, column): self.completer.setCompletionColumn(column) self.pFilterModel.setFilterKeyColumn(column) super(ExtendedComboBox, self).setModelColumn(column)
class FilteredComboBox(QComboBox): """Custom QComboBox with filtering option.""" def __init__(self, parent=None): super(FilteredComboBox, self).__init__(parent) self.setFocusPolicy(Qt.StrongFocus) self.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLength) self.setEditable(True) self.filter_proxy_model = QSortFilterProxyModel(self) self.filter_proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive) self.filter_proxy_model.setSortCaseSensitivity(Qt.CaseInsensitive) self.filter_proxy_model.setSourceModel(self.model()) self.completer = QCompleter(self.filter_proxy_model, self) self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion) self.setCompleter(self.completer) self.setMinimumSize(150, 25) self.setFont(QFont("Segoe UI", 10)) self.setStyleSheet("QComboBox {background-color: white;}") self.setMaxVisibleItems(10) self.completer.activated.connect(self.on_completer_activated) self.lineEdit().textEdited.connect( self.filter_proxy_model.setFilterFixedString) def on_completer_activated(self, text): """Set active combobox item when a completer item is picked.""" if not text: return idx = self.findText(text) self.setCurrentIndex(idx) self.activated[str].emit(self.itemText(idx)) def setModel(self, model): """Set completer model after the combobox model.""" super(FilteredComboBox, self).setModel(model) self.filter_proxy_model.setSourceModel(model) self.completer.setModel(self.filter_proxy_model) def setModelColumn(self, column_idx): """Set the correct column for completer and combobox model using column index.""" self.completer.setCompletionColumn(column_idx) self.filter_proxy_model.setFilterKeyColumn(column_idx) super(FilteredComboBox, self).setModelColumn(column_idx)
class ExtendedCombobox(QComboBox): """ Overwrite combobox to provide text filtering of combobox list. """ def __init__(self, parent): """ Initialise ExtendedCombobox :param parent: Parent of combobox :type parent: PyQt5.QtWidgets.QWidget """ super().__init__(parent) self.setFocusPolicy(Qt.StrongFocus) self.setEditable(True) self.completer = QCompleter(self) # always show all completions self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion) self.p_filter_model = QSortFilterProxyModel(self) self.p_filter_model.setFilterCaseSensitivity(Qt.CaseInsensitive) self.completer.setPopup(self.view()) self.setCompleter(self.completer) self.lineEdit().textEdited.connect(self.p_filter_model.setFilterFixedString) self.completer.activated.connect(self.setTextIfCompleterIsClicked) def setModel(self, model): # pylint:disable=invalid-name """ Set the model to use the Filter model :param model: The model to be used by the combobox :type model: PyQt5.QtGui.QStandardItemModel """ super().setModel(model) self.p_filter_model.setSourceModel(model) self.completer.setModel(self.p_filter_model) def setModelColumn(self, column): # pylint:disable=invalid-name """ :param model: The model to be used by the combobox :type model: PyQt5.QtGui.QStandardItemModel """ self.completer.setCompletionColumn(column) self.p_filter_model.setFilterKeyColumn(column) super().setModelColumn(column) def view(self): """ A QListView of items stored in the model :return: items stored in the model :rtype: PyQt5.QtWidgets.QListView """ return self.completer.popup() def index(self): """ Index of the current item in the combobox. :return: index of the current item :rtype: int """ return self.currentIndex() def setTextIfCompleterIsClicked(self, text): # pylint:disable=invalid-name """ :param text: The current text of the qlineedit :type text: str If the combobx lineedit is clicked, set the lineedits current item as the combobox's current item """ if text: index = self.findText(text) self.setCurrentIndex(index)
class ThreeDiResultSelectionWidget(QWidget, FORM_CLASS): """Dialog for selecting model (spatialite and result files netCDFs) TODO: actually, it is two dialogs in one: a selector of results and a separate downloader of results. They are not really connected. So splitting them makes the code simpler. And, more importantly, it makes the UI clearer. """ closingDialog = pyqtSignal() def __init__( self, parent=None, iface=None, ts_datasources=None, downloadable_results=None, tool=None, ): """Constructor :parent: Qt parent Widget :iface: QGiS interface :ts_datasources: TimeseriesDatasourceModel instance :downloadable_results: DownloadableResultModel instance :tool: the tool class which instantiated this widget. Is used here for storing volatile information """ super().__init__(parent) self.tool = tool self.iface = iface self.setupUi(self) # login administration self.login_dialog = LoginDialog() # NOTE: autoDefault was set on ``log_in_button`` (via Qt Designer), # which makes pressing Enter work for logging in. self.login_dialog.log_in_button.clicked.connect(self.handle_log_in) # set models on table views and update view columns self.ts_datasources = ts_datasources self.resultTableView.setModel(self.ts_datasources) self.ts_datasources.set_column_sizes_on_view(self.resultTableView) self.downloadable_results = downloadable_results self.download_proxy_model = QSortFilterProxyModel() self.download_proxy_model.setSourceModel(self.downloadable_results) self.download_proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive) self.filterLineEdit.textChanged.connect( self.download_proxy_model.setFilterFixedString) self.downloadResultTableView.setModel(self.download_proxy_model) self.toggle_login_interface() # connect signals self.selectTsDatasourceButton.clicked.connect( self.select_ts_datasource) self.closeButton.clicked.connect(self.close) self.removeTsDatasourceButton.clicked.connect( self.remove_selected_ts_datasource) self.selectModelSpatialiteButton.clicked.connect( self.select_model_spatialite_file) self.loginButton.clicked.connect(self.on_login_button_clicked) # set combobox list combo_list = [ datasource for datasource in self.get_3di_spatialites_legendlist() ] if (self.ts_datasources.model_spatialite_filepath and self.ts_datasources.model_spatialite_filepath not in combo_list): combo_list.append(self.ts_datasources.model_spatialite_filepath) if not self.ts_datasources.model_spatialite_filepath: combo_list.append("") self.modelSpatialiteComboBox.addItems(combo_list) if self.ts_datasources.model_spatialite_filepath: current_index = self.modelSpatialiteComboBox.findText( self.ts_datasources.model_spatialite_filepath) self.modelSpatialiteComboBox.setCurrentIndex(current_index) else: current_index = self.modelSpatialiteComboBox.findText("") self.modelSpatialiteComboBox.setCurrentIndex(current_index) self.modelSpatialiteComboBox.currentIndexChanged.connect( self.model_spatialite_change) self.thread = None def on_close(self): """ Clean object on close """ self.selectTsDatasourceButton.clicked.disconnect( self.select_ts_datasource) self.closeButton.clicked.disconnect(self.close) self.removeTsDatasourceButton.clicked.disconnect( self.remove_selected_ts_datasource) self.selectModelSpatialiteButton.clicked.disconnect( self.select_model_spatialite_file) self.loginButton.clicked.disconnect(self.on_login_button_clicked) # stop the thread when we close the widget if self.thread: self.thread.output.disconnect(self.update_download_result_model) self.thread.stop() def closeEvent(self, event): """ Close widget, called by Qt on close :param event: QEvent, close event """ self.closingDialog.emit() self.on_close() event.accept() def keyPressEvent(self, event): """Handle key press events on the widget.""" # Close window if the Escape key is pressed if event.key() == Qt.Key_Escape: self.close() def select_ts_datasource(self): """ Open File dialog for selecting netCDF result files, triggered by button :return: boolean, if file is selected """ settings = QSettings("3di", "qgisplugin") try: init_path = settings.value("last_used_datasource_path", type=str) except TypeError: logger.debug( "Last used datasource path is no string, setting it to our home dir." ) init_path = os.path.expanduser("~") filename, __ = QFileDialog.getOpenFileName( self, "Open resultaten file", init_path, "NetCDF (subgrid_map.nc results_3di.nc)", ) if filename: # Little test for checking if there is an id mapping file available # If not we check if an .h5 file is available # If not we're not going to proceed datasource_type = detect_netcdf_version(filename) logger.info("Netcdf result file selected: %s, type is %s", filename, datasource_type) if datasource_type == "netcdf-groundwater": try: find_h5_file(filename) except FileNotFoundError: logger.warning( "Groundwater h5 not found (%s), warning the user.", filename) pop_up_info( "You selected a netcdf that was created " "(after May 2018) with a 3Di calculation" "core that is able to include groundwater" " calculations. The ThreeDiToolbox reads " "this netcdf together with an .h5 file, we " "could however not find this .h5 file. Please " "add this file next to the netcdf and try " "again", title="Error", ) return False elif datasource_type == "netcdf": logger.warning( "Result file (%s) version is too old. Warning the user.", filename) pop_up_info( "The selected result data is too old and no longer " "supported in this version of ThreediToolbox. Please " "recalculate the results with a newer version of the " "threedicore or use the ThreediToolbox plugin for QGIS 2", title="Error", ) # TODO: the below "return False" was previously missing. Check # if Reinout was right in adding it :-) return False items = [{ "type": datasource_type, "name": os.path.basename(filename).lower().rstrip(".nc"), "file_path": filename, }] self.ts_datasources.insertRows(items) settings.setValue("last_used_datasource_path", os.path.dirname(filename)) return True return False def remove_selected_ts_datasource(self): """ Remove selected result files from model, called by 'remove' button """ selection_model = self.resultTableView.selectionModel() # get unique rows in selected fields rows = set( [index.row() for index in selection_model.selectedIndexes()]) for row in reversed(sorted(rows)): self.ts_datasources.removeRows(row, 1) def get_3di_spatialites_legendlist(self): """ Get list of spatialite data sources currently active in canvas :return: list of strings, unique spatialite paths """ tdi_spatialites = [] for layer in self.iface.layerTreeView().selectedLayers(): if (layer.name() in list(LAYER_QH_TYPE_MAPPING.keys()) and layer.dataProvider().name() == "spatialite"): source = layer.dataProvider().dataSourceUri().split("'")[1] if source not in tdi_spatialites: tdi_spatialites.append(source) return tdi_spatialites def model_spatialite_change(self, nr): """ Change active modelsource. Called by combobox when selected spatialite changed :param nr: integer, nr of item selected in combobox """ filepath = self.modelSpatialiteComboBox.currentText() logger.info("Different spatialite 3di model selected: %s", filepath) self.ts_datasources.model_spatialite_filepath = filepath # Just emitting some dummy model indices cuz what else can we do, there # is no corresponding rows/columns that's been changed self.ts_datasources.dataChanged.emit(QModelIndex(), QModelIndex()) def select_model_spatialite_file(self): """ Open file dialog on click on button 'load model' :return: Boolean, if file is selected """ settings = QSettings("3di", "qgisplugin") try: init_path = settings.value("last_used_spatialite_path", type=str) except TypeError: logger.debug( "Last used datasource path is no string, setting it to our home dir." ) init_path = os.path.expanduser("~") filepath, __ = QFileDialog.getOpenFileName( self, "Open 3Di model spatialite file", init_path, "Spatialite (*.sqlite)") if filepath == "": return False self.ts_datasources.spatialite_filepath = filepath index_nr = self.modelSpatialiteComboBox.findText(filepath) if index_nr < 0: self.modelSpatialiteComboBox.addItem(filepath) index_nr = self.modelSpatialiteComboBox.findText(filepath) self.modelSpatialiteComboBox.setCurrentIndex(index_nr) add_spatialite_connection(filepath, self.iface) settings.setValue("last_used_spatialite_path", os.path.dirname(filepath)) return True def on_login_button_clicked(self): """Handle log in and out.""" if self.logged_in: self.handle_log_out() else: self.login_dialog.user_name_input.setFocus() self.login_dialog.show() def handle_log_out(self): self.set_logged_out_status() if self.thread: self.thread.stop() num_rows = len(self.downloadable_results.rows) self.downloadable_results.removeRows(0, num_rows) self.toggle_login_interface() def toggle_login_interface(self): """Enable/disable aspects of the interface based on login status.""" # TODO: better to use signals maybe? if self.logged_in: self.loginButton.setText("Log out") self.downloadResultTableView.setEnabled(True) self.downloadResultButton.setEnabled(True) else: self.loginButton.setText("Log in") self.downloadResultTableView.setEnabled(False) self.downloadResultButton.setEnabled(False) def handle_log_in(self): """Handle logging in and populating DownloadableResultModel.""" # Get the username and password username = self.login_dialog.user_name_input.text() password = self.login_dialog.user_password_input.text() if username == "" or password == "": pop_up_info("Username or password cannot be empty.") return try: scenarios_endpoint = Endpoint(username=username, password=password, endpoint="scenarios") endpoint = scenarios_endpoint.get_paginated(page_size=10) except HTTPError as e: logger.exception("Error trying to log in") if e.code == 401: pop_up_info("Incorrect username and/or password.") else: pop_up_info(str(e)) return self.set_logged_in_status(username, password) self.toggle_login_interface() # don't persist info in the dialog: useful when logged out self.login_dialog.user_name_input.clear() self.login_dialog.user_password_input.clear() # start thread self.thread = DownloadWorker(endpoint=endpoint, username=username, password=password) self.thread.connection_failure.connect(self.handle_connection_failure) self.thread.output.connect(self.update_download_result_model) self.thread.start() # return to widget self.login_dialog.close() def update_download_result_model(self, items): self.downloadable_results.insertRows(items) def handle_connection_failure(self, status, reason): pop_up_info("Something went wrong trying to connect to " "lizard: {0} {1}".format(status, reason)) self.handle_log_out() @property def username(self): return self.tool.username @username.setter def username(self, username): self.tool.username = username @property def password(self): return self.tool.password @password.setter def password(self, password): self.tool.password = password @property def logged_in(self): """Return the logged in status.""" return self.tool.logged_in def set_logged_in_status(self, username, password): """Set logged in status to True.""" # TODO: it doesn't really do that. I *suspect* that the parent class # has a logged_in() method that checks if username and password are # set there. It's all quite indirect. self.username = username self.password = password def set_logged_out_status(self): """Set logged in status to False.""" self.username = None self.password = None