class Completer(object): """Comleter class to use in the query text editor.""" # ---------------------------------------------------------------------- def __init__(self): """Initialize Completer class with the keywords and functions.""" with io.open( r'completer_data\keywords.txt', 'r', encoding='utf-8') as f: lowercase_keywords = [k.rstrip().lower() for k in f.readlines()] uppercase_keywords = [k.upper() for k in lowercase_keywords] titlecase_keywords = [k.title() for k in lowercase_keywords] with io.open( r'completer_data\functions.txt', 'r', encoding='utf-8') as f: titlecase_funcs = [f.rstrip() for f in f.readlines()] uppercase_funcs = [f.upper() for f in titlecase_funcs] lowercase_funcs = [f.lower() for f in titlecase_funcs] all_keywords_and_funcs = [ lowercase_keywords, uppercase_keywords, titlecase_keywords, lowercase_funcs, uppercase_funcs, titlecase_funcs, ] self.standard_items = [ keyword for sublist in all_keywords_and_funcs for keyword in sublist ] self.completer = QCompleter(self.standard_items) self.completer.setModelSorting(QCompleter.CaseInsensitivelySortedModel) self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.completer.setWrapAround(False) return # ---------------------------------------------------------------------- def update_completer_string_list(self, items): """Update completer string list to include additional strings. The list of additional strings include geodatabase items. """ cur_items = [] titlecase_items = [i.title() for i in items] uppercase_items = [i.upper() for i in items] lowercase_items = [i.lower() for i in items] cur_items.extend(self.standard_items) cur_items.extend(titlecase_items + uppercase_items + lowercase_items) self.completer.model().setStringList(cur_items) return
def __init__(self, parent): super().__init__(parent) self._tags: list = None self.cat_tags: dict = None self._h_layout = QHBoxLayout(self) self._h_layout.setContentsMargins(0, 0, 0, 0) self._h_layout.setSpacing(1) self._line_edit = QLineEdit(self) self._line_edit.returnPressed.connect(self.__create_tags) self._line_edit.textChanged.connect(self.__on_text_change) self._h_layout.addWidget(self._line_edit, 1) completer = QCompleter(["easy"], self) self._line_edit.setCompleter(completer) self._model_item = completer.model()
class MainWindow(QMainWindow, AllWindows): def __init__(self): """ The .ui File has the following objects: MainWindow --> QMainWindow centralwidget --> QWidget txtSearch --> QLineEdit txtResult --> QTextEdit chkLike --> QCheckBox btnSearch --> QPushButton menubar --> QMenuBar menuReDict --> QMenu actionSettings --> QAction actionQuit --> QAction menuView --> QMenu actionZoomIn --> QAction actionZoomOut --> QAction menuHistory --> QMenu actionHistory --> QAction menuHelp --> QMenu actionAbout --> QAction statusbar --> QStatusBar """ super(QMainWindow, self).__init__() super(AllWindows, self).__init__() self.init_ui() def init_ui(self): self.ui = uic.loadUi(os.path.abspath("_gui/main.ui"), self) self.search = Search() self.load_form_pers(self) self.signal = MySignal() self.ui.btnSearch.clicked.connect(self.search_word) # Read Font-Size from Settings and apply to Result self.font = QtGui.QFont() self.font.setPointSize(int(self.db.get_property(2))) self.ui.txtResult.setFont(self.font) # MenuBar self.ui.menubar.setNativeMenuBar(False) self.txtSearch.textEdited.connect(self.txt_search_changed) exitAct = self.ui.actionQuit exitAct.setShortcut('Ctrl+Q') exitAct.triggered.connect(self.exit) zoominAct = self.ui.actionZoomIn zoominAct.setShortcut('Ctrl++') zoominAct.triggered.connect(self.zoom_in) zoomoutAct = self.ui.actionZoomOut zoomoutAct.setShortcut('Ctrl+-') zoomoutAct.triggered.connect(self.zoom_out) HistoryAct = self.ui.actionHistory HistoryAct.triggered.connect(self.history) AboutAct = self.ui.actionAbout AboutAct.triggered.connect(self.about) SettingsAct = self.ui.actionSettings SettingsAct.triggered.connect(self.settings_) self.signal.startLoading.connect(self.loading) self.signal.stopLoading.connect(self.stop_loading) self.threadpool = QThreadPool() self.open_thread() self.completer = QCompleter([]) self.completer.setFilterMode(Qt.MatchContains) self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.completer.setCompletionMode(QCompleter.PopupCompletion) self.completer.activated.connect(self.search_word) model = QStringListModel() self.completer.setModel(model) delegate = CompleterDelegate(self.ui.txtSearch) self.completer.popup().setItemDelegate(delegate) self.completer.popup().setFont(self.font) self.txtSearch.setCompleter(self.completer) self.ui.txtResult.setContextMenuPolicy(Qt.ActionsContextMenu) lookup = QAction("look-up", self) lookup.triggered.connect(self.look_up) self.ui.txtResult.addAction(lookup) self.spinner = QtWaitingSpinner(self, True, True, Qt.ApplicationModal) self.spinner.setInnerRadius(150) self.spinner.setNumberOfLines(50) self.spinner.setColor(QColor(230, 126, 34)) self.ui.txtSearch.setFocus() self.ui.txtResult.installEventFilter(self) self.ui.txtResult.grabGesture(Qt.PinchGesture) self.ui.txtResult.grabGesture(Qt.TapAndHoldGesture) QScroller.grabGesture(self.txtResult.viewport(), QScroller.LeftMouseButtonGesture) clipboard_enabled = self.db.get_property(5) if clipboard_enabled == '1': self.clip = QApplication.clipboard() # on Mac unfortunately this event will only be fired when the application # is active, i.e. not in background. On Windows it works fine. It might # have to come back to a timer with an endless for loop which checks with # paste whether something has changed. But how then to count Ctrl-C presses? self.clip.changed.connect(self.clipboard_changed) self.timer = QTimer() self.timer.timeout.connect(self.watch_clipboard) seconds = int(self.db.get_property(6)) self.timer.start(seconds * 1000) def watch_clipboard(self): global clipboard_event clipboard_event = 0 def clipboard_changed(self): global clipboard_event clipboard_event += 1 setting = int(self.db.get_property(7)) if clipboard_event == setting: word = clpboard_cleanse(self.clip.text()) cursor = self.txtResult.textCursor() sel = cursor.selectedText() if word != sel: self.search_word(word) self.bring_to_front() def eventFilter(self, o, event): # handle gesture event from txtResult if event.type() == QEvent.Gesture: fontsize = int(self.db.get_property(2)) g = event.gesture(Qt.PinchGesture) f = event.gesture(Qt.TapAndHoldGesture) # handle pinch if g != None: scale = g.scaleFactor() fontsize = ceil(fontsize * scale) self.zoom_fix(fontsize) self.ui.update() # handle tap elif f != None: c = self.ui.txtResult.textCursor() if c.selectedText() == '': c.movePosition(QTextCursor.StartOfWord, QTextCursor.MoveAnchor) c.movePosition(QTextCursor.EndOfWord, QTextCursor.KeepAnchor) self.ui.txtResult.setTextCursor(c) return True return False def loading(self): self.spinner.start() def stop_loading(self): self.spinner.stop() self.txtSearch.setPlaceholderText("Enter word...") self.txtSearch.setFocus() def txt_search_changed(self, changeValue): global autocomplete entries = list() names = autocomplete.search(word=changeValue, max_cost=3, size=10) for name in names: entries.append(name[0]) model = self.completer.model() model.setStringList(entries) def open_thread(self): worker = Worker(self.initialize_things) self.threadpool.start(worker) def initialize_things(self): self.signal.startLoading.emit() db = Database() prop = db.get_property(4) global autocomplete sql = ('SELECT DISTINCT Word FROM Word' if prop == 'Lemmata' else 'SELECT Flection FROM Flection') cmwords = db.read_database(sql) words = dict() for r in cmwords: words.setdefault(r[0], {}) del cmwords autocomplete = AutoComplete(words=words) self.signal.stopLoading.emit() def bring_to_front(self): """ Brings the window to the front and places the cursor in the textbox """ self.txtSearch.setFocus() self.setWindowState(self.windowState() & ~Qt.WindowMinimized | Qt.WindowActive) self.raise_() self.activateWindow() self.show() def keyPressEvent(self, qKeyEvent): if qKeyEvent.key() == Qt.Key_Return: self.search_word(self.ui.txtSearch.text().lower()) else: super().keyPressEvent(qKeyEvent) def exit(self): global subThread subThread = False qApp.quit() def about(self): About() def settings_(self): Settings_() def search_word(self, word=False): self.ui.txtResult.clear() if not word: wrd = self.ui.txtSearch.text().lower() result = self.search.search_word(wrd, self.ui.chkLike.checkState()) else: self.ui.txtSearch.setText(word.lower()) result = self.search.search_word(word, self.ui.chkLike.checkState()) self.set_result(result) self.ui.txtSearch.selectAll() def set_result(self, result): self.ui.txtResult.append(result) self.ui.txtResult.moveCursor(QtGui.QTextCursor.Start) def look_up(self): cursor = self.txtResult.textCursor() LookupDialog(cursor.selectedText()).search_word() def set_result(self, result: object): self.ui.txtResult.append(result) self.ui.txtResult.moveCursor(QtGui.QTextCursor.Start) def zoom_out(self): size = int(self.db.get_property(2)) - 1 self.font.setPointSize(size) self.ui.txtResult.setFont(self.font) self.db.set_property(2, size) def zoom_in(self): size = int(self.db.get_property(2)) + 1 self.font.setPointSize(size) self.ui.txtResult.setFont(self.font) self.db.set_property(2, size) def zoom_fix(self, size: int) -> None: self.font.setPointSize(size) self.ui.txtResult.setFont(self.font) self.db.set_property(2, size) return None def history(self): History(self.search_word) def closeEvent(self, event): global subThread subThread = False self.save_form_pers(self) QtWidgets.QMainWindow.closeEvent(self, event)
class DiscoveryPlugin: def __init__(self, _iface): # Save reference to the QGIS interface self.iface = _iface # initialize plugin directory self.plugin_dir = os.path.dirname(__file__) # Variables to facilitate delayed queries and database connection management self.db_timer = QTimer() self.line_edit_timer = QTimer() self.line_edit_timer.setSingleShot(True) self.line_edit_timer.timeout.connect(self.reset_line_edit_after_move) self.next_query_time = None self.last_query_time = time.time() self.db_conn = None self.search_delay = 0.5 # s self.query_sql = '' self.query_text = '' self.query_dict = {} self.db_idle_time = 60.0 # s self.display_time = 5000 # ms self.bar_info_time = 30 # s self.search_results = [] self.tool_bar = None self.search_line_edit = None self.completer = None self.conn_info = {} self.marker = QgsVertexMarker(iface.mapCanvas()) self.marker.setIconSize(15) self.marker.setPenWidth(2) self.marker.setColor(QColor(226, 27, 28)) #51,160,44)) self.marker.setZValue(11) self.marker.setVisible(False) self.marker2 = QgsVertexMarker(iface.mapCanvas()) self.marker2.setIconSize(16) self.marker2.setPenWidth(4) self.marker2.setColor(QColor(255, 255, 255, 200)) self.marker2.setZValue(10) self.marker2.setVisible(False) self.is_displayed = False self.rubber_band = QgsRubberBand(iface.mapCanvas(), False) self.rubber_band.setVisible(False) self.rubber_band.setWidth(3) self.rubber_band.setStrokeColor(QColor(226, 27, 28)) self.rubber_band.setFillColor(QColor(226, 27, 28, 63)) def initGui(self): # Create a new toolbar self.tool_bar = self.iface.addToolBar('Discovery') self.tool_bar.setObjectName('Discovery_Plugin') # Create action that will start plugin configuration self.action_config = QAction( QIcon(os.path.join(self.plugin_dir, "discovery_logo.png")), u"Configure Discovery", self.tool_bar) self.action_config.triggered.connect(self.show_config_dialog) self.tool_bar.addAction(self.action_config) # Add combobox for configs self.config_combo = QComboBox() settings = QgsSettings() settings.beginGroup("/Discovery") config_list = settings.value("config_list") if config_list: for conf in config_list: self.config_combo.addItem(conf) elif settings.childGroups(): # support for prev version key = "Config1" config_list = [] config_list.append(key) settings.setValue("config_list", config_list) self.config_combo.addItem(key) settings.setValue(key + "data_type", settings.value("data_type")) settings.setValue(key + "file", settings.value("file")) settings.setValue(key + "connection", settings.value("connection")) settings.setValue(key + "schema", settings.value("schema")) settings.setValue(key + "table", settings.value("table")) settings.setValue(key + "search_column", settings.value("search_column")) settings.setValue(key + "echo_search_column", settings.value("echo_search_column")) settings.setValue(key + "display_columns", settings.value("display_columns")) settings.setValue(key + "geom_column", settings.value("geom_column")) settings.setValue(key + "scale_expr", settings.value("scale_expr")) settings.setValue(key + "bbox_expr", settings.value("bbox_expr")) delete_config_from_settings("", settings) self.tool_bar.addWidget(self.config_combo) # Add search edit box self.search_line_edit = QgsFilterLineEdit() self.search_line_edit.setPlaceholderText('Search for...') self.search_line_edit.setMaximumWidth(768) self.tool_bar.addWidget(self.search_line_edit) self.config_combo.currentIndexChanged.connect( self.change_configuration) # Set up the completer self.completer = QCompleter([]) # Initialise with en empty list self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.completer.setMaxVisibleItems(1000) self.completer.setModelSorting( QCompleter.UnsortedModel) # Sorting done in PostGIS self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion ) # Show all fetched possibilities self.completer.activated[QModelIndex].connect(self.on_result_selected) self.completer.highlighted[QModelIndex].connect( self.on_result_highlighted) self.search_line_edit.setCompleter(self.completer) # Connect any signals self.search_line_edit.textEdited.connect(self.on_search_text_changed) # Search results self.search_results = [] # Set up a timer to periodically perform db queries as required self.db_timer.timeout.connect(self.do_db_operations) self.db_timer.start(100) # Read config self.read_config(config_list[0] if config_list else "") self.locator_filter = locator_filter.DiscoveryLocatorFilter(self) self.iface.registerLocatorFilter(self.locator_filter) # Debug # import pydevd; pydevd.settrace('localhost', port=5678) def unload(self): # Stop timer self.db_timer.stop() # Disconnect any signals self.db_timer.timeout.disconnect(self.do_db_operations) self.completer.highlighted[QModelIndex].disconnect( self.on_result_highlighted) self.completer.activated[QModelIndex].disconnect( self.on_result_selected) self.search_line_edit.textEdited.disconnect( self.on_search_text_changed) # Remove the new toolbar self.tool_bar.clear() # Clear all actions self.iface.mainWindow().removeToolBar(self.tool_bar) self.iface.deregisterLocatorFilter(self.locator_filter) self.locator_filter = None def clear_suggestions(self): model = self.completer.model() model.setStringList([]) def on_search_text_changed(self, new_search_text): """ This function is called whenever the user modified the search text 1. Open a database connection 2. Make the query 3. Update the QStringListModel with these results 4. Store the other details in self.search_results """ self.query_text = new_search_text if len(new_search_text) < 3: # Clear any previous suggestions in case the user is 'backspacing' self.clear_suggestions() return if self.data_type == "postgres": query_text, query_dict = dbutils.get_search_sql( new_search_text, self.postgisgeomcolumn, self.postgissearchcolumn, self.echosearchcolumn, self.postgisdisplaycolumn, self.extra_expr_columns, self.postgisschema, self.postgistable) self.schedule_search(query_text, query_dict) elif self.data_type == "gpkg": query_text = (new_search_text, self.postgissearchcolumn, self.echosearchcolumn, self.postgisdisplaycolumn.split(","), self.extra_expr_columns, self.layer) self.schedule_search(query_text, None) elif self.data_type == "mssql": query_text = mssql_utils.get_search_sql( new_search_text, self.postgisgeomcolumn, self.postgissearchcolumn, self.echosearchcolumn, self.postgisdisplaycolumn, self.extra_expr_columns, self.postgisschema, self.postgistable) self.schedule_search(query_text, None) def do_db_operations(self): if self.next_query_time is not None and self.next_query_time < time.time( ): # It's time to run a query self.next_query_time = None # Prevent this query from being repeated self.last_query_time = time.time() self.perform_search() else: # We're not performing a query, close the db connection if it's been open for > 60s if time.time() > self.last_query_time + self.db_idle_time: self.db_conn = None def perform_search(self): db = self.get_db() self.search_results = [] suggestions = [] if self.data_type == "postgres": cur = db.cursor() try: cur.execute(self.query_sql, self.query_dict) except psycopg2.Error as e: err_info = "Failed to execute the search query. Please, check your settings. Error message:\n\n" err_info += f"{e.pgerror}" QMessageBox.critical(None, "Discovery", err_info) return result_set = cur.fetchall() elif self.data_type == "mssql": result_set = mssql_utils.execute(db, self.query_sql) elif self.data_type == "gpkg": result_set = gpkg_utils.search_gpkg(*self.query_sql) for row in result_set: geom, epsg, suggestion_text = row[0], row[1], row[2] extra_data = {} for idx, extra_col in enumerate(self.extra_expr_columns): extra_data[extra_col] = row[3 + idx] self.search_results.append( (geom, epsg, suggestion_text, extra_data)) suggestions.append(suggestion_text) model = self.completer.model() model.setStringList(suggestions) self.completer.complete() def schedule_search(self, query_text, query_dict): # Update the search text and the time after which the query should be executed self.query_sql = query_text self.query_dict = query_dict self.next_query_time = time.time() + self.search_delay def show_bar_info(self, info_text): """Optional show info bar message with selected result information""" self.iface.messageBar().clearWidgets() if self.bar_info_time: self.iface.messageBar().pushMessage("Discovery", info_text, level=Qgis.Info, duration=self.bar_info_time) def on_result_selected(self, result_index): # What to do when the user makes a selection self.select_result(self.search_results[result_index.row()]) def select_result(self, result_data): geometry_text, src_epsg, suggestion_text, extra_data = result_data location_geom = QgsGeometry.fromWkt(geometry_text) canvas = self.iface.mapCanvas() dst_srid = canvas.mapSettings().destinationCrs().authid() transform = QgsCoordinateTransform( QgsCoordinateReferenceSystem(src_epsg), QgsCoordinateReferenceSystem(dst_srid), canvas.mapSettings().transformContext()) # Ensure the geometry from the DB is reprojected to the same SRID as the map canvas location_geom.transform(transform) location_centroid = location_geom.centroid().asPoint() # show temporary marker if location_geom.type() == QgsWkbTypes.PointGeometry: self.show_marker(location_centroid) elif location_geom.type() == QgsWkbTypes.LineGeometry or \ location_geom.type() == QgsWkbTypes.PolygonGeometry: self.show_line_rubber_band(location_geom) else: #unsupported geometry type pass # Adjust map canvas extent zoom_method = 'Move and Zoom' if zoom_method == 'Move and Zoom': # with higher priority try to use exact bounding box to zoom to features (if provided) bbox_str = eval_expression(self.bbox_expr, extra_data) rect = bbox_str_to_rectangle(bbox_str) if rect is not None: # transform the rectangle in case of OTF projection rect = transform.transformBoundingBox(rect) else: # bbox is not available - so let's just use defined scale # compute target scale. If the result is 2000 this means the target scale is 1:2000 rect = location_geom.boundingBox() if rect.isEmpty(): scale_denom = eval_expression(self.scale_expr, extra_data, default=2000.) rect = canvas.mapSettings().extent() rect.scale(scale_denom / canvas.scale(), location_centroid) else: # enlarge geom bbox to have some margin rect.scale(1.2) canvas.setExtent(rect) elif zoom_method == 'Move': current_extent = QgsGeometry.fromRect( self.iface.mapCanvas().extent()) dx = location_centroid.x() - location_centroid.x() dy = location_centroid.y() - location_centroid.y() current_extent.translate(dx, dy) canvas.setExtent(current_extent.boundingBox()) canvas.refresh() self.line_edit_timer.start(0) if self.info_to_clipboard: QApplication.clipboard().setText(suggestion_text) suggestion_text += ' (copied to clipboard)' self.show_bar_info(suggestion_text) def on_result_highlighted(self, result_idx): self.line_edit_timer.start(0) def reset_line_edit_after_move(self): self.search_line_edit.setText(self.query_text) def get_db(self): # Create a new new connection if required if self.db_conn is None: if self.data_type == "postgres": self.db_conn = dbutils.get_connection(self.conn_info) elif self.data_type == "mssql": self.db_conn = mssql_utils.get_mssql_conn(self.conn_info) return self.db_conn def change_configuration(self): self.search_line_edit.setText("") self.line_edit_timer.start(0) self.read_config(self.config_combo.currentText()) def read_config(self, key=""): # the following code reads the configuration file which setups the plugin to search in the correct database, # table and method settings = QgsSettings() settings.beginGroup("/Discovery") connection = settings.value(key + "connection", "", type=str) self.data_type = settings.value(key + "data_type", "", type=str) self.file = settings.value(key + "file", "", type=str) self.postgisschema = settings.value(key + "schema", "", type=str) self.postgistable = settings.value(key + "table", "", type=str) self.postgissearchcolumn = settings.value(key + "search_column", "", type=str) self.echosearchcolumn = settings.value(key + "echo_search_column", True, type=bool) self.postgisdisplaycolumn = settings.value(key + "display_columns", "", type=str) self.postgisgeomcolumn = settings.value(key + "geom_column", "", type=str) if settings.value("marker_time_enabled", True, type=bool): self.display_time = settings.value("marker_time", 5000, type=int) else: self.display_time = -1 if settings.value("bar_info_time_enabled", True, type=bool): self.bar_info_time = settings.value("bar_info_time", 30, type=int) else: self.bar_info_time = 0 self.info_to_clipboard = settings.value("info_to_clipboard", True, type=bool) scale_expr = settings.value(key + "scale_expr", "", type=str) bbox_expr = settings.value(key + "bbox_expr", "", type=str) if self.is_displayed: self.hide_marker() self.hide_rubber_band() self.is_displayed = False self.make_enabled(False) # assume the config is invalid first self.db_conn = None if self.data_type == "postgres": self.conn_info = dbutils.get_postgres_conn_info(connection) self.layer = None if len(connection) == 0 or len(self.postgisschema) == 0 or len(self.postgistable) == 0 or \ len(self.postgissearchcolumn) == 0 or len(self.postgisgeomcolumn) == 0: return if len(self.conn_info) == 0: iface.messageBar().pushMessage( "Discovery", "The database connection '%s' does not exist!" % connection, level=Qgis.Critical) return if self.data_type == "mssql": self.conn_info = mssql_utils.get_mssql_conn_info(connection) self.layer = None if len(connection) == 0 or len(self.postgisschema) == 0 or len(self.postgistable) == 0 or \ len(self.postgissearchcolumn) == 0 or len(self.postgisgeomcolumn) == 0: return if len(self.conn_info) == 0: iface.messageBar().pushMessage( "Discovery", "The database connection '%s' does not exist!" % connection, level=Qgis.Critical) return elif self.data_type == "gpkg": self.layer = QgsVectorLayer( self.file + '|layername=' + self.postgistable, self.postgistable, 'ogr') self.conn_info = None self.extra_expr_columns = [] self.scale_expr = None self.bbox_expr = None self.make_enabled(True) # optional scale expression when zooming in to results if len(scale_expr) != 0: expr = QgsExpression(scale_expr) if expr.hasParserError(): iface.messageBar().pushMessage("Discovery", "Invalid scale expression: " + expr.parserErrorString(), level=Qgis.Warning) else: self.scale_expr = scale_expr self.extra_expr_columns += expr.referencedColumns() # optional bbox expression when zooming in to results if len(bbox_expr) != 0: expr = QgsExpression(bbox_expr) if expr.hasParserError(): iface.messageBar().pushMessage("Discovery", "Invalid bbox expression: " + expr.parserErrorString(), level=Qgis.Warning) else: self.bbox_expr = bbox_expr self.extra_expr_columns += expr.referencedColumns() def show_config_dialog(self): dlg = config_dialog.ConfigDialog() if (self.config_combo.currentIndex() >= 0): dlg.configOptions.setCurrentIndex(self.config_combo.currentIndex()) if dlg.exec_(): dlg.write_config() self.config_combo.clear() for key in [ dlg.configOptions.itemText(i) for i in range(dlg.configOptions.count()) ]: self.config_combo.addItem(key) self.config_combo.setCurrentIndex(dlg.configOptions.currentIndex()) self.change_configuration() def make_enabled(self, enabled): self.search_line_edit.setEnabled(enabled) self.search_line_edit.setPlaceholderText( "Search for..." if enabled else "Search disabled: check configuration") def show_marker(self, point): for m in [self.marker, self.marker2]: m.setCenter(point) m.setOpacity(1.0) m.setVisible(True) if self.display_time == -1: self.is_displayed = True else: QTimer.singleShot(self.display_time, self.hide_marker) def hide_marker(self): opacity = self.marker.opacity() if opacity > 0.: # produce a fade out effect opacity -= 0.1 self.marker.setOpacity(opacity) self.marker2.setOpacity(opacity) QTimer.singleShot(100, self.hide_marker) else: self.marker.setVisible(False) self.marker2.setVisible(False) def show_line_rubber_band(self, geom): self.rubber_band.reset(geom.type()) self.rubber_band.setToGeometry(geom, None) self.rubber_band.setVisible(True) self.rubber_band.setOpacity(1.0) self.rubber_band.show() if self.display_time == -1: self.is_displayed = True else: QTimer.singleShot(self.display_time, self.hide_rubber_band) pass def hide_rubber_band(self): opacity = self.rubber_band.opacity() if opacity > 0.: # produce a fade out effect opacity -= 0.1 self.rubber_band.setOpacity(opacity) QTimer.singleShot(100, self.hide_rubber_band) else: self.rubber_band.setVisible(False) self.rubber_band.hide()
class SeedConfirmDisplay(QVBoxLayout): def __init__( self, title=None, icon=True, options=None, is_seed=None, parent=None, for_seed_words=True, full_check=True, *, config: 'SimpleConfig', ): QVBoxLayout.__init__(self) self.parent = parent self.options = options self.config = config self.seed_type = 'electrum' if title: self.addWidget(WWLabel(title)) assert for_seed_words self.seed_e = CompletionTextEdit() self.seed_e.setTabChangesFocus(False) # so that tab auto-completes self.is_seed = is_seed self.saved_is_seed = self.is_seed self.seed_e.textChanged.connect(self.on_edit) self.initialize_completer() self.seed_e.setMaximumHeight(75) hbox = QHBoxLayout() if icon: logo = QLabel() logo.setPixmap(QPixmap(icon_path("seed.png")) .scaledToWidth(64, mode=Qt.SmoothTransformation)) logo.setMaximumWidth(60) hbox.addWidget(logo) hbox.addWidget(self.seed_e) self.addLayout(hbox) hbox = QHBoxLayout() hbox.addStretch(1) self.seed_type_label = QLabel('') if full_check: hbox.addWidget(self.seed_type_label) # options self.is_ext = False self.opt_button = None if options: self.opt_button = EnterButton(_('Options'), self.seed_options) seed_types = [ (value, title) for value, title in ( ('electrum', _('Electrum')), ('bip39', _('BIP39 seed')), ) ] seed_type_values = [t[0] for t in seed_types] def f(choices_layout): self.seed_type = seed_type_values[choices_layout.selected_index()] self.is_seed = (lambda x: bool(x)) if self.seed_type != 'bip39' else self.saved_is_seed self.seed_status.setText('') self.on_edit(from_click=True) self.initialize_completer() self.seed_warning.setText(None) if self.seed_type == 'bip39': if self.opt_button: self.opt_button.setVisible(False) else: if self.opt_button: self.opt_button.setVisible(True) if options and full_check: hbox.addWidget(self.opt_button) self.addLayout(hbox) checked_index = seed_type_values.index(self.seed_type) titles = [t[1] for t in seed_types] self.clayout = ChoicesLayout(_('Seed type'), titles, on_clicked=f, checked_index=checked_index) if full_check: hbox.addLayout(self.clayout.layout()) self.addStretch(1) self.seed_status = WWLabel('') self.addWidget(self.seed_status) self.seed_warning = WWLabel('') self.addWidget(self.seed_warning) self.lang = 'en' def seed_options(self): dialog = QDialog() vbox = QVBoxLayout(dialog) if 'ext' in self.options: cb_ext = QCheckBox(_('Extend this seed with custom words')) cb_ext.setChecked(self.is_ext) vbox.addWidget(cb_ext) vbox.addLayout(Buttons(OkButton(dialog))) if not dialog.exec_(): return None self.is_ext = cb_ext.isChecked() if 'ext' in self.options else False #self.seed_type = 'electrum' def initialize_completer(self): if self.seed_type != 'slip39': bip39_english_list = Mnemonic('en').wordlist old_list = old_mnemonic.wordlist only_old_list = set(old_list) - set(bip39_english_list) self.wordlist = list(bip39_english_list) + list(only_old_list) # concat both lists self.wordlist.sort() class CompleterDelegate(QStyledItemDelegate): def initStyleOption(self, option, index): super().initStyleOption(option, index) # Some people complained that due to merging the two word lists, # it is difficult to restore from a metal backup, as they planned # to rely on the "4 letter prefixes are unique in bip39 word list" property. # So we color words that are only in old list. if option.text in only_old_list: # yellow bg looks ~ok on both light/dark theme, regardless if (un)selected option.backgroundBrush = ColorScheme.YELLOW.as_color(background=True) delegate = CompleterDelegate(self.seed_e) else: self.wordlist = list(slip39.get_wordlist()) delegate = None self.completer = QCompleter(self.wordlist) if delegate: self.completer.popup().setItemDelegate(delegate) self.seed_e.set_completer(self.completer) def get_seed_words(self): return self.seed_e.text().split() def get_seed(self): if self.seed_type != 'slip39': return ' '.join(self.get_seed_words()) else: return self.slip39_seed def on_edit(self, *, from_click=False): s = ' '.join(self.get_seed_words()) b = self.is_seed(s) from electrum.keystore import bip39_is_checksum_valid from electrum.mnemonic import Wordlist, filenames lang = '' for type, file in filenames.items(): word_list = Wordlist.from_file(file) is_checksum, is_wordlist = bip39_is_checksum_valid(s, wordlist=word_list) if is_wordlist: lang = type break if self.seed_type == 'bip39': status = ('checksum: ' + ('ok' if is_checksum else 'failed')) if is_wordlist else 'unknown wordlist' label = 'BIP39 - ' + lang + ' (%s)'%status if lang and lang != self.lang: if lang == 'en': bip39_english_list = Mnemonic('en').wordlist old_list = old_mnemonic.wordlist only_old_list = set(old_list) - set(bip39_english_list) self.wordlist = list(bip39_english_list) + list(only_old_list) # concat both lists self.wordlist.sort() self.completer.model().setStringList(self.wordlist) self.lang = 'en' else: self.wordlist = list(Mnemonic(lang).wordlist) self.wordlist.sort() self.completer.model().setStringList(self.wordlist) self.lang = lang b = is_checksum else: t = seed_type(s) label = _('Seed Type') + ': ' + t if t else '' if is_checksum and is_wordlist and not from_click: # This is a valid bip39 and this method was called from typing # Emulate selecting the bip39 option self.clayout.group.buttons()[1].click() return self.seed_type_label.setText(label) self.parent.next_button.setEnabled(b) # disable suggestions if user already typed an unknown word for word in self.get_seed_words()[:-1]: if word not in self.wordlist: self.seed_e.disable_suggestions() return self.seed_e.enable_suggestions()
class SeedLayout(QVBoxLayout): def __init__( self, seed=None, title=None, icon=True, msg=None, options=None, is_seed=None, passphrase=None, parent=None, for_seed_words=True, *, config: 'SimpleConfig', ): QVBoxLayout.__init__(self) self.parent = parent self.options = options self.config = config self.seed_type = 'bip39' if title: self.addWidget(WWLabel(title)) if seed: # "read only", we already have the text if for_seed_words: self.seed_e = ButtonsTextEdit() else: # e.g. xpub self.seed_e = ShowQRTextEdit(config=self.config) self.seed_e.setReadOnly(True) self.seed_e.setText(seed) else: # we expect user to enter text assert for_seed_words self.seed_e = CompletionTextEdit() self.seed_e.setTabChangesFocus(False) # so that tab auto-completes self.is_seed = is_seed self.saved_is_seed = self.is_seed self.seed_e.textChanged.connect(self.on_edit) self.initialize_completer() self.seed_e.setMaximumHeight(75) hbox = QHBoxLayout() if icon: logo = QLabel() logo.setPixmap(QPixmap(icon_path("seed.png")) .scaledToWidth(64, mode=Qt.SmoothTransformation)) logo.setMaximumWidth(60) hbox.addWidget(logo) hbox.addWidget(self.seed_e) self.addLayout(hbox) hbox = QHBoxLayout() hbox.addStretch(1) self.seed_type_label = QLabel('') hbox.addWidget(self.seed_type_label) seed_types = [ (value, title) for value, title in ( ('bip39', _('BIP39 seed')), ('electrum', _('Electrum')), # ('slip39', _('SLIP39 seed')), ) #if value in self.options or value == 'electrum' ] seed_type_values = [t[0] for t in seed_types] if len(seed_types) >= 2: def f(choices_layout): self.seed_type = seed_type_values[choices_layout.selected_index()] self.is_seed = (lambda x: bool(x)) if self.seed_type != 'bip39' else self.saved_is_seed self.slip39_current_mnemonic_invalid = None self.seed_status.setText('') #self.on_edit() self.update_share_buttons() self.initialize_completer() self.seed_warning.setText(msg) checked_index = seed_type_values.index(self.seed_type) titles = [t[1] for t in seed_types] clayout = ChoicesLayout(_('Seed type'), titles, on_clicked=f, checked_index=checked_index) hbox.addLayout(clayout.layout()) # options self.is_ext = False if options: opt_button = EnterButton(_('Options'), self.seed_options) hbox.addWidget(opt_button) self.addLayout(hbox) if passphrase: hbox = QHBoxLayout() passphrase_e = QLineEdit() passphrase_e.setText(passphrase) passphrase_e.setReadOnly(True) hbox.addWidget(QLabel(_("Your seed extension is") + ':')) hbox.addWidget(passphrase_e) self.addLayout(hbox) # slip39 shares self.slip39_mnemonic_index = 0 self.slip39_mnemonics = [""] self.slip39_seed = None self.slip39_current_mnemonic_invalid = None hbox = QHBoxLayout() hbox.addStretch(1) self.prev_share_btn = QPushButton(_("Previous share")) self.prev_share_btn.clicked.connect(self.on_prev_share) hbox.addWidget(self.prev_share_btn) self.next_share_btn = QPushButton(_("Next share")) self.next_share_btn.clicked.connect(self.on_next_share) hbox.addWidget(self.next_share_btn) self.update_share_buttons() self.addLayout(hbox) self.addStretch(1) self.seed_status = WWLabel('') self.addWidget(self.seed_status) self.seed_warning = WWLabel('') if msg: self.seed_warning.setText(seed_warning_msg(seed)) self.addWidget(self.seed_warning) self.lang = 'en' def initialize_completer(self): if self.seed_type != 'slip39': bip39_english_list = Mnemonic('en').wordlist old_list = old_mnemonic.wordlist only_old_list = set(old_list) - set(bip39_english_list) self.wordlist = list(bip39_english_list) + list(only_old_list) # concat both lists self.wordlist.sort() class CompleterDelegate(QStyledItemDelegate): def initStyleOption(self, option, index): super().initStyleOption(option, index) # Some people complained that due to merging the two word lists, # it is difficult to restore from a metal backup, as they planned # to rely on the "4 letter prefixes are unique in bip39 word list" property. # So we color words that are only in old list. if option.text in only_old_list: # yellow bg looks ~ok on both light/dark theme, regardless if (un)selected option.backgroundBrush = ColorScheme.YELLOW.as_color(background=True) delegate = CompleterDelegate(self.seed_e) else: self.wordlist = list(slip39.get_wordlist()) delegate = None self.completer = QCompleter(self.wordlist) if delegate: self.completer.popup().setItemDelegate(delegate) self.seed_e.set_completer(self.completer) def get_seed_words(self): return self.seed_e.text().split() def get_seed(self): if self.seed_type != 'slip39': return ' '.join(self.get_seed_words()) else: return self.slip39_seed def on_edit(self): s = ' '.join(self.get_seed_words()) b = self.is_seed(s) if self.seed_type == 'bip39': from electrum.keystore import bip39_is_checksum_valid from electrum.mnemonic import Wordlist, filenames lang = '' for type, file in filenames.items(): word_list = Wordlist.from_file(file) is_checksum, is_wordlist = bip39_is_checksum_valid(s, wordlist=word_list) if is_wordlist: lang = type break status = ('checksum: ' + ('ok' if is_checksum else 'failed')) if is_wordlist else 'unknown wordlist' label = 'BIP39 - ' + lang + ' (%s)'%status if lang and lang != self.lang: if lang == 'en': bip39_english_list = Mnemonic('en').wordlist old_list = old_mnemonic.wordlist only_old_list = set(old_list) - set(bip39_english_list) self.wordlist = list(bip39_english_list) + list(only_old_list) # concat both lists self.wordlist.sort() self.completer.model().setStringList(self.wordlist) self.lang = 'en' else: self.wordlist = list(Mnemonic(lang).wordlist) self.wordlist.sort() self.completer.model().setStringList(self.wordlist) self.lang = lang elif self.seed_type == 'slip39': self.slip39_mnemonics[self.slip39_mnemonic_index] = s try: slip39.decode_mnemonic(s) except slip39.Slip39Error as e: share_status = str(e) current_mnemonic_invalid = True else: share_status = _('Valid.') current_mnemonic_invalid = False label = _('SLIP39 share') + ' #%d: %s' % (self.slip39_mnemonic_index + 1, share_status) # No need to process mnemonics if the current mnemonic remains invalid after editing. if not (self.slip39_current_mnemonic_invalid and current_mnemonic_invalid): self.slip39_seed, seed_status = slip39.process_mnemonics(self.slip39_mnemonics) self.seed_status.setText(seed_status) self.slip39_current_mnemonic_invalid = current_mnemonic_invalid b = self.slip39_seed is not None self.update_share_buttons() else: t = seed_type(s) label = _('Seed Type') + ': ' + t if t else '' self.seed_type_label.setText(label) self.parent.next_button.setEnabled(b) # disable suggestions if user already typed an unknown word for word in self.get_seed_words()[:-1]: if word not in self.wordlist: self.seed_e.disable_suggestions() return self.seed_e.enable_suggestions() def update_share_buttons(self): if self.seed_type != 'slip39': self.prev_share_btn.hide() self.next_share_btn.hide() return finished = self.slip39_seed is not None self.prev_share_btn.show() self.next_share_btn.show() self.prev_share_btn.setEnabled(self.slip39_mnemonic_index != 0) self.next_share_btn.setEnabled( # already pressed "prev" and undoing that: self.slip39_mnemonic_index < len(self.slip39_mnemonics) - 1 # finished entering latest share and starting new one: or (bool(self.seed_e.text().strip()) and not self.slip39_current_mnemonic_invalid and not finished) ) def on_prev_share(self): if not self.slip39_mnemonics[self.slip39_mnemonic_index]: del self.slip39_mnemonics[self.slip39_mnemonic_index] self.slip39_mnemonic_index -= 1 self.seed_e.setText(self.slip39_mnemonics[self.slip39_mnemonic_index]) self.slip39_current_mnemonic_invalid = None def on_next_share(self): if not self.slip39_mnemonics[self.slip39_mnemonic_index]: del self.slip39_mnemonics[self.slip39_mnemonic_index] else: self.slip39_mnemonic_index += 1 if len(self.slip39_mnemonics) <= self.slip39_mnemonic_index: self.slip39_mnemonics.append("") self.seed_e.setFocus() self.seed_e.setText(self.slip39_mnemonics[self.slip39_mnemonic_index]) self.slip39_current_mnemonic_invalid = None
class TextStatusEditComplete(TextStatusEdit): """ Adds Completion functions to the base class This class extends 'TextStatusEdit' by: 1. providing a QCompleter to validate lines for the 'fixupText' and 'lineChanged' signals 2. providing a popup for suggested completions as the user is typing 3. auto-completing the line when the user selects a suggestion. The task of auto completion and providing suggestions is provided directly by this class. The task validating and cleaning up text is provided by the PluginFinder. """ def __init__(self, parent: QWidget = None): super().__init__(parent) self._dataModel = None self._monitorDbChanges = False self._enableAutoCompletion = False self._completedAndSelected = False self._completer = QCompleter(self) self._completer.setWidget(self) self._completer.setWrapAround(False) self._completer.setCompletionMode(QCompleter.PopupCompletion) self._completer.setCaseSensitivity(Qt.CaseInsensitive) self._completer.setFilterMode(Qt.MatchStartsWith) self._completer.setModelSorting( QCompleter.CaseInsensitivelySortedModel) self._completer.activated.connect(self.replaceLine) self._pluginFinder = PluginFinder(self._completer, self) self.fixupText.connect(self._pluginFinder.fixupText) self.lineChanged.connect(self._pluginFinder.setRowForLine) QShortcut(Qt.CTRL + Qt.Key_E, self, self.toggleAutoCompletion) QShortcut(Qt.CTRL + Qt.Key_T, self, self.suggestCompletions) # --- Methods related to the completer's underlying data model def setModel(self, model: QAbstractItemModel): self._completer.setModel(model) def _updateModelSignals(self): """ We do not need to check for column changes due to the way our PluginModel is structured. """ if self._dataModel is not None: self._dataModel.rowsMoved.disconnect(self.resetData) self._dataModel.rowsInserted.disconnect(self.resetData) self._dataModel.rowsRemoved.disconnect(self.resetData) self._dataModel.modelReset.disconnect(self.resetData) self._dataModel.dataChanged.disconnect(self.resetData) self._dataModel.layoutChanged.disconnect(self.resetData) if self._monitorDbChanges: self._dataModel = self._completer.model() if self._dataModel is not None: self._dataModel.rowsMoved.connect(self.resetData) self._dataModel.rowsInserted.connect(self.resetData) self._dataModel.rowsRemoved.connect(self.resetData) self._dataModel.modelReset.connect(self.resetData) self._dataModel.dataChanged.connect(self.resetData) self._dataModel.layoutChanged.connect(self.resetData) else: self._dataModel = None def monitorDbChanges(self, enable: bool): """ Enable invalidating line status when the data model changes. Depending on the underlying data model, it may be unnecessary to monitor these changes, or, a higher level class can monitor specific signals more efficiently. So, this is not enabled by default. """ if self._monitorDbChanges == enable: return self._monitorDbChanges = enable if enable: self._dataModel = self._completer.model() self._completer.completionModel().sourceModelChanged.connect( self._updateModelSignals) else: self._completer.completionModel().sourceModelChanged.disconnect( self._updateModelSignals) self._updateModelSignals() # ---- Methods related to line completion def completer(self): return self._completer def enableAutoCompletion(self, enable: bool): self._enableAutoCompletion = enable def toggleAutoCompletion(self): self.enableAutoCompletion(not self._enableAutoCompletion) def _textUnderCursor(self): tc = self.textCursor() if tc.positionInBlock() == 0 and len(tc.block().text()) > 1: tc.movePosition(QTextCursor.NextCharacter) tc.movePosition(QTextCursor.StartOfLine, QTextCursor.KeepAnchor) return tc.selectedText().lstrip() def suggestCompletions(self): if self.isLineInvalid(self.textCursor().blockNumber()): self._suggestCompletionsForText(self._textUnderCursor()) def _suggestCompletionsForText(self, prefix: str): if not prefix: return if prefix != self._completer.completionPrefix(): self._completer.setCompletionPrefix(prefix) self._completer.popup().setCurrentIndex( self._completer.completionModel().index(0, 0)) if self._completer.completionCount() == 1: self._insertSuggestion(self._completer.currentCompletion()) else: rect = self.cursorRect() rect.moveRight(self.statusAreaWidth()) rect.setWidth( self._completer.popup().sizeHintForColumn( self._completer.completionColumn()) + self._completer.popup().verticalScrollBar().sizeHint().width()) self._completer.complete(rect) def _insertSuggestion(self, text: str): """ Only one suggestion matched, prefill line """ cursor = self.textCursor() # handle when cursor is in middle of line if not cursor.atBlockEnd(): cursor.beginEditBlock() cursor.select(QTextCursor.LineUnderCursor) cursor.removeSelectedText() cursor.insertText(text) cursor.movePosition(QTextCursor.StartOfLine) cursor.movePosition(QTextCursor.EndOfLine, QTextCursor.KeepAnchor) self._completedAndSelected = True self.setTextCursor(cursor) cursor.endEditBlock() return # handle when cursor at end of line cursor.beginEditBlock() numCharsToComplete = len(text) - len( self._completer.completionPrefix()) insertionPosition = cursor.position() cursor.movePosition(QTextCursor.EndOfLine, QTextCursor.KeepAnchor) cursor.removeSelectedText() cursor.insertText(text[-numCharsToComplete:]) cursor.setPosition(insertionPosition) cursor.movePosition(QTextCursor.EndOfLine, QTextCursor.KeepAnchor) self._completedAndSelected = True self.setTextCursor(cursor) cursor.endEditBlock() def keyPressEvent(self, event: QKeyEvent): if self._completedAndSelected and self.handledCompletedAndSelected( event): return self._completedAndSelected = False if self._completer.popup().isVisible(): ignoredKeys = [ Qt.Key_Up, Qt.Key_Down, Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab, Qt.Key_Escape, ] if event.key() in ignoredKeys: event.ignore() return self._completer.popup().hide() super().keyPressEvent(event) if not self._enableAutoCompletion: return ctrlOrShift = (event.modifiers() & Qt.ShiftModifier == Qt.ShiftModifier or event.modifiers() & Qt.ControlModifier == Qt.ControlModifier) if ctrlOrShift and not event.text(): return if self.textCursor().atBlockEnd(): self.suggestCompletions() def mousePressEvent(self, event: QMouseEvent): if self._completedAndSelected: self._completedAndSelected = False self.document().undo() super().mousePressEvent(event) def handledCompletedAndSelected(self, event: QKeyEvent): """ The line is prefilled when only one completion matches. The user can accept the suggestion by pressing 'Enter'. The user can reject the suggestion by pressing 'Esc' or by continuing to type. """ self._completedAndSelected = False cursor = self.textCursor() acceptKeys = [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Tab] if event.key() in acceptKeys: self.replaceLine(self._completer.currentCompletion()) elif event.key() == Qt.Key_Escape: self.document().undo() else: self.document().undo() return False self.setTextCursor(cursor) event.accept() return True def replaceLine(self, text: str): cursor = self.textCursor() cursor.beginEditBlock() cursor.select(QTextCursor.LineUnderCursor) cursor.removeSelectedText() cursor.insertText(text) cursor.movePosition(QTextCursor.EndOfLine) self.setTextCursor(cursor) cursor.endEditBlock() # ---- Methods related to Context Menu def createStandardContextMenu(self, pos: QPoint): menu = super().createStandardContextMenu(pos) menu.addSeparator() autoCompletionAction = menu.addAction( QIcon(), self.tr("Enable Auto Complete"), self.toggleAutoCompletion, QKeySequence(Qt.CTRL + Qt.Key_E), ) autoCompletionAction.setCheckable(True) autoCompletionAction.setChecked(self._enableAutoCompletion) completionAction = menu.addAction( QIcon(), self.tr("Suggest Completions"), self.suggestCompletions, QKeySequence(Qt.CTRL + Qt.Key_T), ) completionAction.setEnabled( self.isLineInvalid(self.textCursor().blockNumber())) return menu
class MainWindow(QWidget): def __init__(self, parent=None, path_config='config.ini', path_style='./styles/style.qss', logger=None): super(MainWindow, self).__init__(parent) self.path_config = path_config self.config = Config(self.path_config) self.settings = self.config.getConfig() self.logger = logger self.drawInitialMenu(path_style) def drawInitialMenu(self, path_style='./styles/style.qss'): self.path_to_data = self.settings.get('path', 'data') self.path_to_library = self.settings.get('path', 'library') self.path_to_playlist = self.settings.get('path', 'playlist') self.path_to_database = self.settings.get('path', 'database') self.language = self.settings.get('general', 'language') self.style = self.settings.get('general', 'style') try: with open(path_style, "r") as f: stylesheet = f.read() except: stylesheet = ''' QWidget{background-color: #f0f8ff; color:#0f0f7f} QPushButton{color: #ffffff; background-color: #7070ff; border: 2px; border-radius: 5px; font-weight:bold} QPushButton:hover,QPushButton:pressed{color: #ffffff; background-color:#9f9f9f} QPushButton:disabled{color: #ffffff; background-color:#dfdfdf} QLineEdit{color:#0f0f7f; border:1px solid #a0a0a0; border-radius: 5px; background-color:#dff3ff} QLineEdit:hover{color:#0f0f7f; border:1px solid #7070ff; border-radius: 5px} QLineEdit:disabled{color:#d0d0d0; border:1px solid #d0d0d0; border-radius: 5px} QSpinBox{color:#0f0f7f; border:1px solid #a0a0a0; border-radius: 5px; background-color:#dff3ff} QSpinBox:hover{color:#0f0f7f; border:1px solid #7070ff; border-radius: 5px} QProgressBar{text-align:center; color:#7f7faf; font-weight:bold} QProgressBar::chunk{background-color: #50ff50; width: 10px; margin: 1px;} ''' self.setStyleSheet(stylesheet) if self.language == "en": self.sentences = LANG_ENG else: self.sentences = LANG_JA self.apg = APG(self.path_to_database, self.logger) self.width = int(self.settings.get('screen', 'width')) self.height = int(self.settings.get('screen', 'height')) self.label_data = QLabel(self.sentences["path_to_data"], self) self.label_data.setGeometry(self.width * 0.08, self.height * 0.10, self.width * 0.2, 30) self.label_library = QLabel(self.sentences["path_to_library"], self) self.label_library.setGeometry(self.width * 0.08, self.height * 0.20, self.width * 0.2, 30) self.label_playlist = QLabel(self.sentences["path_to_playlist"], self) self.label_playlist.setGeometry(self.width * 0.08, self.height * 0.30, self.width * 0.2, 30) self.label_advanced = QLabel(self.sentences["advanced_settings"], self) self.label_advanced.setGeometry(self.width * 0.08, self.height * 0.40, self.width * 0.22, 30) self.setFixedSize(self.width, self.height) self.button_data = QPushButton(self.sentences["select"], self) self.button_data.setObjectName('path_data') self.button_data.setGeometry(self.width * 0.78, self.height * 0.10, self.width * 0.15, 30) self.button_data.clicked.connect(self.buttonClicked) self.button_library = QPushButton(self.sentences["select"], self) self.button_library.setObjectName('path_library') self.button_library.setGeometry(self.width * 0.78, self.height * 0.20, self.width * 0.15, 30) self.button_library.clicked.connect(self.buttonClicked) self.button_playlist = QPushButton(self.sentences["select"], self) self.button_playlist.setObjectName('path_playlist') self.button_playlist.setGeometry(self.width * 0.78, self.height * 0.30, self.width * 0.15, 30) self.button_playlist.clicked.connect(self.buttonClicked) self.line_data = QLineEdit(self.path_to_data, self) self.line_data.setGeometry(self.width * 0.27, self.height * 0.10, self.width * 0.5, 30) self.line_data.setToolTip(self.sentences["tips_data"]) self.line_library = QLineEdit(self.path_to_library, self) self.line_library.setGeometry(self.width * 0.27, self.height * 0.20, self.width * 0.5, 30) self.line_library.setToolTip(self.sentences["tips_library"]) self.line_playlist = QLineEdit(self.path_to_playlist, self) self.line_playlist.setGeometry(self.width * 0.27, self.height * 0.30, self.width * 0.5, 30) self.line_playlist.setToolTip(self.sentences["tips_playlist"]) self.line_keyword = QLineEdit(self) self.line_keyword.setGeometry(self.width * 0.25, self.height * 0.55, self.width * 0.3, 30) self.line_keyword.setEnabled(False) self.line_keyword.setToolTip(self.sentences["tips_line_keyword"]) self.completer = QCompleter(self.apg.getCandidate(target="anime"), self) self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.completer.setCompletionMode(QCompleter.PopupCompletion) self.completer.popup().setStyleSheet( "background-color:#3c3c3c; color:#cccccc") self.line_keyword.setCompleter(self.completer) self.logger.info("Completer was called.") self.label_category = QLabel(self.sentences["category"], self) self.label_category.setGeometry(self.width * 0.1, self.height * 0.47, self.width * 0.2, 30) self.check_anime = QCheckBox(self.sentences["anime"], self) self.check_anime.setObjectName("anime") self.check_anime.toggle() self.check_anime.setGeometry(self.width * 0.22, self.height * 0.47, self.width * 0.15, 30) self.check_anime.clicked.connect(self.checkClicked) self.check_anime.setToolTip(self.sentences["tips_cat_anime"]) self.check_game = QCheckBox(self.sentences["game"], self) self.check_game.setObjectName("game") self.check_game.toggle() self.check_game.setGeometry(self.width * 0.33, self.height * 0.47, self.width * 0.15, 30) self.check_game.clicked.connect(self.checkClicked) self.check_game.setToolTip(self.sentences["tips_cat_game"]) self.check_sf = QCheckBox(self.sentences["sf"], self) self.check_sf.setObjectName("sf") self.check_sf.toggle() self.check_sf.setGeometry(self.width * 0.44, self.height * 0.47, self.width * 0.15, 30) self.check_sf.clicked.connect(self.checkClicked) self.check_sf.setToolTip(self.sentences["tips_cat_sf"]) self.keyword = QCheckBox(self.sentences["anime_title"], self) self.keyword.setObjectName("keyword") self.keyword.setGeometry(self.width * 0.10, self.height * 0.55, self.width * 0.15, 30) self.keyword.clicked.connect(self.checkClicked) self.keyword.setToolTip(self.sentences["tips_keyword"]) # Define the push button for start the process self.run = QPushButton(self.sentences["run"], self) self.run.setObjectName('run') self.run.setGeometry(self.width * 0.35, self.height * 0.85, self.width * 0.15, 30) self.run.clicked.connect(self.buttonClicked) self.run.setEnabled(True) self.run.setToolTip(self.sentences["tips_run"]) # Define the push button for making the database self.db_update = QPushButton(self.sentences["db_update"], self) self.db_update.setObjectName('db_update') self.db_update.setGeometry(self.width * 0.75, self.height * 0.65, self.width * 0.15, 30) self.db_update.clicked.connect(self.buttonClicked) self.db_update.setEnabled(True) self.db_update.setToolTip(self.sentences["tips_database"]) # Define the push button for stop the process self.stop = QPushButton(self.sentences["stop"], self) self.stop.setObjectName('stop') self.stop.setGeometry(self.width * 0.55, self.height * 0.85, self.width * 0.15, 30) self.stop.clicked.connect(self.buttonClicked) self.stop.setEnabled(False) self.stop.setToolTip(self.sentences["tips_stop"]) # Define the progress bar self.progress = QProgressBar(self) self.progress.setGeometry(self.width * 0.55, self.height * 0.95, self.width * 0.4, 20) self.progress.setMaximum(100) # Define the status bar self.status = QStatusBar(self) self.status.setGeometry(self.width * 0.01, self.height * 0.95, self.width * 0.5, 20) self.status.showMessage(self.sentences["init"]) self.setWindowTitle('Anison Playlist Generator') self.setWindowTitle self.thread_prog = SubProgress() self.thread_prog.signal.connect(self.updateProgress) self.thread_updateDB = None self.thread_genPlaylist = None def __del__(self): self.config.saveConfig(name=self.path_config, path_library=self.path_to_library, path_data=self.path_to_data, path_playlist=self.path_to_playlist, path_database=self.path_to_database, width=800, height=480, path_style="./styles/style.qss", language=self.language) def paintEvent(self, event): self.painter = QPainter(self) self.painter.setBrush(QColor(30, 30, 30)) self.painter.setPen(QColor(160, 160, 160)) rect = QRect(int(self.width * 0.05), int(self.height * 0.43), int(self.width * 0.90), int(self.height * 0.40)) self.painter.drawRoundedRect(rect, 20.0, 20.0) self.painter.end() def updateText(self, var, folder=True, ext=".m3u"): updated_path = "" if folder: updated_path = QFileDialog.getExistingDirectory( None, 'rootpath', var.text()) else: updated_path = QFileDialog.getOpenFileName(None, 'rootpath', var.text())[0] if updated_path == "" or (os.path.isfile(updated_path) and os.path.splitext(updated_path)[1] != ext): var.setText(var.text()) else: var.setText(updated_path) return updated_path def checkClicked(self): sender = self.sender() if sender.objectName() == "keyword": if self.keyword.checkState(): self.status.showMessage(sender.text().replace(":", "") + self.sentences["enable"]) self.line_keyword.setEnabled(True) else: self.status.showMessage(sender.text().replace(":", "") + self.sentences["disable"]) self.line_keyword.setEnabled(False) elif sender.objectName() == "anime": if self.check_anime.checkState(): self.status.showMessage(self.check_anime.text() + self.sentences["enable"]) else: self.status.showMessage(self.check_anime.text() + self.sentences["disable"]) elif sender.objectName() == "game": if self.check_game.checkState(): self.status.showMessage(self.check_game.text() + self.sentences["enable"]) else: self.status.showMessage(self.check_game.text() + self.sentences["disable"]) elif sender.objectName() == "sf": if self.check_sf.checkState(): self.status.showMessage(self.check_sf.text() + self.sentences["enable"]) else: self.status.showMessage(self.check_sf.text() + self.sentences["disable"]) def buttonClicked(self): sender = self.sender() self.status.showMessage(sender.text() + self.sentences["pressed"]) if sender.objectName() == "path_data": self.path_to_data = self.updateText(self.line_data) elif sender.objectName() == "path_library": self.path_to_library = self.updateText(self.line_library) elif sender.objectName() == "path_playlist": self.path_to_playlist = self.updateText(self.line_playlist, False, ".m3u") elif sender.objectName() == "run": self.logger.info("Started to making the playlist.") # 既に実行しているスレッドがある場合は処理を中止 if self.checkThreadRunning(): self.logger.info( "GenPlaylist: The previous thread is running.") QMessageBox.warning(None, self.sentences["warn_overwrite"], "直前の処理を中断しています.", QMessageBox.Ok) return # ファイルの拡張子の確認 if os.path.splitext(self.line_playlist.text())[1] != ".m3u": self.status.showMessage(self.sentences["warn_ext"]) return # プレイリストが存在するか確認 if os.path.exists(self.line_playlist.text()): react = QMessageBox.warning( None, self.sentences["warn_overwrite"], self.sentences["message_overwrite"], QMessageBox.Yes, QMessageBox.No) if react == QMessageBox.No: self.lockInput(enabled=True) return # チェックボックスの状態の確認 check_categories = { "anison": self.check_anime.checkState(), "game": self.check_game.checkState(), "sf": self.check_sf.checkState() } # プレイリスト作成用のスレッドの呼び出し self.thread_genPlaylist = GenPlaylist( apg=self.apg, keyword=self.line_keyword.text() if self.keyword.checkState() else "", use_key=1 if self.keyword.checkState() else 0, path_playlist=self.line_playlist.text(), check_categories=check_categories) self.thread_genPlaylist.signal.connect(self.generatePlaylist) # マルチスレッドによる処理の開始 self.lockInput(enabled=False) self.thread_prog.start() self.thread_genPlaylist.start() elif sender.objectName() == "db_update": self.logger.info("Started to updating the database.") # 既に実行しているスレッドがある場合は処理を中止 if self.checkThreadRunning(): self.logger.info("UpdateDB: The previous thread is running.") QMessageBox.warning(None, self.sentences["warn_overwrite"], "直前の処理を中断中です", QMessageBox.Ok) return # データベース更新処理の確認 react = QMessageBox.warning(None, self.sentences["warn_overwrite"], "データベースを更新しますか?", QMessageBox.Yes, QMessageBox.No) # Noなら処理を中止 if react == QMessageBox.No: self.lockInput(enabled=True) return # スレッドの実行準備 self.thread_updateDB = UpdateDB( apg=self.apg, path_data=self.line_data.text(), path_library=self.line_library.text()) self.thread_updateDB.signal.connect(self.updateDB) # ボタンなどの入力が出来ないようにする self.lockInput(enabled=False) # マルチスレッドによる処理の開始 self.thread_prog.start() self.thread_updateDB.start() elif sender.objectName() == "stop": self.apg.stop() self.apg.reset() self.thread_prog.wait() self.thread_prog.quit() if self.thread_updateDB != None: self.thread_updateDB.wait() self.thread_updateDB.quit() elif self.thread_genPlaylist != None: self.thread_genPlaylist.wait() self.thread_genPlaylist.quit() self.status.showMessage(self.sentences["warn_stop"]) self.lockInput(enabled=True) def checkThreadRunning(self): """ マルチスレッド処理が行われているかを返す関数 """ check_updateDB = (self.thread_updateDB != None) and self.thread_updateDB.isRunning() check_genPlaylist = (self.thread_genPlaylist != None) and self.thread_genPlaylist.isRunning() return check_updateDB or check_genPlaylist def lockInput(self, enabled=True): """ ボタン入力などの有効/無効を切り替える関数 """ self.stop.setEnabled(not enabled) self.run.setEnabled(enabled) self.db_update.setEnabled(enabled) self.check_anime.setEnabled(enabled) self.check_game.setEnabled(enabled) self.check_sf.setEnabled(enabled) self.keyword.setEnabled(enabled) self.line_keyword.setEnabled(enabled) self.button_data.setEnabled(enabled) self.button_library.setEnabled(enabled) self.button_playlist.setEnabled(enabled) self.line_data.setEnabled(enabled) self.line_playlist.setEnabled(enabled) self.line_library.setEnabled(enabled) return def updateProgress(self, signal): """ プログレスバーの値を更新する関数 """ db, library, playlist = self.apg.getProgress() value = 0 if 0 < playlist and playlist < 100: value = playlist elif 0 < db and db < 100: value = db else: value = library self.progress.setValue(value) def resetProgress(self): """ ProgressBarの値を0にする関数 """ self.progress.setValue(0) def generatePlaylist(self, signal): """ GenPlaylistスレッド実行時に呼び出される関数 空白を受信したらスレッドを終了する """ if signal != "": self.status.showMessage(signal) else: self.apg.reset() self.thread_genPlaylist.wait() self.thread_genPlaylist.quit() self.thread_prog.wait() self.thread_prog.quit() self.resetProgress() self.lockInput(enabled=True) # メッセージの表示 self.status.showMessage(self.sentences["fin_making_playlist"]) def updateDB(self, signal): """ UpdateDBスレッド実行時に呼び出される関数 空白を受信したらスレッドを終了する """ if signal != "": self.status.showMessage(signal) else: self.apg.reset() self.thread_updateDB.wait() self.thread_updateDB.quit() self.thread_prog.wait() self.thread_prog.quit() self.resetProgress() self.lockInput(enabled=True) # 検索候補の更新 self.completer.model().setStringList( self.apg.getCandidate(target="anime")) # メッセージの表示 self.status.showMessage(self.sentences["fin_updating_database"])
class GuiApplication(QApplication, BaseApplication): # TODO: figure out how to catch close/delete window from window frame def __init__(self, *args, **kw): QApplication.__init__(self, *args, **kw) BaseApplication.__init__(self) self.view = qtutils.create_form( "main.ui", opengl={"graphicsView": ChimeraGraphics}, connections={ "actionOpen.triggered": self.open, "actionQuit.triggered": self.quit, "lineEdit.textChanged": self.save_command, "lineEdit.returnPressed": self.process_command, "graphicsViewGL.mousePress": self.mouse_press, "graphicsViewGL.mouseRelease": self.mouse_release, "graphicsViewGL.mouseMove": self.mouse_drag, # TODO: why are't these needed? #"graphicsViewGL.keyPress": "lineEdit.event", #"graphicsViewGL.keyRelease": "lineEdit.event", }) self.view.setWindowTitle(self.applicationName()) self.statusbar = self.find_object("statusbar") assert self.statusbar is not None self.graphics = self.find_object("graphicsViewGL") assert self.graphics is not None self.graphics.makeCurrent() self.graphics.setFocusPolicy(Qt.WheelFocus) self.line_edit = self.find_object("lineEdit") assert self.line_edit is not None self.completer = QCompleter(self.line_edit) self.completer.setModel(QStringListModel(self.completer)) #self.completer.setCompletionMode(QCompleter.PopupCompletion) self.line_edit.setCompleter(self.completer) self._mouse_mode = None self.view.show() self.cursors = { # TODO: custom cursors "pick": Qt.PointingHandCursor, "vsphere_z": Qt.IBeamCursor, "vsphere_rot": Qt.ClosedHandCursor, "translate": Qt.SizeAllCursor, } self.timer = QTimer(self.view) self.active_timer = False from chimera2.trackchanges import track from chimera2 import scene track.add_handler(scene.View, self._update_cb) def _update_cb(self, *args, **kw): if self.graphics: self.graphics.updateGL() def physicalDotsPerInch(self): screen = self.primaryScreen() return screen.physicalDotsPerInch() def find_object(self, name): return self.view.findChild(QObject, name) @pyqtSlot() def open(self): # QFileDialog.getOpenFileName(QWidget parent=None, str caption='', str directory='', str filter='', str initialFilter='', QFileDialog.Options options=0) -> (str, str) from chimera2 import io filename, filter = QFileDialog.getOpenFileName( self.view, caption="Open File", filter=io.qt_open_file_filter()) if filename: from chimera2 import commands commands.cmd_open(filename) @property def mouse_mode(self): return self._mouse_mode @mouse_mode.setter def mouse_mode(self, mode): if mode == self._mouse_mode: return self._mouse_mode = mode cursor = self.cursors.get(mode, None) if cursor: self.graphics.setCursor(cursor) else: self.graphics.setCursor(QCursor()) @pyqtSlot(QEvent) def mouse_press(self, event): buttons = event.buttons() x = event.x() y = event.y() if buttons & Qt.RightButton: self.graphics.pick(x, y) self.mouse_mode = "pick" elif buttons & Qt.MiddleButton: self.mouse_mode = "translate" self.xy = event.globalPos() elif buttons & Qt.LeftButton: zrot = self.graphics.vsphere_press(x, y) if zrot: self.mouse_mode = "vsphere_z" else: self.mouse_mode = "vsphere_rot" @pyqtSlot(QEvent) def mouse_release(self, event): if self.mouse_mode in ("vsphere_z", "vsphere_rot"): self.graphics.vsphere_release() self.mouse_mode = None @pyqtSlot(QEvent) def mouse_drag(self, event): if self.mouse_mode in ("vsphere_z", "vsphere_rot"): x = event.x() y = event.y() throttle = event.modifiers() & Qt.ShiftModifier zrot = self.graphics.vsphere_drag(x, y, throttle) if zrot: self.mouse_mode = "vsphere_z" else: self.mouse_mode = "vsphere_rot" elif self.mouse_mode == "translate": xy = event.globalPos() delta = xy - self.xy self.xy = xy self.graphics.translate_xy(delta) @pyqtSlot(str) def save_command(self, text): self.command.parse_text(text) self.completer.setCompletionPrefix(self.command.completion_prefix) self.completer.model().setStringList(self.command.completions) self.completer.complete() @pyqtSlot() def process_command(self, cmd=None): self.status("") BaseApplication.process_command(self)
class DiscoveryPlugin: def __init__(self, _iface): # Save reference to the QGIS interface self.iface = _iface # initialize plugin directory self.plugin_dir = os.path.dirname(__file__) # Variables to facilitate delayed queries and database connection management self.icon_loading = ':plugins/_Discovery_sqlite/icons/loading.gif' self.db_timer = QTimer() self.line_edit_timer = QTimer() self.line_edit_timer.setSingleShot(True) self.line_edit_timer.timeout.connect(self.reset_line_edit_after_move) self.next_query_time = None self.last_query_time = time.time() self.db_conn = None self.search_delay = 0.5 # s self.query_sql = '' self.query_text = '' self.query_dict = {} self.db_idle_time = 60.0 # s self.search_results = [] self.tool_bar = None self.search_line_edit = None self.completer = None self.marker = QgsVertexMarker(iface.mapCanvas()) self.marker.setIconSize(15) self.marker.setPenWidth(2) self.marker.setColor(QColor(226, 27, 28)) #51,160,44)) self.marker.setZValue(11) self.marker.setVisible(False) self.marker2 = QgsVertexMarker(iface.mapCanvas()) self.marker2.setIconSize(16) self.marker2.setPenWidth(4) self.marker2.setColor(QColor(255, 255, 255, 200)) self.marker2.setZValue(10) self.marker2.setVisible(False) def initGui(self): # Create a new toolbar self.tool_bar = self.iface.addToolBar(u'Панель поиска') self.tool_bar.setObjectName('Discovery_sqlite_Plugin') # Add search edit box self.search_line_edit = QgsFilterLineEdit() self.search_line_edit.setSelectOnFocus(True) self.search_line_edit.setShowSearchIcon(True) self.search_line_edit.setPlaceholderText( u'Поиск адреса или участка...') # self.search_line_edit.setMaximumWidth(768) self.tool_bar.addWidget(self.search_line_edit) # loading indicator self.load_movie = QMovie() self.label_load = QLabel() self.tool_bar.addWidget(self.label_load) # Set up the completer model = QStandardItemModel() self.completer = QCompleter([]) # Initialise with en empty list self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.completer.setMaxVisibleItems(30) self.completer.setModelSorting( QCompleter.UnsortedModel) # Sorting done in PostGIS self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion ) # Show all fetched possibilities self.completer.setModel(model) tableView = QTableView() tableView.verticalHeader().setVisible(False) tableView.horizontalHeader().setVisible(False) tableView.setSelectionBehavior(QTableView.SelectRows) tableView.setShowGrid(False) # tableView.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) tableView.verticalHeader().setSectionResizeMode(QHeaderView.Fixed) fontsize = QFontMetrics( tableView.verticalHeader().font()).height() + 2 #font size tableView.verticalHeader().setDefaultSectionSize( fontsize) #font size 15 tableView.horizontalHeader().setSectionResizeMode( QHeaderView.ResizeToContents) tableView.horizontalHeader().setStretchLastSection(True) tableView.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.completer.setCompletionColumn(0) self.completer.setPopup(tableView) self.completer.activated[QModelIndex].connect(self.on_result_selected) self.completer.highlighted[QModelIndex].connect( self.on_result_highlighted) self.search_line_edit.setCompleter(self.completer) # Connect any signals self.search_line_edit.textEdited.connect(self.on_search_text_changed) self.search_line_edit.returnPressed.connect(self.returnPressed) self.read_config() # Search results self.search_results = [] # Set up a timer to periodically perform db queries as required self.db_timer.timeout.connect(self.schedule_search) def read_config(self): qstring = '' self.data = self.settings_path() try: for line in self.data[1:]: words = line.split(';') postgissearchcolumn = words[2].strip() postgistable = words[0].strip() geomcolumn = words[3].strip() layername = words[4].strip() isSelect = int(words[5].strip()) connection = sqlite3.connect(os.path.join(self.dbfile.strip())) cur = connection.cursor() qstring = u'select {2} from {0} where length({1})>0 LIMIT 1'.format( postgistable, postgissearchcolumn, geomcolumn) cur.execute(qstring) connection.close() self.make_enabled(True) # assume the config is invalid first except Exception as E: print(E) self.make_enabled(False) # включить или выключить поисковую строку в зависимости от результата проверки настроек def make_enabled(self, enabled): self.search_line_edit.setEnabled(enabled) self.search_line_edit.setPlaceholderText( u"Поиск адреса или участка..." if enabled else u"Поиск отключен: проверьте конфигурацию") def settings_path(self): p = os.path.join(self.plugin_dir, 'layers.ini') f = codecs.open(p, 'r', encoding='cp1251') data = f.readlines() dbfileline = data[0] if dbfileline[:2] == u'\\\\': self.dbfile = dbfileline elif dbfileline[1] == u':': self.dbfile = dbfileline else: self.dbfile = os.path.join(self.plugin_dir, dbfileline) f.close() return data def unload(self): self.db_timer.stop() self.db_timer.timeout.disconnect(self.schedule_search) self.completer.highlighted[QModelIndex].disconnect( self.on_result_highlighted) self.completer.activated[QModelIndex].disconnect( self.on_result_selected) self.search_line_edit.textEdited.disconnect( self.on_search_text_changed) self.search_line_edit.returnPressed.disconnect(self.returnPressed) self.tool_bar.clear() # Clear all actions self.iface.mainWindow().removeToolBar(self.tool_bar) def clear_suggestions(self): model = self.completer.model() model.clear() # model.setStringList([]) def returnPressed(self): if self.completer.popup().isHidden(): self.do_search(self.search_line_edit.text()) # def setLoading(self, isLoading): # if self.label_load is None: # return # if isLoading: # load_movie = QMovie() # load_movie.setFileName(self.icon_loading) # self.label_load.setMovie(load_movie) # load_movie.start() # else: # load_movie = QMovie() # load_movie.stop() # self.label_load.setMovie(load_movie) def schedule_search(self): if self.next_query_time is not None and self.next_query_time < time.time( ): self.next_query_time = None # Prevent this query from being repeated self.last_query_time = time.time() self.do_search(self.search_line_edit.text()) self.db_timer.stop() # self.setLoading(False) self.search_line_edit.setShowSpinner(False) else: # self.setLoading(True) self.search_line_edit.setShowSpinner(True) if time.time() > self.last_query_time + self.db_idle_time: self.db_conn = None # def on_search_text_changed(self, new_search_text): def on_search_text_changed(self, new_search_text): # self.setLoading(False) self.search_line_edit.setShowSpinner(False) if len(new_search_text) < 3: self.db_timer.stop() self.clear_suggestions() return self.db_timer.start(300) self.next_query_time = time.time() + self.search_delay def do_search(self, new_search_text): if len(new_search_text) < 3: self.clear_suggestions() return self.clear_suggestions() self.query_text = new_search_text self.search_results = [] self.suggestions = [] for index, line in enumerate(self.data[1:]): curline_layer = line words = curline_layer.split(';') searchcolumn = words[2].strip() # поле со значением для поиска postgistable = words[0].strip() # таблица geomcolumn = words[3].strip() # поле с геометрией layername = words[4].strip( ) # имя слоя в легенде для соответствий и выделения isSelect = int( words[5].strip()) # выделять ли объект в слое layername descript = words[1].strip( ) # описание. Выводится в списке результатов query_text, query_dict = self.get_search_sql( new_search_text, searchcolumn, postgistable) query_sql = query_text query_dict = query_dict self.perform_search(query_sql, query_dict, descript, postgistable, layername, isSelect, searchcolumn) # QStringList - просто одна строка в выводе # if len(self.suggestions) > 0: # model = self.completer.model() # model.setStringList(self.suggestions) # print(model) # self.completer.complete() if len(self.suggestions) > 0: # model = self.completer.model() model = QStandardItemModel() font = QFont() font.setItalic(True) font.setPointSize(7) # заполняем модель for i, line in enumerate(self.suggestions): #icon pixmap = QPixmap(':plugins/_Discovery_sqlite/icons/' + line[2] + '.png') pixmap = pixmap.scaledToHeight(10) pixmap = pixmap.scaledToWidth(10) # itemImage = QStandardItem() # itemImage.setData(pixmap, Qt.DecorationRole) # model.setItem(i, 0, itemImage) itemLayer = QStandardItem(u"{1}[{0}]".format( line[1], u' ' * 50)) itemLayer.setFont(font) itemValue = QStandardItem(line[0]) itemValue.setData(pixmap, Qt.DecorationRole) model.setItem(i, 0, itemValue) model.setItem(i, 1, itemLayer) self.completer.setModel(model) self.completer.complete() else: model = self.completer.model() # self.suggestions.append(u"<Не найдено>") # для QStringList # model.setStringList(self.suggestions) # для QStringList model.setItem( 0, 0, QStandardItem('<Не найдено>')) # для QStandardItemModel self.completer.complete() def perform_search(self, query_sql, query_dict, descript, tablename, layername, isSelect, searchcolumn): cur = self.get_db_cur() cur.execute(query_sql, query_dict) for row in cur.fetchall(): geom, suggestion_text = row[0], row[1] self.search_results.append(geom) self.suggestions.append([ suggestion_text, descript, tablename, layername, isSelect, searchcolumn ]) # self.suggestions.append(suggestion_text) # для QStringList def get_search_sql(self, search_text, search_column, table): wildcarded_search_string = '' for part in search_text.split(): wildcarded_search_string += '%' + part #.lower() wildcarded_search_string += '%' wildcarded_search_string = wildcarded_search_string query_dict = {'search_text': wildcarded_search_string} # wildcarded_search_string = wildcarded_search_string.encode('cp1251') query_text = u"SELECT WKT_GEOMETRY AS geom, {0} AS suggestion_string FROM {1} WHERE ({0}) LIKE '{2}' ORDER BY {0} LIMIT 1000".format( search_column, table, wildcarded_search_string) # query_text = query_text.decode('cp1251') return query_text, query_dict def on_result_selected(self, result_index): resultIndexRow = result_index.row() if len(self.search_results) < 1: self.search_line_edit.setPlaceholderText(u'') return # What to do when the user makes a selection geometry_text = self.search_results[resultIndexRow] location_geom = QgsGeometry.fromWkt(geometry_text) canvas = self.iface.mapCanvas() # dst_srid = canvas.mapRenderer().destinationCrs().authid() # Ensure the geometry from the DB is reprojected to the same SRID as the map canvas location_centroid = location_geom.centroid().asPoint() result_text = self.completer.completionModel().index( resultIndexRow, 0).data() if self.suggestions[resultIndexRow][2] in ( u"adres_nd") and location_geom.type() == 0: # point self.show_marker(location_centroid) self.iface.mapCanvas().setExtent(location_geom.boundingBox()) self.iface.mapCanvas().zoomScale(1000) layer_build = self.find_layer(u"Здания") if layer_build != None: layer_build.selectByIds([]) for feat in layer_build.getFeatures( QgsFeatureRequest().setFilterRect( QgsRectangle(self.iface.mapCanvas().extent()))): if location_geom.intersects(feat.geometry()): # self.show_marker_feature(feat.geometry()) self.iface.setActiveLayer(layer_build) layer_build.selectByIds([feat.id()]) layer_build.triggerRepaint() return else: #not point layername = self.suggestions[resultIndexRow][3] isSelect = self.suggestions[resultIndexRow][4] searchcolumn = self.suggestions[resultIndexRow][5] box = location_geom.boundingBox() if box.height() > box.width(): max = box.height() else: max = box.width() box.grow(max * 0.10) self.iface.mapCanvas().setExtent(box) if isSelect == 1: selLayer = self.find_layer(layername) if selLayer is not None: for feat in selLayer.getFeatures( QgsFeatureRequest().setFilterRect(box)): # print(feat[searchcolumn], str(result_text).strip()) try: if str(feat[searchcolumn]) == str( result_text).strip(): self.iface.setActiveLayer(selLayer) selLayer.selectByIds([feat.id()]) selLayer.triggerRepaint() break except Exception as E: print(E) break self.show_marker_feature(location_geom) canvas.refresh() self.line_edit_timer.start(0) # self.db_timer.stop() def get_db_cur(self): # Create a new new connection if required if self.db_conn is None: self.db_conn = sqlite3.connect(os.path.join(self.dbfile.strip())) return self.db_conn.cursor() def on_result_highlighted(self, result_idx): self.line_edit_timer.start(0) def reset_line_edit_after_move(self): self.search_line_edit.setText(self.query_text) def find_layer(self, layer_name): for search_layer in self.iface.mapCanvas().layers(): if search_layer.name() == layer_name: return search_layer return None def show_marker(self, point): for m in [self.marker, self.marker2]: m.setCenter(point) m.setOpacity(1.0) m.setVisible(True) QTimer.singleShot(4000, self.hide_marker) def hide_marker(self): opacity = self.marker.opacity() if opacity > 0.: # produce a fade out effect opacity -= 0.1 self.marker.setOpacity(opacity) self.marker2.setOpacity(opacity) QTimer.singleShot(100, self.hide_marker) else: self.marker.setVisible(False) self.marker2.setVisible(False) def show_marker_feature(self, geom): if geom.type() == 2: #poly self.r = QgsRubberBand(iface.mapCanvas(), True) elif geom.type() == 1: #line self.r = QgsRubberBand(iface.mapCanvas(), False) self.r.setToGeometry(geom, None) self.r.setColor(QColor(255, 0, 0, 200)) self.r.setFillColor(QColor(255, 0, 0, 50)) self.r.setWidth(2) self.r.setZValue(9) QTimer.singleShot(4000, self.hide_marker_feature) def hide_marker_feature(self): opacity = self.r.opacity() if opacity > 0.: # produce a fade out effect opacity -= 0.1 self.r.setOpacity(opacity) QTimer.singleShot(100, self.hide_marker_feature) else: iface.mapCanvas().scene().removeItem(self.r)