Beispiel #1
0
class Autocomplete(QComboBox):
    def __init__(self, parent = None):
        super(Autocomplete, self).__init__(parent)
        self.setFocusPolicy(Qt.StrongFocus)
        self.setEditable(True)

        # add a filter model to filter matching items
        self.pFilterModel = QSortFilterProxyModel(self)
        self.pFilterModel.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.pFilterModel.setSourceModel(self.model())

        # add a completer, which uses the filter model
        self.completer = QCompleter(self.pFilterModel, self)
        # always show all (filtered) completions
        self.completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion)

        self.setCompleter(self.completer)
        self.resize(500, 30)

        # connect signals
        def filter(text):
            self.pFilterModel.setFilterFixedString(str(text))

        self.lineEdit().textEdited.connect(filter)
        self.completer.activated.connect(self.on_completer_activated)

    # on selection of an item from the completer, select the corresponding item from combobox
    def on_completer_activated(self, text):
        if text:
            index = self.findText(str(text))
            self.setCurrentIndex(index)
    def __init__(self, parent=None):
        QWidget.__init__(self, parent)
        self.setupUi(self)
        self.toolBox.setStyleSheet(style.toolBoxSS())

        self.mw = mainWindow()
        self.txtGeneralSplitScenes.setStyleSheet(style.lineEditSS())

        # TreeView to select parent
        # We use a proxy to display only folders
        proxy = QSortFilterProxyModel()
        proxy.setFilterKeyColumn(Outline.type)
        proxy.setFilterFixedString("folder")
        proxy.setSourceModel(self.mw.mdlOutline)
        self.treeGeneralParent.setModel(proxy)
        for i in range(1, self.mw.mdlOutline.columnCount()):
            self.treeGeneralParent.hideColumn(i)
        self.treeGeneralParent.setCurrentIndex(self.getParentIndex())
        self.chkGeneralParent.toggled.connect(self.treeGeneralParent.setVisible)
        self.treeGeneralParent.hide()
Beispiel #3
0
    def __init__(self, parent=None):
        QWidget.__init__(self, parent)
        self.setupUi(self)
        self.toolBox.setStyleSheet(style.toolBoxSS())

        self.mw = mainWindow()
        self.txtGeneralSplitScenes.setStyleSheet(style.lineEditSS())

        # TreeView to select parent
        # We use a proxy to display only folders
        proxy = QSortFilterProxyModel()
        proxy.setFilterKeyColumn(Outline.type)
        proxy.setFilterFixedString("folder")
        proxy.setSourceModel(self.mw.mdlOutline)
        self.treeGeneralParent.setModel(proxy)
        for i in range(1, self.mw.mdlOutline.columnCount()):
            self.treeGeneralParent.hideColumn(i)
        self.treeGeneralParent.setCurrentIndex(self.getParentIndex())
        self.chkGeneralParent.toggled.connect(
            self.treeGeneralParent.setVisible)
        self.treeGeneralParent.hide()
Beispiel #4
0
class SelectionDialog(CustomStandardDialog, Ui_SelectionDialog):
    def __init__(self,
                 source_table,
                 selection_mode=QAbstractItemView.ExtendedSelection,
                 selection_behavior=QAbstractItemView.SelectItems):
        CustomStandardDialog.__init__(self)
        self.setupUi(self)
        self.source_table = source_table
        self.resize(self.dataView.horizontalHeader().width(), self.height())
        self.proxyModel = QSortFilterProxyModel(self)
        self.proxyModel.setSourceModel(source_table)
        self.proxyModel.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
        self.proxyModel.setFilterKeyColumn(-1)
        self.dataView.setModel(self.proxyModel)
        self.dataView.setSelectionBehavior(selection_behavior)
        self.dataView.setSelectionMode(selection_mode)
        self.proxyModel.sort(0)

        self.dataView.selectionModel().selectionChanged.connect(
            self.activate_button)
        self.dataView.doubleClicked.connect(self.accept)

        self.restore_dialog_geometry()

    def selected_items(self):
        return [
            self.source_table.item_from_row(x)
            for x in self.dataView.get_selected_rows()
        ]

    @QtCore.pyqtSlot()
    def activate_button(self):
        self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(
            len(self.dataView.selectedIndexes()))

    @QtCore.pyqtSlot(str)
    def update_filter(self, new):
        self.proxyModel.setFilterFixedString(new)
Beispiel #5
0
class Explorer(QWidget):
    """
    This class implements the diagram predicate node explorer.
    """
    def __init__(self, mainwindow):
        """
        Initialize the Explorer.
        :type mainwindow: MainWindow
        """
        super().__init__(mainwindow)
        self.expanded = {}
        self.searched = {}
        self.scrolled = {}
        self.mainview = None
        self.iconA = QIcon(':/icons/treeview-icon-attribute')
        self.iconC = QIcon(':/icons/treeview-icon-concept')
        self.iconD = QIcon(':/icons/treeview-icon-datarange')
        self.iconI = QIcon(':/icons/treeview-icon-instance')
        self.iconR = QIcon(':/icons/treeview-icon-role')
        self.iconV = QIcon(':/icons/treeview-icon-value')
        self.search = StringField(self)
        self.search.setAcceptDrops(False)
        self.search.setClearButtonEnabled(True)
        self.search.setPlaceholderText('Search...')
        self.search.setFixedHeight(30)
        self.model = QStandardItemModel(self)
        self.proxy = QSortFilterProxyModel(self)
        self.proxy.setDynamicSortFilter(False)
        self.proxy.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.proxy.setSortCaseSensitivity(Qt.CaseSensitive)
        self.proxy.setSourceModel(self.model)
        self.view = ExplorerView(mainwindow, self)
        self.view.setModel(self.proxy)
        self.mainLayout = QVBoxLayout(self)
        self.mainLayout.setContentsMargins(0, 0, 0, 0)
        self.mainLayout.addWidget(self.search)
        self.mainLayout.addWidget(self.view)
        self.setContentsMargins(0, 0, 0, 0)
        self.setMinimumWidth(216)
        self.setMinimumHeight(160)

        connect(self.view.doubleClicked, self.itemDoubleClicked)
        connect(self.view.pressed, self.itemPressed)
        connect(self.view.collapsed, self.itemCollapsed)
        connect(self.view.expanded, self.itemExpanded)
        connect(self.search.textChanged, self.filterItem)

    ####################################################################################################################
    #                                                                                                                  #
    #   EVENTS                                                                                                         #
    #                                                                                                                  #
    ####################################################################################################################

    def paintEvent(self, paintEvent):
        """
        This is needed for the widget to pick the stylesheet.
        :type paintEvent: QPaintEvent
        """
        option = QStyleOption()
        option.initFrom(self)
        painter = QPainter(self)
        style = self.style()
        style.drawPrimitive(QStyle.PE_Widget, option, painter, self)

    ####################################################################################################################
    #                                                                                                                  #
    #   SLOTS                                                                                                          #
    #                                                                                                                  #
    ####################################################################################################################

    @pyqtSlot('QGraphicsItem')
    def add(self, item):
        """
        Add a node in the tree view.
        :type item: AbstractItem
        """
        if item.node and item.predicate:
            parent = self.parentFor(item)
            if not parent:
                parent = ParentItem(item)
                parent.setIcon(self.iconFor(item))
                self.model.appendRow(parent)
                self.proxy.sort(0, Qt.AscendingOrder)
            child = ChildItem(item)
            child.setData(item)
            parent.appendRow(child)
            self.proxy.sort(0, Qt.AscendingOrder)

    @pyqtSlot(str)
    def filterItem(self, key):
        """
        Executed when the search box is filled with data.
        :type key: str
        """
        if self.mainview:
            self.proxy.setFilterFixedString(key)
            self.proxy.sort(Qt.AscendingOrder)
            self.searched[self.mainview] = key

    @pyqtSlot('QModelIndex')
    def itemCollapsed(self, index):
        """
        Executed when an item in the tree view is collapsed.
        :type index: QModelIndex
        """
        if self.mainview:
            if self.mainview in self.expanded:
                item = self.model.itemFromIndex(self.proxy.mapToSource(index))
                expanded = self.expanded[self.mainview]
                expanded.remove(item.text())

    @pyqtSlot('QModelIndex')
    def itemDoubleClicked(self, index):
        """
        Executed when an item in the tree view is double clicked.
        :type index: QModelIndex
        """
        item = self.model.itemFromIndex(self.proxy.mapToSource(index))
        node = item.data()
        if node:
            self.selectNode(node)
            self.focusNode(node)

    @pyqtSlot('QModelIndex')
    def itemExpanded(self, index):
        """
        Executed when an item in the tree view is expanded.
        :type index: QModelIndex
        """
        if self.mainview:
            item = self.model.itemFromIndex(self.proxy.mapToSource(index))
            if self.mainview not in self.expanded:
                self.expanded[self.mainview] = set()
            expanded = self.expanded[self.mainview]
            expanded.add(item.text())

    @pyqtSlot('QModelIndex')
    def itemPressed(self, index):
        """
        Executed when an item in the tree view is clicked.
        :type index: QModelIndex
        """
        item = self.model.itemFromIndex(self.proxy.mapToSource(index))
        node = item.data()
        if node:
            self.selectNode(node)

    @pyqtSlot('QGraphicsItem')
    def remove(self, item):
        """
        Remove a node from the tree view.
        :type item: AbstractItem
        """
        if item.node and item.predicate:
            parent = self.parentFor(item)
            if parent:
                child = self.childFor(parent, item)
                if child:
                    parent.removeRow(child.index().row())
                if not parent.rowCount():
                    self.model.removeRow(parent.index().row())

    ####################################################################################################################
    #                                                                                                                  #
    #   AUXILIARY METHODS                                                                                              #
    #                                                                                                                  #
    ####################################################################################################################

    @staticmethod
    def childFor(parent, node):
        """
        Search the item representing this node among parent children.
        :type parent: QStandardItem
        :type node: AbstractNode
        """
        key = ChildItem.key(node)
        for i in range(parent.rowCount()):
            child = parent.child(i)
            if child.text() == key:
                return child
        return None

    def parentFor(self, node):
        """
        Search the parent element of the given node.
        :type node: AbstractNode
        :rtype: QStandardItem
        """
        key = ParentItem.key(node)
        for i in self.model.findItems(key, Qt.MatchExactly):
            n = i.child(0).data()
            if node.item is n.item:
                return i
        return None

    ####################################################################################################################
    #                                                                                                                  #
    #   INTERFACE                                                                                                      #
    #                                                                                                                  #
    ####################################################################################################################

    def browse(self, view):
        """
        Set the widget to inspect the given view.
        :type view: MainView
        """
        self.reset()
        self.mainview = view

        if self.mainview:

            scene = self.mainview.scene()
            connect(scene.index.sgnItemAdded, self.add)
            connect(scene.index.sgnItemRemoved, self.remove)

            for item in scene.index.nodes():
                self.add(item)

            if self.mainview in self.expanded:
                expanded = self.expanded[self.mainview]
                for i in range(self.model.rowCount()):
                    item = self.model.item(i)
                    index = self.proxy.mapFromSource(
                        self.model.indexFromItem(item))
                    self.view.setExpanded(index, item.text() in expanded)

            key = ''
            if self.mainview in self.searched:
                key = self.searched[self.mainview]
            self.search.setText(key)

            if self.mainview in self.scrolled:
                rect = self.rect()
                item = first(self.model.findItems(
                    self.scrolled[self.mainview]))
                for i in range(self.model.rowCount()):
                    self.view.scrollTo(
                        self.proxy.mapFromSource(
                            self.model.indexFromItem(self.model.item(i))))
                    index = self.proxy.mapToSource(
                        self.view.indexAt(rect.topLeft()))
                    if self.model.itemFromIndex(index) is item:
                        break

    def reset(self):
        """
        Clear the widget from inspecting the current view.
        """
        if self.mainview:

            rect = self.rect()
            item = self.model.itemFromIndex(
                self.proxy.mapToSource(self.view.indexAt(rect.topLeft())))
            if item:
                node = item.data()
                key = ParentItem.key(node) if node else item.text()
                self.scrolled[self.mainview] = key
            else:
                self.scrolled.pop(self.mainview, None)

            try:
                scene = self.mainview.scene()
                disconnect(scene.index.sgnItemAdded, self.add)
                disconnect(scene.index.sgnItemRemoved, self.remove)
            except RuntimeError:
                pass
            finally:
                self.mainview = None

        self.model.clear()

    def flush(self, view):
        """
        Flush the cache of the given mainview.
        :type view: MainView
        """
        self.expanded.pop(view, None)
        self.searched.pop(view, None)
        self.scrolled.pop(view, None)

    def iconFor(self, node):
        """
        Returns the icon for the given node.
        :type node:
        """
        if node.item is Item.AttributeNode:
            return self.iconA
        if node.item is Item.ConceptNode:
            return self.iconC
        if node.item is Item.ValueDomainNode:
            return self.iconD
        if node.item is Item.ValueRestrictionNode:
            return self.iconD
        if node.item is Item.IndividualNode:
            if node.identity is Identity.Instance:
                return self.iconI
            if node.identity is Identity.Value:
                return self.iconV
        if node.item is Item.RoleNode:
            return self.iconR

    def focusNode(self, node):
        """
        Focus the given node in the main view.
        :type node: AbstractNode
        """
        if self.mainview:
            self.mainview.centerOn(node)

    def selectNode(self, node):
        """
        Select the given node in the main view.
        :type node: AbstractNode
        """
        if self.mainview:
            scene = self.mainview.scene()
            scene.clearSelection()
            node.setSelected(True)
Beispiel #6
0
class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        self._version = "0.1.20"
        self.setWindowIcon(QIcon("GUI/icons/logo.png"))
        self.setWindowTitle("Tasmota Device Manager {}".format(self._version))

        self.main_splitter = QSplitter()
        self.devices_splitter = QSplitter(Qt.Vertical)

        self.mqtt_queue = []
        self.devices = {}

        self.fulltopic_queue = []
        old_settings = QSettings()

        self.settings = QSettings("{}/TDM/tdm.cfg".format(QDir.homePath()),
                                  QSettings.IniFormat)
        self.setMinimumSize(QSize(1280, 800))

        for k in old_settings.allKeys():
            self.settings.setValue(k, old_settings.value(k))
            old_settings.remove(k)

        self.device_model = TasmotaDevicesModel()
        self.telemetry_model = TasmotaDevicesTree()
        self.console_model = ConsoleModel()

        self.sorted_console_model = QSortFilterProxyModel()
        self.sorted_console_model.setSourceModel(self.console_model)
        self.sorted_console_model.setFilterKeyColumn(CnsMdl.FRIENDLY_NAME)

        self.setup_mqtt()
        self.setup_telemetry_view()
        self.setup_main_layout()
        self.add_devices_tab()
        self.build_toolbars()
        self.setStatusBar(QStatusBar())

        self.queue_timer = QTimer()
        self.queue_timer.timeout.connect(self.mqtt_publish_queue)
        self.queue_timer.start(500)

        self.auto_timer = QTimer()
        self.auto_timer.timeout.connect(self.autoupdate)

        self.load_window_state()

        if self.settings.value("connect_on_startup", False, bool):
            self.actToggleConnect.trigger()

    def setup_main_layout(self):
        self.mdi = QMdiArea()
        self.mdi.setActivationOrder(QMdiArea.ActivationHistoryOrder)
        self.mdi.setViewMode(QMdiArea.TabbedView)
        self.mdi.setDocumentMode(True)

        mdi_widget = QWidget()
        mdi_widget.setLayout(VLayout())
        mdi_widget.layout().addWidget(self.mdi)

        self.devices_splitter.addWidget(mdi_widget)

        vl_console = VLayout()
        hl_filter = HLayout()
        self.cbFilter = QCheckBox("Console filtering")
        self.cbxFilterDevice = QComboBox()
        self.cbxFilterDevice.setEnabled(False)
        self.cbxFilterDevice.setFixedWidth(200)
        self.cbxFilterDevice.setModel(self.device_model)
        self.cbxFilterDevice.setModelColumn(DevMdl.FRIENDLY_NAME)
        hl_filter.addWidgets([self.cbFilter, self.cbxFilterDevice])
        hl_filter.addStretch(0)
        vl_console.addLayout(hl_filter)

        self.console_view = TableView()
        self.console_view.setModel(self.console_model)
        self.console_view.setupColumns(columns_console)
        self.console_view.setAlternatingRowColors(True)
        self.console_view.verticalHeader().setDefaultSectionSize(20)
        self.console_view.setMinimumHeight(200)

        vl_console.addWidget(self.console_view)

        console_widget = QWidget()
        console_widget.setLayout(vl_console)

        self.devices_splitter.addWidget(console_widget)
        self.main_splitter.insertWidget(0, self.devices_splitter)
        self.setCentralWidget(self.main_splitter)
        self.console_view.clicked.connect(self.select_cons_entry)
        self.console_view.doubleClicked.connect(self.view_payload)

        self.cbFilter.toggled.connect(self.toggle_console_filter)
        self.cbxFilterDevice.currentTextChanged.connect(
            self.select_console_filter)

    def setup_telemetry_view(self):
        tele_widget = QWidget()
        vl_tele = VLayout()
        self.tview = QTreeView()
        self.tview.setMinimumWidth(300)
        self.tview.setModel(self.telemetry_model)
        self.tview.setAlternatingRowColors(True)
        self.tview.setUniformRowHeights(True)
        self.tview.setIndentation(15)
        self.tview.setSizePolicy(
            QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum))
        self.tview.expandAll()
        self.tview.resizeColumnToContents(0)
        vl_tele.addWidget(self.tview)
        tele_widget.setLayout(vl_tele)
        self.main_splitter.addWidget(tele_widget)

    def setup_mqtt(self):
        self.mqtt = MqttClient()
        self.mqtt.connecting.connect(self.mqtt_connecting)
        self.mqtt.connected.connect(self.mqtt_connected)
        self.mqtt.disconnected.connect(self.mqtt_disconnected)
        self.mqtt.connectError.connect(self.mqtt_connectError)
        self.mqtt.messageSignal.connect(self.mqtt_message)

    def add_devices_tab(self):
        tabDevicesList = DevicesListWidget(self)
        self.mdi.addSubWindow(tabDevicesList)
        tabDevicesList.setWindowState(Qt.WindowMaximized)

    def load_window_state(self):
        wndGeometry = self.settings.value('window_geometry')
        if wndGeometry:
            self.restoreGeometry(wndGeometry)
        spltState = self.settings.value('splitter_state')
        if spltState:
            self.main_splitter.restoreState(spltState)

    def build_toolbars(self):
        main_toolbar = Toolbar(orientation=Qt.Horizontal,
                               iconsize=16,
                               label_position=Qt.ToolButtonTextBesideIcon)
        main_toolbar.setObjectName("main_toolbar")
        self.addToolBar(main_toolbar)

        main_toolbar.addAction(QIcon("./GUI/icons/connections.png"), "Broker",
                               self.setup_broker)
        self.actToggleConnect = QAction(QIcon("./GUI/icons/disconnect.png"),
                                        "MQTT")
        self.actToggleConnect.setCheckable(True)
        self.actToggleConnect.toggled.connect(self.toggle_connect)
        main_toolbar.addAction(self.actToggleConnect)

        self.actToggleAutoUpdate = QAction(QIcon("./GUI/icons/automatic.png"),
                                           "Auto telemetry")
        self.actToggleAutoUpdate.setCheckable(True)
        self.actToggleAutoUpdate.toggled.connect(self.toggle_autoupdate)
        main_toolbar.addAction(self.actToggleAutoUpdate)

        main_toolbar.addSeparator()
        main_toolbar.addAction(QIcon("./GUI/icons/bssid.png"), "BSSId",
                               self.bssid)
        main_toolbar.addAction(QIcon("./GUI/icons/export.png"), "Export list",
                               self.export)

    def initial_query(self, idx, queued=False):
        for q in initial_queries:
            topic = "{}status".format(self.device_model.commandTopic(idx))
            if queued:
                self.mqtt_queue.append([topic, q])
            else:
                self.mqtt.publish(topic, q, 1)
            self.console_log(topic, "Asked for STATUS {}".format(q), q)

    def setup_broker(self):
        brokers_dlg = BrokerDialog()
        if brokers_dlg.exec_(
        ) == QDialog.Accepted and self.mqtt.state == self.mqtt.Connected:
            self.mqtt.disconnect()

    def toggle_autoupdate(self, state):
        if state:
            self.auto_timer.setInterval(5000)
            self.auto_timer.start()

    def toggle_connect(self, state):
        if state and self.mqtt.state == self.mqtt.Disconnected:
            self.broker_hostname = self.settings.value('hostname', 'localhost')
            self.broker_port = self.settings.value('port', 1883, int)
            self.broker_username = self.settings.value('username')
            self.broker_password = self.settings.value('password')

            self.mqtt.hostname = self.broker_hostname
            self.mqtt.port = self.broker_port

            if self.broker_username:
                self.mqtt.setAuth(self.broker_username, self.broker_password)
            self.mqtt.connectToHost()
        elif not state and self.mqtt.state == self.mqtt.Connected:
            self.mqtt_disconnect()

    def autoupdate(self):
        if self.mqtt.state == self.mqtt.Connected:
            for d in range(self.device_model.rowCount()):
                idx = self.device_model.index(d, 0)
                cmnd = self.device_model.commandTopic(idx)
                self.mqtt.publish(cmnd + "STATUS", payload=8)

    def mqtt_connect(self):
        self.broker_hostname = self.settings.value('hostname', 'localhost')
        self.broker_port = self.settings.value('port', 1883, int)
        self.broker_username = self.settings.value('username')
        self.broker_password = self.settings.value('password')

        self.mqtt.hostname = self.broker_hostname
        self.mqtt.port = self.broker_port

        if self.broker_username:
            self.mqtt.setAuth(self.broker_username, self.broker_password)

        if self.mqtt.state == self.mqtt.Disconnected:
            self.mqtt.connectToHost()

    def mqtt_disconnect(self):
        self.mqtt.disconnectFromHost()

    def mqtt_connecting(self):
        self.statusBar().showMessage("Connecting to broker")

    def mqtt_connected(self):
        self.actToggleConnect.setIcon(QIcon("./GUI/icons/connect.png"))
        self.statusBar().showMessage("Connected to {}:{} as {}".format(
            self.broker_hostname, self.broker_port,
            self.broker_username if self.broker_username else '[anonymous]'))

        self.mqtt_subscribe()

        for d in range(self.device_model.rowCount()):
            idx = self.device_model.index(d, 0)
            self.initial_query(idx)

    def mqtt_subscribe(self):
        main_topics = ["+/stat/+", "+/tele/+", "stat/#", "tele/#"]

        for d in range(self.device_model.rowCount()):
            idx = self.device_model.index(d, 0)
            if not self.device_model.isDefaultTemplate(idx):
                main_topics.append(self.device_model.commandTopic(idx))
                main_topics.append(self.device_model.statTopic(idx))

        for t in main_topics:
            self.mqtt.subscribe(t)

    def mqtt_publish_queue(self):
        for q in self.mqtt_queue:
            t, p = q
            self.mqtt.publish(t, p)
            self.mqtt_queue.pop(self.mqtt_queue.index(q))

    def mqtt_disconnected(self):
        self.actToggleConnect.setIcon(QIcon("./GUI/icons/disconnect.png"))
        self.statusBar().showMessage("Disconnected")

    def mqtt_connectError(self, rc):
        reason = {
            1: "Incorrect protocol version",
            2: "Invalid client identifier",
            3: "Server unavailable",
            4: "Bad username or password",
            5: "Not authorized",
        }
        self.statusBar().showMessage("Connection error: {}".format(reason[rc]))
        self.actToggleConnect.setChecked(False)

    def mqtt_message(self, topic, msg):
        found = self.device_model.findDevice(topic)
        if found.reply == 'LWT':
            if not msg:
                msg = "offline"

            if found.index.isValid():
                self.console_log(topic, "LWT update: {}".format(msg), msg)
                self.device_model.updateValue(found.index, DevMdl.LWT, msg)
                self.initial_query(found.index, queued=True)

            elif msg == "Online":
                self.console_log(
                    topic,
                    "LWT for unknown device '{}'. Asking for FullTopic.".
                    format(found.topic), msg, False)
                self.mqtt_queue.append(
                    ["cmnd/{}/fulltopic".format(found.topic), ""])
                self.mqtt_queue.append(
                    ["{}/cmnd/fulltopic".format(found.topic), ""])

        elif found.reply == 'RESULT':
            try:
                full_topic = loads(msg).get('FullTopic')
                new_topic = loads(msg).get('Topic')
                template_name = loads(msg).get('NAME')
                ota_url = loads(msg).get('OtaUrl')
                teleperiod = loads(msg).get('TelePeriod')

                if full_topic:
                    # TODO: update FullTopic for existing device AFTER the FullTopic changes externally (the message will arrive from new FullTopic)
                    if not found.index.isValid():
                        self.console_log(
                            topic, "FullTopic for {}".format(found.topic), msg,
                            False)

                        new_idx = self.device_model.addDevice(found.topic,
                                                              full_topic,
                                                              lwt='online')
                        tele_idx = self.telemetry_model.addDevice(
                            TasmotaDevice, found.topic)
                        self.telemetry_model.devices[found.topic] = tele_idx
                        #TODO: add QSortFilterProxyModel to telemetry treeview and sort devices after adding

                        self.initial_query(new_idx)
                        self.console_log(
                            topic,
                            "Added {} with fulltopic {}, querying for STATE".
                            format(found.topic, full_topic), msg)
                        self.tview.expand(tele_idx)
                        self.tview.resizeColumnToContents(0)

                elif new_topic:
                    if found.index.isValid() and found.topic != new_topic:
                        self.console_log(
                            topic, "New topic for {}".format(found.topic), msg)

                        self.device_model.updateValue(found.index,
                                                      DevMdl.TOPIC, new_topic)

                        tele_idx = self.telemetry_model.devices.get(
                            found.topic)

                        if tele_idx:
                            self.telemetry_model.setDeviceName(
                                tele_idx, new_topic)
                            self.telemetry_model.devices[
                                new_topic] = self.telemetry_model.devices.pop(
                                    found.topic)

                elif template_name:
                    self.device_model.updateValue(
                        found.index, DevMdl.MODULE,
                        "{} (0)".format(template_name))

                elif ota_url:
                    self.device_model.updateValue(found.index, DevMdl.OTA_URL,
                                                  ota_url)

                elif teleperiod:
                    self.device_model.updateValue(found.index,
                                                  DevMdl.TELEPERIOD,
                                                  teleperiod)

            except JSONDecodeError as e:
                self.console_log(
                    topic,
                    "JSON payload decode error. Check error.log for additional info."
                )
                with open("{}/TDM/error.log".format(QDir.homePath()),
                          "a+") as l:
                    l.write("{}\t{}\t{}\t{}\n".format(
                        QDateTime.currentDateTime().toString(
                            "yyyy-MM-dd hh:mm:ss"), topic, msg, e.msg))

        elif found.index.isValid():
            ok = False
            try:
                if msg.startswith("{"):
                    payload = loads(msg)
                else:
                    payload = msg
                ok = True
            except JSONDecodeError as e:
                self.console_log(
                    topic,
                    "JSON payload decode error. Check error.log for additional info."
                )
                with open("{}/TDM/error.log".format(QDir.homePath()),
                          "a+") as l:
                    l.write("{}\t{}\t{}\t{}\n".format(
                        QDateTime.currentDateTime().toString(
                            "yyyy-MM-dd hh:mm:ss"), topic, msg, e.msg))

            if ok:
                try:
                    if found.reply == 'STATUS':
                        self.console_log(topic, "Received device status", msg)
                        payload = payload['Status']
                        self.device_model.updateValue(
                            found.index, DevMdl.FRIENDLY_NAME,
                            payload['FriendlyName'][0])
                        self.telemetry_model.setDeviceFriendlyName(
                            self.telemetry_model.devices[found.topic],
                            payload['FriendlyName'][0])
                        module = payload['Module']
                        if module == 0:
                            self.mqtt.publish(
                                self.device_model.commandTopic(found.index) +
                                "template")
                        else:
                            self.device_model.updateValue(
                                found.index, DevMdl.MODULE,
                                modules.get(module, 'Unknown'))
                        self.device_model.updateValue(found.index,
                                                      DevMdl.MODULE_ID, module)

                    elif found.reply == 'STATUS1':
                        self.console_log(topic, "Received program information",
                                         msg)
                        payload = payload['StatusPRM']
                        self.device_model.updateValue(
                            found.index, DevMdl.RESTART_REASON,
                            payload.get('RestartReason'))
                        self.device_model.updateValue(found.index,
                                                      DevMdl.OTA_URL,
                                                      payload.get('OtaUrl'))

                    elif found.reply == 'STATUS2':
                        self.console_log(topic,
                                         "Received firmware information", msg)
                        payload = payload['StatusFWR']
                        self.device_model.updateValue(found.index,
                                                      DevMdl.FIRMWARE,
                                                      payload['Version'])
                        self.device_model.updateValue(found.index, DevMdl.CORE,
                                                      payload['Core'])

                    elif found.reply == 'STATUS3':
                        self.console_log(topic, "Received syslog information",
                                         msg)
                        payload = payload['StatusLOG']
                        self.device_model.updateValue(found.index,
                                                      DevMdl.TELEPERIOD,
                                                      payload['TelePeriod'])

                    elif found.reply == 'STATUS5':
                        self.console_log(topic, "Received network status", msg)
                        payload = payload['StatusNET']
                        self.device_model.updateValue(found.index, DevMdl.MAC,
                                                      payload['Mac'])
                        self.device_model.updateValue(found.index, DevMdl.IP,
                                                      payload['IPAddress'])

                    elif found.reply in ('STATE', 'STATUS11'):
                        self.console_log(topic, "Received device state", msg)
                        if found.reply == 'STATUS11':
                            payload = payload['StatusSTS']
                        self.parse_state(found.index, payload)

                    elif found.reply in ('SENSOR', 'STATUS8'):
                        self.console_log(topic, "Received telemetry", msg)
                        if found.reply == 'STATUS8':
                            payload = payload['StatusSNS']
                        self.parse_telemetry(found.index, payload)

                    elif found.reply.startswith('POWER'):
                        self.console_log(
                            topic, "Received {} state".format(found.reply),
                            msg)
                        payload = {found.reply: msg}
                        self.parse_power(found.index, payload)

                except KeyError as k:
                    self.console_log(
                        topic,
                        "JSON key error. Check error.log for additional info.")
                    with open("{}/TDM/error.log".format(QDir.homePath()),
                              "a+") as l:
                        l.write("{}\t{}\t{}\tKeyError: {}\n".format(
                            QDateTime.currentDateTime().toString(
                                "yyyy-MM-dd hh:mm:ss"), topic, payload,
                            k.args[0]))

    def parse_power(self, index, payload, from_state=False):
        old = self.device_model.power(index)
        power = {
            k: payload[k]
            for k in payload.keys() if k.startswith("POWER")
        }
        # TODO: fix so that number of relays get updated properly after module/no. of relays change
        needs_update = False
        if old:
            # if from_state and len(old) != len(power):
            #     needs_update = True
            #
            # else:
            for k in old.keys():
                needs_update |= old[k] != power.get(k, old[k])
                if needs_update:
                    break
        else:
            needs_update = True

        if needs_update:
            self.device_model.updateValue(index, DevMdl.POWER, power)

    def parse_state(self, index, payload):
        bssid = payload['Wifi'].get('BSSId')
        if not bssid:
            bssid = payload['Wifi'].get('APMac')
        self.device_model.updateValue(index, DevMdl.BSSID, bssid)
        self.device_model.updateValue(index, DevMdl.SSID,
                                      payload['Wifi']['SSId'])
        self.device_model.updateValue(index, DevMdl.CHANNEL,
                                      payload['Wifi'].get('Channel', "n/a"))
        self.device_model.updateValue(index, DevMdl.RSSI,
                                      payload['Wifi']['RSSI'])
        self.device_model.updateValue(index, DevMdl.UPTIME, payload['Uptime'])
        self.device_model.updateValue(index, DevMdl.LOADAVG,
                                      payload.get('LoadAvg'))
        self.device_model.updateValue(index, DevMdl.LINKCOUNT,
                                      payload['Wifi'].get('LinkCount', "n/a"))
        self.device_model.updateValue(index, DevMdl.DOWNTIME,
                                      payload['Wifi'].get('Downtime', "n/a"))

        self.parse_power(index, payload, True)

        tele_idx = self.telemetry_model.devices.get(
            self.device_model.topic(index))

        if tele_idx:
            tele_device = self.telemetry_model.getNode(tele_idx)
            self.telemetry_model.setDeviceFriendlyName(
                tele_idx, self.device_model.friendly_name(index))

            pr = tele_device.provides()
            for k in pr.keys():
                self.telemetry_model.setData(pr[k], payload.get(k))

    def parse_telemetry(self, index, payload):
        device = self.telemetry_model.devices.get(
            self.device_model.topic(index))
        if device:
            node = self.telemetry_model.getNode(device)
            time = node.provides()['Time']
            if 'Time' in payload:
                self.telemetry_model.setData(time, payload.pop('Time'))

            temp_unit = "C"
            pres_unit = "hPa"

            if 'TempUnit' in payload:
                temp_unit = payload.pop('TempUnit')

            if 'PressureUnit' in payload:
                pres_unit = payload.pop('PressureUnit')

            for sensor in sorted(payload.keys()):
                if sensor == 'DS18x20':
                    for sns_name in payload[sensor].keys():
                        d = node.devices().get(sensor)
                        if not d:
                            d = self.telemetry_model.addDevice(
                                DS18x20, payload[sensor][sns_name]['Type'],
                                device)
                        self.telemetry_model.getNode(d).setTempUnit(temp_unit)
                        payload[sensor][sns_name]['Id'] = payload[sensor][
                            sns_name].pop('Address')

                        pr = self.telemetry_model.getNode(d).provides()
                        for pk in pr.keys():
                            self.telemetry_model.setData(
                                pr[pk], payload[sensor][sns_name].get(pk))
                        self.tview.expand(d)

                elif sensor.startswith('DS18B20'):
                    d = node.devices().get(sensor)
                    if not d:
                        d = self.telemetry_model.addDevice(
                            DS18x20, sensor, device)
                    self.telemetry_model.getNode(d).setTempUnit(temp_unit)
                    pr = self.telemetry_model.getNode(d).provides()
                    for pk in pr.keys():
                        self.telemetry_model.setData(pr[pk],
                                                     payload[sensor].get(pk))
                    self.tview.expand(d)

                if sensor == 'COUNTER':
                    d = node.devices().get(sensor)
                    if not d:
                        d = self.telemetry_model.addDevice(
                            CounterSns, "Counter", device)
                    pr = self.telemetry_model.getNode(d).provides()
                    for pk in pr.keys():
                        self.telemetry_model.setData(pr[pk],
                                                     payload[sensor].get(pk))
                    self.tview.expand(d)

                else:
                    d = node.devices().get(sensor)
                    if not d:
                        d = self.telemetry_model.addDevice(
                            sensor_map.get(sensor, Node), sensor, device)
                    pr = self.telemetry_model.getNode(d).provides()
                    if 'Temperature' in pr:
                        self.telemetry_model.getNode(d).setTempUnit(temp_unit)
                    if 'Pressure' in pr or 'SeaPressure' in pr:
                        self.telemetry_model.getNode(d).setPresUnit(pres_unit)
                    for pk in pr.keys():
                        self.telemetry_model.setData(pr[pk],
                                                     payload[sensor].get(pk))
                    self.tview.expand(d)
        # self.tview.resizeColumnToContents(0)

    def console_log(self, topic, description, payload="", known=True):
        longest_tp = 0
        longest_fn = 0
        short_topic = "/".join(topic.split("/")[0:-1])
        fname = self.devices.get(short_topic, "")
        if not fname:
            device = self.device_model.findDevice(topic)
            fname = self.device_model.friendly_name(device.index)
            self.devices.update({short_topic: fname})
        self.console_model.addEntry(topic, fname, description, payload, known)

        if len(topic) > longest_tp:
            longest_tp = len(topic)
            self.console_view.resizeColumnToContents(1)

        if len(fname) > longest_fn:
            longest_fn = len(fname)
            self.console_view.resizeColumnToContents(1)

    def view_payload(self, idx):
        if self.cbFilter.isChecked():
            idx = self.sorted_console_model.mapToSource(idx)
        row = idx.row()
        timestamp = self.console_model.data(
            self.console_model.index(row, CnsMdl.TIMESTAMP))
        topic = self.console_model.data(
            self.console_model.index(row, CnsMdl.TOPIC))
        payload = self.console_model.data(
            self.console_model.index(row, CnsMdl.PAYLOAD))

        dlg = PayloadViewDialog(timestamp, topic, payload)
        dlg.exec_()

    def select_cons_entry(self, idx):
        self.cons_idx = idx

    def export(self):
        fname, _ = QFileDialog.getSaveFileName(self,
                                               "Export device list as...",
                                               directory=QDir.homePath(),
                                               filter="CSV files (*.csv)")
        if fname:
            if not fname.endswith(".csv"):
                fname += ".csv"

            with open(fname, "w", encoding='utf8') as f:
                column_titles = [
                    'mac', 'topic', 'friendly_name', 'full_topic',
                    'cmnd_topic', 'stat_topic', 'tele_topic', 'module',
                    'module_id', 'firmware', 'core'
                ]
                c = csv.writer(f)
                c.writerow(column_titles)

                for r in range(self.device_model.rowCount()):
                    d = self.device_model.index(r, 0)
                    c.writerow([
                        self.device_model.mac(d),
                        self.device_model.topic(d),
                        self.device_model.friendly_name(d),
                        self.device_model.fullTopic(d),
                        self.device_model.commandTopic(d),
                        self.device_model.statTopic(d),
                        self.device_model.teleTopic(d),
                        modules.get(self.device_model.module(d)),
                        self.device_model.module(d),
                        self.device_model.firmware(d),
                        self.device_model.core(d)
                    ])

    def bssid(self):
        BSSIdDialog().exec_()
        # if dlg.exec_() == QDialog.Accepted:

    def toggle_console_filter(self, state):
        self.cbxFilterDevice.setEnabled(state)
        if state:
            self.console_view.setModel(self.sorted_console_model)
        else:
            self.console_view.setModel(self.console_model)

    def select_console_filter(self, fname):
        self.sorted_console_model.setFilterFixedString(fname)

    def closeEvent(self, e):
        self.settings.setValue("window_geometry", self.saveGeometry())
        self.settings.setValue("splitter_state",
                               self.main_splitter.saveState())
        self.settings.sync()
        e.accept()
Beispiel #7
0
class FilteringComboBox(Widget):
    """Combination of QCombobox and QLineEdit with autocompletionself.
    Line edit and completer model is taken from QSqlTable mod

    Parameters:
        table (str): db table name containing data for combobox
        column (str): column name containing data for combobox
        color (str): 'rgb(r, g, b)' used for primary color
        font_size (int): default text font size in pt
        _model (QSqlTableModel): data model
        _col (int): display data model source coulumn
        _proxy (QSortFilterProxyModel): completer data model.
                                        _proxy.sourceModel() == _model
        _le (QLineEdit): QCombobox LineEdit


    Methods:
        createEditor(): (Widget): returns user input widgets
        value(): (str): returns user input text value
        setValue(value(str)): sets editor widget display value
        style(): (str): Returns CSS stylesheet string for input widget
        updateModel(): updates input widget model

    Args:
        table (str): db table name containing data for combobox
        column (str): column name containing data for combobox
    """
    def __init__(self,
                 parent,
                 placeholderText,
                 table,
                 column,
                 color='rgb(0,145,234)',
                 image=''):
        self.table = table
        self.column = column
        self.color = color
        super().__init__(parent, placeholderText, image)
        self.updateModel()

    def createEditor(self):
        # setup data model
        self._model = QSqlTableModel()
        self._model.setTable(self.table)
        self._col = self._model.fieldIndex(self.column)

        # setup filter model for sorting and filtering
        self._proxy = QSortFilterProxyModel()
        self._proxy.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self._proxy.setSourceModel(self._model)
        self._proxy.setFilterKeyColumn(self._col)

        # setup completer
        self._completer = QCompleter()
        self._completer.setModel(self._proxy)
        self._completer.setCompletionColumn(self._col)
        self._completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion)

        # setup combobox
        editor = QComboBox()
        editor.setModel(self._proxy)
        editor.setModelColumn(self._col)
        editor.setEditable(True)
        editor.setFocusPolicy(Qt.StrongFocus)
        editor.setInsertPolicy(QComboBox.NoInsert)
        editor.setCompleter(self._completer)

        # setup connections
        editor.currentTextChanged[str].connect(self.onActivated)

        # setup editor appearence
        style = self.style()
        editor.setStyleSheet(style)
        editor.lineEdit().setStyleSheet(style)
        font = editor.font()
        self._completer.popup().setFont(font)

        return editor

    @pyqtSlot(str)
    def onActivated(self, text):
        print('combo_box filter text', text)
        if not text:  # placeholder text displayed and label is not visible
            self._editor.setCurrentIndex(-1)
            if self.toggle:
                self._label.showLabel(False)
        else:  # selected text is displayed and label is visible
            # self._editor.showPopup()
            # self._editor.lineEdit().setFocus()
            print('current copmpletion string',
                  self._completer.currentCompletion())
            self._proxy.setFilterFixedString(text)
            if self.toggle:
                self._label.showLabel(True)

    def style(self):
        """Returns stylesheet for editors

        Returns:
            style (str)
        """
        style = """
            QLineEdit {{
                border: none;
                padding-bottom: 2px;
                border-bottom: 1px solid rgba(0,0,0,0.42);
                background-color: white;
                color: rgba(0,0,0,0.42);
                font-size: {font_size}pt;}}

            QLineEdit:editable {{
                padding-bottom: 2px;
                border-bottom: 1px rgba(0,0,0,0.42);
                color: rgba(0,0,0,0.42);}}

            QLineEdit:disabled {{
                border: none;
                padding-bottom: 2px;
                border-bottom: 1px rgba(0,0,0,0.42);
                color: rgba(0,0,0,0.38);}}

            QLineEdit:hover {{
                padding-bottom: 2px;
                border-bottom: 2px solid rgba(0,0,0,0.6);
                color: rgba(0,0,0,0.54);
                }}

            QLineEdit:focus {{
                padding-bottom: 2px;
                border-bottom: 2px solid {color};
                color: rgba(0,0,0,0.87);}}

            QLineEdit:pressed {{
                border-bottom: 2px {color};
                font: bold;
                color: rgba(0,0,0,0.87)}}

            QComboBox {{
                border: none;
                padding-bottom: 2px;
                font-size: {font_size}pt;
                }}

            QComboBox::down-arrow {{
                image: url('dropdown.png');
                background-color: white;
                border: 0px white;
                padding: 0px;
                margin: 0px;
                height:14px;
                width:14px;}}

            QComboBox::drop-down{{
                subcontrol-position: center right;
                border: 0px;
                margin: 0px;
            }}

            QComboBox QAbstractItemView {{
                font: {font_size};}}

        """.format(color=self.color,
                   font_size=self._editor_font_size,
                   dropdown=DROPDOWN_PNG)
        return style

    def updateModel(self):
        model = self._editor.model().sourceModel()
        col = self._editor.modelColumn()
        model.select()
        model.sort(col, Qt.AscendingOrder)

    def value(self):
        return self._editor.currentText()

    def setValue(self, value):
        self._editor.setCurrentText(value)
Beispiel #8
0
class ListViewSearch(QListView):
    """
    An QListView with an implicit and transparent row filtering.
    """
    def __init__(self, *a, preferred_size=None, **ak):
        super().__init__(*a, **ak)
        self.__search = QLineEdit(self, placeholderText="筛选...")
        self.__search.textEdited.connect(self.__setFilterString)
        # Use an QSortFilterProxyModel for filtering. Note that this is
        # never set on the view, only its rows insertes/removed signals are
        # connected to observe an update row hidden state.
        self.__pmodel = QSortFilterProxyModel(
            self, filterCaseSensitivity=Qt.CaseInsensitive)
        self.__pmodel.rowsAboutToBeRemoved.connect(
            self.__filter_rowsAboutToBeRemoved)
        self.__pmodel.rowsInserted.connect(self.__filter_rowsInserted)
        self.__layout()
        self.preferred_size = preferred_size

    def setFilterPlaceholderText(self, text: str):
        self.__search.setPlaceholderText(text)

    def filterPlaceholderText(self) -> str:
        return self.__search.placeholderText()

    def setFilterProxyModel(self, proxy: QSortFilterProxyModel) -> None:
        """
        Set an instance of QSortFilterProxyModel that will be used for filtering
        the model. The `proxy` must be a filtering proxy only; it MUST not sort
        the row of the model.
        The FilterListView takes ownership of the proxy.
        """
        self.__pmodel.rowsAboutToBeRemoved.disconnect(
            self.__filter_rowsAboutToBeRemoved)
        self.__pmodel.rowsInserted.disconnect(self.__filter_rowsInserted)
        self.__pmodel = proxy
        proxy.setParent(self)
        self.__pmodel.rowsAboutToBeRemoved.connect(
            self.__filter_rowsAboutToBeRemoved)
        self.__pmodel.rowsInserted.connect(self.__filter_rowsInserted)
        self.__pmodel.setSourceModel(self.model())
        self.__filter_reset()

    def filterProxyModel(self) -> QSortFilterProxyModel:
        return self.__pmodel

    def setModel(self, model: QAbstractItemModel) -> None:
        super().setModel(model)
        self.__pmodel.setSourceModel(model)
        self.__filter_reset()

    def setRootIndex(self, index: QModelIndex) -> None:
        super().setRootIndex(index)
        self.__filter_reset()

    def __filter_reset(self):
        root = self.rootIndex()
        pm = self.__pmodel
        for i in range(self.__pmodel.rowCount(root)):
            self.setRowHidden(i, not pm.filterAcceptsRow(i, root))

    def __setFilterString(self, string: str):
        self.__pmodel.setFilterFixedString(string)

    def setFilterString(self, string: str):
        """Set the filter string."""
        self.__search.setText(string)
        self.__pmodel.setFilterFixedString(string)

    def filterString(self):
        """Return the filter string."""
        return self.__search.text()

    def __filter_set(self, rows: Iterable[int], state: bool):
        for r in rows:
            self.setRowHidden(r, state)

    def __filter_rowsAboutToBeRemoved(self, parent: QModelIndex, start: int,
                                      end: int) -> None:
        fmodel = self.__pmodel
        mrange = QItemSelection(fmodel.index(start, 0, parent),
                                fmodel.index(end, 0, parent))
        mranges = fmodel.mapSelectionToSource(mrange)
        for mrange in mranges:
            self.__filter_set(range(mrange.top(), mrange.bottom() + 1), True)

    def __filter_rowsInserted(self, parent: QModelIndex, start: int,
                              end: int) -> None:
        fmodel = self.__pmodel
        mrange = QItemSelection(fmodel.index(start, 0, parent),
                                fmodel.index(end, 0, parent))
        mranges = fmodel.mapSelectionToSource(mrange)
        for mrange in mranges:
            self.__filter_set(range(mrange.top(), mrange.bottom() + 1), False)

    def resizeEvent(self, event: QResizeEvent) -> None:
        super().resizeEvent(event)

    def updateGeometries(self) -> None:
        super().updateGeometries()
        self.__layout()

    def __layout(self):
        margins = self.viewportMargins()
        search = self.__search
        sh = search.sizeHint()
        size = self.size()
        margins.setTop(sh.height())
        vscroll = self.verticalScrollBar()
        style = self.style()
        transient = style.styleHint(QStyle.SH_ScrollBar_Transient, None,
                                    vscroll)
        w = size.width()
        if vscroll.isVisibleTo(self) and not transient:
            w = w - vscroll.width() - 1
        search.setGeometry(0, 0, w, sh.height())
        self.setViewportMargins(margins)

    def sizeHint(self):
        return (self.preferred_size
                if self.preferred_size is not None else super().sizeHint())
Beispiel #9
0
class MainWindow(QMainWindow, Ui_MainWindow):
	"""
	Main window for the application with groups and password lists
	"""

	KEY_IDX = 0  # column where key is shown in password table
	PASSWORD_IDX = 1  # column where password is shown in password table
	COMMENTS_IDX = 2  # column where comments is shown in password table
	NO_OF_PASSWDTABLE_COLUMNS = 3  # 3 columns: key + value/passwd/secret + comments
	CACHE_IDX = 0  # column of QWidgetItem in whose data we cache decrypted passwords+comments

	def __init__(self, pwMap, settings, dbFilename):
		"""
		@param pwMap: a PasswordMap instance with encrypted passwords
		@param dbFilename: file name for saving pwMap
		"""
		super(MainWindow, self).__init__()
		self.setupUi(self)

		self.logger = settings.logger
		self.settings = settings
		self.pwMap = pwMap
		self.selectedGroup = None
		self.modified = False  # modified flag for "Save?" question on exit
		self.dbFilename = dbFilename

		self.groupsModel = QStandardItemModel(parent=self)
		self.groupsModel.setHorizontalHeaderLabels([u"Password group"])
		self.groupsFilter = QSortFilterProxyModel(parent=self)
		self.groupsFilter.setSourceModel(self.groupsModel)

		self.groupsTree.setModel(self.groupsFilter)
		self.groupsTree.setContextMenuPolicy(Qt.CustomContextMenu)
		self.groupsTree.customContextMenuRequested.connect(self.showGroupsContextMenu)
		# Dont use e following line, it would cause loadPasswordsBySelection
		# to be called twice on mouse-click.
		# self.groupsTree.clicked.connect(self.loadPasswordsBySelection)
		self.groupsTree.selectionModel().selectionChanged.connect(self.loadPasswordsBySelection)
		self.groupsTree.setSortingEnabled(True)

		self.passwordTable.setContextMenuPolicy(Qt.CustomContextMenu)
		self.passwordTable.customContextMenuRequested.connect(self.showPasswdContextMenu)
		self.passwordTable.setSelectionBehavior(QAbstractItemView.SelectRows)
		self.passwordTable.setSelectionMode(QAbstractItemView.SingleSelection)

		shortcut = QShortcut(QKeySequence(u"Ctrl+C"), self.passwordTable, self.copyPasswordFromSelection)
		shortcut.setContext(Qt.WidgetShortcut)

		self.actionQuit.triggered.connect(self.close)
		self.actionQuit.setShortcut(QKeySequence(u"Ctrl+Q"))
		self.actionExport.triggered.connect(self.exportCsv)
		self.actionImport.triggered.connect(self.importCsv)
		self.actionBackup.triggered.connect(self.saveBackup)
		self.actionAbout.triggered.connect(self.printAbout)
		self.actionSave.triggered.connect(self.saveDatabase)
		self.actionSave.setShortcut(QKeySequence(u"Ctrl+S"))

		# headerKey = QTableWidgetItem(u"Key")
		# headerValue = QTableWidgetItem(u"Password/Value")
		# headerComments = QTableWidgetItem(u"Comments")
		# self.passwordTable.setColumnCount(self.NO_OF_PASSWDTABLE_COLUMNS)
		# self.passwordTable.setHorizontalHeaderItem(self.KEY_IDX, headerKey)
		# self.passwordTable.setHorizontalHeaderItem(self.PASSWORD_IDX, headerValue)
		# self.passwordTable.setHorizontalHeaderItem(self.COMMENTS_IDX, headerComments)
		#
		# self.passwordTable.resizeRowsToContents()
		# self.passwordTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
		# self.passwordTable.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
		# self.passwordTable.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)

		self.searchEdit.textChanged.connect(self.filterGroups)

		if pwMap is not None:
			self.setPwMap(pwMap)

		self.clipboard = QApplication.clipboard()

		self.timer = QTimer(parent=self)
		self.timer.timeout.connect(self.clearClipboard)

	def setPwMap(self, pwMap):
		""" if not done in __init__ pwMap can be supplied later """
		self.pwMap = pwMap
		groupNames = self.pwMap.groups.keys()
		for groupName in groupNames:
			item = QStandardItem(groupName)
			self.groupsModel.appendRow(item)
		self.groupsTree.sortByColumn(0, Qt.AscendingOrder)
		self.settings.mlogger.log("pwMap was initialized.",
			logging.DEBUG, "GUI IO")

	def setModified(self, modified):
		"""
		Sets the modified flag so that user is notified when exiting
		with unsaved changes.
		"""
		self.modified = modified
		self.setWindowTitle("TrezorPass" + "*" * int(self.modified))

	def showGroupsContextMenu(self, point):
		"""
		Show context menu for group management.

		@param point: point in self.groupsTree where click occured
		"""
		self.addGroupMenu = QMenu(self)
		newGroupAction = QAction('Add group', self)
		editGroupAction = QAction('Rename group', self)
		deleteGroupAction = QAction('Delete group', self)
		self.addGroupMenu.addAction(newGroupAction)
		self.addGroupMenu.addAction(editGroupAction)
		self.addGroupMenu.addAction(deleteGroupAction)

		# disable deleting if no point is clicked on
		proxyIdx = self.groupsTree.indexAt(point)
		itemIdx = self.groupsFilter.mapToSource(proxyIdx)
		item = self.groupsModel.itemFromIndex(itemIdx)
		if item is None:
			deleteGroupAction.setEnabled(False)

		action = self.addGroupMenu.exec_(self.groupsTree.mapToGlobal(point))

		if action == newGroupAction:
			self.createGroupWithCheck()
		elif action == editGroupAction:
			self.editGroupWithCheck(item)
		elif action == deleteGroupAction:
			self.deleteGroupWithCheck(item)

	def showPasswdContextMenu(self, point):
		"""
		Show context menu for password management

		@param point: point in self.passwordTable where click occured
		"""
		self.passwdMenu = QMenu(self)
		showPasswordAction = QAction('Show password', self)
		copyPasswordAction = QAction('Copy password', self)
		copyPasswordAction.setShortcut(QKeySequence("Ctrl+C"))
		showCommentsAction = QAction('Show comments', self)
		copyCommentsAction = QAction('Copy comments', self)
		showAllAction = QAction('Show all of group', self)
		newItemAction = QAction('New item', self)
		deleteItemAction = QAction('Delete item', self)
		editItemAction = QAction('Edit item', self)
		self.passwdMenu.addAction(showPasswordAction)
		self.passwdMenu.addAction(copyPasswordAction)
		self.passwdMenu.addSeparator()
		self.passwdMenu.addAction(showCommentsAction)
		self.passwdMenu.addAction(copyCommentsAction)
		self.passwdMenu.addSeparator()
		self.passwdMenu.addAction(showAllAction)
		self.passwdMenu.addSeparator()
		self.passwdMenu.addAction(newItemAction)
		self.passwdMenu.addAction(deleteItemAction)
		self.passwdMenu.addAction(editItemAction)

		# disable creating if no group is selected
		if self.selectedGroup is None:
			newItemAction.setEnabled(False)
			showAllAction.setEnabled(False)

		# disable deleting if no point is clicked on
		item = self.passwordTable.itemAt(point.x(), point.y())
		if item is None:
			deleteItemAction.setEnabled(False)
			showPasswordAction.setEnabled(False)
			copyPasswordAction.setEnabled(False)
			showCommentsAction.setEnabled(False)
			copyCommentsAction.setEnabled(False)
			editItemAction.setEnabled(False)

		action = self.passwdMenu.exec_(self.passwordTable.mapToGlobal(point))
		if action == newItemAction:
			self.createPassword()
		elif action == deleteItemAction:
			self.deletePassword(item)
		elif action == editItemAction:
			self.editPassword(item)
		elif action == copyPasswordAction:
			self.copyPasswordFromItem(item)
		elif action == showPasswordAction:
			self.showPassword(item)
		elif action == copyCommentsAction:
			self.copyCommentsFromItem(item)
		elif action == showCommentsAction:
			self.showComments(item)
		elif action == showAllAction:
			self.showAll()

	def createGroup(self, groupName, group=None):
		"""
		Slot to create a password group.
		"""
		newItem = QStandardItem(groupName)
		self.groupsModel.appendRow(newItem)
		self.pwMap.addGroup(groupName)
		if group is not None:
			self.pwMap.replaceGroup(groupName, group)

		# make new item selected to save a few clicks
		itemIdx = self.groupsModel.indexFromItem(newItem)
		proxyIdx = self.groupsFilter.mapFromSource(itemIdx)
		self.groupsTree.selectionModel().select(proxyIdx,
			QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
		self.groupsTree.sortByColumn(0, Qt.AscendingOrder)

		# Make item's passwords loaded so new key-value entries can be created
		# right away - better from UX perspective.
		self.loadPasswords(newItem)

		self.setModified(True)
		self.settings.mlogger.log("Group '%s' was created." % (groupName),
			logging.DEBUG, "GUI IO")

	def createGroupWithCheck(self):
		"""
		Slot to create a password group.
		"""
		dialog = AddGroupDialog(self.pwMap.groups, self.settings)
		if not dialog.exec_():
			return
		groupName = dialog.newGroupName()
		self.createGroup(groupName)

	def createRenamedGroupWithCheck(self, groupNameOld, groupNameNew):
		"""
		Creates a copy of a group by name as utf-8 encoded string
		with a new group name.
		A more appropriate name for the method would be:
		createRenamedGroup().
		Since the entries inside the group are encrypted
		with the groupName, we cannot simply make a copy.
		We must decrypt with old name and afterwards encrypt
		with new name.
		If the group has many entries, each entry would require a 'Confirm'
		press on Trezor. So, to mkae it faster and more userfriendly
		we use the backup key to decrypt. This requires a single
		Trezor 'Confirm' press independent of how many entries there are
		in the group.
		@param groupNameOld: name of group to copy and rename
		@type groupNameOld: string
		@param groupNameNew: name of group to be created
		@type groupNameNew: string
		"""
		if groupNameOld not in self.pwMap.groups:
			raise KeyError("Password group does not exist")
		# with less than 3 rows dont bother the user with a pop-up
		rowCount = len(self.pwMap.groups[groupNameOld].entries)
		if rowCount < 3:
			self.pwMap.createRenamedGroupSecure(groupNameOld, groupNameNew)
			return

		msgBox = QMessageBox(parent=self)
		msgBox.setText("Do you want to use the more secure way?")
		msgBox.setIcon(QMessageBox.Question)
		msgBox.setWindowTitle("How to decrypt?")
		msgBox.setDetailedText("The more secure way requires pressing 'Confirm' "
			"on Trezor once for each entry in the group. %d presses in this "
			"case. This is recommended. "
			"Select 'Yes'.\n\n"
			"The less secure way requires only a single 'Confirm' click on the "
			"Trezor. This is not recommended. "
			"Select 'No'." % (rowCount))
		msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
		msgBox.setDefaultButton(QMessageBox.Yes)
		res = msgBox.exec_()

		if res == QMessageBox.Yes:
			moreSecure = True
		else:
			moreSecure = False
		groupNew = self.pwMap.createRenamedGroup(groupNameOld, groupNameNew, moreSecure)
		self.settings.mlogger.log("Copy of group '%s' with new name '%s' "
			"was created the %s way." %
			(groupNameOld, groupNameNew, 'secure' if moreSecure else 'fast'),
			logging.DEBUG, "GUI IO")
		return(groupNew)

	def editGroup(self, item, groupNameOld, groupNameNew):
		"""
		Slot to edit name a password group.
		"""
		groupNew = self.createRenamedGroupWithCheck(groupNameOld, groupNameNew)
		self.deleteGroup(item)
		self.createGroup(groupNameNew, groupNew)
		self.settings.mlogger.log("Group '%s' was renamed to '%s'." % (groupNameOld, groupNameNew),
			logging.DEBUG, "GUI IO")

	def editGroupWithCheck(self, item):
		"""
		Slot to edit name a password group.
		"""
		groupNameOld = encoding.normalize_nfc(item.text())
		dialog = AddGroupDialog(self.pwMap.groups, self.settings)
		dialog.setWindowTitle("Edit group name")
		dialog.groupNameLabel.setText("New name for group")
		dialog.setNewGroupName(groupNameOld)
		if not dialog.exec_():
			return

		groupNameNew = dialog.newGroupName()
		self.editGroup(item, groupNameOld, groupNameNew)

	def deleteGroup(self, item):  # without checking user
		groupName = encoding.normalize_nfc(item.text())
		self.selectedGroup = None
		del self.pwMap.groups[groupName]

		itemIdx = self.groupsModel.indexFromItem(item)
		self.groupsModel.takeRow(itemIdx.row())
		self.passwordTable.setRowCount(0)
		self.groupsTree.clearSelection()

		self.setModified(True)
		self.settings.mlogger.log("Group '%s' was deleted." % (groupName),
			logging.DEBUG, "GUI IO")

	def deleteGroupWithCheck(self, item):
		msgBox = QMessageBox(text="Are you sure about delete?", parent=self)
		msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
		res = msgBox.exec_()

		if res != QMessageBox.Yes:
			return

		self.deleteGroup(item)

	def deletePassword(self, item):
		msgBox = QMessageBox(text="Are you sure about delete?", parent=self)
		msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
		res = msgBox.exec_()

		if res != QMessageBox.Yes:
			return

		row = self.passwordTable.row(item)
		self.passwordTable.removeRow(row)
		group = self.pwMap.groups[self.selectedGroup]
		group.removeEntry(row)

		self.passwordTable.resizeRowsToContents()
		self.passwordTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
		self.passwordTable.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
		self.passwordTable.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)
		self.setModified(True)
		self.settings.mlogger.log("Row '%d' was deleted." % (row),
			logging.DEBUG, "GUI IO")

	def logCache(self, row):
		item = self.passwordTable.item(row, self.CACHE_IDX)
		cachedTuple = item.data(Qt.UserRole)
		if cachedTuple is None:
			cachedPassword, cachedComments = (None, None)
		else:
			cachedPassword, cachedComments = cachedTuple
		if cachedPassword is not None:
			cachedPassword = u'***'
		if cachedComments is not None:
			cachedComments = cachedComments[0:3] + u'...'
		self.settings.mlogger.log("Cache holds '%s' and '%s'." %
			(cachedPassword, cachedComments), logging.DEBUG, "Cache")

	def cachePasswordComments(self, row, password, comments):
		item = self.passwordTable.item(row, self.CACHE_IDX)
		item.setData(Qt.UserRole, QVariant((password, comments)))

	def cachedPassword(self, row):
		"""
		Retrieve cached password for given row of currently selected group.
		Returns password as string or None if no password cached.
		"""
		item = self.passwordTable.item(row, self.CACHE_IDX)
		cachedTuple = item.data(Qt.UserRole)
		if cachedTuple is None:
			cachedPassword, cachedComments = (None, None)
		else:
			cachedPassword, cachedComments = cachedTuple
		return cachedPassword

	def cachedComments(self, row):
		"""
		Retrieve cached comments for given row of currently selected group.
		Returns comments as string or None if no coments cached.
		"""
		item = self.passwordTable.item(row, self.CACHE_IDX)
		cachedTuple = item.data(Qt.UserRole)
		if cachedTuple is None:
			cachedPassword, cachedComments = (None, None)
		else:
			cachedPassword, cachedComments = cachedTuple
		return cachedComments

	def cachedOrDecryptPassword(self, row):
		"""
		Try retrieving cached password for item in given row, otherwise
		decrypt with Trezor.
		"""
		cached = self.cachedPassword(row)

		if cached is not None:
			return cached
		else:  # decrypt with Trezor
			group = self.pwMap.groups[self.selectedGroup]
			pwEntry = group.entry(row)
			encPwComments = pwEntry[1]

			decryptedPwComments = self.pwMap.decryptPassword(encPwComments, self.selectedGroup)
			lngth = int(decryptedPwComments[0:4])
			decryptedPassword = decryptedPwComments[4:4+lngth]
			decryptedComments = decryptedPwComments[4+lngth:]
			# while we are at it, cache the comments too
			self.cachePasswordComments(row, decryptedPassword, decryptedComments)
			self.settings.mlogger.log("Decrypted password and comments "
				"for '%s', row '%d'." % (pwEntry[0], row),
				logging.DEBUG, "GUI IO")
		return decryptedPassword

	def cachedOrDecryptComments(self, row):
		"""
		Try retrieving cached comments for item in given row, otherwise
		decrypt with Trezor.
		"""
		cached = self.cachedComments(row)

		if cached is not None:
			return cached
		else:  # decrypt with Trezor
			group = self.pwMap.groups[self.selectedGroup]
			pwEntry = group.entry(row)
			encPwComments = pwEntry[1]

			decryptedPwComments = self.pwMap.decryptPassword(encPwComments, self.selectedGroup)
			lngth = int(decryptedPwComments[0:4])
			decryptedPassword = decryptedPwComments[4:4+lngth]
			decryptedComments = decryptedPwComments[4+lngth:]
			self.cachePasswordComments(row, decryptedPassword, decryptedComments)
			self.settings.mlogger.log("Decrypted password and comments "
				"for '%s', row '%d'." % (pwEntry[0], row),
				logging.DEBUG, "GUI IO")
		return decryptedComments

	def showPassword(self, item):
		# check if this password has been decrypted, use cached version
		row = self.passwordTable.row(item)
		self.logCache(row)
		try:
			decryptedPassword = self.cachedOrDecryptPassword(row)
		except CallException:
			return
		item = QTableWidgetItem(decryptedPassword)

		self.passwordTable.setItem(row, self.PASSWORD_IDX, item)

	def showComments(self, item):
		# check if these comments has been decrypted, use cached version
		row = self.passwordTable.row(item)
		try:
			decryptedComments = self.cachedOrDecryptComments(row)
		except CallException:
			return
		item = QTableWidgetItem(decryptedComments)

		self.passwordTable.setItem(row, self.COMMENTS_IDX, item)

	def showAllSecure(self):
		rowCount = self.passwordTable.rowCount()
		for row in range(rowCount):
			try:
				decryptedPassword = self.cachedOrDecryptPassword(row)
			except CallException:
				return
			item = QTableWidgetItem(decryptedPassword)
			self.passwordTable.setItem(row, self.PASSWORD_IDX, item)
			try:
				decryptedComments = self.cachedOrDecryptComments(row)
			except CallException:
				return
			item = QTableWidgetItem(decryptedComments)
			self.passwordTable.setItem(row, self.COMMENTS_IDX, item)

		self.settings.mlogger.log("Showed all entries for group '%s' the secure way." %
			(self.selectedGroup), logging.DEBUG, "GUI IO")

	def showAllFast(self):
		try:
			privateKey = self.pwMap.backupKey.unwrapPrivateKey()
		except CallException:
			return
		group = self.pwMap.groups[self.selectedGroup]

		row = 0
		for key, _, bkupPw in group.entries:
			decryptedPwComments = self.pwMap.backupKey.decryptPassword(bkupPw, privateKey)
			lngth = int(decryptedPwComments[0:4])
			password = decryptedPwComments[4:4+lngth]
			comments = decryptedPwComments[4+lngth:]
			item = QTableWidgetItem(key)
			pwItem = QTableWidgetItem(password)
			commentsItem = QTableWidgetItem(comments)
			self.passwordTable.setItem(row, self.KEY_IDX, item)
			self.passwordTable.setItem(row, self.PASSWORD_IDX, pwItem)
			self.passwordTable.setItem(row, self.COMMENTS_IDX, commentsItem)
			self.cachePasswordComments(row, password, comments)
			row = row+1

		self.passwordTable.resizeRowsToContents()
		self.passwordTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
		self.passwordTable.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
		self.passwordTable.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)

		self.settings.mlogger.log("Showed all entries for group '%s' the fast way." %
			(self.selectedGroup), logging.DEBUG, "GUI IO")

	def showAll(self):
		"""
		show all passwords and comments in plaintext in GUI
		can be called without any password selectedGroup
		a group must be selected
		"""
		# with less than 3 rows dont bother the user with a pop-up
		if self.passwordTable.rowCount() < 3:
			self.showAllSecure()
			return

		msgBox = QMessageBox(parent=self)
		msgBox.setText("Do you want to use the more secure way?")
		msgBox.setIcon(QMessageBox.Question)
		msgBox.setWindowTitle("How to decrypt?")
		msgBox.setDetailedText("The more secure way requires pressing 'Confirm' "
			"on Trezor once for each entry in the group. %d presses in this "
			"case. This is recommended. "
			"Select 'Yes'.\n\n"
			"The less secure way requires only a single 'Confirm' click on the "
			"Trezor. This is not recommended. "
			"Select 'No'." % (self.passwordTable.rowCount()))
		msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
		msgBox.setDefaultButton(QMessageBox.Yes)
		res = msgBox.exec_()

		if res == QMessageBox.Yes:
			self.showAllSecure()
		else:
			self.showAllFast()

	def createPassword(self):
		"""
		Slot to create key-value password entry.
		"""
		if self.selectedGroup is None:
			return
		group = self.pwMap.groups[self.selectedGroup]
		dialog = AddPasswordDialog(self.pwMap.trezor, self.settings)
		if not dialog.exec_():
			return

		plainPw = dialog.pw1()
		plainComments = dialog.comments()
		if len(plainPw) + len(plainComments) > basics.MAX_SIZE_OF_PASSWDANDCOMMENTS:
			self.settings.mlogger.log("Password and/or comments too long. "
				"Combined they must not be larger than %d." %
				basics.MAX_SIZE_OF_PASSWDANDCOMMENTS,
				logging.CRITICAL, "User IO")
			return

		row = self.passwordTable.rowCount()
		self.passwordTable.setRowCount(row+1)
		item = QTableWidgetItem(dialog.key())
		pwItem = QTableWidgetItem("*****")
		commentsItem = QTableWidgetItem("*****")
		self.passwordTable.setItem(row, self.KEY_IDX, item)
		self.passwordTable.setItem(row, self.PASSWORD_IDX, pwItem)
		self.passwordTable.setItem(row, self.COMMENTS_IDX, commentsItem)

		plainPwComments = ("%4d" % len(plainPw)) + plainPw + plainComments
		encPw = self.pwMap.encryptPassword(plainPwComments, self.selectedGroup)
		bkupPw = self.pwMap.backupKey.encryptPassword(plainPwComments)
		group.addEntry(dialog.key(), encPw, bkupPw)

		self.cachePasswordComments(row, plainPw, plainComments)

		self.passwordTable.resizeRowsToContents()
		self.passwordTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
		self.passwordTable.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
		self.passwordTable.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)
		self.setModified(True)
		self.settings.mlogger.log("Password and comments entry "
			"for '%s', row '%d' was created." % (dialog.key(), row),
			logging.DEBUG, "GUI IO")

	def editPassword(self, item):
		row = self.passwordTable.row(item)
		group = self.pwMap.groups[self.selectedGroup]
		try:
			decrypted = self.cachedOrDecryptPassword(row)
			decryptedComments = self.cachedOrDecryptComments(row)
		except CallException:
			return

		dialog = AddPasswordDialog(self.pwMap.trezor, self.settings)
		entry = group.entry(row)
		dialog.keyEdit.setText(encoding.normalize_nfc(entry[0]))
		dialog.pwEdit1.setText(encoding.normalize_nfc(decrypted))
		dialog.pwEdit2.setText(encoding.normalize_nfc(decrypted))
		doc = QTextDocument(encoding.normalize_nfc(decryptedComments), parent=self)
		dialog.commentsEdit.setDocument(doc)

		if not dialog.exec_():
			return

		item = QTableWidgetItem(dialog.key())
		pwItem = QTableWidgetItem("*****")
		commentsItem = QTableWidgetItem("*****")
		self.passwordTable.setItem(row, self.KEY_IDX, item)
		self.passwordTable.setItem(row, self.PASSWORD_IDX, pwItem)
		self.passwordTable.setItem(row, self.COMMENTS_IDX, commentsItem)

		plainPw = dialog.pw1()
		plainComments = dialog.comments()
		if len(plainPw) + len(plainComments) > basics.MAX_SIZE_OF_PASSWDANDCOMMENTS:
			self.settings.mlogger.log("Password and/or comments too long. "
				"Combined they must not be larger than %d." %
				basics.MAX_SIZE_OF_PASSWDANDCOMMENTS,
				logging.CRITICAL, "User IO")
			return

		plainPwComments = ("%4d" % len(plainPw)) + plainPw + plainComments

		encPw = self.pwMap.encryptPassword(plainPwComments, self.selectedGroup)
		bkupPw = self.pwMap.backupKey.encryptPassword(plainPwComments)
		group.updateEntry(row, dialog.key(), encPw, bkupPw)

		self.cachePasswordComments(row, plainPw, plainComments)

		self.setModified(True)
		self.settings.mlogger.log("Password and comments entry "
			"for '%s', row '%d' was edited." % (dialog.key(), row),
			logging.DEBUG, "GUI IO")

	def copyPasswordFromSelection(self):
		"""
		Copy selected password to clipboard. Password is decrypted if
		necessary.
		"""
		indexes = self.passwordTable.selectedIndexes()
		if not indexes:
			return

		# there will be more indexes as the selection is on a row
		row = indexes[0].row()
		item = self.passwordTable.item(row, self.PASSWORD_IDX)
		self.copyPasswordFromItem(item)

	def copyPasswordFromItem(self, item):
		row = self.passwordTable.row(item)
		try:
			decryptedPassword = self.cachedOrDecryptPassword(row)
		except CallException:
			return

		self.clipboard.setText(decryptedPassword)
		# Do not log contents of clipboard, contains secrets!
		self.settings.mlogger.log("Copied text to clipboard.", logging.DEBUG,
			"Clipboard")
		if basics.CLIPBOARD_TIMEOUT_IN_SEC > 0:
			self.timer.start(basics.CLIPBOARD_TIMEOUT_IN_SEC*1000)  # cancels previous timer

	def copyCommentsFromItem(self, item):
		row = self.passwordTable.row(item)
		try:
			decryptedComments = self.cachedOrDecryptComments(row)
		except CallException:
			return

		self.clipboard.setText(decryptedComments)
		# Do not log contents of clipboard, contains secrets!
		self.settings.mlogger.log("Copied text to clipboard.", logging.DEBUG,
			"Clipboard")
		if basics.CLIPBOARD_TIMEOUT_IN_SEC > 0:
			self.timer.start(basics.CLIPBOARD_TIMEOUT_IN_SEC*1000)  # cancels previous timer

	def clearClipboard(self):
		self.clipboard.clear()
		self.timer.stop()  # cancels previous timer
		self.settings.mlogger.log("Clipboard cleared.", logging.DEBUG,
			"Clipboard")

	def loadPasswords(self, item):
		"""
		Slot that should load items for group that has been clicked on.
		"""
		self.passwordTable.clear()  # clears cahce, but also clears the header, the 3 titles
		headerKey = QTableWidgetItem(u"Key")
		headerValue = QTableWidgetItem(u"Password/Value")
		headerComments = QTableWidgetItem(u"Comments")
		self.passwordTable.setColumnCount(self.NO_OF_PASSWDTABLE_COLUMNS)
		self.passwordTable.setHorizontalHeaderItem(self.KEY_IDX, headerKey)
		self.passwordTable.setHorizontalHeaderItem(self.PASSWORD_IDX, headerValue)
		self.passwordTable.setHorizontalHeaderItem(self.COMMENTS_IDX, headerComments)

		groupName = encoding.normalize_nfc(item.text())
		self.selectedGroup = groupName
		group = self.pwMap.groups[groupName]
		self.passwordTable.setRowCount(len(group.entries))

		i = 0
		for key, encValue, bkupValue in group.entries:
			item = QTableWidgetItem(key)
			pwItem = QTableWidgetItem("*****")
			commentsItem = QTableWidgetItem("*****")
			self.passwordTable.setItem(i, self.KEY_IDX, item)
			self.passwordTable.setItem(i, self.PASSWORD_IDX, pwItem)
			self.passwordTable.setItem(i, self.COMMENTS_IDX, commentsItem)
			i = i+1

		self.passwordTable.resizeRowsToContents()
		self.passwordTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
		self.passwordTable.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
		self.passwordTable.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)
		self.settings.mlogger.log("Loaded password group '%s'." % (groupName),
			logging.DEBUG, "GUI IO")

	def loadPasswordsBySelection(self):
		proxyIdx = self.groupsTree.currentIndex()
		itemIdx = self.groupsFilter.mapToSource(proxyIdx)
		selectedItem = self.groupsModel.itemFromIndex(itemIdx)

		if not selectedItem:
			return

		self.loadPasswords(selectedItem)

	def filterGroups(self, substring):
		"""
		Filter groupsTree view to have items containing given substring.
		"""
		self.groupsFilter.setFilterFixedString(substring)
		self.groupsTree.sortByColumn(0, Qt.AscendingOrder)

	def printAbout(self):
		"""
		Show window with about and version information.
		"""
		msgBox = QMessageBox(QMessageBox.Information, "About",
			"About <b>TrezorPass</b>: <br><br>TrezorPass is a safe " +
			"Password Manager application for people owning a Trezor who prefer to " +
			"keep their passwords local and not on the cloud. All passwords are " +
			"stored locally in a single file.<br><br>" +
			"<b>" + basics.NAME + " Version: </b>" + basics.VERSION_STR +
			" from " + basics.VERSION_DATE_STR +
			"<br><br><b>Python Version: </b>" + sys.version.replace(" \n", "; ") +
			"<br><br><b>Qt Version: </b>" + QT_VERSION_STR +
			"<br><br><b>PyQt Version: </b>" + PYQT_VERSION_STR, parent=self)
		msgBox.setIconPixmap(QPixmap("icons/TrezorPass.svg"))
		msgBox.exec_()

	def saveBackup(self):
		"""
		First it saves any pending changes to the pwdb database file. Then it uses an operating system call
		to copy the file appending a timestamp at the end of the file name.
		"""
		if self.modified:
			self.saveDatabase()
		backupFilename = self.settings.dbFilename + u"." + time.strftime('%Y%m%d%H%M%S')
		copyfile(self.settings.dbFilename, backupFilename)
		self.settings.mlogger.log("Backup of the encrypted database file has been created "
			"and placed into file \"%s\" (%d bytes)." % (backupFilename, os.path.getsize(backupFilename)),
			logging.INFO, "User IO")

	def importCsv(self):
		"""
		Read a properly formated CSV file from disk
		and add its contents to the current entries.

		Import format in CSV should be : group, key, password, comments

		There is no error checking, so be extra careful.

		Make a backup first.

		Entries from CSV will be *added* to existing pwdb. If this is not desired
		create an empty pwdb file first.

		GroupNames are unique, so if a groupname exists then
		key-password-comments tuples are added to the already existing group.
		If a group name does not exist, a new group is created and the
		key-password-comments tuples are added to the newly created group.

		Keys are not unique. So key-password-comments are always added.
		If a key with a given name existed before and the CSV file contains a
		key with the same name, then the key-password-comments is added and
		after the import the given group has 2 keys with the same name.
		Both keys exist then, the old from before the import, and the new one from the import.

		Examples of valid CSV file format: Some example lines
		First Bank account,login,myloginname,	# no comment
		[email protected],2-factor-authentication key,abcdef12345678,seed to regenerate 2FA codes	# with comment
		[email protected],recovery phrase,"passwd with 2 commas , ,",	# with comma
		[email protected],large multi-line comments,,"first line, some comma,
		second line"
		"""
		if self.modified:
			self.saveDatabase()
		copyfile(self.settings.dbFilename, self.settings.dbFilename + ".beforeCsvImport.backup")
		self.settings.mlogger.log("WARNING: You are about to import entries from a "
			"CSV file into your current password-database file. For safety "
			"reasons please make a backup copy now.\nFurthermore, this"
			"operation can be slow, so please be patient.", logging.NOTSET,
			"CSV import")
		dialog = QFileDialog(self, "Select CSV file to import",
			"", "CSV files (*.csv)")
		dialog.setAcceptMode(QFileDialog.AcceptOpen)

		res = dialog.exec_()
		if not res:
			return

		fname = encoding.normalize_nfc(dialog.selectedFiles()[0])
		listOfAddedGroupNames = self.pwMap.importCsv(fname)
		for groupNameNew in listOfAddedGroupNames:
			item = QStandardItem(groupNameNew)
			self.groupsModel.appendRow(item)
		self.groupsTree.sortByColumn(0, Qt.AscendingOrder)
		self.setModified(True)

	def exportCsv(self):
		"""
		Uses backup key encrypted by Trezor to decrypt all passwords
		at once and export them into a single paintext CSV file.

		Export format is CSV: group, key, password, comments
		"""
		self.settings.mlogger.log("WARNING: During backup/export all passwords will be "
			"written in plaintext to disk. If possible you should consider performing this "
			"operation on an offline or air-gapped computer. Be aware of the risks.",
			logging.NOTSET,	"CSV export")
		dialog = QFileDialog(self, "Select backup export file",
			"", "CSV files (*.csv)")
		dialog.setAcceptMode(QFileDialog.AcceptSave)

		res = dialog.exec_()
		if not res:
			return

		fname = encoding.normalize_nfc(dialog.selectedFiles()[0])
		self.pwMap.exportCsv(fname)

	def saveDatabase(self):
		"""
		Save main database file.
		"""
		self.pwMap.save(self.dbFilename)
		self.setModified(False)
		self.settings.mlogger.log("TrezorPass password database file was "
			"saved to '%s'." % (self.dbFilename), logging.DEBUG, "GUI IO")

	def closeEvent(self, event):
		if self.modified:
			msgBox = QMessageBox(text="Password database is modified. Save on exit?", parent=self)
			msgBox.setStandardButtons(QMessageBox.Yes |
				QMessageBox.No | QMessageBox.Cancel)
			reply = msgBox.exec_()

			if not reply or reply == QMessageBox.Cancel:
				event.ignore()
				return
			elif reply == QMessageBox.Yes:
				self.saveDatabase()

		event.accept()
Beispiel #10
0
class DictionaryEditorWidget(QDialog):
    userDictPath = os.path.join('files', 'DICT', 'User.DICT')
    pyboDictPath = os.path.join('files', 'DICT', 'Tibetan.DICT')

    def __init__(self, parent=None):
        super().__init__(parent)
        self.parent = parent
        self.pyboDict = self.getPyboDict()
        self.userDict = self.getUserDict()
        self.tokens = []
        self.resize(400, 600)
        self.setWindowTitle('Dictionary Editor')
        self.setupTable()
        self.initUI()

    @property
    def dict(self):
        mergedDict = self.pyboDict.copy()
        for key, value in self.userDict.items():
            removed = False
            if key.startswith('--'):
                removed = True
                key = key[2:]
            if removed:
                mergedDict.pop(key)
            else:
                mergedDict[key] = value
        return mergedDict

    def setupTable(self):
        self.model = TableModel(parent=self,
                                header=('Text', 'Tag'),
                                data=[[k, v] for k, v in self.dict.items()])
        self.proxyModel = QSortFilterProxyModel()
        self.proxyModel.setSourceModel(self.model)

        self.tableView = QTableView()
        self.tableView.horizontalHeader().setSectionResizeMode(
            QHeaderView.Stretch)
        self.tableView.setFixedHeight(500)
        self.tableView.setModel(self.proxyModel)

    def initUI(self):
        self.searchLabel = QLabel()
        self.searchLabel.setPixmap(
            QIcon('files/searching').pixmap(QSize(30, 30)))

        self.searchField = QLineEdit()
        self.searchField.textChanged.connect(self.search)

        hbox = QHBoxLayout()
        hbox.addWidget(self.searchLabel)
        hbox.addWidget(self.searchField)

        self.addButton = QPushButton()
        self.addButton.setFlat(True)
        self.addButton.setIcon(QIcon('files/add.png'))
        self.addButton.setIconSize(QSize(30, 30))
        self.addButton.clicked.connect(self.addWord)

        self.removeButton = QPushButton()
        self.removeButton.setFlat(True)
        self.removeButton.setIcon(QIcon('files/delete.png'))
        self.removeButton.setIconSize(QSize(30, 30))
        self.removeButton.clicked.connect(self.removeWord)

        hbox2 = QHBoxLayout()
        hbox2.addWidget(self.addButton)
        hbox2.addStretch()
        hbox2.addWidget(self.removeButton)

        self.fbox = QFormLayout()
        self.fbox.addRow(hbox)
        self.fbox.addRow(hbox2)
        self.fbox.addRow(self.tableView)

        self.setLayout(self.fbox)

    def search(self, text):
        self.proxyModel.setFilterKeyColumn(-1)  # -1 means all cols
        self.proxyModel.setFilterFixedString(text)

    def getPyboDict(self):
        if not os.path.isfile(self.pyboDictPath):
            self.downloadPyboDict()
        return self.getDict(self.pyboDictPath)

    def downloadPyboDict(self):
        import pkg_resources
        resourcePkg = 'pybo'
        resourcePath = '/'.join(('resources', 'trie', 'Tibetan.DICT'))
        reader = pkg_resources.resource_stream(resourcePkg, resourcePath)
        file = reader.read()
        reader.close()

        with open(self.pyboDictPath, 'w', encoding='UTF-8') as f:
            f.write(file.decode())

    def getUserDict(self):
        if not os.path.isfile(self.userDictPath):
            with open(self.userDictPath, 'w', encoding='UTF-8') as f:
                f.write('')
        return self.getDict(self.userDictPath)

    def getDict(self, filename):
        dictionary = OrderedDict()
        with open(filename, 'r', encoding='UTF-8') as f:
            rows = f.readlines()
            for row in rows:
                content, tag = row.split()
                dictionary[content] = tag
        return dictionary

    def removeWord(self):
        rows = sorted(
            set(index.row() for index in
                self.tableView.selectionModel().selectedIndexes()))
        for row in reversed(rows):
            self.model.data.pop(row)
        self.model.saveDict()

    def addWord(self):
        self.model.data.insert(0, ['...', '...'])
        self.model.layoutChanged.emit()

    def getAllTags(self):
        pyboTags = set(value for key, value in self.pyboDict.items())
        userTags = set(value for key, value in self.getUserDict().items())
        return pyboTags | userTags
Beispiel #11
0
class MainModel():
    '''Data model for pyspector.'''

    def __init__(self):
        '''Initializes a MainModel instance.'''
        self._searchText = ''
        self._matchCase = False
        self._includePrivateMembers = False
        self._includeInheritedMembers = False
        self._sortByType = True

        # Initialize icons.
        iconDir = f'{dirname(dirname(__file__))}/icons'
        self._icons = {
            'module': QIcon(f'{iconDir}/module.svg'),
            'abstract base class': QIcon(f'{iconDir}/abstract.svg'),
            'class': QIcon(f'{iconDir}/class.svg'),
            'function': QIcon(f'{iconDir}/function.svg'),
            'property': QIcon(f'{iconDir}/property.svg'),
            'object': QIcon(f'{iconDir}/object.svg')
        }

        # Create the unfiltered tree model.
        self._treeModel = QStandardItemModel()

        # Create regular expressions that exclude or include private members.
        self._excludePrivateRegEx = QRegularExpression('^[^_]|^__')
        self._includePrivateRegEx = QRegularExpression('')

        # Create regular expressions that exclude or include inherited members.
        self._excludeInheritedRegEx = QRegularExpression('^$')
        self._includeInheritedRegEx = QRegularExpression('')

        # Create a filtered tree model used to exclude or include private members.
        self._intermediateTreeModel = QSortFilterProxyModel()
        self._intermediateTreeModel.setSourceModel(self._treeModel)
        privateRegEx = self._includePrivateRegEx if self._includePrivateMembers else self._excludePrivateRegEx
        self._intermediateTreeModel.setFilterRegularExpression(privateRegEx)

        # Create a filtered tree model used to exclude or include inherited members.
        self._secondIntermediateTreeModel = QSortFilterProxyModel()
        self._secondIntermediateTreeModel.setSourceModel(self._intermediateTreeModel)
        self._secondIntermediateTreeModel.setFilterKeyColumn(2)
        inheritedRegEx = self._includeInheritedRegEx if self._includeInheritedMembers else self._excludeInheritedRegEx
        self._secondIntermediateTreeModel.setFilterRegularExpression(inheritedRegEx)

        # Create a filtered tree model that matches the search text.
        self._filteredTreeModel = QSortFilterProxyModel()
        self._filteredTreeModel.setSourceModel(self._secondIntermediateTreeModel)
        self._filteredTreeModel.setRecursiveFilteringEnabled(True)
        self._filteredTreeModel.setFilterFixedString(self._searchText)
        self._filteredTreeModel.setFilterCaseSensitivity(1 if self._matchCase else 0)

    @property
    def filteredTreeModel(self) -> QSortFilterProxyModel:
        '''The filtered version of the tree model.'''
        return self._filteredTreeModel

    @property
    def searchText(self) -> str:
        '''The current search text.'''
        return self._searchText

    @searchText.setter
    def searchText(self, value: str) -> None:
        self._searchText = value
        self._filteredTreeModel.setFilterFixedString(value)

    @property
    def matchCase(self) -> bool:
        '''Whether or not case-sensitive matching is used.'''
        return self._matchCase

    @matchCase.setter
    def matchCase(self, value: bool) -> None:
        self._matchCase = value
        self._filteredTreeModel.setFilterCaseSensitivity(1 if value else 0)

    @property
    def includePrivateMembers(self) -> bool:
        '''Whether or not private members (beginning with "_") are included in the tree.'''
        return self._includePrivateMembers

    @includePrivateMembers.setter
    def includePrivateMembers(self, value: bool) -> None:
        self._includePrivateMembers = value
        regEx = self._includePrivateRegEx if value else self._excludePrivateRegEx
        self._intermediateTreeModel.setFilterRegularExpression(regEx)

    @property
    def includeInheritedMembers(self) -> bool:
        '''Whether or not inherited members are included in the tree.'''
        return self._includeInheritedMembers

    @includeInheritedMembers.setter
    def includeInheritedMembers(self, value: bool) -> None:
        self._includeInheritedMembers = value
        regEx = self._includeInheritedRegEx if value else self._excludeInheritedRegEx
        self._secondIntermediateTreeModel.setFilterRegularExpression(regEx)

    @property
    def sortByType(self) -> bool:
        '''Whether or not  members are sorted by type.'''
        return self._sortByType

    @sortByType.setter
    def sortByType(self, value: bool) -> None:
        self._sortByType = value
        self._sort()

    def setModuleNames(self, moduleNames) -> None:
        '''Adds the specified modules to the tree, removing any that are no longer needed.'''
        # Remove any modules that aren't in the list.
        rootItem: QStandardItem = self._treeModel.invisibleRootItem()
        for i in range(rootItem.rowCount() - 1, -1, -1):
            if rootItem.child(i).text() not in moduleNames:
                rootItem.removeRow(i)

        # Add all the modules in the list.
        for moduleName in moduleNames:
            self._addModule(moduleName)

        # Sort.
        self._sort()

    def _sort(self) -> None:
        # Sort all items alphabetically by name.
        self._treeModel.sort(0)

        # Optionally, sort by type.
        if self.sortByType:
            self._treeModel.sort(1)

    def _dumpTree(self, parentIndex = QModelIndex(), depth = 0) -> None:
        rowCount = self._treeModel.rowCount(parentIndex)
        for row in range(rowCount):
            index = self._treeModel.index(row, 0, parentIndex)
            item = self._treeModel.itemFromIndex(index)
            indent = '  ' * depth
            id = item.data()['id']
            print(f'{indent}{id}')
            self._dumpTree(index, depth + 1)

    def findItemByName(self, name: str) -> QModelIndex:
        '''Finds the item with the specified name.'''

        # Create search predicates for matching and containing name.
        if self.matchCase:
            itemHasName = lambda item: item.text() == name
            itemContainsName = lambda  item: name in item.text()
        else:
            nameNoCase = name.casefold()
            itemHasName = lambda item: item.text().casefold() == nameNoCase
            itemContainsName = lambda item: nameNoCase in item.text().casefold()

        # Try for a full match, then a partial match.
        for predicate in [itemHasName, itemContainsName]:
            index = utilities.findIndexInModel(self._filteredTreeModel, predicate)
            if index.isValid():
                break

        return index

    def findItemById(self, id: str) -> QModelIndex:
        '''Finds the item with the specified ID.'''
        print(f'looking for item with id {id}')
        predicate = lambda item: item.data()['id'] == id
        index = utilities.findIndexInModel(self._filteredTreeModel, predicate)
        return index

    def _addModule(self, moduleName, depth = 0):
        # Check to see if module has already been added.
        rootItem = self._treeModel.invisibleRootItem()
        if self._parentContainsItem(rootItem, moduleName):
            return

        try:
            module = importlib.import_module(moduleName)
            item = self._addItem(rootItem, moduleName, moduleName, 'module', module)
            self._inspectObject(item, module, depth)
        except:
            self._addItem(rootItem, moduleName, moduleName, 'module', None, error = 'Could not import module.')

    def _parentContainsItem(self, parentItem: QStandardItem, id: str) -> bool:
        for row in range(parentItem.rowCount()):
            childId = parentItem.child(row).data()['id']
            if childId == id:
                return True
        return False

    def _inspectObject(self, parentItem: QStandardItem, obj: object, depth: int) -> None:
        '''Recursively adds object to the hierarchical model.'''
        for (memberName, memberValue) in inspect.getmembers(obj):
            memberType = self._getMemberType(memberValue)

            # Skip "magic" members that are classes -- they cause problems.
            if memberName.startswith('__') and memberType == 'class':
                continue

            # Skip modules within modules.
            if memberType == 'module':
                # TODO: Should we add nested modules? Seems useful, but leads to a segfault in the
                # case of matplotlib. 
                #print(f'{"  "*depth}adding nested module {memberName}: {memberValue.__name__}')
                #self._addModule(memberValue, depth + 1)
                continue

            # Don't add the same item twice.
            parentId = parentItem.data()['id']
            id = f'{parentId}/{memberName}'
            if self._parentContainsItem(parentItem, id):
                continue

            # Check inheritance of class members.
            inheritance = 'inherited' if inspect.isclass(obj) and memberName not in obj.__dict__ else ''

            # For functions, try to include the signature in the name.
            name = memberName
            if memberType == 'function':
                try:
                    name += str(inspect.signature(memberValue))
                except:
                    pass

            # Add an item for the current member.
            item = self._addItem(parentItem, id, name, memberType, memberValue, inheritance)

            # Recurse into classes (but not if it's the same class we're inspecting).
            if 'class' in memberType and memberValue != obj:
                print(f'{"  "*depth}inspecting class {memberName} in module {memberValue.__module__}')
                self._inspectObject(item, memberValue, depth + 1)

            # Recurse into property getter, setter, deleter functions.
            # TODO: Generalize this to data descriptors other than just the 'property' class.
            if type(memberValue) == property:
                if memberValue.fget:
                    self._addItem(item, f'{id}/get', '[get]', 'function', memberValue.fget)
                if memberValue.fset:
                    self._addItem(item, f'{id}/set', '[set]', 'function', memberValue.fset)
                if memberValue.fdel:
                    self._addItem(item, f'{id}/delete', '[delete]', 'function', memberValue.fdel)

    def _addItem(self, parentItem: QStandardItem, id: str, name: str, type: str, value: object, inheritance: str = '', error: str = '') -> QStandardItem:
        '''Adds one model item to a parent model item.'''
        key = type if type in self._icons else 'object'
        item1 = QStandardItem(self._icons[key], name)
        item1.setData({ 'id': id, 'type': type, 'value': value, 'error': error })
        item1.setEditable(False)
        if len(error):
            item1.setBackground(QBrush(QColor(255, 0, 0, 64)))
        item2 = QStandardItem(type)
        item2.setEditable(False)
        item3 = QStandardItem(inheritance)
        item3.setEditable(False)
        parentItem.appendRow([item1, item2, item3])
        return item1

    def _getMemberType(self, memberValue: object) -> str:
        '''Attempts to determine the type of a member from its value.'''
        if inspect.ismodule(memberValue):
            return 'module'
        if inspect.isabstract(memberValue):
            return 'abstract base class'
        if inspect.isclass(memberValue):
            return 'class'
        if inspect.isfunction(memberValue) or inspect.isbuiltin(memberValue) or inspect.isroutine(memberValue):
            return 'function'
        if inspect.isdatadescriptor(memberValue):
            return 'property'
        return 'object'
class DictionaryEditorWidget(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.parent = parent

        self.pyboDict = dict()
        self.initPyboDict()

        self.tokens = []
        self.resize(400, 600)
        self.setWindowTitle('Dictionary Editor')
        self.setupTable()
        self.initUI()

    def getDict(self):
        mergedDict = self.pyboDict.copy()
        # mergedDict = {'ཉམ་ཐག་པ': 'VERB'}

        for token in Token.objects.all():
            if token.type == Token.TYPE_UPDATE:
                mergedDict[token.text] = token.pos
            else:  # Dict.ACTION_DELETE
                del mergedDict[token.text]

        return mergedDict

    def setupTable(self):
        dict = self.getDict()
        self.model = TableModel(parent=self,
                                header=('Text', 'Tag'),
                                data=[[k, v] for k, v in dict.items()])

        # print(self.model.bt.has_word('ནང་ཆོས་'))

        self.proxyModel = QSortFilterProxyModel()
        self.proxyModel.eventFilter = lambda: self.removeButton.setDisabled(
            True)

        self.proxyModel.setSourceModel(self.model)

        self.tableView = QTableView()
        self.tableView.horizontalHeader().setSectionResizeMode(
            QHeaderView.Stretch)
        self.tableView.setFixedHeight(500)
        self.tableView.setModel(self.proxyModel)

    def initUI(self):
        self.searchLabel = QLabel()
        self.searchLabel.setPixmap(
            QIcon(os.path.join(BASE_DIR, 'icons',
                               'searching.png')).pixmap(QSize(20, 20)))

        self.searchField = QLineEdit()
        self.searchField.textChanged.connect(self.search)

        hbox = QHBoxLayout()
        hbox.addWidget(self.searchLabel)
        hbox.addWidget(self.searchField)

        self.addButton = QPushButton()
        self.addButton.setFlat(True)
        self.addButton.setIcon(
            QIcon(os.path.join(BASE_DIR, 'icons', 'add.png')))
        self.addButton.setIconSize(QSize(30, 30))
        self.addButton.clicked.connect(self.addWord)

        self.removeButton = QPushButton()
        self.removeButton.setFlat(True)
        self.removeButton.setIcon(
            QIcon(os.path.join(BASE_DIR, "icons", "delete.png")))
        self.removeButton.setIconSize(QSize(30, 30))
        self.removeButton.clicked.connect(self.removeWord)

        hbox2 = QHBoxLayout()
        hbox2.addWidget(self.addButton)
        hbox2.addStretch()
        hbox2.addWidget(self.removeButton)

        self.fbox = QFormLayout()
        self.fbox.addRow(hbox)
        self.fbox.addRow(hbox2)
        self.fbox.addRow(self.tableView)

        self.setLayout(self.fbox)

    def search(self, text):
        self.proxyModel.setFilterKeyColumn(-1)  # -1 means all cols
        self.proxyModel.setFilterFixedString(text)

    def initPyboDict(self):
        # import pkg_resources
        # resourcePkg = 'pybo'
        # resourcePath = '/'.join(('resources', 'lexica_bo', 'Tibetan.DICT'))
        # reader = pkg_resources.resource_stream(resourcePkg, resourcePath)

        resourcePath = '/'.join(
            ('resources', 'dictionaries', 'lexica_bo', 'Tibetan.DICT'))
        reader = open(resourcePath, mode="r", encoding="utf-8", newline="")
        file = reader.read()

        for line in file.splitlines():
            key, val = line.split()
            self.pyboDict[key] = val

        # file.decode()
        reader.close()

    def removeWord(self):
        rows = sorted(
            set(index.row() for index in
                self.tableView.selectionModel().selectedIndexes()))
        for row in reversed(rows):
            self.model.data.pop(row)
        self.model.saveDict()

    def addWord(self, text=None):
        if text is not None:
            self.model.data.insert(0, [text, ''])
        else:
            self.model.data.insert(0, ['', ''])
        self.model.layoutChanged.emit()

    def getAllTags(self):
        return set(self.getDict().values())
Beispiel #13
0
class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        self._version = "0.1.11"
        self.setWindowIcon(QIcon("GUI/icons/logo.png"))
        self.setWindowTitle("Tasmota Device Manager {}".format(self._version))

        self.main_splitter = QSplitter()
        self.devices_splitter = QSplitter(Qt.Vertical)

        self.fulltopic_queue = []
        self.settings = QSettings()
        self.setMinimumSize(QSize(1280,800))

        self.device_model = TasmotaDevicesModel()
        self.telemetry_model = TasmotaDevicesTree()
        self.console_model = ConsoleModel()

        self.sorted_console_model = QSortFilterProxyModel()
        self.sorted_console_model.setSourceModel(self.console_model)
        self.sorted_console_model.setFilterKeyColumn(CnsMdl.FRIENDLY_NAME)

        self.setup_mqtt()
        self.setup_telemetry_view()
        self.setup_main_layout()
        self.add_devices_tab()
        self.build_toolbars()
        self.setStatusBar(QStatusBar())

        self.queue_timer = QTimer()
        self.queue_timer.setSingleShot(True)
        self.queue_timer.timeout.connect(self.mqtt_ask_for_fulltopic)

        self.build_cons_ctx_menu()

        self.load_window_state()

    def setup_main_layout(self):
        self.mdi = QMdiArea()
        self.mdi.setActivationOrder(QMdiArea.ActivationHistoryOrder)
        self.mdi.setViewMode(QMdiArea.TabbedView)
        self.mdi.setDocumentMode(True)

        mdi_widget = QWidget()
        mdi_widget.setLayout(VLayout())
        mdi_widget.layout().addWidget(self.mdi)

        self.devices_splitter.addWidget(mdi_widget)

        vl_console = VLayout()
        self.console_view = TableView()
        self.console_view.setModel(self.sorted_console_model)
        self.console_view.setupColumns(columns_console)
        self.console_view.setAlternatingRowColors(True)
        self.console_view.setSortingEnabled(True)
        self.console_view.sortByColumn(CnsMdl.TIMESTAMP, Qt.DescendingOrder)
        self.console_view.verticalHeader().setDefaultSectionSize(20)
        self.console_view.setMinimumHeight(200)
        self.console_view.setContextMenuPolicy(Qt.CustomContextMenu)

        vl_console.addWidget(self.console_view)

        console_widget = QWidget()
        console_widget.setLayout(vl_console)

        self.devices_splitter.addWidget(console_widget)
        self.main_splitter.insertWidget(0, self.devices_splitter)
        self.setCentralWidget(self.main_splitter)
        self.console_view.clicked.connect(self.select_cons_entry)
        self.console_view.doubleClicked.connect(self.view_payload)
        self.console_view.customContextMenuRequested.connect(self.show_cons_ctx_menu)

    def setup_telemetry_view(self):
        tele_widget = QWidget()
        vl_tele = VLayout()
        self.tview = QTreeView()
        self.tview.setMinimumWidth(300)
        self.tview.setModel(self.telemetry_model)
        self.tview.setAlternatingRowColors(True)
        self.tview.setUniformRowHeights(True)
        self.tview.setIndentation(15)
        self.tview.setSizePolicy(QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum))
        self.tview.expandAll()
        self.tview.resizeColumnToContents(0)
        vl_tele.addWidget(self.tview)
        tele_widget.setLayout(vl_tele)
        self.main_splitter.addWidget(tele_widget)

    def setup_mqtt(self):
        self.mqtt = MqttClient()
        self.mqtt.connecting.connect(self.mqtt_connecting)
        self.mqtt.connected.connect(self.mqtt_connected)
        self.mqtt.disconnected.connect(self.mqtt_disconnected)
        self.mqtt.connectError.connect(self.mqtt_connectError)
        self.mqtt.messageSignal.connect(self.mqtt_message)

    def add_devices_tab(self):
        tabDevicesList = DevicesListWidget(self)
        self.mdi.addSubWindow(tabDevicesList)
        tabDevicesList.setWindowState(Qt.WindowMaximized)

    def load_window_state(self):
        wndGeometry = self.settings.value('window_geometry')
        if wndGeometry:
            self.restoreGeometry(wndGeometry)
        spltState = self.settings.value('splitter_state')
        if spltState:
            self.main_splitter.restoreState(spltState)

    def build_toolbars(self):
        main_toolbar = Toolbar(orientation=Qt.Horizontal, iconsize=32, label_position=Qt.ToolButtonIconOnly)
        main_toolbar.setObjectName("main_toolbar")
        self.addToolBar(main_toolbar)

        main_toolbar.addAction(QIcon("./GUI/icons/connections.png"), "Configure MQTT broker", self.setup_broker)
        agBroker = QActionGroup(self)
        agBroker.setExclusive(True)

        self.actConnect = CheckableAction(QIcon("./GUI/icons/connect.png"), "Connect to the broker", agBroker)
        self.actDisconnect = CheckableAction(QIcon("./GUI/icons/disconnect.png"), "Disconnect from broker", agBroker)

        self.actDisconnect.setChecked(True)

        self.actConnect.triggered.connect(self.mqtt_connect)
        self.actDisconnect.triggered.connect(self.mqtt_disconnect)

        main_toolbar.addActions(agBroker.actions())
        main_toolbar.addSeparator()

    def initial_query(self, idx):
        for q in initial_queries:
            topic = "{}status".format(self.device_model.commandTopic(idx))
            self.mqtt.publish(topic, q)
            q = q if q else ''
            self.console_log(topic, "Asked for STATUS {}".format(q), q)

    def setup_broker(self):
        brokers_dlg = BrokerDialog()
        if brokers_dlg.exec_() == QDialog.Accepted and self.mqtt.state == self.mqtt.Connected:
            self.mqtt.disconnect()

    def mqtt_connect(self):
        self.broker_hostname = self.settings.value('hostname', 'localhost')
        self.broker_port = self.settings.value('port', 1883, int)
        self.broker_username = self.settings.value('username')
        self.broker_password = self.settings.value('password')

        self.mqtt.hostname = self.broker_hostname
        self.mqtt.port = self.broker_port

        if self.broker_username:
            self.mqtt.setAuth(self.broker_username, self.broker_password)

        if self.mqtt.state == self.mqtt.Disconnected:
            self.mqtt.connectToHost()

    def mqtt_disconnect(self):
        self.mqtt.disconnectFromHost()

    def mqtt_connecting(self):
        self.statusBar().showMessage("Connecting to broker")

    def mqtt_connected(self):
        self.statusBar().showMessage("Connected to {}:{} as {}".format(self.broker_hostname, self.broker_port, self.broker_username if self.broker_username else '[anonymous]'))

        self.mqtt_subscribe()

        for d in range(self.device_model.rowCount()):
            idx = self.device_model.index(d, 0)
            self.initial_query(idx)

    def mqtt_subscribe(self):
        main_topics = ["+/stat/+", "+/tele/+", "stat/#", "tele/#"]

        for d in range(self.device_model.rowCount()):
            idx = self.device_model.index(d, 0)
            if not self.device_model.isDefaultTemplate(idx):
                main_topics.append(self.device_model.commandTopic(idx))
                main_topics.append(self.device_model.statTopic(idx))

        for t in main_topics:
            self.mqtt.subscribe(t)

    def mqtt_ask_for_fulltopic(self):
        for i in range(len(self.fulltopic_queue)):
            self.mqtt.publish(self.fulltopic_queue.pop(0))

    def mqtt_disconnected(self):
        self.statusBar().showMessage("Disconnected")

    def mqtt_connectError(self, rc):
        reason = {
            1: "Incorrect protocol version",
            2: "Invalid client identifier",
            3: "Server unavailable",
            4: "Bad username or password",
            5: "Not authorized",
        }
        self.statusBar().showMessage("Connection error: {}".format(reason[rc]))
        self.actDisconnect.setChecked(True)

    def mqtt_message(self, topic, msg):
        found = self.device_model.findDevice(topic)
        if found.reply == 'LWT':
            if not msg:
                msg = "offline"

            if found.index.isValid():
                self.console_log(topic, "LWT update: {}".format(msg), msg)
                self.device_model.updateValue(found.index, DevMdl.LWT, msg)

            elif msg == "Online":
                self.console_log(topic, "LWT for unknown device '{}'. Asking for FullTopic.".format(found.topic), msg, False)
                self.fulltopic_queue.append("cmnd/{}/fulltopic".format(found.topic))
                self.fulltopic_queue.append("{}/cmnd/fulltopic".format(found.topic))
                self.queue_timer.start(1500)

        elif found.reply == 'RESULT':
            full_topic = loads(msg).get('FullTopic')
            new_topic = loads(msg).get('Topic')
            template_name = loads(msg).get('NAME')

            if full_topic:
                # TODO: update FullTopic for existing device AFTER the FullTopic changes externally (the message will arrive from new FullTopic)
                if not found.index.isValid():
                    self.console_log(topic, "FullTopic for {}".format(found.topic), msg, False)

                    new_idx = self.device_model.addDevice(found.topic, full_topic, lwt='online')
                    tele_idx = self.telemetry_model.addDevice(TasmotaDevice, found.topic)
                    self.telemetry_model.devices[found.topic] = tele_idx
                    #TODO: add QSortFilterProxyModel to telemetry treeview and sort devices after adding

                    self.initial_query(new_idx)
                    self.console_log(topic, "Added {} with fulltopic {}, querying for STATE".format(found.topic, full_topic), msg)
                    self.tview.expand(tele_idx)
                    self.tview.resizeColumnToContents(0)

            if new_topic:
                if found.index.isValid() and found.topic != new_topic:
                    self.console_log(topic, "New topic for {}".format(found.topic), msg)

                    self.device_model.updateValue(found.index, DevMdl.TOPIC, new_topic)

                    tele_idx = self.telemetry_model.devices.get(found.topic)

                    if tele_idx:
                        self.telemetry_model.setDeviceName(tele_idx, new_topic)
                        self.telemetry_model.devices[new_topic] = self.telemetry_model.devices.pop(found.topic)

            if template_name:
                self.device_model.updateValue(found.index, DevMdl.MODULE, template_name)

        elif found.index.isValid():
            if found.reply == 'STATUS':
                self.console_log(topic, "Received device status", msg)
                payload = loads(msg)['Status']
                self.device_model.updateValue(found.index, DevMdl.FRIENDLY_NAME, payload['FriendlyName'][0])
                self.telemetry_model.setDeviceFriendlyName(self.telemetry_model.devices[found.topic], payload['FriendlyName'][0])
                self.tview.resizeColumnToContents(0)
                module = payload['Module']
                if module == '0':
                    self.mqtt.publish(self.device_model.commandTopic(found.index)+"template")
                else:
                    self.device_model.updateValue(found.index, DevMdl.MODULE, module)

            elif found.reply == 'STATUS1':
                self.console_log(topic, "Received program information", msg)
                payload = loads(msg)['StatusPRM']
                self.device_model.updateValue(found.index, DevMdl.RESTART_REASON, payload['RestartReason'])

            elif found.reply == 'STATUS2':
                self.console_log(topic, "Received firmware information", msg)
                payload = loads(msg)['StatusFWR']
                self.device_model.updateValue(found.index, DevMdl.FIRMWARE, payload['Version'])
                self.device_model.updateValue(found.index, DevMdl.CORE, payload['Core'])

            elif found.reply == 'STATUS3':
                self.console_log(topic, "Received syslog information", msg)
                payload = loads(msg)['StatusLOG']
                self.device_model.updateValue(found.index, DevMdl.TELEPERIOD, payload['TelePeriod'])

            elif found.reply == 'STATUS5':
                self.console_log(topic, "Received network status", msg)
                payload = loads(msg)['StatusNET']
                self.device_model.updateValue(found.index, DevMdl.MAC, payload['Mac'])
                self.device_model.updateValue(found.index, DevMdl.IP, payload['IPAddress'])

            elif found.reply == 'STATUS8':
                self.console_log(topic, "Received telemetry", msg)
                payload = loads(msg)['StatusSNS']
                self.parse_telemetry(found.index, payload)

            elif found.reply == 'STATUS11':
                self.console_log(topic, "Received device state", msg)
                payload = loads(msg)['StatusSTS']
                self.parse_state(found.index, payload)

            elif found.reply == 'SENSOR':
                self.console_log(topic, "Received telemetry", msg)
                payload = loads(msg)
                self.parse_telemetry(found.index, payload)

            elif found.reply == 'STATE':
                self.console_log(topic, "Received device state", msg)
                payload = loads(msg)
                self.parse_state(found.index, payload)

            elif found.reply.startswith('POWER'):
                self.console_log(topic, "Received {} state".format(found.reply), msg)
                payload = {found.reply: msg}
                self.parse_power(found.index, payload)

    def parse_power(self, index, payload):
        old = self.device_model.power(index)
        power = {k: payload[k] for k in payload.keys() if k.startswith("POWER")}
        needs_update = False
        if old:
            for k in old.keys():
                needs_update |= old[k] != power.get(k, old[k])
                if needs_update:
                    break
        else:
            needs_update = True

        if needs_update:
            self.device_model.updateValue(index, DevMdl.POWER, power)

    def parse_state(self, index, payload):
        bssid = payload['Wifi'].get('BSSId')
        if not bssid:
            bssid = payload['Wifi'].get('APMac')
        self.device_model.updateValue(index, DevMdl.BSSID, bssid)
        self.device_model.updateValue(index, DevMdl.SSID, payload['Wifi']['SSId'])
        self.device_model.updateValue(index, DevMdl.CHANNEL, payload['Wifi'].get('Channel'))
        self.device_model.updateValue(index, DevMdl.RSSI, payload['Wifi']['RSSI'])
        self.device_model.updateValue(index, DevMdl.UPTIME, payload['Uptime'])
        self.device_model.updateValue(index, DevMdl.LOADAVG, payload.get('LoadAvg'))

        self.parse_power(index, payload)

        tele_idx = self.telemetry_model.devices.get(self.device_model.topic(index))

        if tele_idx:
            tele_device = self.telemetry_model.getNode(tele_idx)
            self.telemetry_model.setDeviceFriendlyName(tele_idx, self.device_model.friendly_name(index))

            pr = tele_device.provides()
            for k in pr.keys():
                self.telemetry_model.setData(pr[k], payload.get(k))

    def parse_telemetry(self, index, payload):
        device = self.telemetry_model.devices.get(self.device_model.topic(index))
        if device:
            node = self.telemetry_model.getNode(device)
            time = node.provides()['Time']
            if 'Time' in payload:
                self.telemetry_model.setData(time, payload.pop('Time'))

            temp_unit = "C"
            pres_unit = "hPa"

            if 'TempUnit' in payload:
                temp_unit = payload.pop('TempUnit')

            if 'PressureUnit' in payload:
                pres_unit = payload.pop('PressureUnit')

            for sensor in sorted(payload.keys()):
                if sensor == 'DS18x20':
                    for sns_name in payload[sensor].keys():
                        d = node.devices().get(sensor)
                        if not d:
                            d = self.telemetry_model.addDevice(DS18x20, payload[sensor][sns_name]['Type'], device)
                        self.telemetry_model.getNode(d).setTempUnit(temp_unit)
                        payload[sensor][sns_name]['Id'] = payload[sensor][sns_name].pop('Address')

                        pr = self.telemetry_model.getNode(d).provides()
                        for pk in pr.keys():
                            self.telemetry_model.setData(pr[pk], payload[sensor][sns_name].get(pk))
                        self.tview.expand(d)

                elif sensor.startswith('DS18B20'):
                    d = node.devices().get(sensor)
                    if not d:
                        d = self.telemetry_model.addDevice(DS18x20, sensor, device)
                    self.telemetry_model.getNode(d).setTempUnit(temp_unit)
                    pr = self.telemetry_model.getNode(d).provides()
                    for pk in pr.keys():
                        self.telemetry_model.setData(pr[pk], payload[sensor].get(pk))
                    self.tview.expand(d)

                if sensor == 'COUNTER':
                    d = node.devices().get(sensor)
                    if not d:
                        d = self.telemetry_model.addDevice(CounterSns, "Counter", device)
                    pr = self.telemetry_model.getNode(d).provides()
                    for pk in pr.keys():
                        self.telemetry_model.setData(pr[pk], payload[sensor].get(pk))
                    self.tview.expand(d)

                else:
                    d = node.devices().get(sensor)
                    if not d:
                        d = self.telemetry_model.addDevice(sensor_map.get(sensor, Node), sensor, device)
                    pr = self.telemetry_model.getNode(d).provides()
                    if 'Temperature' in pr:
                        self.telemetry_model.getNode(d).setTempUnit(temp_unit)
                    if 'Pressure' in pr or 'SeaPressure' in pr:
                        self.telemetry_model.getNode(d).setPresUnit(pres_unit)
                    for pk in pr.keys():
                        self.telemetry_model.setData(pr[pk], payload[sensor].get(pk))
                    self.tview.expand(d)
        self.tview.resizeColumnToContents(0)

    def console_log(self, topic, description, payload, known=True):
        device = self.device_model.findDevice(topic)
        fname = self.device_model.friendly_name(device.index)
        self.console_model.addEntry(topic, fname, description, payload, known)
        self.console_view.resizeColumnToContents(1)

    def view_payload(self, idx):
        idx = self.sorted_console_model.mapToSource(idx)
        row = idx.row()
        timestamp = self.console_model.data(self.console_model.index(row, CnsMdl.TIMESTAMP))
        topic = self.console_model.data(self.console_model.index(row, CnsMdl.TOPIC))
        payload = self.console_model.data(self.console_model.index(row, CnsMdl.PAYLOAD))

        dlg = PayloadViewDialog(timestamp, topic, payload)
        dlg.exec_()

    def select_cons_entry(self, idx):
        self.cons_idx = idx

    def build_cons_ctx_menu(self):
        self.cons_ctx_menu = QMenu()
        self.cons_ctx_menu.addAction("View payload", lambda: self.view_payload(self.cons_idx))
        self.cons_ctx_menu.addSeparator()
        self.cons_ctx_menu.addAction("Show only this device", lambda: self.cons_set_filter(self.cons_idx))
        self.cons_ctx_menu.addAction("Show all devices", self.cons_set_filter)

    def show_cons_ctx_menu(self, at):
        self.select_cons_entry(self.console_view.indexAt(at))
        self.cons_ctx_menu.popup(self.console_view.viewport().mapToGlobal(at))

    def cons_set_filter(self, idx=None):
        if idx:
            idx = self.sorted_console_model.mapToSource(idx)
            topic = self.console_model.data(self.console_model.index(idx.row(), CnsMdl.FRIENDLY_NAME))
            self.sorted_console_model.setFilterFixedString(topic)
        else:
            self.sorted_console_model.setFilterFixedString("")

    def closeEvent(self, e):
        self.settings.setValue("window_geometry", self.saveGeometry())
        self.settings.setValue("splitter_state", self.main_splitter.saveState())
        self.settings.sync()
        e.accept()