示例#1
0
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
示例#2
0
 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()
示例#3
0
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)
示例#4
0
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
示例#7
0
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
示例#8
0
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"])
示例#9
0
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)