Exemplo n.º 1
0
class BrowserEdgeWindow(QObject):
    def __init__(self, all_nodes, change_manager):
        super(BrowserEdgeWindow, self).__init__()

        self.all_nodes = all_nodes
        self.change_manager = change_manager

        # main window
        self.w = QMainWindow()

        # main window decoration
        self.w.setGeometry(0, 0, 640, 900)
        self.w.setWindowTitle("Open Similarity Edge")
        self.w.setWindowIcon(QIcon(window_icon))
        self.w.setWindowModality(Qt.ApplicationModal)

        # the browser edge table model
        self.browser_edge_table_model = BrowserEdgeTableModel(all_nodes)

        # the proxy model
        self.proxy_model = QSortFilterProxyModel()

        # the browser edge table
        self.browser_edge_table = QTableView()
        self.browser_edge_table.setSortingEnabled(True)
        self.browser_edge_table.setSelectionBehavior(
            QAbstractItemView.SelectRows)
        self.browser_edge_table.setSelectionMode(
            QAbstractItemView.SingleSelection)
        self.browser_edge_table.clicked.connect(self._select_index)
        self.proxy_model.setSourceModel(self.browser_edge_table_model)
        self.browser_edge_table.setModel(self.proxy_model)
        self.w.setCentralWidget(self.browser_edge_table)

    def show_window(self, edge_records):
        self.browser_edge_table_model.set_edge_records(edge_records)
        self.w.show()

    @pyqtSlot(QModelIndex)
    def _select_index(self, model_index):
        list_index = self.proxy_model.mapToSource(model_index).row()
        edge_record = self.browser_edge_table_model.edge_records[list_index]
        node_record1 = self.all_nodes[edge_record.index1 - 1]
        node_record2 = self.all_nodes[edge_record.index2 - 1]
        print("Open similarity edge")
        print(edge_record.text())
        self.change_manager.edge_selected_event(edge_record, node_record1,
                                                node_record2)
        self.w.hide()
class GuiPropertyDock():

    '''
    GuiPropertyDock
    '''
    dock_name = "properties-dock"
    dock_displayed_name = _("Properties")

    def __init__(self):
        '''
        Constructor
        '''
        super(GuiPropertyDock, self).__init__()
        self.widget = None
        self.core_part = None  # CorePropertyDock
        self._sheet_id = None
        self.tree_sheet = None

    @property
    def sheet_id(self):
        return self._sheet_id

    @sheet_id.setter
    def sheet_id(self, sheet_id):
        if self._sheet_id == sheet_id:
            pass
        self._sheet_id = sheet_id
        if self.sheet_id is not None:
            self.tree_sheet = gui_cfg.core.tree_sheet_manager.get_tree_sheet_from_sheet_id(
                self.sheet_id)
            self.core_part = self.tree_sheet.get_instance_of(self.dock_name)
            self.core_part.sheet_id = sheet_id

    def get_widget(self):

        if self.widget is None:
            self.widget = QWidget()
            self.ui = properties_dock_ui.Ui_PropertiesDock()
            self.ui.setupUi(self.widget)

            if self.tree_sheet is not None and self.core_part is not None:
                table_model = self.core_part.property_table_model

                # filter :
                self.filter = QSortFilterProxyModel(self.widget)
                self.filter.setFilterKeyColumn(-1)
                self.filter.setFilterCaseSensitivity(False)
                self.filter.setSourceModel(table_model)

                # model :
                self.ui.tableView.setModel(self.filter)

                # connect :
                self.ui.addPropButton.clicked.connect(self.add_property_row)
                self.ui.removePropButton.clicked.connect(
                    self.remove_property_row)
                self.ui.filterLineEdit.textChanged.connect(
                    self.filter.setFilterFixedString)
                self.ui.tableView.clicked.connect(self.set_current_row)

            self.widget.gui_part = self
        return self.widget

    @pyqtSlot()
    def add_property_row(self):
        index = self.filter.mapToSource(self.ui.tableView.currentIndex())
        self.core_part.add_property_row(index)

    @pyqtSlot()
    def remove_property_row(self):
        index = self.filter.mapToSource(self.ui.tableView.currentIndex())
        self.core_part.remove_property_row(index)

    @pyqtSlot('QModelIndex')
    def set_current_row(self, model_index):
        self.ui.tableView.setCurrentIndex(model_index)
Exemplo n.º 3
0
class VistaListaProdotti(QWidget):
    def __init__(self, parent=None):
        super(VistaListaProdotti, self).__init__(parent)
        self.carrello = ControlloreCarrello()
        self.controller = ControlloreListaProdotti()
        self.setWindowIcon(QtGui.QIcon('logos/logo.png'))
        main_layout = QHBoxLayout()

        v_layout = QVBoxLayout()

        self.list_view = QListView()
        self.update_ui()

        #Crea un elenco fittizio sopra l'elenco reale per poter usare la barra di ricerca
        self.filter_proxy_model = QSortFilterProxyModel()
        self.filter_proxy_model.setSourceModel(self.listview_model)
        self.filter_proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.filter_proxy_model.setFilterKeyColumn(0)

        self.list_view.setModel(self.filter_proxy_model)

        search_field = QLineEdit()
        search_field.setStyleSheet('font-size: 15px; height: 30px;')
        search_field.textChanged.connect(
            self.filter_proxy_model.setFilterRegExp)

        v_layout.addWidget(search_field)
        v_layout.addWidget(self.list_view)
        main_layout.addLayout(v_layout)

        buttons_layout = QVBoxLayout()

        buttons_layout.addItem(QSpacerItem(40, 40))

        #Bottone per aprire un prodotto
        open_button = QPushButton("Apri")
        open_button.clicked.connect(self.show_selected_info)
        buttons_layout.addWidget(open_button)

        #Bottone per creare un nuovo prodotto
        new_button = QPushButton("Nuovo")
        new_button.clicked.connect(self.show_new_prodotto)
        buttons_layout.addWidget(new_button)
        buttons_layout.addStretch()
        main_layout.addLayout(buttons_layout)

        self.setLayout(main_layout)
        self.resize(600, 300)
        self.setWindowTitle("Lista Prodotti")

    #Metodo che mostra a schermo le informazioni del prodotto selezionato
    def show_selected_info(self):
        try:
            sourceindex = self.list_view.selectedIndexes()[0].row()
            prodotto_selezionato = self.controller.get_prodotto_by_index(
                sourceindex)
            self.vista_prodotto = VistaProdotto(
                prodotto_selezionato, self.controller.elimina_prodotto_by_id,
                self.update_ui, self.carrello)
            self.vista_prodotto.show()
        except IndexError:
            QMessageBox.critical(self, 'Errore',
                                 'Per favore, seleziona un prodotto',
                                 QMessageBox.Ok, QMessageBox.Ok)

    #Metodo aprire la vista di inserimento del nuovo prodotto
    def show_new_prodotto(self):
        self.vista_inserisci_prodotto = VistaInserisciProdotto(
            self.controller, self.update_ui)
        self.vista_inserisci_prodotto.show()

    #Metodo che serve per generare e/o aggiornare la vista
    def update_ui(self):
        self.listview_model = QStandardItemModel(self.list_view)
        for prodotto in self.controller.get_lista_dei_prodotti():
            item = QStandardItem()
            item.setText(prodotto.marca + " " + prodotto.nome)
            item.setEditable(False)
            font = item.font()
            font.setPointSize(18)
            item.setFont(font)
            self.listview_model.appendRow(item)
        self.list_view.setModel(self.listview_model)

    # salva i dati sul file pickle alla chiusura della view
    def closeEvent(self, event):
        self.controller.save_data()

    #Metodo per collegare l'indice selezionato all'elenco fittizio all'indice dell'elenco reale
    def toSourceIndex(self, index):
        return self.filter_proxy_model.mapToSource(index).row()
Exemplo n.º 4
0
class WatchPointViewer(QTreeView):
    """
    Class implementing the watch expression viewer widget.
    
    Watch expressions will be shown with all their details. They can be
    modified through the context menu of this widget.
    """
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent the parent (QWidget)
        """
        super(WatchPointViewer, self).__init__(parent)
        self.setObjectName("WatchExpressionViewer")

        self.__model = None

        self.setItemsExpandable(False)
        self.setRootIsDecorated(False)
        self.setAlternatingRowColors(True)
        self.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)

        self.setWindowTitle(self.tr("Watchpoints"))

        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.__showContextMenu)
        self.doubleClicked.connect(self.__doubleClicked)

        self.__createPopupMenus()

    def setModel(self, model):
        """
        Public slot to set the watch expression model.
        
        @param model reference to the watch expression model (WatchPointModel)
        """
        self.__model = model

        self.sortingModel = QSortFilterProxyModel()
        self.sortingModel.setDynamicSortFilter(True)
        self.sortingModel.setSourceModel(self.__model)
        super(WatchPointViewer, self).setModel(self.sortingModel)

        header = self.header()
        header.setSortIndicator(0, Qt.AscendingOrder)
        header.setSortIndicatorShown(True)
        if qVersion() >= "5.0.0":
            header.setSectionsClickable(True)
        else:
            header.setClickable(True)

        self.setSortingEnabled(True)

        self.__layoutDisplay()

    def __layoutDisplay(self):
        """
        Private slot to perform a layout operation.
        """
        self.__resizeColumns()
        self.__resort()

    def __resizeColumns(self):
        """
        Private slot to resize the view when items get added, edited or
        deleted.
        """
        self.header().resizeSections(QHeaderView.ResizeToContents)
        self.header().setStretchLastSection(True)

    def __resort(self):
        """
        Private slot to resort the tree.
        """
        self.model().sort(self.header().sortIndicatorSection(),
                          self.header().sortIndicatorOrder())

    def __toSourceIndex(self, index):
        """
        Private slot to convert an index to a source index.
        
        @param index index to be converted (QModelIndex)
        @return mapped index (QModelIndex)
        """
        return self.sortingModel.mapToSource(index)

    def __fromSourceIndex(self, sindex):
        """
        Private slot to convert a source index to an index.
        
        @param sindex source index to be converted (QModelIndex)
        @return mapped index (QModelIndex)
        """
        return self.sortingModel.mapFromSource(sindex)

    def __setRowSelected(self, index, selected=True):
        """
        Private slot to select a complete row.
        
        @param index index determining the row to be selected (QModelIndex)
        @param selected flag indicating the action (bool)
        """
        if not index.isValid():
            return

        if selected:
            flags = QItemSelectionModel.SelectionFlags(
                QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
        else:
            flags = QItemSelectionModel.SelectionFlags(
                QItemSelectionModel.Deselect | QItemSelectionModel.Rows)
        self.selectionModel().select(index, flags)

    def __createPopupMenus(self):
        """
        Private method to generate the popup menus.
        """
        self.menu = QMenu()
        self.menu.addAction(self.tr("Add"), self.__addWatchPoint)
        self.menu.addAction(self.tr("Edit..."), self.__editWatchPoint)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Enable"), self.__enableWatchPoint)
        self.menu.addAction(self.tr("Enable all"), self.__enableAllWatchPoints)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Disable"), self.__disableWatchPoint)
        self.menu.addAction(self.tr("Disable all"),
                            self.__disableAllWatchPoints)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Delete"), self.__deleteWatchPoint)
        self.menu.addAction(self.tr("Delete all"), self.__deleteAllWatchPoints)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Configure..."), self.__configure)

        self.backMenuActions = {}
        self.backMenu = QMenu()
        self.backMenu.addAction(self.tr("Add"), self.__addWatchPoint)
        self.backMenuActions["EnableAll"] = \
            self.backMenu.addAction(self.tr("Enable all"),
                                    self.__enableAllWatchPoints)
        self.backMenuActions["DisableAll"] = \
            self.backMenu.addAction(self.tr("Disable all"),
                                    self.__disableAllWatchPoints)
        self.backMenuActions["DeleteAll"] = \
            self.backMenu.addAction(self.tr("Delete all"),
                                    self.__deleteAllWatchPoints)
        self.backMenu.addSeparator()
        self.backMenu.addAction(self.tr("Configure..."), self.__configure)
        self.backMenu.aboutToShow.connect(self.__showBackMenu)

        self.multiMenu = QMenu()
        self.multiMenu.addAction(self.tr("Add"), self.__addWatchPoint)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Enable selected"),
                                 self.__enableSelectedWatchPoints)
        self.multiMenu.addAction(self.tr("Enable all"),
                                 self.__enableAllWatchPoints)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Disable selected"),
                                 self.__disableSelectedWatchPoints)
        self.multiMenu.addAction(self.tr("Disable all"),
                                 self.__disableAllWatchPoints)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Delete selected"),
                                 self.__deleteSelectedWatchPoints)
        self.multiMenu.addAction(self.tr("Delete all"),
                                 self.__deleteAllWatchPoints)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Configure..."), self.__configure)

    def __showContextMenu(self, coord):
        """
        Private slot to show the context menu.
        
        @param coord the position of the mouse pointer (QPoint)
        """
        cnt = self.__getSelectedItemsCount()
        if cnt <= 1:
            index = self.indexAt(coord)
            if index.isValid():
                cnt = 1
                self.__setRowSelected(index)
        coord = self.mapToGlobal(coord)
        if cnt > 1:
            self.multiMenu.popup(coord)
        elif cnt == 1:
            self.menu.popup(coord)
        else:
            self.backMenu.popup(coord)

    def __clearSelection(self):
        """
        Private slot to clear the selection.
        """
        for index in self.selectedIndexes():
            self.__setRowSelected(index, False)

    def __findDuplicates(self,
                         cond,
                         special,
                         showMessage=False,
                         index=QModelIndex()):
        """
        Private method to check, if an entry already exists.
        
        @param cond condition to check (string)
        @param special special condition to check (string)
        @param showMessage flag indicating a message should be shown,
            if a duplicate entry is found (boolean)
        @param index index that should not be considered duplicate
            (QModelIndex)
        @return flag indicating a duplicate entry (boolean)
        """
        idx = self.__model.getWatchPointIndex(cond, special)
        duplicate = idx.isValid() and \
            idx.internalPointer() != index.internalPointer()
        if showMessage and duplicate:
            if not special:
                msg = self.tr("""<p>A watch expression '<b>{0}</b>'"""
                              """ already exists.</p>""")\
                    .format(Utilities.html_encode(cond))
            else:
                msg = self.tr(
                    """<p>A watch expression '<b>{0}</b>'"""
                    """ for the variable <b>{1}</b> already exists.</p>""")\
                    .format(special, Utilities.html_encode(cond))
            E5MessageBox.warning(self,
                                 self.tr("Watch expression already exists"),
                                 msg)

        return duplicate

    def __addWatchPoint(self):
        """
        Private slot to handle the add watch expression context menu entry.
        """
        from .EditWatchpointDialog import EditWatchpointDialog
        dlg = EditWatchpointDialog(("", False, True, 0, ""), self)
        if dlg.exec_() == QDialog.Accepted:
            cond, temp, enabled, ignorecount, special = dlg.getData()
            if not self.__findDuplicates(cond, special, True):
                self.__model.addWatchPoint(cond, special,
                                           (temp, enabled, ignorecount))
                self.__resizeColumns()
                self.__resort()

    def __doubleClicked(self, index):
        """
        Private slot to handle the double clicked signal.
        
        @param index index of the entry that was double clicked (QModelIndex)
        """
        if index.isValid():
            self.__doEditWatchPoint(index)

    def __editWatchPoint(self):
        """
        Private slot to handle the edit watch expression context menu entry.
        """
        index = self.currentIndex()
        if index.isValid():
            self.__doEditWatchPoint(index)

    def __doEditWatchPoint(self, index):
        """
        Private slot to edit a watch expression.
        
        @param index index of watch expression to be edited (QModelIndex)
        """
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            wp = self.__model.getWatchPointByIndex(sindex)
            if not wp:
                return

            cond, special, temp, enabled, count = wp[:5]

            from .EditWatchpointDialog import EditWatchpointDialog
            dlg = EditWatchpointDialog((cond, temp, enabled, count, special),
                                       self)
            if dlg.exec_() == QDialog.Accepted:
                cond, temp, enabled, count, special = dlg.getData()
                if not self.__findDuplicates(cond, special, True, sindex):
                    self.__model.setWatchPointByIndex(sindex, cond, special,
                                                      (temp, enabled, count))
                    self.__resizeColumns()
                    self.__resort()

    def __setWpEnabled(self, index, enabled):
        """
        Private method to set the enabled status of a watch expression.
        
        @param index index of watch expression to be enabled/disabled
            (QModelIndex)
        @param enabled flag indicating the enabled status to be set (boolean)
        """
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            self.__model.setWatchPointEnabledByIndex(sindex, enabled)

    def __enableWatchPoint(self):
        """
        Private slot to handle the enable watch expression context menu entry.
        """
        index = self.currentIndex()
        self.__setWpEnabled(index, True)
        self.__resizeColumns()
        self.__resort()

    def __enableAllWatchPoints(self):
        """
        Private slot to handle the enable all watch expressions context menu
        entry.
        """
        index = self.model().index(0, 0)
        while index.isValid():
            self.__setWpEnabled(index, True)
            index = self.indexBelow(index)
        self.__resizeColumns()
        self.__resort()

    def __enableSelectedWatchPoints(self):
        """
        Private slot to handle the enable selected watch expressions context
        menu entry.
        """
        for index in self.selectedIndexes():
            if index.column() == 0:
                self.__setWpEnabled(index, True)
        self.__resizeColumns()
        self.__resort()

    def __disableWatchPoint(self):
        """
        Private slot to handle the disable watch expression context menu entry.
        """
        index = self.currentIndex()
        self.__setWpEnabled(index, False)
        self.__resizeColumns()
        self.__resort()

    def __disableAllWatchPoints(self):
        """
        Private slot to handle the disable all watch expressions context menu
        entry.
        """
        index = self.model().index(0, 0)
        while index.isValid():
            self.__setWpEnabled(index, False)
            index = self.indexBelow(index)
        self.__resizeColumns()
        self.__resort()

    def __disableSelectedWatchPoints(self):
        """
        Private slot to handle the disable selected watch expressions context
        menu entry.
        """
        for index in self.selectedIndexes():
            if index.column() == 0:
                self.__setWpEnabled(index, False)
        self.__resizeColumns()
        self.__resort()

    def __deleteWatchPoint(self):
        """
        Private slot to handle the delete watch expression context menu entry.
        """
        index = self.currentIndex()
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            self.__model.deleteWatchPointByIndex(sindex)

    def __deleteAllWatchPoints(self):
        """
        Private slot to handle the delete all watch expressions context menu
        entry.
        """
        self.__model.deleteAll()

    def __deleteSelectedWatchPoints(self):
        """
        Private slot to handle the delete selected watch expressions context
        menu entry.
        """
        idxList = []
        for index in self.selectedIndexes():
            sindex = self.__toSourceIndex(index)
            if sindex.isValid() and index.column() == 0:
                idxList.append(sindex)
        self.__model.deleteWatchPoints(idxList)

    def __showBackMenu(self):
        """
        Private slot to handle the aboutToShow signal of the background menu.
        """
        if self.model().rowCount() == 0:
            self.backMenuActions["EnableAll"].setEnabled(False)
            self.backMenuActions["DisableAll"].setEnabled(False)
            self.backMenuActions["DeleteAll"].setEnabled(False)
        else:
            self.backMenuActions["EnableAll"].setEnabled(True)
            self.backMenuActions["DisableAll"].setEnabled(True)
            self.backMenuActions["DeleteAll"].setEnabled(True)

    def __getSelectedItemsCount(self):
        """
        Private method to get the count of items selected.
        
        @return count of items selected (integer)
        """
        count = len(self.selectedIndexes()) // (self.__model.columnCount() - 1)
        # column count is 1 greater than selectable
        return count

    def __configure(self):
        """
        Private method to open the configuration dialog.
        """
        e5App().getObject("UserInterface")\
            .showPreferences("debuggerGeneralPage")
Exemplo n.º 5
0
class DevicesListWidget(QWidget):
    def __init__(self, parent, *args, **kwargs):
        super(DevicesListWidget, self).__init__(*args, **kwargs)
        self.setWindowTitle("Devices list")
        self.setWindowState(Qt.WindowMaximized)
        self.setLayout(VLayout(margin=0, spacing=0))

        self.mqtt = parent.mqtt
        self.mdi = parent.mdi
        self.idx = None

        self.nam = QNetworkAccessManager()
        self.backup = bytes()

        self.settings = QSettings("{}/TDM/tdm.cfg".format(QDir.homePath()),
                                  QSettings.IniFormat)
        self.hidden_columns = self.settings.value("hidden_columns", [1, 2])

        self.tb = Toolbar(Qt.Horizontal, 16, Qt.ToolButtonTextBesideIcon)
        self.tb.addAction(QIcon("GUI/icons/add.png"), "Add", self.device_add)

        self.layout().addWidget(self.tb)

        self.device_list = TableView()
        self.model = parent.device_model
        self.telemetry_model = parent.telemetry_model
        self.sorted_device_model = QSortFilterProxyModel()
        self.sorted_device_model.setSourceModel(parent.device_model)
        self.device_list.setModel(self.sorted_device_model)
        self.device_list.setupColumns(columns, self.hidden_columns)
        self.device_list.setSortingEnabled(True)
        self.device_list.setWordWrap(True)
        self.device_list.setItemDelegate(DeviceDelegate())
        self.device_list.sortByColumn(DevMdl.TOPIC, Qt.AscendingOrder)
        self.device_list.setContextMenuPolicy(Qt.CustomContextMenu)
        self.layout().addWidget(self.device_list)

        self.device_list.clicked.connect(self.select_device)
        self.device_list.doubleClicked.connect(self.device_config)
        self.device_list.customContextMenuRequested.connect(
            self.show_list_ctx_menu)

        self.device_list.horizontalHeader().setContextMenuPolicy(
            Qt.CustomContextMenu)
        self.device_list.horizontalHeader().customContextMenuRequested.connect(
            self.show_header_ctx_menu)

        self.ctx_menu = QMenu()
        self.ctx_menu_relays = None
        self.create_actions()

        self.build_header_ctx_menu()

    def create_actions(self):
        self.ctx_menu.addAction(QIcon("GUI/icons/configure.png"), "Configure",
                                self.device_config)
        self.ctx_menu.addAction(QIcon("GUI/icons/delete.png"), "Remove",
                                self.device_delete)
        self.ctx_menu.addSeparator()
        self.ctx_menu.addAction(QIcon("GUI/icons/refresh.png"), "Refresh",
                                self.ctx_menu_refresh)
        self.ctx_menu.addSeparator()
        self.ctx_menu.addAction(QIcon("GUI/icons/on.png"), "Power ON",
                                lambda: self.ctx_menu_power(state="ON"))
        self.ctx_menu.addAction(QIcon("GUI/icons/off.png"), "Power OFF",
                                lambda: self.ctx_menu_power(state="OFF"))

        self.ctx_menu_relays = QMenu("Relays")
        self.ctx_menu_relays.setIcon(QIcon("GUI/icons/switch.png"))
        relays_btn = self.ctx_menu.addMenu(self.ctx_menu_relays)
        self.ctx_menu_relays.setEnabled(False)
        self.ctx_menu.addSeparator()
        self.ctx_menu.addAction(QIcon("GUI/icons/clear.png"), "Clear retained",
                                self.ctx_menu_clean_retained)
        self.ctx_menu.addSeparator()

        self.ctx_menu_copy = QMenu("Copy")
        self.ctx_menu_copy.setIcon(QIcon("GUI/icons/copy.png"))
        copy_btn = self.ctx_menu.addMenu(self.ctx_menu_copy)

        self.ctx_menu.addSeparator()
        self.ctx_menu.addAction("Set teleperiod", self.ctx_menu_teleperiod)
        self.ctx_menu.addAction(QIcon("GUI/icons/restart.png"), "Restart",
                                self.ctx_menu_restart)
        self.ctx_menu.addAction(QIcon("GUI/icons/web.png"), "Open WebUI",
                                self.ctx_menu_webui)
        self.ctx_menu.addSeparator()

        self.ctx_menu_ota = QMenu("OTA upgrade")
        self.ctx_menu_ota.addAction("Set OTA URL", self.ctx_menu_ota_set_url)
        self.ctx_menu_ota.addAction("Upgrade", self.ctx_menu_ota_set_upgrade)
        ota_btn = self.ctx_menu.addMenu(self.ctx_menu_ota)

        self.ctx_menu.addAction("Config backup", self.ctx_menu_config_backup)

        self.ctx_menu_copy.addAction(
            "IP", lambda: self.ctx_menu_copy_value(DevMdl.IP))
        self.ctx_menu_copy.addAction(
            "MAC", lambda: self.ctx_menu_copy_value(DevMdl.MAC))
        self.ctx_menu_copy.addAction("BSSID", self.ctx_menu_copy_bssid)
        self.ctx_menu_copy.addSeparator()
        self.ctx_menu_copy.addAction(
            "Topic", lambda: self.ctx_menu_copy_value(DevMdl.TOPIC))
        self.ctx_menu_copy.addAction(
            "FullTopic", lambda: self.ctx_menu_copy_value(DevMdl.FULL_TOPIC))
        self.ctx_menu_copy.addAction(
            "STAT topic", lambda: self.ctx_menu_copy_prefix_topic("STAT"))
        self.ctx_menu_copy.addAction(
            "CMND topic", lambda: self.ctx_menu_copy_prefix_topic("CMND"))
        self.ctx_menu_copy.addAction(
            "TELE topic", lambda: self.ctx_menu_copy_prefix_topic("TELE"))

        self.tb.addActions(self.ctx_menu.actions())
        self.tb.widgetForAction(ota_btn).setPopupMode(QToolButton.InstantPopup)
        self.tb.widgetForAction(relays_btn).setPopupMode(
            QToolButton.InstantPopup)
        self.tb.widgetForAction(copy_btn).setPopupMode(
            QToolButton.InstantPopup)

    def ctx_menu_copy_value(self, column):
        if self.idx:
            row = self.idx.row()
            value = self.model.data(self.model.index(row, column))
            QApplication.clipboard().setText(value)

    def ctx_menu_copy_bssid(self):
        if self.idx:
            QApplication.clipboard().setText(self.model.bssid(self.idx))

    def ctx_menu_copy_prefix_topic(self, prefix):
        if self.idx:
            if prefix == "STAT":
                topic = self.model.statTopic(self.idx)
            elif prefix == "CMND":
                topic = self.model.commandTopic(self.idx)
            elif prefix == "TELE":
                topic = self.model.teleTopic(self.idx)
            QApplication.clipboard().setText(topic)

    def ctx_menu_clean_retained(self):
        if self.idx:
            relays = self.model.data(
                self.model.index(self.idx.row(), DevMdl.POWER))
            if relays and len(relays.keys()) > 0:
                cmnd_topic = self.model.commandTopic(self.idx)

                for r in relays.keys():
                    self.mqtt.publish(cmnd_topic + r, retain=True)
                QMessageBox.information(self, "Clear retained",
                                        "Cleared retained messages.")

    def ctx_menu_power(self, relay=None, state=None):
        if self.idx:
            relays = self.model.data(
                self.model.index(self.idx.row(), DevMdl.POWER))
            cmnd_topic = self.model.commandTopic(self.idx)
            if relay:
                self.mqtt.publish(cmnd_topic + relay, payload=state)

            elif relays:
                for r in relays.keys():
                    self.mqtt.publish(cmnd_topic + r, payload=state)

    def ctx_menu_restart(self):
        if self.idx:
            self.mqtt.publish("{}/restart".format(
                self.model.commandTopic(self.idx)),
                              payload="1")

    def ctx_menu_refresh(self):
        if self.idx:
            for q in initial_queries:
                self.mqtt.publish("{}/status".format(
                    self.model.commandTopic(self.idx)),
                                  payload=q)

    def ctx_menu_teleperiod(self):
        if self.idx:
            teleperiod, ok = QInputDialog.getInt(
                self, "Set telemetry period",
                "Input 1 to reset to default\n[Min: 10, Max: 3600]",
                int(
                    self.model.data(
                        self.model.index(self.idx.row(),
                                         DevMdl.TELEPERIOD))), 1, 3600)
            if ok:
                if teleperiod != 1 and teleperiod < 10:
                    teleperiod = 10
            self.mqtt.publish("{}/teleperiod".format(
                self.model.commandTopic(self.idx)),
                              payload=teleperiod)

    def ctx_menu_telemetry(self):
        if self.idx:
            self.mqtt.publish("{}/status".format(
                self.model.commandTopic(self.idx)),
                              payload=8)

    def ctx_menu_webui(self):
        if self.idx:
            QDesktopServices.openUrl(
                QUrl("http://{}".format(self.model.ip(self.idx))))

    def ctx_menu_config_backup(self):
        if self.idx:
            self.backup = bytes()
            ip = self.model.data(self.model.index(self.idx.row(), DevMdl.IP))
            self.dl = self.nam.get(
                QNetworkRequest(QUrl("http://{}/dl".format(ip))))
            self.dl.readyRead.connect(self.get_dump)
            self.dl.finished.connect(self.save_dump)

    def ctx_menu_ota_set_url(self):
        if self.idx:
            current_url = self.model.data(
                self.model.index(self.idx.row(), DevMdl.OTA_URL))
            url, ok = QInputDialog.getText(
                self,
                "Set OTA URL",
                '100 chars max. Set to "1" to reset to default.',
                text=current_url)
            if ok:
                self.mqtt.publish("{}/otaurl".format(
                    self.model.commandTopic(self.idx)),
                                  payload=url)

    def ctx_menu_ota_set_upgrade(self):
        if self.idx:
            current_url = self.model.data(
                self.model.index(self.idx.row(), DevMdl.OTA_URL))
            if QMessageBox.question(
                    self, "OTA Upgrade",
                    "Are you sure to OTA upgrade from\n{}".format(current_url),
                    QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes:
                self.model.setData(
                    self.model.index(self.idx.row(), DevMdl.FIRMWARE),
                    "Upgrade in progress")
                self.mqtt.publish("{}/upgrade".format(
                    self.model.commandTopic(self.idx)),
                                  payload="1")

    def show_list_ctx_menu(self, at):
        self.select_device(self.device_list.indexAt(at))
        self.ctx_menu.popup(self.device_list.viewport().mapToGlobal(at))

    def build_header_ctx_menu(self):
        self.hdr_ctx_menu = QMenu()
        for c in columns.keys():
            a = self.hdr_ctx_menu.addAction(columns[c][0])
            a.setData(c)
            a.setCheckable(True)
            a.setChecked(not self.device_list.isColumnHidden(c))
            a.toggled.connect(self.header_ctx_menu_toggle_col)

    def show_header_ctx_menu(self, at):
        self.hdr_ctx_menu.popup(
            self.device_list.horizontalHeader().viewport().mapToGlobal(at))

    def header_ctx_menu_toggle_col(self, state):
        self.device_list.setColumnHidden(self.sender().data(), not state)
        hidden_columns = [
            int(c) for c in columns.keys()
            if self.device_list.isColumnHidden(c)
        ]
        self.settings.setValue("hidden_columns", hidden_columns)
        self.settings.sync()

    def select_device(self, idx):
        self.idx = self.sorted_device_model.mapToSource(idx)
        self.device = self.model.data(self.model.index(idx.row(),
                                                       DevMdl.TOPIC))

        relays = self.model.data(self.model.index(self.idx.row(),
                                                  DevMdl.POWER))
        if relays and len(relays.keys()) > 1:
            self.ctx_menu_relays.setEnabled(True)
            self.ctx_menu_relays.setEnabled(True)
            self.ctx_menu_relays.clear()

            for r in relays.keys():
                actR = self.ctx_menu_relays.addAction("{} ON".format(r))
                actR.triggered.connect(
                    lambda st, x=r: self.ctx_menu_power(x, "ON"))
                actR = self.ctx_menu_relays.addAction("{} OFF".format(r))
                actR.triggered.connect(
                    lambda st, x=r: self.ctx_menu_power(x, "OFF"))
                self.ctx_menu_relays.addSeparator()
        else:
            self.ctx_menu_relays.setEnabled(False)
            self.ctx_menu_relays.clear()

    def device_config(self, idx=None):
        if self.idx:
            dev_cfg = DevicesConfigWidget(self, self.model.topic(self.idx))
            self.mdi.addSubWindow(dev_cfg)
            dev_cfg.setWindowState(Qt.WindowMaximized)

    def device_add(self):
        rc = self.model.rowCount()
        self.model.insertRow(rc)
        dlg = DeviceEditDialog(self.model, rc)
        dlg.full_topic.setText("%prefix%/%topic%/")

        if dlg.exec_() == QDialog.Accepted:
            self.model.setData(
                self.model.index(rc, DevMdl.FRIENDLY_NAME),
                self.model.data(self.model.index(rc, DevMdl.TOPIC)))
            topic = dlg.topic.text()
            tele_dev = self.telemetry_model.addDevice(TasmotaDevice, topic)
            self.telemetry_model.devices[topic] = tele_dev
        else:
            self.model.removeRow(rc)

    def device_delete(self):
        if self.idx:
            topic = self.model.topic(self.idx)
            if QMessageBox.question(
                    self, "Confirm",
                    "Do you want to remove '{}' from devices list?".format(
                        topic)) == QMessageBox.Yes:
                self.model.removeRows(self.idx.row(), 1)
                tele_idx = self.telemetry_model.devices.get(topic)
                if tele_idx:
                    self.telemetry_model.removeRows(tele_idx.row(), 1)

    def get_dump(self):
        self.backup += self.dl.readAll()

    def save_dump(self):
        fname = self.dl.header(QNetworkRequest.ContentDispositionHeader)
        if fname:
            fname = fname.split('=')[1]
            save_file = QFileDialog.getSaveFileName(
                self, "Save config backup",
                "{}/TDM/{}".format(QDir.homePath(), fname))[0]
            if save_file:
                with open(save_file, "wb") as f:
                    f.write(self.backup)

    def closeEvent(self, event):
        event.ignore()
Exemplo n.º 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()
Exemplo n.º 7
0
class DevicesListWidget(QWidget):
    def __init__(self, parent, *args, **kwargs):
        super(DevicesListWidget, self).__init__(*args, **kwargs)
        self.setWindowTitle("Devices list")
        self.setWindowState(Qt.WindowMaximized)
        self.setLayout(VLayout(margin=0, spacing=0))

        self.mqtt = parent.mqtt
        self.mdi = parent.mdi
        self.idx = None

        self.settings = QSettings()
        self.hidden_columns = self.settings.value("hidden_columns", [1, 2])

        self.tb = Toolbar(Qt.Horizontal, 16, Qt.ToolButtonTextBesideIcon)
        self.tb.addAction(QIcon("GUI/icons/add.png"), "Add", self.device_add)

        self.layout().addWidget(self.tb)

        self.device_list = TableView()
        self.model = parent.device_model
        self.telemetry_model = parent.telemetry_model
        self.sorted_device_model = QSortFilterProxyModel()
        self.sorted_device_model.setSourceModel(parent.device_model)
        self.device_list.setModel(self.sorted_device_model)
        self.device_list.setupColumns(columns, self.hidden_columns)
        self.device_list.setSortingEnabled(True)
        self.device_list.setWordWrap(True)
        self.device_list.setItemDelegate(DeviceDelegate())
        self.device_list.sortByColumn(DevMdl.TOPIC, Qt.AscendingOrder)
        self.device_list.setContextMenuPolicy(Qt.CustomContextMenu)
        self.layout().addWidget(self.device_list)

        self.device_list.clicked.connect(self.select_device)
        self.device_list.doubleClicked.connect(self.device_config)
        self.device_list.customContextMenuRequested.connect(self.show_list_ctx_menu)

        self.device_list.horizontalHeader().setContextMenuPolicy(Qt.CustomContextMenu)
        self.device_list.horizontalHeader().customContextMenuRequested.connect(self.show_header_ctx_menu)

        self.ctx_menu = QMenu()
        self.ctx_menu_relays = None
        self.create_actions()

        self.build_header_ctx_menu()

    def create_actions(self):
        self.ctx_menu.addAction(QIcon("GUI/icons/configure.png"), "Configure", self.device_config)
        self.ctx_menu.addAction(QIcon("GUI/icons/delete.png"), "Remove", self.device_delete)
        self.ctx_menu.addSeparator()
        self.ctx_menu.addAction(QIcon("GUI/icons/refresh.png"), "Refresh", self.ctx_menu_refresh)
        self.ctx_menu.addSeparator()
        self.ctx_menu.addAction(QIcon("GUI/icons/on.png"), "Power ON", lambda: self.ctx_menu_power(state="ON"))
        self.ctx_menu.addAction(QIcon("GUI/icons/off.png"), "Power OFF", lambda: self.ctx_menu_power(state="OFF"))

        self.ctx_menu_relays = QMenu("Relays")
        self.ctx_menu_relays.setIcon(QIcon("GUI/icons/switch.png"))
        relays_btn = self.ctx_menu.addMenu(self.ctx_menu_relays)

        self.ctx_menu_relays.setEnabled(False)
        self.ctx_menu.addSeparator()
        self.ctx_menu.addAction(QIcon("GUI/icons/clear.png"), "Clear retained", self.ctx_menu_clean_retained)
        self.ctx_menu.addSeparator()

        self.ctx_menu_copy = QMenu("Copy")
        self.ctx_menu_copy.setIcon(QIcon("GUI/icons/copy.png"))
        copy_btn = self.ctx_menu.addMenu(self.ctx_menu_copy)

        self.ctx_menu.addSeparator()
        self.ctx_menu.addAction(QIcon("GUI/icons/restart.png"), "Restart", self.ctx_menu_restart)
        self.ctx_menu.addAction(QIcon("GUI/icons/web.png"), "Open WebUI", self.ctx_menu_webui)

        self.ctx_menu_copy.addAction("IP", lambda: self.ctx_menu_copy_value(DevMdl.IP))
        self.ctx_menu_copy.addAction("MAC", lambda: self.ctx_menu_copy_value(DevMdl.MAC))
        self.ctx_menu_copy.addAction("BSSID", lambda: self.ctx_menu_copy_value(DevMdl.BSSID))
        self.ctx_menu_copy.addSeparator()
        self.ctx_menu_copy.addAction("Topic", lambda: self.ctx_menu_copy_value(DevMdl.TOPIC))
        self.ctx_menu_copy.addAction("FullTopic", lambda: self.ctx_menu_copy_value(DevMdl.FULL_TOPIC))
        self.ctx_menu_copy.addAction("STAT topic", lambda: self.ctx_menu_copy_prefix_topic("STAT"))
        self.ctx_menu_copy.addAction("CMND topic", lambda: self.ctx_menu_copy_prefix_topic("CMND"))
        self.ctx_menu_copy.addAction("TELE topic", lambda: self.ctx_menu_copy_prefix_topic("TELE"))

        self.tb.addActions(self.ctx_menu.actions())
        self.tb.widgetForAction(relays_btn).setPopupMode(QToolButton.InstantPopup)
        self.tb.widgetForAction(copy_btn).setPopupMode(QToolButton.InstantPopup)

    def ctx_menu_copy_value(self, column):
        if self.idx:
            row = self.idx.row()
            value = self.model.data(self.model.index(row, column))
            QApplication.clipboard().setText(value)

    def ctx_menu_copy_prefix_topic(self, prefix):
        if self.idx:
            if prefix == "STAT":
                topic = self.model.statTopic(self.idx)
            elif prefix == "CMND":
                topic = self.model.commandTopic(self.idx)
            elif prefix == "TELE":
                topic = self.model.teleTopic(self.idx)
            QApplication.clipboard().setText(topic)

    def ctx_menu_clean_retained(self):
        if self.idx:
            relays = self.model.data(self.model.index(self.idx.row(), DevMdl.POWER))
            if relays and len(relays.keys()>1):
                cmnd_topic = self.model.cmndTopic(self.idx)

                for r in relays.keys():
                    self.mqtt.publish(cmnd_topic + r, retain=True)
                QMessageBox.information(self, "Clear retained", "Cleared reatined messages.")

    def ctx_menu_power(self, relay=None, state=None):
        if self.idx:
            relays = self.model.data(self.model.index(self.idx.row(), DevMdl.POWER))
            cmnd_topic = self.model.commandTopic(self.idx)
            if relay:
                self.mqtt.publish(cmnd_topic+relay, payload=state)

            elif relays:
                for r in relays.keys():
                    self.mqtt.publish(cmnd_topic+r, payload=state)

    def ctx_menu_restart(self):
        if self.idx:
            self.mqtt.publish("{}/restart".format(self.model.commandTopic(self.idx)), payload="1")

    def ctx_menu_refresh(self):
        if self.idx:
            for q in initial_queries:
                self.mqtt.publish("{}/status".format(self.model.commandTopic(self.idx)), payload=q)

    def ctx_menu_telemetry(self):
        if self.idx:
            self.mqtt.publish("{}/status".format(self.model.commandTopic(self.idx)), payload=8)

    def ctx_menu_bssid(self):
        if self.idx:
            bssid = self.model.bssid(self.idx)
            current = self.settings.value("BSSID/{}".format(bssid), "")
            alias, ok = QInputDialog.getText(self, "BSSID alias", "Alias for {}. Clear to remove.".format(bssid), text=current)
            if ok:
                self.settings.setValue("BSSID/{}".format(bssid), alias)
                self.model.refreshBSSID()

    def ctx_menu_webui(self):
        if self.idx:
            QDesktopServices.openUrl(QUrl("http://{}".format(self.model.ip(self.idx))))

    def show_list_ctx_menu(self, at):
        self.select_device(self.device_list.indexAt(at))
        self.ctx_menu.popup(self.device_list.viewport().mapToGlobal(at))

    def build_header_ctx_menu(self):
        self.hdr_ctx_menu = QMenu()
        for c in columns.keys():
            a = self.hdr_ctx_menu.addAction(columns[c][0])
            a.setData(c)
            a.setCheckable(True)
            a.setChecked(not self.device_list.isColumnHidden(c))
            a.toggled.connect(self.header_ctx_menu_toggle_col)

    def show_header_ctx_menu(self, at):
        self.hdr_ctx_menu.popup(self.device_list.horizontalHeader().viewport().mapToGlobal(at))

    def header_ctx_menu_toggle_col(self, state):
        self.device_list.setColumnHidden(self.sender().data(), not state)
        hidden_columns = [int(c) for c in columns.keys() if self.device_list.isColumnHidden(c)]
        self.settings.setValue("hidden_columns", hidden_columns)
        self.settings.sync()

    def select_device(self, idx):
        self.idx = self.sorted_device_model.mapToSource(idx)
        self.device = self.model.data(self.model.index(idx.row(), DevMdl.TOPIC))

        relays = self.model.data(self.model.index(self.idx.row(), DevMdl.POWER))
        if relays and len(relays.keys()) > 1:
            self.ctx_menu_relays.setEnabled(True)
            self.ctx_menu_relays.setEnabled(True)
            self.ctx_menu_relays.clear()

            for r in relays.keys():
                actR = self.ctx_menu_relays.addAction("{} ON".format(r))
                actR.triggered.connect(lambda st, x=r: self.ctx_menu_power(x, "ON"))
                actR = self.ctx_menu_relays.addAction("{} OFF".format(r))
                actR.triggered.connect(lambda st, x=r: self.ctx_menu_power(x, "OFF"))
                self.ctx_menu_relays.addSeparator()
        else:
            self.ctx_menu_relays.setEnabled(False)
            self.ctx_menu_relays.clear()

    def device_config(self, idx=None):
        dev_cfg = DevicesConfigWidget(self, self.model.topic(self.idx))
        self.mdi.addSubWindow(dev_cfg)
        dev_cfg.setWindowState(Qt.WindowMaximized)

    def device_add(self):
        rc = self.model.rowCount()
        self.model.insertRow(rc)
        dlg = DeviceEditDialog(self.model, rc)
        dlg.full_topic.setText("%prefix%/%topic%/")

        if dlg.exec_() == QDialog.Accepted:
            self.model.setData(self.model.index(rc, DevMdl.FRIENDLY_NAME), self.model.data(self.model.index(rc, DevMdl.TOPIC)))
            topic = dlg.topic.text()
            tele_dev = self.telemetry_model.addDevice(TasmotaDevice, topic)
            self.telemetry_model.devices[topic] = tele_dev
        else:
            self.model.removeRow(rc)

    def device_delete(self):
        if self.idx:
            topic = self.model.topic(self.idx)
            if QMessageBox.question(self, "Confirm", "Do you want to remove '{}' from devices list?".format(topic)) == QMessageBox.Yes:
                self.model.removeRows(self.idx.row(),1)
                tele_idx = self.telemetry_model.devices.get(topic)
                if tele_idx:
                    self.telemetry_model.removeRows(tele_idx.row(),1)

    def closeEvent(self, event):
        event.ignore()
Exemplo n.º 8
0
class FontViewWidget(QWidget):
    def __init__(self, statusBar, config, *args, **kwargs):
        super(FontViewWidget, self).__init__(*args, **kwargs)
        self.statusBar = statusBar
        layout = QVBoxLayout(self)
        self.retButton = QToolButton(self)
        self.retButton.setIconSize(QSize(60, 60))
        self.retButton.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
        self.retButton.setMinimumSize(200, 100)
        self.retButton.setMaximumSize(200, 100)
        layout.addWidget(self.retButton)
        # 过滤输入框
        layout.addWidget(
            QLineEdit(self, textChanged=self.doFilter,
                      placeholderText='过滤...'))
        # Material Font
        self.listView = QListView(self)
        self.listView.setMouseTracking(True)
        self.listView.setViewMode(QListView.IconMode)
        self.listView.setMovement(QListView.Static)
        self.listView.setFlow(QListView.LeftToRight)
        self.listView.setWrapping(True)
        self.listView.setEditTriggers(QListView.NoEditTriggers)
        self.listView.setResizeMode(QListView.Adjust)
        self.listView.doubleClicked.connect(self.onDoubleClicked)
        self.listView.entered.connect(self.onEntered)
        self.dmodel = QStandardItemModel(self.listView)
        self.fmodel = QSortFilterProxyModel(self.listView)
        self.fmodel.setSourceModel(self.dmodel)
        self.fmodel.setFilterRole(Qt.ToolTipRole)
        self.listView.setModel(self.fmodel)
        layout.addWidget(self.listView)

        # 字体加载器
        loader = config[0]

        # 添加Item
        fontMap = json.loads(open(config[1],
                                  'rb').read().decode(encoding='utf_8',
                                                      errors='ignore'),
                             encoding='utf_8')
        for name, _ in fontMap.items():
            item = QStandardItem(loader.icon(name), '')
            item.setData(name, Qt.ToolTipRole)
            item.setData(name, Qt.StatusTipRole)
            item.setTextAlignment(Qt.AlignCenter)
            item.setFlags(item.flags())
            self.dmodel.appendRow(item)

    def doFilter(self, _):
        self.fmodel.setFilterRegExp(self.sender().text())

    def onEntered(self, index):
        index = self.fmodel.mapToSource(index)
        text = index.data(Qt.ToolTipRole)
        if text:
            self.retButton.setText(text)
            self.retButton.setIcon(self.dmodel.itemFromIndex(index).icon())

    def onDoubleClicked(self, index):
        index = self.fmodel.mapToSource(index)
        text = index.data(Qt.ToolTipRole)
        if text:
            QApplication.clipboard().setText(text)
            self.statusBar.showMessage('已复制: %s' % text)
Exemplo n.º 9
0
class SearchFileWidget(QWidget):

    language_filter_change = pyqtSignal(list)

    def __init__(self):
        QWidget.__init__(self)

        self._refreshing = False

        self.fileModel = None
        self.proxyFileModel = None
        self.videoModel = None

        self._state = None

        self.timeLastSearch = QTime.currentTime()

        self.ui = Ui_SearchFileWidget()
        self.setup_ui()
        QTimer.singleShot(0, self.on_event_loop_started)

    @pyqtSlot()
    def on_event_loop_started(self):
        lastDir = self._state.get_video_path()
        index = self.fileModel.index(str(lastDir))
        proxyIndex = self.proxyFileModel.mapFromSource(index)
        self.ui.folderView.scrollTo(proxyIndex)

    def set_state(self, state):
        self._state = state
        self._state.signals.interface_language_changed.connect(
            self.on_interface_language_changed)
        self._state.signals.login_status_changed.connect(
            self.on_login_state_changed)

    def setup_ui(self):
        self.ui.setupUi(self)

        self.ui.splitter.setSizes([600, 1000])
        self.ui.splitter.setChildrenCollapsible(False)

        # Set up folder view

        self.fileModel = QFileSystemModel(self)
        self.fileModel.setFilter(QDir.AllDirs | QDir.Dirs | QDir.Drives
                                 | QDir.NoDotAndDotDot | QDir.Readable
                                 | QDir.Executable | QDir.Writable)
        self.fileModel.iconProvider().setOptions(
            QFileIconProvider.DontUseCustomDirectoryIcons)
        self.fileModel.setRootPath(QDir.rootPath())
        self.fileModel.directoryLoaded.connect(self.onFileModelDirectoryLoaded)

        self.proxyFileModel = QSortFilterProxyModel(self)
        self.proxyFileModel.setSortRole(Qt.DisplayRole)
        self.proxyFileModel.setSourceModel(self.fileModel)
        self.proxyFileModel.sort(0, Qt.AscendingOrder)
        self.proxyFileModel.setSortCaseSensitivity(Qt.CaseInsensitive)
        self.ui.folderView.setModel(self.proxyFileModel)

        self.ui.folderView.setHeaderHidden(True)
        self.ui.folderView.hideColumn(3)
        self.ui.folderView.hideColumn(2)
        self.ui.folderView.hideColumn(1)

        self.ui.folderView.expanded.connect(self.onFolderViewExpanded)
        self.ui.folderView.clicked.connect(self.onFolderTreeClicked)
        self.ui.buttonFind.clicked.connect(self.onButtonFind)
        self.ui.buttonRefresh.clicked.connect(self.onButtonRefresh)

        # Setup and disable buttons
        self.ui.buttonFind.setEnabled(False)
        self.ui.buttonSearchSelectFolder.setEnabled(False)
        self.ui.buttonSearchSelectVideos.setEnabled(False)

        # Set up introduction
        self.showInstructions()

        # Set unknown  text here instead of `retranslate()` because widget translates itself
        self.ui.filterLanguageForVideo.set_unknown_text(_('All languages'))
        self.ui.filterLanguageForVideo.set_selected_language(
            UnknownLanguage.create_generic())
        self.ui.filterLanguageForVideo.selected_language_changed.connect(
            self.on_language_combobox_filter_change)

        # Set up video view
        self.videoModel = VideoModel(self)
        self.videoModel.connect_treeview(self.ui.videoView)
        self.ui.videoView.setHeaderHidden(True)
        self.ui.videoView.clicked.connect(self.onClickVideoTreeView)
        self.ui.videoView.selectionModel().currentChanged.connect(
            self.onSelectVideoTreeView)
        self.ui.videoView.customContextMenuRequested.connect(self.onContext)
        self.ui.videoView.setUniformRowHeights(True)
        self.videoModel.dataChanged.connect(self.subtitlesCheckedChanged)
        self.language_filter_change.connect(
            self.videoModel.on_filter_languages_change)

        self.ui.buttonSearchSelectVideos.clicked.connect(
            self.onButtonSearchSelectVideos)
        self.ui.buttonSearchSelectFolder.clicked.connect(
            self.onButtonSearchSelectFolder)
        self.ui.buttonDownload.clicked.connect(self.onButtonDownload)
        self.ui.buttonPlay.clicked.connect(self.onButtonPlay)
        self.ui.buttonIMDB.clicked.connect(self.onViewOnlineInfo)
        self.ui.videoView.setContextMenuPolicy(Qt.CustomContextMenu)

        self.ui.buttonPlay.setEnabled(False)

        # Drag and Drop files to the videoView enabled
        # FIXME: enable drag events for videoView (and instructions view)
        self.ui.videoView.__class__.dragEnterEvent = self.dragEnterEvent
        self.ui.videoView.__class__.dragMoveEvent = self.dragEnterEvent
        self.ui.videoView.__class__.dropEvent = self.dropEvent
        self.ui.videoView.setAcceptDrops(1)

        self.retranslate()

    def retranslate(self):
        introduction = '<p align="center"><h2>{title}</h2></p>' \
            '<p><b>{tab1header}</b><br/>{tab1content}</p>' \
            '<p><b>{tab2header}</b><br/>{tab2content}</p>'\
            '<p><b>{tab3header}</b><br/>{tab3content}</p>'.format(
                title=_('How To Use {title}').format(title=PROJECT_TITLE),
                tab1header=_('1st Tab:'),
                tab2header=_('2nd Tab:'),
                tab3header=_('3rd Tab:'),
                tab1content=_('Select, from the Folder Tree on the left, the folder which contains the videos '
                              'that need subtitles. {project} will then try to automatically find available '
                              'subtitles.').format(project=PROJECT_TITLE),
                tab2content=_('If you don\'t have the videos in your machine, you can search subtitles by '
                              'introducing the title/name of the video.').format(project=PROJECT_TITLE),
                tab3content=_('If you have found some subtitle somewhere else that is not in {project}\'s database, '
                              'please upload those subtitles so next users will be able to '
                              'find them more easily.').format(project=PROJECT_TITLE))
        self.ui.introductionHelp.setHtml(introduction)

    @pyqtSlot(Language)
    def on_interface_language_changed(self, language):
        self.ui.retranslateUi(self)
        self.retranslate()

    @pyqtSlot(str)
    def onFileModelDirectoryLoaded(self, path):
        lastDir = str(self._state.get_video_path())
        qDirLastDir = QDir(lastDir)
        qDirLastDir.cdUp()
        if qDirLastDir.path() == path:
            index = self.fileModel.index(lastDir)
            proxyIndex = self.proxyFileModel.mapFromSource(index)
            self.ui.folderView.scrollTo(proxyIndex)
            self.ui.folderView.setCurrentIndex(proxyIndex)

    @pyqtSlot()
    def on_login_state_changed(self):
        log.debug('on_login_state_changed()')
        nb_connected = self._state.providers.get_number_connected_providers()
        if nb_connected:
            self.ui.buttonSearchSelectFolder.setEnabled(True)
            self.ui.buttonSearchSelectVideos.setEnabled(True)
            self.ui.buttonFind.setEnabled(
                self.get_current_selected_folder() is not None)
        else:
            self.ui.buttonSearchSelectFolder.setEnabled(False)
            self.ui.buttonSearchSelectVideos.setEnabled(False)
            self.ui.buttonFind.setEnabled(False)

    @pyqtSlot(Language)
    def on_language_combobox_filter_change(self, language):
        if language.is_generic():
            self.language_filter_change.emit(
                self._state.get_download_languages())
        else:
            self.language_filter_change.emit([language])

    def on_permanent_language_filter_change(self, languages):
        selected_language = self.ui.filterLanguageForVideo.get_selected_language(
        )
        if selected_language.is_generic():
            self.language_filter_change.emit(languages)

    @pyqtSlot()
    def subtitlesCheckedChanged(self):
        subs = self.videoModel.get_checked_subtitles()
        if subs:
            self.ui.buttonDownload.setEnabled(True)
        else:
            self.ui.buttonDownload.setEnabled(False)

    def showInstructions(self):
        self.ui.stackedSearchResult.setCurrentWidget(self.ui.pageIntroduction)

    def hideInstructions(self):
        self.ui.stackedSearchResult.setCurrentWidget(self.ui.pageSearchResult)

    @pyqtSlot(QModelIndex)
    def onFolderTreeClicked(self, proxyIndex):
        """What to do when a Folder in the tree is clicked"""
        if not proxyIndex.isValid():
            return

        index = self.proxyFileModel.mapToSource(proxyIndex)
        folder_path = self.fileModel.filePath(index)
        self._state.set_video_paths([folder_path])

    def get_current_selected_folder(self):
        proxyIndex = self.ui.folderView.currentIndex()
        index = self.proxyFileModel.mapToSource(proxyIndex)
        folder_path = Path(self.fileModel.filePath(index))
        if not folder_path:
            return None
        return folder_path

    def get_current_selected_item_videomodel(self):
        current_index = self.ui.videoView.currentIndex()
        return self.videoModel.getSelectedItem(current_index)

    @pyqtSlot()
    def onButtonFind(self):
        now = QTime.currentTime()
        if now < self.timeLastSearch.addMSecs(500):
            return
        folder_path = self.get_current_selected_folder()

        self._state.set_video_paths([folder_path])
        self.search_videos([folder_path])

        self.timeLastSearch = QTime.currentTime()

    @pyqtSlot()
    def onButtonRefresh(self):
        currentPath = self.get_current_selected_folder()
        self._refreshing = True

        self.ui.folderView.collapseAll()

        currentPath = self.get_current_selected_folder()
        if not currentPath:
            self._state.set_video_paths([currentPath])

        index = self.fileModel.index(str(currentPath))

        self.ui.folderView.scrollTo(self.proxyFileModel.mapFromSource(index))

    @pyqtSlot(QModelIndex)
    def onFolderViewExpanded(self, proxyIndex):
        if self._refreshing:
            expandedPath = self.fileModel.filePath(
                self.proxyFileModel.mapToSource(proxyIndex))
            if expandedPath == QDir.rootPath():
                currentPath = self.get_current_selected_folder()
                if not currentPath:
                    currentPath = self._state.get_video_path()

                index = self.fileModel.index(str(currentPath))

                self.ui.folderView.scrollTo(
                    self.proxyFileModel.mapFromSource(index))
                self._refreshing = False

    @pyqtSlot()
    def onButtonSearchSelectFolder(self):
        paths = self._state.get_video_paths()
        path = paths[0] if paths else Path()
        selected_path = QFileDialog.getExistingDirectory(
            self, _('Select the directory that contains your videos'),
            str(path))
        if selected_path:
            selected_paths = [Path(selected_path)]
            self._state.set_video_paths(selected_paths)
            self.search_videos(selected_paths)

    @pyqtSlot()
    def onButtonSearchSelectVideos(self):
        paths = self._state.get_video_paths()
        path = paths[0] if paths else Path()
        selected_files, t = QFileDialog.getOpenFileNames(
            self, _('Select the video(s) that need subtitles'), str(path),
            get_select_videos())
        if selected_files:
            selected_files = list(Path(f) for f in selected_files)
            selected_dirs = list(set(p.parent for p in selected_files))
            self._state.set_video_paths(selected_dirs)
            self.search_videos(selected_files)

    def search_videos(self, paths):
        if not self._state.providers.get_number_connected_providers():
            QMessageBox.about(
                self, _('Error'),
                _('You are not connected to a server. Please connect first.'))
            return
        self.ui.buttonFind.setEnabled(False)
        self._search_videos_raw(paths)
        self.ui.buttonFind.setEnabled(True)

    def _search_videos_raw(self, paths):
        # FIXME: must pass mainwindow as argument to ProgressCallbackWidget
        callback = ProgressCallbackWidget(self)
        callback.set_title_text(_('Scanning...'))
        callback.set_label_text(_('Scanning files'))
        callback.set_finished_text(_('Scanning finished'))
        callback.set_block(True)
        callback.show()

        try:
            local_videos, local_subs = scan_videopaths(paths,
                                                       callback=callback,
                                                       recursive=True)
        except OSError:
            callback.cancel()
            QMessageBox.warning(self, _('Error'),
                                _('Some directories are not accessible.'))

        if callback.canceled():
            return

        callback.finish()

        log.debug('Videos found: {}'.format(local_videos))
        log.debug('Subtitles found: {}'.format(local_subs))

        self.hideInstructions()

        if not local_videos:
            QMessageBox.information(self, _('Scan Results'),
                                    _('No video has been found.'))
            return

        total = len(local_videos)

        # FIXME: must pass mainwindow as argument to ProgressCallbackWidget
        # callback = ProgressCallbackWidget(self)
        # callback.set_title_text(_('Asking Server...'))
        # callback.set_label_text(_('Searching subtitles...'))
        # callback.set_updated_text(_('Searching subtitles ( %d / %d )'))
        # callback.set_finished_text(_('Search finished'))
        callback.set_block(True)
        callback.set_range(0, total)

        callback.show()

        try:
            remote_subs = self._state.search_videos(local_videos, callback)
        except ProviderConnectionError:
            log.debug(
                'Unable to search for subtitles of videos: videos={}'.format(
                    list(v.get_filename() for v in local_videos)))
            QMessageBox.about(self, _('Error'),
                              _('Unable to search for subtitles'))
            callback.finish()
            return

        self.videoModel.set_videos(local_videos)

        if remote_subs is None:
            QMessageBox.about(
                self, _('Error'),
                _('Error contacting the server. Please try again later'))
        callback.finish()

        # TODO: CHECK if our local subtitles are already in the server, otherwise suggest to upload

    @pyqtSlot()
    def onButtonPlay(self):
        selected_item = self.get_current_selected_item_videomodel()
        log.debug('Trying to play selected item: {}'.format(selected_item))

        if selected_item is None:
            QMessageBox.warning(self, _('No subtitle selected'),
                                _('Select a subtitle and try again'))
            return

        if isinstance(selected_item, SubtitleFileNetwork):
            selected_item = selected_item.get_subtitles()[0]

        if isinstance(selected_item, VideoFile):
            subtitle_file_path = None
            video = selected_item
        elif isinstance(selected_item, LocalSubtitleFile):
            subtitle_file_path = selected_item.get_filepath()
            video = selected_item.get_super_parent(VideoFile)
        elif isinstance(selected_item, RemoteSubtitleFile):
            video = selected_item.get_super_parent(VideoFile)
            subtitle_file_path = Path(
                tempfile.gettempdir()) / 'subdownloader.tmp.srt'
            log.debug('tmp path is {}'.format(subtitle_file_path))
            log.debug(
                'Temporary subtitle will be downloaded into: {temp_path}'.
                format(temp_path=subtitle_file_path))
            # FIXME: must pass mainwindow as argument to ProgressCallbackWidget
            callback = ProgressCallbackWidget(self)
            callback.set_title_text(_('Playing video + sub'))
            callback.set_label_text(_('Downloading files...'))
            callback.set_finished_text(_('Downloading finished'))
            callback.set_block(True)
            callback.set_range(0, 100)
            callback.show()

            try:
                selected_item.download(
                    subtitle_file_path,
                    self._state.providers.get(selected_item.get_provider),
                    callback)
            except ProviderConnectionError:
                log.debug('Unable to download subtitle "{}"'.format(
                    selected_item.get_filename()),
                          exc_info=sys.exc_info())
                QMessageBox.about(
                    self, _('Error'),
                    _('Unable to download subtitle "{subtitle}"').format(
                        subtitle=selected_item.get_filename()))
                callback.finish()
                return
            callback.finish()
        else:
            QMessageBox.about(
                self, _('Error'),
                '{}\n{}'.format(_('Unknown Error'),
                                _('Please submit bug report')))
            return

        # video = selected_item.get_parent().get_parent().get_parent()
        # FIXME: download subtitle with provider + use returned localSubtitleFile instead of creating one here
        if subtitle_file_path:
            local_subtitle = LocalSubtitleFile(subtitle_file_path)
        else:
            local_subtitle = None
        try:
            player = self._state.get_videoplayer()
            player.play_video(video, local_subtitle)
        except RuntimeError as e:
            QMessageBox.about(self, _('Error'), e.args[0])

    @pyqtSlot(QModelIndex)
    def onClickVideoTreeView(self, index):
        data_item = self.videoModel.getSelectedItem(index)

        if isinstance(data_item, VideoFile):
            video = data_item
            if True:  # video.getMovieInfo():
                self.ui.buttonIMDB.setEnabled(True)
                self.ui.buttonIMDB.setIcon(QIcon(':/images/info.png'))
                self.ui.buttonIMDB.setText(_('Movie Info'))
        elif isinstance(data_item, RemoteSubtitleFile):
            self.ui.buttonIMDB.setEnabled(True)
            self.ui.buttonIMDB.setIcon(
                QIcon(':/images/sites/opensubtitles.png'))
            self.ui.buttonIMDB.setText(_('Subtitle Info'))
        else:
            self.ui.buttonIMDB.setEnabled(False)

    @pyqtSlot(QModelIndex)
    def onSelectVideoTreeView(self, index):
        data_item = self.videoModel.getSelectedItem(index)

        self.ui.buttonPlay.setEnabled(True)
        # if isinstance(data_item, SubtitleFile):
        #     self.ui.buttonPlay.setEnabled(True)
        # else:
        #     self.ui.buttonPlay.setEnabled(False)

    def onContext(self, point):
        # FIXME: code duplication with Main.onContext and/or SearchNameWidget and/or SearchFileWidget
        menu = QMenu('Menu', self)

        listview = self.ui.videoView

        index = listview.currentIndex()
        data_item = listview.model().getSelectedItem(index)
        if data_item is not None:
            if isinstance(data_item, VideoFile):
                video = data_item
                video_identities = video.get_identities()
                if any(video_identities.iter_imdb_identity()):
                    online_action = QAction(QIcon(":/images/info.png"),
                                            _("View IMDb info"), self)
                    online_action.triggered.connect(self.onViewOnlineInfo)
                else:
                    online_action = QAction(QIcon(":/images/info.png"),
                                            _("Set IMDb info..."), self)
                    online_action.triggered.connect(self.on_set_imdb_info)
                menu.addAction(online_action)
            elif isinstance(data_item, SubtitleFile):
                play_action = QAction(QIcon(":/images/play.png"),
                                      _("Play video + subtitle"), self)
                play_action.triggered.connect(self.onButtonPlay)
                menu.addAction(play_action)

                if isinstance(data_item, RemoteSubtitleFile):
                    download_action = QAction(QIcon(":/images/download.png"),
                                              _("Download"), self)
                    download_action.triggered.connect(self.onButtonDownload)
                    menu.addAction(download_action)

                    online_action = QAction(
                        QIcon(":/images/sites/opensubtitles.png"),
                        _("View online info"), self)
                    online_action.triggered.connect(self.onViewOnlineInfo)
                    menu.addAction(online_action)

        # Show the context menu.
        menu.exec_(listview.mapToGlobal(point))

    def _create_choose_target_subtitle_path_cb(self):
        def callback(path, filename):
            selected_path = QFileDialog.getSaveFileName(
                self, _('Choose the target filename'), str(path / filename))
            return selected_path

        return callback

    def onButtonDownload(self):
        # We download the subtitle in the same folder than the video
        rsubs = self.videoModel.get_checked_subtitles()

        sub_downloader = SubtitleDownloadProcess(parent=self.parent(),
                                                 rsubtitles=rsubs,
                                                 state=self._state,
                                                 parent_add=True)
        sub_downloader.download_all()
        new_subs = sub_downloader.downloaded_subtitles()
        self.videoModel.uncheck_subtitles(new_subs)

    def onViewOnlineInfo(self):
        # FIXME: code duplication with Main.onContext and/or SearchNameWidget and/or SearchFileWidget
        # Tab for SearchByHash TODO:replace this 0 by an ENUM value
        listview = self.ui.videoView
        index = listview.currentIndex()
        data_item = self.videoModel.getSelectedItem(index)

        if isinstance(data_item, VideoFile):
            video = data_item
            video_identities = video.get_identities()
            if any(video_identities.iter_imdb_identity()):
                imdb_identity = next(video_identities.iter_imdb_identity())
                webbrowser.open(imdb_identity.get_imdb_url(),
                                new=2,
                                autoraise=1)
            else:
                QMessageBox.information(self.parent(), _('imdb unknown'),
                                        _('imdb is unknown'))

        elif isinstance(data_item, RemoteSubtitleFile):
            sub = data_item
            webbrowser.open(sub.get_link(), new=2, autoraise=1)

    @pyqtSlot()
    def on_set_imdb_info(self):
        # FIXME: DUPLICATED WITH SEARCHNAMEWIDGET
        QMessageBox.about(self, _("Info"), "Not implemented yet. Sorry...")
Exemplo n.º 10
0
class TileStampsDock(QDockWidget):
    setStamp = pyqtSignal(TileStamp)
    
    def __init__(self, stampManager, parent = None):
        super().__init__(parent)
        
        self.mTileStampManager = stampManager
        self.mTileStampModel = stampManager.tileStampModel()
        self.mProxyModel = QSortFilterProxyModel(self.mTileStampModel)
        self.mFilterEdit = QLineEdit(self)
        self.mNewStamp = QAction(self)
        self.mAddVariation = QAction(self)
        self.mDuplicate = QAction(self)
        self.mDelete = QAction(self)
        self.mChooseFolder = QAction(self)

        self.setObjectName("TileStampsDock")
        self.mProxyModel.setSortLocaleAware(True)
        self.mProxyModel.setSortCaseSensitivity(Qt.CaseInsensitive)
        self.mProxyModel.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.mProxyModel.setSourceModel(self.mTileStampModel)
        self.mProxyModel.sort(0)
        self.mTileStampView = TileStampView(self)
        self.mTileStampView.setModel(self.mProxyModel)
        self.mTileStampView.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)
        self.mTileStampView.header().setStretchLastSection(False)
        self.mTileStampView.header().setSectionResizeMode(0, QHeaderView.Stretch)
        self.mTileStampView.header().setSectionResizeMode(1, QHeaderView.ResizeToContents)
        self.mTileStampView.setContextMenuPolicy(Qt.CustomContextMenu)
        self.mTileStampView.customContextMenuRequested.connect(self.showContextMenu)
        self.mNewStamp.setIcon(QIcon(":images/16x16/document-new.png"))
        self.mAddVariation.setIcon(QIcon(":/images/16x16/add.png"))
        self.mDuplicate.setIcon(QIcon(":/images/16x16/stock-duplicate-16.png"))
        self.mDelete.setIcon(QIcon(":images/16x16/edit-delete.png"))
        self.mChooseFolder.setIcon(QIcon(":images/16x16/document-open.png"))
        Utils.setThemeIcon(self.mNewStamp, "document-new")
        Utils.setThemeIcon(self.mAddVariation, "add")
        Utils.setThemeIcon(self.mDelete, "edit-delete")
        Utils.setThemeIcon(self.mChooseFolder, "document-open")

        self.mFilterEdit.setClearButtonEnabled(True)
        self.mFilterEdit.textChanged.connect(self.mProxyModel.setFilterFixedString)
        self.mTileStampModel.stampRenamed.connect(self.ensureStampVisible)
        self.mNewStamp.triggered.connect(self.newStamp)
        self.mAddVariation.triggered.connect(self.addVariation)
        self.mDuplicate.triggered.connect(self.duplicate)
        self.mDelete.triggered.connect(self.delete_)
        self.mChooseFolder.triggered.connect(self.chooseFolder)
        self.mDuplicate.setEnabled(False)
        self.mDelete.setEnabled(False)
        self.mAddVariation.setEnabled(False)
        widget = QWidget(self)
        layout = QVBoxLayout(widget)
        layout.setContentsMargins(5, 5, 5, 5)
        
        buttonContainer = QToolBar()
        buttonContainer.setFloatable(False)
        buttonContainer.setMovable(False)
        buttonContainer.setIconSize(QSize(16, 16))
        buttonContainer.addAction(self.mNewStamp)
        buttonContainer.addAction(self.mAddVariation)
        buttonContainer.addAction(self.mDuplicate)
        buttonContainer.addAction(self.mDelete)
        stretch = QWidget()
        stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
        buttonContainer.addWidget(stretch)
        buttonContainer.addAction(self.mChooseFolder)
        listAndToolBar = QVBoxLayout()
        listAndToolBar.setSpacing(0)
        listAndToolBar.addWidget(self.mFilterEdit)
        listAndToolBar.addWidget(self.mTileStampView)
        listAndToolBar.addWidget(buttonContainer)
        layout.addLayout(listAndToolBar)
        selectionModel = self.mTileStampView.selectionModel()
        selectionModel.currentRowChanged.connect(self.currentRowChanged)
        self.setWidget(widget)
        self.retranslateUi()
        
    def changeEvent(self, e):
        super().changeEvent(e)
        x = e.type()
        if x==QEvent.LanguageChange:
            self.retranslateUi()
        else:
            pass
            
    def keyPressEvent(self, event):
        x = event.key()
        if x==Qt.Key_Delete or x==Qt.Key_Backspace:
            self.delete_()
            return

        super().keyPressEvent(event)

    def currentRowChanged(self, index):
        sourceIndex = self.mProxyModel.mapToSource(index)
        isStamp = self.mTileStampModel.isStamp(sourceIndex)
        self.mDuplicate.setEnabled(isStamp)
        self.mDelete.setEnabled(sourceIndex.isValid())
        self.mAddVariation.setEnabled(isStamp)
        if (isStamp):
            self.setStamp.emit(self.mTileStampModel.stampAt(sourceIndex))
        else:
            variation = self.mTileStampModel.variationAt(sourceIndex)
            if variation:
                # single variation clicked, use it specifically
                self.setStamp.emit(TileStamp(Map(variation.map)))

    def showContextMenu(self, pos):
        index = self.mTileStampView.indexAt(pos)
        if (not index.isValid()):
            return
        menu = QMenu()
        sourceIndex = self.mProxyModel.mapToSource(index)
        if (self.mTileStampModel.isStamp(sourceIndex)):
            addStampVariation = QAction(self.mAddVariation.icon(), self.mAddVariation.text(), menu)
            deleteStamp = QAction(self.mDelete.icon(), self.tr("Delete Stamp"), menu)
            deleteStamp.triggered.connect(self.delete_)
            addStampVariation.triggered.connect(self.addVariation)
            menu.addAction(addStampVariation)
            menu.addSeparator()
            menu.addAction(deleteStamp)
        else :
            removeVariation = QAction(QIcon(":/images/16x16/remove.png"),
                                                   self.tr("Remove Variation"),
                                                   menu)
            Utils.setThemeIcon(removeVariation, "remove")
            removeVariation.triggered.connect(self.delete_)
            menu.addAction(removeVariation)
        
        menu.exec(self.mTileStampView.viewport().mapToGlobal(pos))
    
    def newStamp(self):
        stamp = self.mTileStampManager.createStamp()
        if (self.isVisible() and not stamp.isEmpty()):
            stampIndex = self.mTileStampModel.index(stamp)
            if (stampIndex.isValid()):
                viewIndex = self.mProxyModel.mapFromSource(stampIndex)
                self.mTileStampView.setCurrentIndex(viewIndex)
                self.mTileStampView.edit(viewIndex)

    
    def delete_(self):
        index = self.mTileStampView.currentIndex()
        if (not index.isValid()):
            return
        sourceIndex = self.mProxyModel.mapToSource(index)
        self.mTileStampModel.removeRow(sourceIndex.row(), sourceIndex.parent())
    
    def duplicate(self):
        index = self.mTileStampView.currentIndex()
        if (not index.isValid()):
            return
        sourceIndex = self.mProxyModel.mapToSource(index)
        if (not self.mTileStampModel.isStamp(sourceIndex)):
            return
        stamp = self.mTileStampModel.stampAt = TileStamp(sourceIndex)
        self.mTileStampModel.addStamp(stamp.clone())

    def addVariation(self):
        index = self.mTileStampView.currentIndex()
        if (not index.isValid()):
            return
        sourceIndex = self.mProxyModel.mapToSource(index)
        if (not self.mTileStampModel.isStamp(sourceIndex)):
            return
        stamp = self.mTileStampModel.stampAt(sourceIndex)
        self.mTileStampManager.addVariation(stamp)
    
    def chooseFolder(self):
        prefs = Preferences.instance()
        stampsDirectory = prefs.stampsDirectory()
        stampsDirectory = QFileDialog.getExistingDirectory(self.window(),
                                                            self.tr("Choose the Stamps Folder"),
                                                            stampsDirectory)
        if (not stampsDirectory.isEmpty()):
            prefs.setStampsDirectory(stampsDirectory)
    
    def ensureStampVisible(self, stamp):
        stampIndex = self.mTileStampModel.index(stamp)
        if (stampIndex.isValid()):
            self.mTileStampView.scrollTo(self.mProxyModel.mapFromSource(stampIndex))

    def retranslateUi(self):
        self.setWindowTitle(self.tr("Tile Stamps"))
        self.mNewStamp.setText(self.tr("Add New Stamp"))
        self.mAddVariation.setText(self.tr("Add Variation"))
        self.mDuplicate.setText(self.tr("Duplicate Stamp"))
        self.mDelete.setText(self.tr("Delete Selected"))
        self.mChooseFolder.setText(self.tr("Set Stamps Folder"))
        self.mFilterEdit.setPlaceholderText(self.tr("Filter"))
Exemplo n.º 11
0
class FileChooser(BaseFileChooser):
	def __init__(self, **kwargs):
		super(FileChooser, self).__init__(**kwargs)

		self.options = PropDict()

		self.edit.textEdited.connect(self._onTextEdited)

		self.view.activated.connect(self._onActivated)

		# models
		self.rootChanger = RootChangerProxy()

		fsModel = QFileSystemModel(self)
		self.setModel(fsModel)

		self.filter = QSortFilterProxyModel()
		self.filter.setSourceModel(self.rootChanger)
		self.view.setModel(self.filter)

	def setModel(self, model):
		self.baseModel = model
		self.rootChanger.setSourceModel(self.baseModel)

	@Slot(str)
	def setRoot(self, path):
		self.root = path
		srcIdx = self.baseModel.setRootPath(path)
		self.rootChanger.setRootSource(srcIdx)
		self.view.setRootIndex(QModelIndex())

	@Slot(str)
	def _onTextEdited(self, txt):
		elems = txt.rsplit('/', 1)
		if len(elems) == 2:
			dir, base = elems
		else:
			dir, base = '', elems[0]

		path = os.path.join(self.root, dir)
		self.rootChanger.setRootSource(self.baseModel.index(path))
		self.filter.setFilterRegExp(QRegExp(base, Qt.CaseInsensitive, QRegExp.Wildcard))

		if self.options.get('autosuggest'):
			names = [self.filter.data(self.filter.index(i, 0)).toString()
				 for i in range(self.filter.rowCount(QModelIndex()))]
			names = [n[len(base):] for n in names]
			add = commonPrefix(names)

			cursor = self.edit.cursorPosition()
			self.edit.setText(self.edit.text()[:cursor] + add)
			self.edit.setSelection(cursor, len(self.edit.text()))

	@Slot(QModelIndex)
	def _onActivated(self, idx):
		idx = self.filter.mapToSource(idx)
		idx = self.rootChanger.mapToSource(idx)
		info = self.baseModel.fileInfo(idx)
		if info.isDir():
			return
		path = info.absoluteFilePath()
		self.openFile(path)
Exemplo n.º 12
0
class ReferenceSpectraDialog(QDialog):

    fits_picked = pyqtSignal(str)

    def __init__(self, database, main_spectrum=None):
        super(ReferenceSpectraDialog, self).__init__()
        self.main_spectrum = main_spectrum
        self.ui = Ui_ReferenceSpectraDialog()
        self.ui.setupUi(self)
        self.reference_catalogues = ReferenceCatalogues(database)
        self.full_model = QStandardItemModel()
        self.catalogues_model = QStandardItemModel()
        self.ui.catalogue.setModel(self.catalogues_model)
        self.ui.catalogue.currentTextChanged.connect(
            lambda txt: self.populate())
        for catname, cat in self.reference_catalogues.catalogues.items():
            row = QStandardItem(catname)
            row.setData(cat)
            self.catalogues_model.appendRow(row)

        self.model = QSortFilterProxyModel()
        self.model.setSourceModel(self.full_model)
        self.model.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.model.setFilterKeyColumn(0)
        self.ui.entries.setModel(self.model)
        self.ui.type_filter.currentTextChanged.connect(
            lambda txt: self.model.setFilterWildcard("{}*".format(txt)))
        self.ui.buttonBox.button(QDialogButtonBox.Open).setEnabled(False)
        self.ui.entries.selectionModel().selectionChanged.connect(
            lambda selected, deselected: self.ui.buttonBox.button(
                QDialogButtonBox.Open).setEnabled(len(selected.indexes()) > 0))
        self.accepted.connect(self.load_fits)
        self.populate()

    def set_main_spectrum(self, spectrum):
        self.main_spectrum = spectrum

    def populate(self):
        self.full_model.clear()
        catalogue = self.catalogues_model.item(
            self.ui.catalogue.currentIndex()).data()
        self.full_model.setHorizontalHeaderLabels(['Spectral Type'])
        entries = self.reference_catalogues.spectra(catalogue['name'])
        self.ui.type_filter.clear()
        self.ui.type_filter.addItem('')
        self.ui.type_filter.addItems(
            sorted(set([i['sptype'] for i in entries])))

        for entry in entries:
            item = QStandardItem(entry['sptype'])
            item.setData(entry)
            self.full_model.appendRow(item)

    def load_fits(self):
        original_index = self.model.mapToSource(
            self.ui.entries.selectionModel().selectedIndexes()[0])
        entry = self.full_model.item(original_index.row()).data()
        self.fits_picked.emit(self.reference_catalogues.fits(entry))

    def setup_menu(self, toolbar, axes, settings):
        self.current_line = None
        reference_action = QtCommons.addToolbarPopup(toolbar, "Reference")
        reference_action.menu().addAction("Reference library",
                                          lambda: self.show())
        reference_action.menu().addAction(
            "Load from FITS file", lambda: open_file_sticky(
                'Open Reference Profile', FITS_EXTS, lambda f: self.
                __open_reference(f[0], axes), settings, REFERENCE))
        self.close_action = reference_action.menu().addAction(
            "Close", lambda: self.__close_reference(axes))
        self.close_action.setEnabled(False)
        self.fits_picked.connect(lambda f: self.__open_reference(f, axes))
        self.blackbody_menu = blackbody.BlackBodyAction(
            lambda bb: self.blackbody(bb, axes), reference_action.menu())
        return reference_action.menu()

    def blackbody(self, blackbody, axes):
        self.__open(blackbody.spectrum(), axes)

    def __open_reference(self, file, axes):
        fits_spectrum = FitsSpectrum(fits.open(file))
        self.__open(fits_spectrum.spectrum, axes)

    def __open(self, spectrum, axes):
        self.__close_reference(axes)
        if spectrum.dispersion() < 0.4 and spectrum.dispersion() > 0:
            spectrum.resample(spectrum.dispersion() / 0.4)
        if (self.main_spectrum):
            print("Cutting spectrum: {0}, {1}".format(
                self.main_spectrum.wavelengths[0],
                self.main_spectrum.wavelengths[-1]))
            spectrum.cut_lambda(self.main_spectrum.wavelengths[0],
                                self.main_spectrum.wavelengths[-1])

        spectrum.normalize_to_max()
        self.current_line = Line2D(spectrum.wavelengths,
                                   spectrum.fluxes,
                                   color='gray')
        axes.add_line(self.current_line)
        axes.figure.canvas.draw()
        self.close_action.setEnabled(True)

    def __close_reference(self, axes):
        self.close_action.setEnabled(False)
        if self.current_line:
            try:  # TODO: verify
                self.current_line.remove()
                self.current_line = None
                axes.figure.canvas.draw()
            except:
                pass
Exemplo n.º 13
0
class SkipOnlyDialog(QDialog):
    def __init__(self, columns=None, skip=None, only=None, parent=None):
        super(SkipOnlyDialog, self).__init__(parent)
        self.setWindowTitle("Select Columns")
        self.columns = columns
        self.selected_variables = (
            []
        )  # Names of selected variables.  Only updated when accepting the dialog.
        # Select 'only' by default (unless a skip list is passed in)
        # starting_only and only are booleans to indicate whether 'only' is selected
        # starting_selected and selected are boolean arrays to indicate whether each variable is selected
        if skip is not None:
            self.starting_only = False
            self.starting_selected = [(c in skip) for c in self.columns]
        elif only is not None:
            self.starting_only = True
            self.starting_selected = [(c in only) for c in self.columns]
        elif skip is None and only is None:
            self.starting_only = True
            self.starting_selected = [False for _ in self.columns]
        else:
            raise ValueError("Can't pass both 'skip' and 'only'")

        # Available- Left Side
        self.left_model = QStandardItemModel(self)
        self.left_proxy = QSortFilterProxyModel(self)
        self.left_proxy.setSourceModel(self.left_model)
        self.left_proxy.setFilterKeyColumn(0)  # Filters based on the only column
        self.left_proxy.setFilterCaseSensitivity(
            Qt.CaseInsensitive
        )  # Case insensitive search
        # Selected - Right side
        self.right_model = QStandardItemModel()
        self.right_proxy = QSortFilterProxyModel(self)
        self.right_proxy.setSourceModel(self.right_model)
        self.right_proxy.setFilterKeyColumn(0)  # Filters based on the only column
        self.right_proxy.setFilterCaseSensitivity(
            Qt.CaseInsensitive
        )  # Case insensitive search

        # Setup Layout
        layout = QGridLayout(self)
        layout.setColumnStretch(0, 2)
        layout.setColumnStretch(1, 1)
        layout.setColumnStretch(2, 2)

        # Left Side Group Box ("Available")
        left_list_box = QGroupBox("Available Variables")
        left_list_box_layout = QVBoxLayout()
        # Multiselect listing available columns
        self.left_list = QListView(self)
        self.left_list.setModel(self.left_proxy)
        self.left_list.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.left_list.selectionModel().selectionChanged.connect(
            self.left_selected_change
        )
        self.left_list.setEditTriggers(QAbstractItemView.NoEditTriggers)
        left_list_box_layout.addWidget(self.left_list)
        # Add a search box
        self.left_list_search = QLineEdit(parent=self)
        self.left_list_search.setPlaceholderText("Search...")
        self.left_list_search.textChanged.connect(self.left_proxy.setFilterFixedString)
        left_list_box_layout.addWidget(self.left_list_search)
        # Set layout and add to the main layout
        left_list_box.setLayout(left_list_box_layout)
        layout.addWidget(left_list_box, 0, 0)

        # Add/Remove Buttons
        btns = QWidget()
        btns_layout = QVBoxLayout()
        btns.setLayout(btns_layout)
        # Add
        self.btn_add = QPushButton(text="Add ->", parent=self)
        self.btn_add.clicked.connect(self.add)
        self.btn_add.setEnabled(False)
        btns_layout.addWidget(self.btn_add)
        # Remove
        self.btn_remove = QPushButton(text="<- Remove", parent=self)
        self.btn_remove.clicked.connect(self.remove)
        self.btn_remove.setEnabled(False)
        btns_layout.addWidget(self.btn_remove)
        # Undo Changes
        self.btn_undo = QPushButton(text="Undo Changes", parent=self)
        self.btn_undo.clicked.connect(self.undo)
        self.btn_undo.setEnabled(False)
        btns_layout.addWidget(self.btn_undo)
        # Reset
        self.btn_reset = QPushButton(text="Reset", parent=self)
        self.btn_reset.clicked.connect(self.reset)
        self.btn_reset.setEnabled(False)
        btns_layout.addWidget(self.btn_reset)
        # Add to layout
        layout.addWidget(btns, 0, 1)

        # Right Side Group Box ("Selected")
        right_list_box = QGroupBox("Selected Variables")
        right_list_box_layout = QVBoxLayout()
        # Multiselect listing current selected columns
        self.right_list = QListView(self)
        self.right_list.setModel(self.right_proxy)
        self.right_list.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.right_list.selectionModel().selectionChanged.connect(
            self.right_selected_change
        )
        self.right_list.setEditTriggers(QAbstractItemView.NoEditTriggers)
        right_list_box_layout.addWidget(self.right_list)
        # Add a search box
        self.right_list_search = QLineEdit(parent=self)
        self.right_list_search.setPlaceholderText("Search...")
        self.right_list_search.textChanged.connect(
            self.right_proxy.setFilterFixedString
        )
        right_list_box_layout.addWidget(self.right_list_search)
        # Set layout and add to the main layout
        right_list_box.setLayout(right_list_box_layout)
        layout.addWidget(right_list_box, 0, 2)

        # Radio Select for Skip/Only
        self.radio_btns = QGroupBox("Skip Selected or Only Selected")
        radio_btns_layout = QHBoxLayout()
        self.radio_btns.setLayout(radio_btns_layout)
        self.radio_skip = QRadioButton("skip")
        radio_btns_layout.addWidget(self.radio_skip)
        self.radio_only = QRadioButton("only")
        self.radio_only.setChecked(True)
        radio_btns_layout.addWidget(self.radio_only)
        # If either button changes, a toggle signal is called for each one.  No need to pass the "checked" parameter.
        self.radio_skip.toggled.connect(lambda is_checked: self.update_result())
        layout.addWidget(self.radio_btns, 1, 2)

        # Result label
        self.result_label = QLabel(parent=self)
        self.result_label.setText("0 Variables to be used")
        layout.addWidget(self.result_label, 2, 0)

        # Ok/Cancel
        QBtn = QDialogButtonBox.Ok | QDialogButtonBox.Cancel

        self.buttonBox = QDialogButtonBox(QBtn)
        self.buttonBox.accepted.connect(self.submit)
        self.buttonBox.rejected.connect(self.reject)
        layout.addWidget(self.buttonBox, 2, 2)

        # Run reset to initialize
        self.undo()

    def undo(self):
        """
        Reset both list views and set the parameters ('only' and 'selected') to their starting values
        """
        # Clear lists
        self.left_model.clear()
        self.right_model.clear()
        # Set to the starting state
        self.selected = (
            self.starting_selected.copy()
        )  # Take a copy, don't refer to the same list
        self.only = self.starting_only
        for v, is_selected in zip(self.columns, self.selected):
            if not is_selected:
                self.left_model.appendRow(QStandardItem(v))
            else:
                self.right_model.appendRow(QStandardItem(v))
        self.update_result()
        self.btn_undo.setEnabled(False)  # No reason to undo twice

    def reset(self):
        """
        Remove the initialized state to do a complete reset
        """
        self.starting_only = True
        self.starting_selected = [False for _ in self.columns]
        self.undo()
        self.btn_reset.setEnabled(False)  # No reason to reset twice

    def left_selected_change(self):
        """
        Track the currently selected rows on the left
        """
        left_selected_num = len(self.left_list.selectedIndexes())
        if left_selected_num == 0:
            self.btn_add.setEnabled(False)
        else:
            self.btn_add.setEnabled(True)

    def right_selected_change(self):
        """
        track the currently selected rows on the right
        """
        right_selected_num = len(self.right_list.selectedIndexes())
        if right_selected_num == 0:
            self.btn_remove.setEnabled(False)
        else:
            self.btn_remove.setEnabled(True)

    def reset_list(self, side):
        """Clear the search field and show the full list"""
        if side == "available":
            self.left_list_search.setText("")
            self.left_model.clear()
            for v, is_selected in zip(self.columns, self.selected):
                if not is_selected:
                    self.left_model.appendRow(QStandardItem(v))
        elif side == "selected":
            self.right_list_search.setText("")
            self.right_model.clear()
            for v, is_selected in zip(self.columns, self.selected):
                if is_selected:
                    self.right_model.appendRow(QStandardItem(v))

    def add(self):
        """
        Move currently selected columns on the left to the right side
        """
        # Clear any right-side search
        self.reset_list("selected")
        # Get selection rows (indexed directly in the model)
        left_selected = sorted(
            [
                self.left_proxy.mapToSource(idx).row()
                for idx in self.left_list.selectedIndexes()
            ]
        )
        # Move items
        for idx in left_selected:
            item = self.left_model.takeItem(idx)
            self.right_model.appendRow(item)
            # Mark as selected
            col_idx = self.columns.index(item.text())
            self.selected[col_idx] = True
        # Delete rows after moving them (don't do it during because it causes index changes)
        for idx in reversed(
            left_selected
        ):  # Remove in reverse order, otherwise index changes
            self.left_model.removeRow(idx)
        # Update label
        self.update_result()
        # Disable Add since nothing is now selected
        self.btn_add.setEnabled(False)

    def remove(self):
        """
        Move currently selected columns on the right to the left side
        """
        # Clear any left-side search
        self.reset_list("available")
        # Get selection rows (indexed directly in the model)
        right_selected = sorted(
            [
                self.right_proxy.mapToSource(idx).row()
                for idx in self.right_list.selectedIndexes()
            ]
        )
        # Move items
        for idx in right_selected:
            item = self.right_model.takeItem(idx)
            self.left_model.appendRow(item)
            # Mark as not selected
            col_idx = self.columns.index(item.text())
            self.selected[col_idx] = False
        # Delete rows after moving them (don't do it during because it causes index changes)
        for idx in reversed(
            right_selected
        ):  # Remove in reverse order, otherwise index changes
            self.right_model.removeRow(idx)
        # Update label
        self.update_result()
        # Disable Remove since nothing is now selected
        self.btn_remove.setEnabled(False)

    def update_result(self):
        """
        Update the tracking of what variables will be used
        """
        self.only = self.radio_only.isChecked()
        num_selected = sum(self.selected)
        if num_selected == 0:
            self.result_label.setText(f"Using all {len(self.columns):,} variables")
        elif self.only:
            self.result_label.setText(
                f"Only using {num_selected:,} of {len(self.columns):,} variables"
            )
        else:
            self.result_label.setText(
                f"Skipping {num_selected:,} of {len(self.columns):,} variables"
            )

        # Set the undo button status
        if self.selected == self.starting_selected and self.only == self.starting_only:
            # In the starting state
            self.btn_undo.setEnabled(False)
        else:
            self.btn_undo.setEnabled(True)

        # Set the reset button status
        if num_selected == 0 and self.only:
            # In the default state
            self.btn_reset.setEnabled(False)
        else:
            self.btn_reset.setEnabled(True)

    def submit(self):
        # TODO: Add any warnings here
        self.selected_variables = [
            c for (c, is_selected) in zip(self.columns, self.selected) if is_selected
        ]
        self.accept()

    @staticmethod
    def get_skip_only(columns=None, skip=None, only=None, parent=None):
        if columns is None:
            return "No columns", None, None
        # Launch dialog to select skip and only
        dlg = SkipOnlyDialog(columns, skip, only, parent)
        result = dlg.exec_()
        # Get info from the dialog
        label = dlg.result_label.text()
        if dlg.only:
            skip = None
            only = dlg.selected_variables
        else:
            skip = dlg.selected_variables
            only = None
        # Return
        return label, skip, only
Exemplo n.º 14
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()
Exemplo n.º 15
0
class FileChooser(BaseFileChooser):
	def __init__(self, **kwargs):
		super(FileChooser, self).__init__(**kwargs)

		self.options = PropDict()

		self.edit.textEdited.connect(self._onTextEdited)

		self.view.activated.connect(self._onActivated)

		# models
		self.rootChanger = RootChangerProxy()

		fsModel = QFileSystemModel(self)
		self.setModel(fsModel)

		self.filter = QSortFilterProxyModel()
		self.filter.setSourceModel(self.rootChanger)
		self.view.setModel(self.filter)

	def setModel(self, model):
		self.baseModel = model
		self.rootChanger.setSourceModel(self.baseModel)

	@Slot(str)
	def setRoot(self, path):
		self.root = path
		srcIdx = self.baseModel.setRootPath(path)
		self.rootChanger.setRootSource(srcIdx)
		self.view.setRootIndex(QModelIndex())

	@Slot(str)
	def _onTextEdited(self, txt):
		elems = txt.rsplit('/', 1)
		if len(elems) == 2:
			dir, base = elems
		else:
			dir, base = '', elems[0]

		path = os.path.join(self.root, dir)
		self.rootChanger.setRootSource(self.baseModel.index(path))
		self.filter.setFilterRegExp(QRegExp(base, Qt.CaseInsensitive, QRegExp.Wildcard))

		if self.options.get('autosuggest'):
			names = [self.filter.data(self.filter.index(i, 0)).toString()
				 for i in range(self.filter.rowCount(QModelIndex()))]
			names = [n[len(base):] for n in names]
			add = commonPrefix(names)

			cursor = self.edit.cursorPosition()
			self.edit.setText(self.edit.text()[:cursor] + add)
			self.edit.setSelection(cursor, len(self.edit.text()))

	@Slot(QModelIndex)
	def _onActivated(self, idx):
		idx = self.filter.mapToSource(idx)
		idx = self.rootChanger.mapToSource(idx)
		info = self.baseModel.fileInfo(idx)
		if info.isDir():
			return
		path = info.absoluteFilePath()
		self.openFile(path)
Exemplo n.º 16
0
class ReferenceSpectraDialog(QDialog):
    
    fits_picked = pyqtSignal(str)
    
    def __init__(self, database, main_spectrum = None):
        super(ReferenceSpectraDialog, self).__init__()
        self.main_spectrum = main_spectrum
        self.ui = Ui_ReferenceSpectraDialog()
        self.ui.setupUi(self)
        self.reference_catalogues = ReferenceCatalogues(database)
        self.full_model = QStandardItemModel()
        self.catalogues_model = QStandardItemModel()
        self.ui.catalogue.setModel(self.catalogues_model)
        self.ui.catalogue.currentTextChanged.connect(lambda txt: self.populate())
        for catname, cat in self.reference_catalogues.catalogues.items():
            row = QStandardItem(catname)
            row.setData(cat)
            self.catalogues_model.appendRow(row)
        
        self.model = QSortFilterProxyModel()
        self.model.setSourceModel(self.full_model)
        self.model.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.model.setFilterKeyColumn(0)
        self.ui.entries.setModel(self.model)
        self.ui.type_filter.currentTextChanged.connect(lambda txt: self.model.setFilterWildcard("{}*".format(txt) ) )
        self.ui.buttonBox.button(QDialogButtonBox.Open).setEnabled(False)
        self.ui.entries.selectionModel().selectionChanged.connect(lambda selected, deselected: self.ui.buttonBox.button(QDialogButtonBox.Open).setEnabled(len(selected.indexes()) > 0)  )
        self.accepted.connect(self.load_fits)
        self.populate()
            
    def set_main_spectrum(self, spectrum):
        self.main_spectrum = spectrum
            
    def populate(self):
        self.full_model.clear()
        catalogue = self.catalogues_model.item(self.ui.catalogue.currentIndex()).data()
        self.full_model.setHorizontalHeaderLabels(['Spectral Type'])
        entries = self.reference_catalogues.spectra(catalogue['name'])
        self.ui.type_filter.clear()
        self.ui.type_filter.addItem('')
        self.ui.type_filter.addItems( sorted(set([i['sptype'] for i in entries])) )
        
        for entry in entries:
            item = QStandardItem(entry['sptype'])
            item.setData(entry)
            self.full_model.appendRow(item)
            
    def load_fits(self):
        original_index = self.model.mapToSource(self.ui.entries.selectionModel().selectedIndexes()[0])
        entry = self.full_model.item(original_index.row()).data()
        self.fits_picked.emit(self.reference_catalogues.fits(entry))
        
        
    def setup_menu(self, toolbar, axes, settings):
        self.current_line = None
        reference_action = QtCommons.addToolbarPopup(toolbar, "Reference")
        reference_action.menu().addAction("Reference library", lambda: self.show())
        reference_action.menu().addAction("Load from FITS file", lambda: open_file_sticky('Open Reference Profile', FITS_EXTS, lambda f: self.__open_reference(f[0], axes), settings, REFERENCE ))
        self.close_action = reference_action.menu().addAction("Close", lambda: self.__close_reference(axes))
        self.close_action.setEnabled(False)
        self.fits_picked.connect(lambda f: self.__open_reference(f, axes))
        self.blackbody_menu = blackbody.BlackBodyAction(lambda bb: self.blackbody(bb, axes), reference_action.menu())
        return reference_action.menu()
    
    def blackbody(self, blackbody, axes):
        self.__open(blackbody.spectrum(), axes)

    def __open_reference(self, file, axes):
        fits_spectrum = FitsSpectrum(fits.open(file))
        self.__open(fits_spectrum.spectrum, axes)
        
    def __open(self, spectrum, axes):
        self.__close_reference(axes)
        if spectrum.dispersion() < 0.4 and spectrum.dispersion() > 0:
            spectrum.resample(spectrum.dispersion() /0.4)
        if(self.main_spectrum):
            print("Cutting spectrum: {0}, {1}".format(self.main_spectrum.wavelengths[0], self.main_spectrum.wavelengths[-1]))
            spectrum.cut_lambda(self.main_spectrum.wavelengths[0], self.main_spectrum.wavelengths[-1])
            
        spectrum.normalize_to_max()
        self.current_line = Line2D(spectrum.wavelengths, spectrum.fluxes, color='gray')
        axes.add_line(self.current_line)
        axes.figure.canvas.draw()
        self.close_action.setEnabled(True)
        
    def __close_reference(self, axes):
        self.close_action.setEnabled(False)
        if self.current_line:
            try: # TODO: verify
                self.current_line.remove()
                self.current_line = None
                axes.figure.canvas.draw()
            except:
                pass
Exemplo n.º 17
0
class MainWindow(QMainWindow):
    """Controller class for VmsParser

    ### Args:
        QMainWindow ([class]): Create the controller class
    """

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        #Load VIEW from resource file
        uic.loadUi(self.resourcePath('gui.ui'), self)


        #Create first data for the model
        self.data = [VAMAS_File()]
        #Create the MODEL
        self.model = ParameterModel(self.data)

        #Selected first model column
        self.selectedModelColumn = 1
     

        #Create the maptlotlib FigureCanvas object, 
        #which defines a single set of axes as self.axes.
        self.spectralPlot = MplCanvas(self, width=8, height=4, dpi=100)
        #self.spectralPlot.axes.plot([0,1,2,3,4], [10,1,20,3,40])
        toolbar = NavigationToolbar(self.spectralPlot, self)
        self.plotToolBar.addWidget(toolbar)
        self.plotWidget.addWidget(self.spectralPlot)        

        #Popup menu event
        self.dataSelector.currentIndexChanged.connect(lambda index: (self.selectModelColumn(index+1)))
        self.dataSelector.setCurrentIndex(0)    

        #Display model as table
        self.vmsTable.setModel(self.model)
        self.vmsTable.verticalHeader().setMaximumWidth(160)
        self.vmsTable.setColumnWidth(0, 30)
        #Hide the first row with model-column selector checkbox
        self.vmsTable.hideRow(0)
        self.model.dataChanged.connect(lambda index: (self.modelEditedEvent(index)))
        
        #Proxy model with swapped rows/columns for paramTabel
        self.proxy = QTransposeProxyModel()
        self.proxy.setSourceModel(self.model)

        #Sorting model
        self.proxy2 = QSortFilterProxyModel()
        self.proxy2.setSourceModel(self.proxy)

        #Display sorted proxy model for param table
        self.paramTable.setModel(self.proxy2)
        #Show only selected columns
        self.initSelectedFields()  
        #Movable columns
        self.paramTable.horizontalHeader().setSectionsMovable(True)
        self.paramTable.setColumnWidth(0, 30)
        #Double click event selects row from model and updates view
        self.paramTable.doubleClicked.connect(lambda index: (self.selectModelColumn(self.proxy2.mapToSource(index).row())))
        #Hide first row with model-row selector checkboxes
        self.paramTable.hideRow(0)

        #Menu Bar events
        self.actionSave.triggered.connect(self.saveModel)
        self.actionLoad.triggered.connect(self.loadModel)  
        self.actionAppend_Files.triggered.connect(self.appendData)                
        self.actionQuit.triggered.connect(self.close)
    
        #Button events
        self.buttonPrevArea.clicked.connect(self.goToPreviousColumn)
        
        self.buttonNextArea.clicked.connect(self.goToNextColumn)

        self.colNumber.setText("0/0")

        self.show()

    def modelEditedEvent(self, index):
        """Event triggered when model data changed by clicking a checkbox

        ### Arguments:
            index {QModelIndex} -- row/colum of changed data
        """
        print("Model edited event row {0}, column {1} changed".format(index.row(), index.column()))
        #First column changes selected fields which are displayed in paramTable
        if index.column() == 0:
            if self.model.selectedRows[index.row()-1] == True:
                self.paramTable.showColumn(index.row())
            else:
                self.paramTable.hideColumn(index.row())

        #First row changes data columns to be plotted
        if index.row() == 0:
            self.updatePlot()


    def initSelectedFields(self):
        """Init selected dataclass fields = columns in param Table with default values
        """
        #Show only selected columns
        visibleColumns = ["sampleName", "blockName", "posName", "date", "technique", "analyserSettingStr", "analyzerPEorRR", "dwellTime"]
        for r in range(self.proxy.columnCount()):
            if not self.proxy.headerData(r, Qt.Horizontal) in visibleColumns:
                self.paramTable.hideColumn(r)
            else:
                self.model.selectedRows[r-1] = True
        self.paramTable.showColumn(0)  
    
    def goToNextColumn(self):
        """Select next column from model
        """
        if self.selectedModelColumn < self.model.columnCount()-1:
            self.selectedModelColumn+=1
            self.updateSelectedData()
    
    def goToPreviousColumn(self):
        """Select previous column from model
        """
        if self.selectedModelColumn > 1:
            self.selectedModelColumn-=1
            self.updateSelectedData()            
    
    def selectModelColumn(self, index):
        """Select indexed data column from the model, update the datamapper and view

        ### Arguments:
            index {int} -- Model column index
        """
        # self.dataMapper.submit()
        #self.dataMapper.setCurrentIndex(index)
        print("Selecting model column " + str(index))
        self.selectedModelColumn = index
        self.updateSelectedData()


    def updateSelectedData(self):
        """update dataSelector PopupMenu, Table and plot after a different data column selected from the model
        """
        #Update colNumber label
        self.colNumber.setText(str(self.selectedModelColumn) + "/" + str(self.model.columnCount()-1))
        #Select corresponding line in popupmenu without triggering event
        self.dataSelector.blockSignals(True)
        self.dataSelector.setCurrentIndex(self.selectedModelColumn-1)
        self.dataSelector.blockSignals(False)
        #Show only only the selected column (=dataclass) from the model in the vmsTable
        for column in range(self.model.columnCount()):
            if column == self.selectedModelColumn or column == 0:
                self.vmsTable.showColumn(column)
            else:
                self.vmsTable.hideColumn(column)
        
        #print ("Combobox currentIndex", self.dataSelector.currentIndex(), " from ", self.dataSelector.count())
        #Select line in inverse paramTable
        index = self.proxy.index(self.selectedModelColumn, 0) #Get column = row index from Transpose Proxy model
        print("Param table selecting row {0}".format(self.proxy2.mapFromSource(index).row()))
        #Deselect all
        self.paramTable.clearSelection()
        self.paramTable.selectRow(self.proxy2.mapFromSource(index).row()) #Convert to row index in sorted Proxy model

        #Plot data
        #Get current data class object
        self.updatePlot()

    def updatePlot(self):
        """update the plot with the selected data
        """
        #Get current data class object
        data = self.model.getObject(self.selectedModelColumn)
        #print("plotting column " + str(self.selectedModelColumn))
        if len(data.yAxisValuesList) > 0:
            self.spectralPlot.axes.cla()
            self.spectralPlot.axes.plot(data.xAxisValuesList, data.yAxisValuesList[0])
            #Create axis labels from data
            self.spectralPlot.axes.set_xlabel("{0} [{1}]".format(data.xAxisLabel, data.xAxisUnit))
            self.spectralPlot.axes.set_ylabel("{0} [{1}]".format(data.yAxisVarsLabelList[0], data.yAxisVarsUnitList[0]))


        #Plot also selected data columns
        for colIndex, checked in enumerate(self.model.selectedColumns):
            if checked and colIndex+1 != self.selectedModelColumn: #Starts from 0
                #print("plotting also column " + str(colIndex+1))
                #Get current data class object
                data = self.model.getObject(colIndex+1) #Starts from 0
                self.spectralPlot.axes.plot(data.xAxisValuesList, data.yAxisValuesList[0])

        #redraw
        self.spectralPlot.draw()            
            
            


    def resourcePath(self, relPath):
        """To access resources when bundled as an executable using PyInstaller relative paths are redirected to temporary _MEIPASS folder
            Ref.: https://blog.aaronhktan.com/posts/2018/05/14/pyqt5-pyinstaller-executable
        ### Args:
            relPath {str}: Relative path to resource file

        ### Returns:
            {str}: Converted path
        """
        if hasattr(sys, '_MEIPASS'):
            return os.path.join(sys._MEIPASS, relPath)
        return os.path.join(os.path.abspath('.'), relPath)


    def saveModel(self):
        """Present file dialog to save model data to config (.ini) file
        """
        #Present file dialog using last saved folder
        lastFolder = self.getLastSaveFolder()
        dialog = QFileDialog(self)
        dialog.setWindowTitle("Save settings")
        dialog.setNameFilter("Ini files (*.ini)")
        dialog.setFileMode(QFileDialog.AnyFile)
        dialog.setDirectory(lastFolder)
        dialog.setAcceptMode(QFileDialog.AcceptSave)
        if dialog.exec_() == QDialog.Accepted:
            fileName = dialog.selectedFiles()[0]
            self.saveLastFolder(fileName)
            #Save the model to config file
            self.model.saveModelToConfigFile(fileName)
            print ("Saved to " + fileName)
        


    def getLastSaveFolder(self):
        """ Try to get a selected folder from QSettings file
            (Mac: ~\library\preferences\)
            Defaults to userfolder ~
        ### Returns:
            {str}: folder path
        """
        try:
            settings = QSettings('vmsParser', 'vmsParser')
            print(settings.fileName())
            lastFolder = settings.value('saveFolder', type=str)
            
        except:
            lastFolder = os.path.expanduser('~')

        return lastFolder


    def saveLastFolder(self, foldername):
        """ Saves the last visited folder as QSettings
            (Mac: ~\library\preferences\)
        ### Args:
            foldername {str}: foldername
        """
        settings = QSettings('vmsParser', 'vmsParser')
        settings.setValue('saveFolder', os.path.dirname(foldername))

    def loadModel(self):
        """Present file dialog to load model data from .vms files
        """
        print("model column count before load " + str(self.model.columnCount()))
        #Present file dialog using last saved folder
        fileNames = self.vmsFileSelectorDialog()
        if fileNames: #Continue if files selected
            #Clear popup menu before loading new data
            self.dataSelector.clear()
            dataList = self.loadfilesIntoList(fileNames)
            #Supply the new datalist to the model to replace its data
            self.model.loadData(dataList)
            #Select first model column
            self.selectedModelColumn = 1
            #Update the column number
            self.colNumber.setText("1/" + str(self.model.columnCount()-1))
            #Refill popup menu with filenames
            for data in self.model.getData():
                self.dataSelector.addItem(os.path.basename(data.fileName))
            self.vmsTable.resizeRowsToContents() #Resize rows in table view to make space for multiline comments
    
    def appendData(self):
        """Present file dialog to append files
        """
        if self.dataSelector.currentText() == "":
            self.loadModel()
        else:
            #Present file dialog using last saved folder
            print("model column count before append " + str(self.model.columnCount()))
            print("Selected model column " + str(self.selectedModelColumn))
            fileNames = self.vmsFileSelectorDialog()
            if fileNames: #Continue if files selected
                #Deselect all
                self.paramTable.clearSelection()
                dataList = self.loadfilesIntoList(fileNames)
                #Get the number of columns before insert
                oldColumnNum = self.model.columnCount()
                for data in dataList:
                    self.dataSelector.addItem(os.path.basename(data.fileName))
                    self.model.appendData(data)
                self.selectModelColumn(oldColumnNum)
                self.vmsTable.resizeRowsToContents() #Resize rows in table view to make space for multiline comments
            
    def loadfilesIntoList(self, fileNames):
        """Load a list of vamas files into list of dataclasses to use as new model data

        ### Arguments:
            fileNames {list} -- List of filenames
        """
        data = list()   
        for filename in fileNames:
            vms = VAMAS_File(fileName=filename)
            vms.readVamasFile()
            data.append(vms)
        return data
    
    def vmsFileSelectorDialog(self):
        """ Present file dialog to select one or more vamas files
            Starting with last used folder from App preferences

        ### Returns:
            {list} -- list of selected filePaths
        """
        lastFolder = self.getLastSaveFolder()
        dialog = QFileDialog(self)
        dialog.setWindowTitle("Open vms files")
        dialog.setNameFilter("vms files (*.vms)")
        dialog.setFileMode(QFileDialog.ExistingFiles) #Select multiple existing files
        dialog.setDirectory(lastFolder)
        dialog.setAcceptMode(QFileDialog.AcceptOpen)
        if dialog.exec_() == QDialog.Accepted:
            fileNames = dialog.selectedFiles()
            #Save selected foldername for next time
            self.saveLastFolder(fileNames[0])
            return fileNames
        else:
            return None
class QItemCollectionSelector(QObject):
    selectedChanged = pyqtSignal(AmountItem)

    def __init__(self, tableView, actor, allowContext):
        QObject.__init__(self)
        self.actor = actor
        actor.ownedItems.aItemChanged.connect(self.actorsItemsChanged)
        self.preFilter = None
        self.filter = None
        # table
        self.tableView = tableView
        self.tableView.setSortingEnabled(True)
        self.tableView.selectionChanged = self.selectedItemChanged
        self.tableModel = ItemTableModel(self.actor.ownedItems)
        self.proxyModel = QSortFilterProxyModel()
        self.proxyModel.setSourceModel(self.tableModel)
        self.proxyModel.setSortRole(Qt.EditRole)
        self.tableView.setModel(self.proxyModel)
        resizeColumns(self.tableView, self.tableModel.columnCount())
        # bunch func exec
        if allowContext:
            self.tableView.setContextMenuPolicy(Qt.CustomContextMenu)
            self.tableView.customContextMenuRequested.connect(self.itemTableViewContextMenu)
            self.cachedFuncs = None
            self.cachedIndex = None

    def setConstantPreFilter(self, preFilter):
        self.preFilter = preFilter
        self.updateFilter(self.filter)

    def getCurrentSelected(self):
        index = self.proxyModel.mapToSource(self.tableView.selectionModel().currentIndex())
        if not index.isValid():
            return AmountItem(None, None)
        return self.tableModel.filteredAItemList[index.row()]

    def actorsItemsChanged(self, batch):
        if batch[0] == AmountList.CHANGE_COMPLETELY:
            self.tableModel.reapplyFilter()
            return
        for i in batch:
            code = i[1]
            aItem = i[0]
            if code == AmountList.CHANGE_AMOUNT:
                #dataChanged
                i = self.tableModel.filteredAItemList.getIndexById(aItem.item.id)
                if i is None:
                    continue
                index = self.tableModel.index(i, 0)
                self.tableModel.dataChanged.emit(index, index)
            elif code == AmountList.CHANGE_REMOVED:
                i = self.tableModel.filteredAItemList.getIndexById(aItem.item.id)
                if i == None:
                    continue
                self.tableModel.layoutAboutToBeChanged.emit()
                del self.tableModel.filteredAItemList.aItemList[i]
                self.tableModel.layoutChanged.emit()
            elif code == AmountList.CHANGE_APPEND:
                if not self.tableModel.cachedFilter.isValidItem(aItem):
                    continue
                self.tableModel.layoutAboutToBeChanged.emit()
                self.tableModel.filteredAItemList.append(aItem)
                self.tableModel.layoutChanged.emit()


    def itemTableViewContextMenu(self, pos):
        menu = QMenu()
        funcs = self.getFuncsAllSelectedItemsHave()
        if funcs == None:
            return
        for k, v in funcs.iteritems():
            menu.addAction(tStr(k), lambda func=v: func(self.getCurrentSelected(), self.actor))

        menu.exec_(self.tableView.mapToGlobal(pos))

    def getFuncsAllSelectedItemsHave(self):
        index = self.tableView.selectedIndexes()
        if index == self.cachedIndex:
            return self.cachedFuncs

        if len(index) == 0:
            return None

        funcs = {}
        for i in index:
            f = self.getCurrentSelected().item.getExtraFunctionality()
            if funcs == {}:
                funcs = f
            else:
                newFuncs = {}
                for k in funcs.iterkeys():
                    if k in f:
                        newFuncs[k] = f[k]
                funcs = newFuncs

        self.cachedIndex = index
        self.cachedFuncs = funcs
        return funcs

    def updateFilter(self, filter):
        self.filter = filter
        if self.preFilter != None:
            if filter == None:
                filter = self.preFilter
            else:
                filter = ItemFilterJoin(self.preFilter, ItemFilterJoin.AND, filter)
        self.tableModel.setFilter(filter)

    def selectedItemChanged(self, selected, deselected):
        QTableView.selectionChanged(self.tableView, selected, deselected)
        self.selectedChanged.emit(self.getCurrentSelected())

    def resizeColumns(self):
        resizeColumns(self.tableView, self.tableModel.columnCount())
Exemplo n.º 19
0
class IncidentView(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(IncidentView, self).__init__(parent)
        self.view = QtWidgets.QTreeView()
        self.view.setAllColumnsShowFocus(True)
        self.view.setUniformRowHeights(True)
        box = QtWidgets.QVBoxLayout()
        box.addWidget(self.view)
        self.load_trace = QtWidgets.QPushButton('&Trace')
        self.load_trace.setToolTip('Load into the Trace Window')
        self.load_trace.setEnabled(False)
        for activation_signal in [
                self.view.activated,
                self.view.entered,
                self.view.pressed]:
            activation_signal.connect(lambda _: self.update_controls_state())
        self.load_trace.clicked.connect(self.load_current_trace)
        self.view.doubleClicked.connect(self.jump_to_index)
        hbox = QtWidgets.QHBoxLayout()
        self.filter = QtWidgets.QLineEdit()
        self.filter.textChanged.connect(self.filter_model)
        filter_label = QtWidgets.QLabel('&Search')
        filter_label.setBuddy(self.filter)
        hbox.addWidget(filter_label)
        hbox.addWidget(self.filter)
        hbox.addWidget(self.load_trace)
        box.addLayout(hbox)
        self.setLayout(box)
        self.model = None
        self.proxy = None

    def display(self, incidents, locations):
        self.model = IncidentModel(incidents, locations, self)
        self.proxy = QSortFilterProxyModel(self)
        self.proxy.setSourceModel(self.model)
        self.proxy.setFilterRole(self.model.filter_role)
        self.proxy.setFilterRegExp(QRegExp(self.filter.text()))
        self.view.setModel(self.proxy)

    def filter_model(self, txt):
        if self.proxy:
            self.proxy.setFilterRegExp(QRegExp(txt))

    def update_controls_state(self):
        curr = self.view.currentIndex()
        self.load_trace.setEnabled(curr.isValid() and
                                   curr.parent().isValid())

    def load_current_trace(self):
        idx = self.proxy.mapToSource(self.view.currentIndex())
        if not idx.isValid() or index_level(idx) not in (1, 2):
            raise ValueError('load_current_trace: invalid index')

        if index_level(idx) == 2:
            idx = idx.parent()

        incident = self.model.incidents[idx.parent().row()]
        location = incident.locations[idx.row()]
        backtrace = self.model.locations[location]

        for p in reversed(backtrace):
            self.load_trace_point(p)

    def jump_to_index(self, idx):
        idx = self.proxy.mapToSource(idx)
        if index_level(idx) != 2:
            # don't mess with parents, they are used to create children
            return
        grandpa = idx.parent().parent()
        incident = self.model.incidents[grandpa.row()]
        location = incident.locations[idx.parent().row()]
        trace = self.model.locations[location]
        point = trace[idx.row()]
        self.show_trace_point(point)

    def load_trace_point(self, p):
        add_insn_to_trace_view(p.addr)

    def show_trace_point(self, p):
        idaapi.jumpto(p.addr)
Exemplo n.º 20
0
class Table(QWidget):
    def __init__(self,
                 name,
                 data,
                 columns=None,
                 index=False,
                 checkable=False,
                 parent=None):
        QWidget.__init__(self, parent)

        self.name = name
        self.index = index
        self.checkable = checkable

        if not any([data, columns]):
            self.columns = []
        else:
            self.columns = columns if columns else list(data[0].keys())

        if checkable:
            self.columns.insert(0, '')

        if index:
            self.columns.insert(0, 'ID')

        self.setData(data)

        self.initUI()

    def initUI(self):
        #  Layout UI elements of table

        mainLayout = QVBoxLayout()

        self.proxyModel = QSortFilterProxyModel()
        self.proxyModel.setDynamicSortFilter(True)

        self.sourceModel = QStandardItemModel(0, len(self.columns), self)

        for i, column in enumerate(self.columns):
            self.sourceModel.setHeaderData(i, Qt.Horizontal, column)

        self.proxyModel.setSourceModel(self.sourceModel)

        self.proxyGroupBox = QGroupBox(self.name)

        self.proxyView = DeselectableTreeView()
        self.proxyView.setRootIsDecorated(False)
        self.proxyView.setAlternatingRowColors(True)
        self.proxyView.setModel(self.proxyModel)

        if not self.checkable:
            self.proxyView.setSortingEnabled(True)
            self.proxyView.sortByColumn(0, Qt.AscendingOrder)

        self.proxyView.setEditTriggers(QAbstractItemView.NoEditTriggers)

        proxyLayout = QGridLayout()
        proxyLayout.addWidget(self.proxyView, 0, 0, 1, 3)
        self.proxyGroupBox.setLayout(proxyLayout)

        mainLayout.addWidget(self.proxyGroupBox)
        self.setLayout(mainLayout)
        self.update(self.data)

    def setSourceModel(self, model):
        self.proxyModel.setSourceModel(model)

    def setData(self, data):
        self.data = []
        for i, item in enumerate(data):
            d = {}
            for col in self.columns:
                if col == 'ID':
                    d[col] = item.get(col, i + 1)
                else:
                    d[col] = item.get(col)
            self.data.append(d)

        #self.data = [{k: item.get(k, i+1) for k in self.columns} for i, item in enumerate(data)]

    def sortBy(self, colName):
        idx = 0
        try:
            idx = self.columns.index(colName)
        except:
            pass

        self.proxyView.sortByColumn(idx, Qt.AscendingOrder)

    def rowCount(self):
        return self.sourceModel.rowCount()

    def columnCount(self):
        return self.sourceModel.columnCount()

    def addRow(self, row_i, rowData):
        self.sourceModel.insertRow(row_i)

        for col_i, data in enumerate(rowData.values()):
            if self.checkable and col_i == 0:
                self.proxyView.setColumnWidth(col_i, 1)
                item = QStandardItem(True)
                item.setCheckable(True)
                item.setCheckState(False)
                self.sourceModel.setItem(row_i, col_i, item)
            else:
                self.sourceModel.setData(self.sourceModel.index(row_i, col_i),
                                         data)

    def update(self, data):
        self.setData(data)
        self.sourceModel.removeRows(0, self.sourceModel.rowCount())

        for i, data in enumerate(self.data):
            self.addRow(i, data)

    def getSelectedRowIndex(self):
        '''
        Returns the index of the selected row from the source model
        '''
        try:
            return self.proxyModel.mapToSource(
                self.proxyView.selectedIndexes()[0]).row()
        except:
            return False

    def getCheckedRowData(self):
        selectedData = []
        for row_i in range(self.sourceModel.rowCount()):
            item = self.sourceModel.item(row_i, 0)
            if item.checkState():
                selectedData.append(self.data[row_i])

        return selectedData
Exemplo n.º 21
0
class BreakPointViewer(QTreeView):
    """
    Class implementing the Breakpoint viewer widget.
    
    Breakpoints will be shown with all their details. They can be modified
    through the context menu of this widget.
    
    @signal sourceFile(str, int) emitted to show the source of a breakpoint
    """
    sourceFile = pyqtSignal(str, int)

    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent the parent (QWidget)
        """
        super(BreakPointViewer, self).__init__(parent)
        self.setObjectName("BreakPointViewer")

        self.__model = None

        self.setItemsExpandable(False)
        self.setRootIsDecorated(False)
        self.setAlternatingRowColors(True)
        self.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)

        self.setWindowTitle(self.tr("Breakpoints"))

        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.__showContextMenu)
        self.doubleClicked.connect(self.__doubleClicked)

        self.__createPopupMenus()

        self.condHistory = []
        self.fnHistory = []
        self.fnHistory.append('')

        self.__loadRecent()

    def setModel(self, model):
        """
        Public slot to set the breakpoint model.
        
        @param model reference to the breakpoint model (BreakPointModel)
        """
        self.__model = model

        self.sortingModel = QSortFilterProxyModel()
        self.sortingModel.setDynamicSortFilter(True)
        self.sortingModel.setSourceModel(self.__model)
        super(BreakPointViewer, self).setModel(self.sortingModel)

        header = self.header()
        header.setSortIndicator(0, Qt.AscendingOrder)
        header.setSortIndicatorShown(True)
        header.setSectionsClickable(True)

        self.setSortingEnabled(True)

        self.__layoutDisplay()

    def __layoutDisplay(self):
        """
        Private slot to perform a layout operation.
        """
        self.__resizeColumns()
        self.__resort()

    def __resizeColumns(self):
        """
        Private slot to resize the view when items get added, edited or
        deleted.
        """
        self.header().resizeSections(QHeaderView.ResizeToContents)
        self.header().setStretchLastSection(True)

    def __resort(self):
        """
        Private slot to resort the tree.
        """
        self.model().sort(self.header().sortIndicatorSection(),
                          self.header().sortIndicatorOrder())

    def __toSourceIndex(self, index):
        """
        Private slot to convert an index to a source index.
        
        @param index index to be converted (QModelIndex)
        @return mapped index (QModelIndex)
        """
        return self.sortingModel.mapToSource(index)

    def __fromSourceIndex(self, sindex):
        """
        Private slot to convert a source index to an index.
        
        @param sindex source index to be converted (QModelIndex)
        @return mapped index (QModelIndex)
        """
        return self.sortingModel.mapFromSource(sindex)

    def __setRowSelected(self, index, selected=True):
        """
        Private slot to select a complete row.
        
        @param index index determining the row to be selected (QModelIndex)
        @param selected flag indicating the action (bool)
        """
        if not index.isValid():
            return

        if selected:
            flags = QItemSelectionModel.SelectionFlags(
                QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
        else:
            flags = QItemSelectionModel.SelectionFlags(
                QItemSelectionModel.Deselect | QItemSelectionModel.Rows)
        self.selectionModel().select(index, flags)

    def __createPopupMenus(self):
        """
        Private method to generate the popup menus.
        """
        self.menu = QMenu()
        self.menu.addAction(self.tr("Add"), self.__addBreak)
        self.menu.addAction(self.tr("Edit..."), self.__editBreak)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Enable"), self.__enableBreak)
        self.menu.addAction(self.tr("Enable all"), self.__enableAllBreaks)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Disable"), self.__disableBreak)
        self.menu.addAction(self.tr("Disable all"), self.__disableAllBreaks)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Delete"), self.__deleteBreak)
        self.menu.addAction(self.tr("Delete all"), self.__deleteAllBreaks)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Goto"), self.__showSource)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Configure..."), self.__configure)

        self.backMenuActions = {}
        self.backMenu = QMenu()
        self.backMenu.addAction(self.tr("Add"), self.__addBreak)
        self.backMenuActions["EnableAll"] = self.backMenu.addAction(
            self.tr("Enable all"), self.__enableAllBreaks)
        self.backMenuActions["DisableAll"] = self.backMenu.addAction(
            self.tr("Disable all"), self.__disableAllBreaks)
        self.backMenuActions["DeleteAll"] = self.backMenu.addAction(
            self.tr("Delete all"), self.__deleteAllBreaks)
        self.backMenu.aboutToShow.connect(self.__showBackMenu)
        self.backMenu.addSeparator()
        self.backMenu.addAction(self.tr("Configure..."), self.__configure)

        self.multiMenu = QMenu()
        self.multiMenu.addAction(self.tr("Add"), self.__addBreak)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Enable selected"),
                                 self.__enableSelectedBreaks)
        self.multiMenu.addAction(self.tr("Enable all"), self.__enableAllBreaks)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Disable selected"),
                                 self.__disableSelectedBreaks)
        self.multiMenu.addAction(self.tr("Disable all"),
                                 self.__disableAllBreaks)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Delete selected"),
                                 self.__deleteSelectedBreaks)
        self.multiMenu.addAction(self.tr("Delete all"), self.__deleteAllBreaks)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Configure..."), self.__configure)

    def __showContextMenu(self, coord):
        """
        Private slot to show the context menu.
        
        @param coord the position of the mouse pointer (QPoint)
        """
        cnt = self.__getSelectedItemsCount()
        if cnt <= 1:
            index = self.indexAt(coord)
            if index.isValid():
                cnt = 1
                self.__setRowSelected(index)
        coord = self.mapToGlobal(coord)
        if cnt > 1:
            self.multiMenu.popup(coord)
        elif cnt == 1:
            self.menu.popup(coord)
        else:
            self.backMenu.popup(coord)

    def __clearSelection(self):
        """
        Private slot to clear the selection.
        """
        for index in self.selectedIndexes():
            self.__setRowSelected(index, False)

    def __addBreak(self):
        """
        Private slot to handle the add breakpoint context menu entry.
        """
        from .EditBreakpointDialog import EditBreakpointDialog

        dlg = EditBreakpointDialog((self.fnHistory[0], None),
                                   None,
                                   self.condHistory,
                                   self,
                                   modal=1,
                                   addMode=1,
                                   filenameHistory=self.fnHistory)
        if dlg.exec_() == QDialog.Accepted:
            fn, line, cond, temp, enabled, count = dlg.getAddData()
            if fn is not None:
                if fn in self.fnHistory:
                    self.fnHistory.remove(fn)
                self.fnHistory.insert(0, fn)

            if cond:
                if cond in self.condHistory:
                    self.condHistory.remove(cond)
                self.condHistory.insert(0, cond)

            self.__saveRecent()

            self.__model.addBreakPoint(fn, line, (cond, temp, enabled, count))
            self.__resizeColumns()
            self.__resort()

    def __doubleClicked(self, index):
        """
        Private slot to handle the double clicked signal.
        
        @param index index of the entry that was double clicked (QModelIndex)
        """
        if index.isValid():
            self.__editBreakpoint(index)

    def __editBreak(self):
        """
        Private slot to handle the edit breakpoint context menu entry.
        """
        index = self.currentIndex()
        if index.isValid():
            self.__editBreakpoint(index)

    def __editBreakpoint(self, index):
        """
        Private slot to edit a breakpoint.
        
        @param index index of breakpoint to be edited (QModelIndex)
        """
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            bp = self.__model.getBreakPointByIndex(sindex)
            if not bp:
                return

            fn, line, cond, temp, enabled, count = bp[:6]

            from .EditBreakpointDialog import EditBreakpointDialog
            dlg = EditBreakpointDialog((fn, line),
                                       (cond, temp, enabled, count),
                                       self.condHistory,
                                       self,
                                       modal=True)
            if dlg.exec_() == QDialog.Accepted:
                cond, temp, enabled, count = dlg.getData()
                if cond:
                    if cond in self.condHistory:
                        self.condHistory.remove(cond)
                    self.condHistory.insert(0, cond)

                    self.__saveRecent()

                self.__model.setBreakPointByIndex(sindex, fn, line,
                                                  (cond, temp, enabled, count))
                self.__resizeColumns()
                self.__resort()

    def __setBpEnabled(self, index, enabled):
        """
        Private method to set the enabled status of a breakpoint.
        
        @param index index of breakpoint to be enabled/disabled (QModelIndex)
        @param enabled flag indicating the enabled status to be set (boolean)
        """
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            self.__model.setBreakPointEnabledByIndex(sindex, enabled)

    def __enableBreak(self):
        """
        Private slot to handle the enable breakpoint context menu entry.
        """
        index = self.currentIndex()
        self.__setBpEnabled(index, True)
        self.__resizeColumns()
        self.__resort()

    def __enableAllBreaks(self):
        """
        Private slot to handle the enable all breakpoints context menu entry.
        """
        index = self.model().index(0, 0)
        while index.isValid():
            self.__setBpEnabled(index, True)
            index = self.indexBelow(index)
        self.__resizeColumns()
        self.__resort()

    def __enableSelectedBreaks(self):
        """
        Private slot to handle the enable selected breakpoints context menu
        entry.
        """
        for index in self.selectedIndexes():
            if index.column() == 0:
                self.__setBpEnabled(index, True)
        self.__resizeColumns()
        self.__resort()

    def __disableBreak(self):
        """
        Private slot to handle the disable breakpoint context menu entry.
        """
        index = self.currentIndex()
        self.__setBpEnabled(index, False)
        self.__resizeColumns()
        self.__resort()

    def __disableAllBreaks(self):
        """
        Private slot to handle the disable all breakpoints context menu entry.
        """
        index = self.model().index(0, 0)
        while index.isValid():
            self.__setBpEnabled(index, False)
            index = self.indexBelow(index)
        self.__resizeColumns()
        self.__resort()

    def __disableSelectedBreaks(self):
        """
        Private slot to handle the disable selected breakpoints context menu
        entry.
        """
        for index in self.selectedIndexes():
            if index.column() == 0:
                self.__setBpEnabled(index, False)
        self.__resizeColumns()
        self.__resort()

    def __deleteBreak(self):
        """
        Private slot to handle the delete breakpoint context menu entry.
        """
        index = self.currentIndex()
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            self.__model.deleteBreakPointByIndex(sindex)

    def __deleteAllBreaks(self):
        """
        Private slot to handle the delete all breakpoints context menu entry.
        """
        self.__model.deleteAll()

    def __deleteSelectedBreaks(self):
        """
        Private slot to handle the delete selected breakpoints context menu
        entry.
        """
        idxList = []
        for index in self.selectedIndexes():
            sindex = self.__toSourceIndex(index)
            if sindex.isValid() and index.column() == 0:
                idxList.append(sindex)
        self.__model.deleteBreakPoints(idxList)

    def __showSource(self):
        """
        Private slot to handle the goto context menu entry.
        """
        index = self.currentIndex()
        sindex = self.__toSourceIndex(index)
        bp = self.__model.getBreakPointByIndex(sindex)
        if not bp:
            return

        fn, line = bp[:2]
        self.sourceFile.emit(fn, line)

    def highlightBreakpoint(self, fn, lineno):
        """
        Public slot to handle the clientLine signal.
        
        @param fn filename of the breakpoint (string)
        @param lineno line number of the breakpoint (integer)
        """
        sindex = self.__model.getBreakPointIndex(fn, lineno)
        if sindex.isValid():
            return

        index = self.__fromSourceIndex(sindex)
        if index.isValid():
            self.__clearSelection()
            self.__setRowSelected(index, True)

    def handleResetUI(self):
        """
        Public slot to reset the breakpoint viewer.
        """
        self.__clearSelection()

    def __showBackMenu(self):
        """
        Private slot to handle the aboutToShow signal of the background menu.
        """
        if self.model().rowCount() == 0:
            self.backMenuActions["EnableAll"].setEnabled(False)
            self.backMenuActions["DisableAll"].setEnabled(False)
            self.backMenuActions["DeleteAll"].setEnabled(False)
        else:
            self.backMenuActions["EnableAll"].setEnabled(True)
            self.backMenuActions["DisableAll"].setEnabled(True)
            self.backMenuActions["DeleteAll"].setEnabled(True)

    def __getSelectedItemsCount(self):
        """
        Private method to get the count of items selected.
        
        @return count of items selected (integer)
        """
        count = len(self.selectedIndexes()) // (self.__model.columnCount() - 1)
        # column count is 1 greater than selectable
        return count

    def __configure(self):
        """
        Private method to open the configuration dialog.
        """
        e5App().getObject("UserInterface").showPreferences(
            "debuggerGeneralPage")

    def __loadRecent(self):
        """
        Private method to load the recently used file names.
        """
        Preferences.Prefs.rsettings.sync()

        # load recently used file names
        self.fnHistory = []
        self.fnHistory.append('')
        rs = Preferences.Prefs.rsettings.value(recentNameBreakpointFiles)
        if rs is not None:
            recent = [
                f for f in Preferences.toList(rs) if QFileInfo(f).exists()
            ]
            self.fnHistory.extend(
                recent[:Preferences.getDebugger("RecentNumber")])

        # load recently entered condition expressions
        self.condHistory = []
        rs = Preferences.Prefs.rsettings.value(recentNameBreakpointConditions)
        if rs is not None:
            self.condHistory = Preferences.toList(
                rs)[:Preferences.getDebugger("RecentNumber")]

    def __saveRecent(self):
        """
        Private method to save the list of recently used file names.
        """
        recent = [f for f in self.fnHistory if f]
        Preferences.Prefs.rsettings.setValue(recentNameBreakpointFiles, recent)
        Preferences.Prefs.rsettings.setValue(recentNameBreakpointConditions,
                                             self.condHistory)
        Preferences.Prefs.rsettings.sync()
Exemplo n.º 22
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()
Exemplo n.º 23
0
class MasternodesWidget(QWidget):
    """Widget that displays masternodes."""
    def __init__(self, manager, parent=None):
        super(MasternodesWidget, self).__init__(parent)
        self.manager = manager
        self.model = MasternodesModel(self.manager)
        self.proxy_model = QSortFilterProxyModel()
        self.proxy_model.setSourceModel(self.model)
        self.view = QTableView()
        self.view.setModel(self.proxy_model)
        for header in [self.view.horizontalHeader(), self.view.verticalHeader()]:
            header.setHighlightSections(False)

        header = self.view.horizontalHeader()
        header.setSectionResizeMode(MasternodesModel.ALIAS, QHeaderView.ResizeToContents)
        header.setSectionResizeMode(MasternodesModel.VIN, QHeaderView.Stretch)
        header.setSectionResizeMode(MasternodesModel.COLLATERAL, QHeaderView.ResizeToContents)
        header.setSectionResizeMode(MasternodesModel.DELEGATE, QHeaderView.ResizeToContents)
        self.view.verticalHeader().setVisible(False)

        self.view.setSelectionMode(QAbstractItemView.SingleSelection)
        self.view.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.view.setSortingEnabled(True)
        self.view.sortByColumn(self.model.ALIAS, Qt.AscendingOrder)

        vbox = QVBoxLayout()
        vbox.setContentsMargins(0, 0, 0, 0)
        vbox.addWidget(self.view)
        self.setLayout(vbox)

    def select_masternode(self, alias):
        """Select the row that represents alias."""
        self.view.clearSelection()
        for i in range(self.proxy_model.rowCount()):
            idx = self.proxy_model.index(i, 0)
            mn_alias = str(self.proxy_model.data(idx))
            if mn_alias == alias:
                self.view.selectRow(i)
                break

    def populate_collateral_key(self, row):
        """Fill in the collateral key for a masternode based on its collateral output.

        row refers to the desired row in the proxy model, not the actual model.
        """
        mn = self.masternode_for_row(row)
        self.manager.populate_masternode_output(mn.alias)
        # Emit dataChanged for the collateral key.
        index = self.model.index(row, self.model.COLLATERAL)
        self.model.dataChanged.emit(index, index)

    def refresh_items(self):
        self.model.dataChanged.emit(QModelIndex(), QModelIndex())

    def add_masternode(self, masternode, save = True):
        self.model.add_masternode(masternode, save=save)

    def remove_masternode(self, alias, save = True):
        self.model.remove_masternode(alias, save=save)

    def masternode_for_row(self, row):
        idx = self.proxy_model.mapToSource(self.proxy_model.index(row, 0))
        return self.model.masternode_for_row(idx.row())

    def import_masternode_conf_lines(self, conf_lines, pw):
        return self.model.import_masternode_conf_lines(conf_lines, pw)
Exemplo n.º 24
0
class WatchPointViewer(QTreeView):
    """
    Class implementing the watch expression viewer widget.
    
    Watch expressions will be shown with all their details. They can be
    modified through the context menu of this widget.
    """
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent the parent (QWidget)
        """
        super(WatchPointViewer, self).__init__(parent)
        self.setObjectName("WatchExpressionViewer")
        
        self.__model = None
        
        self.setItemsExpandable(False)
        self.setRootIsDecorated(False)
        self.setAlternatingRowColors(True)
        self.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        
        self.setWindowTitle(self.tr("Watchpoints"))
        
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.__showContextMenu)
        self.doubleClicked.connect(self.__doubleClicked)
        
        self.__createPopupMenus()
        
    def setModel(self, model):
        """
        Public slot to set the watch expression model.
        
        @param model reference to the watch expression model (WatchPointModel)
        """
        self.__model = model
        
        self.sortingModel = QSortFilterProxyModel()
        self.sortingModel.setDynamicSortFilter(True)
        self.sortingModel.setSourceModel(self.__model)
        super(WatchPointViewer, self).setModel(self.sortingModel)
        
        header = self.header()
        header.setSortIndicator(0, Qt.AscendingOrder)
        header.setSortIndicatorShown(True)
        if qVersion() >= "5.0.0":
            header.setSectionsClickable(True)
        else:
            header.setClickable(True)
        
        self.setSortingEnabled(True)
        
        self.__layoutDisplay()
        
    def __layoutDisplay(self):
        """
        Private slot to perform a layout operation.
        """
        self.__resizeColumns()
        self.__resort()
        
    def __resizeColumns(self):
        """
        Private slot to resize the view when items get added, edited or
        deleted.
        """
        self.header().resizeSections(QHeaderView.ResizeToContents)
        self.header().setStretchLastSection(True)
    
    def __resort(self):
        """
        Private slot to resort the tree.
        """
        self.model().sort(self.header().sortIndicatorSection(),
                          self.header().sortIndicatorOrder())
        
    def __toSourceIndex(self, index):
        """
        Private slot to convert an index to a source index.
        
        @param index index to be converted (QModelIndex)
        @return mapped index (QModelIndex)
        """
        return self.sortingModel.mapToSource(index)
        
    def __fromSourceIndex(self, sindex):
        """
        Private slot to convert a source index to an index.
        
        @param sindex source index to be converted (QModelIndex)
        @return mapped index (QModelIndex)
        """
        return self.sortingModel.mapFromSource(sindex)
        
    def __setRowSelected(self, index, selected=True):
        """
        Private slot to select a complete row.
        
        @param index index determining the row to be selected (QModelIndex)
        @param selected flag indicating the action (bool)
        """
        if not index.isValid():
            return
        
        if selected:
            flags = QItemSelectionModel.SelectionFlags(
                QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
        else:
            flags = QItemSelectionModel.SelectionFlags(
                QItemSelectionModel.Deselect | QItemSelectionModel.Rows)
        self.selectionModel().select(index, flags)
        
    def __createPopupMenus(self):
        """
        Private method to generate the popup menus.
        """
        self.menu = QMenu()
        self.menu.addAction(self.tr("Add"), self.__addWatchPoint)
        self.menu.addAction(self.tr("Edit..."), self.__editWatchPoint)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Enable"), self.__enableWatchPoint)
        self.menu.addAction(self.tr("Enable all"),
                            self.__enableAllWatchPoints)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Disable"), self.__disableWatchPoint)
        self.menu.addAction(self.tr("Disable all"),
                            self.__disableAllWatchPoints)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Delete"), self.__deleteWatchPoint)
        self.menu.addAction(self.tr("Delete all"),
                            self.__deleteAllWatchPoints)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Configure..."), self.__configure)

        self.backMenuActions = {}
        self.backMenu = QMenu()
        self.backMenu.addAction(self.tr("Add"), self.__addWatchPoint)
        self.backMenuActions["EnableAll"] = \
            self.backMenu.addAction(self.tr("Enable all"),
                                    self.__enableAllWatchPoints)
        self.backMenuActions["DisableAll"] = \
            self.backMenu.addAction(self.tr("Disable all"),
                                    self.__disableAllWatchPoints)
        self.backMenuActions["DeleteAll"] = \
            self.backMenu.addAction(self.tr("Delete all"),
                                    self.__deleteAllWatchPoints)
        self.backMenu.addSeparator()
        self.backMenu.addAction(self.tr("Configure..."), self.__configure)
        self.backMenu.aboutToShow.connect(self.__showBackMenu)

        self.multiMenu = QMenu()
        self.multiMenu.addAction(self.tr("Add"), self.__addWatchPoint)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Enable selected"),
                                 self.__enableSelectedWatchPoints)
        self.multiMenu.addAction(self.tr("Enable all"),
                                 self.__enableAllWatchPoints)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Disable selected"),
                                 self.__disableSelectedWatchPoints)
        self.multiMenu.addAction(self.tr("Disable all"),
                                 self.__disableAllWatchPoints)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Delete selected"),
                                 self.__deleteSelectedWatchPoints)
        self.multiMenu.addAction(self.tr("Delete all"),
                                 self.__deleteAllWatchPoints)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Configure..."), self.__configure)
    
    def __showContextMenu(self, coord):
        """
        Private slot to show the context menu.
        
        @param coord the position of the mouse pointer (QPoint)
        """
        cnt = self.__getSelectedItemsCount()
        if cnt <= 1:
            index = self.indexAt(coord)
            if index.isValid():
                cnt = 1
                self.__setRowSelected(index)
        coord = self.mapToGlobal(coord)
        if cnt > 1:
            self.multiMenu.popup(coord)
        elif cnt == 1:
            self.menu.popup(coord)
        else:
            self.backMenu.popup(coord)
    
    def __clearSelection(self):
        """
        Private slot to clear the selection.
        """
        for index in self.selectedIndexes():
            self.__setRowSelected(index, False)
    
    def __findDuplicates(self, cond, special, showMessage=False,
                         index=QModelIndex()):
        """
        Private method to check, if an entry already exists.
        
        @param cond condition to check (string)
        @param special special condition to check (string)
        @param showMessage flag indicating a message should be shown,
            if a duplicate entry is found (boolean)
        @param index index that should not be considered duplicate
            (QModelIndex)
        @return flag indicating a duplicate entry (boolean)
        """
        idx = self.__model.getWatchPointIndex(cond, special)
        duplicate = idx.isValid() and \
            idx.internalPointer() != index.internalPointer()
        if showMessage and duplicate:
            if not special:
                msg = self.tr("""<p>A watch expression '<b>{0}</b>'"""
                              """ already exists.</p>""")\
                    .format(Utilities.html_encode(cond))
            else:
                msg = self.tr(
                    """<p>A watch expression '<b>{0}</b>'"""
                    """ for the variable <b>{1}</b> already exists.</p>""")\
                    .format(special, Utilities.html_encode(cond))
            E5MessageBox.warning(
                self,
                self.tr("Watch expression already exists"),
                msg)
        
        return duplicate
    
    def __addWatchPoint(self):
        """
        Private slot to handle the add watch expression context menu entry.
        """
        from .EditWatchpointDialog import EditWatchpointDialog
        dlg = EditWatchpointDialog(("", False, True, 0, ""), self)
        if dlg.exec_() == QDialog.Accepted:
            cond, temp, enabled, ignorecount, special = dlg.getData()
            if not self.__findDuplicates(cond, special, True):
                self.__model.addWatchPoint(cond, special,
                                           (temp, enabled, ignorecount))
                self.__resizeColumns()
                self.__resort()

    def __doubleClicked(self, index):
        """
        Private slot to handle the double clicked signal.
        
        @param index index of the entry that was double clicked (QModelIndex)
        """
        if index.isValid():
            self.__doEditWatchPoint(index)

    def __editWatchPoint(self):
        """
        Private slot to handle the edit watch expression context menu entry.
        """
        index = self.currentIndex()
        if index.isValid():
            self.__doEditWatchPoint(index)
    
    def __doEditWatchPoint(self, index):
        """
        Private slot to edit a watch expression.
        
        @param index index of watch expression to be edited (QModelIndex)
        """
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            wp = self.__model.getWatchPointByIndex(sindex)
            if not wp:
                return
            
            cond, special, temp, enabled, count = wp[:5]
            
            from .EditWatchpointDialog import EditWatchpointDialog
            dlg = EditWatchpointDialog(
                (cond, temp, enabled, count, special), self)
            if dlg.exec_() == QDialog.Accepted:
                cond, temp, enabled, count, special = dlg.getData()
                if not self.__findDuplicates(cond, special, True, sindex):
                    self.__model.setWatchPointByIndex(
                        sindex, cond, special, (temp, enabled, count))
                    self.__resizeColumns()
                    self.__resort()

    def __setWpEnabled(self, index, enabled):
        """
        Private method to set the enabled status of a watch expression.
        
        @param index index of watch expression to be enabled/disabled
            (QModelIndex)
        @param enabled flag indicating the enabled status to be set (boolean)
        """
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            self.__model.setWatchPointEnabledByIndex(sindex, enabled)
        
    def __enableWatchPoint(self):
        """
        Private slot to handle the enable watch expression context menu entry.
        """
        index = self.currentIndex()
        self.__setWpEnabled(index, True)
        self.__resizeColumns()
        self.__resort()

    def __enableAllWatchPoints(self):
        """
        Private slot to handle the enable all watch expressions context menu
        entry.
        """
        index = self.model().index(0, 0)
        while index.isValid():
            self.__setWpEnabled(index, True)
            index = self.indexBelow(index)
        self.__resizeColumns()
        self.__resort()

    def __enableSelectedWatchPoints(self):
        """
        Private slot to handle the enable selected watch expressions context
        menu entry.
        """
        for index in self.selectedIndexes():
            if index.column() == 0:
                self.__setWpEnabled(index, True)
        self.__resizeColumns()
        self.__resort()

    def __disableWatchPoint(self):
        """
        Private slot to handle the disable watch expression context menu entry.
        """
        index = self.currentIndex()
        self.__setWpEnabled(index, False)
        self.__resizeColumns()
        self.__resort()

    def __disableAllWatchPoints(self):
        """
        Private slot to handle the disable all watch expressions context menu
        entry.
        """
        index = self.model().index(0, 0)
        while index.isValid():
            self.__setWpEnabled(index, False)
            index = self.indexBelow(index)
        self.__resizeColumns()
        self.__resort()

    def __disableSelectedWatchPoints(self):
        """
        Private slot to handle the disable selected watch expressions context
        menu entry.
        """
        for index in self.selectedIndexes():
            if index.column() == 0:
                self.__setWpEnabled(index, False)
        self.__resizeColumns()
        self.__resort()

    def __deleteWatchPoint(self):
        """
        Private slot to handle the delete watch expression context menu entry.
        """
        index = self.currentIndex()
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            self.__model.deleteWatchPointByIndex(sindex)
        
    def __deleteAllWatchPoints(self):
        """
        Private slot to handle the delete all watch expressions context menu
        entry.
        """
        self.__model.deleteAll()

    def __deleteSelectedWatchPoints(self):
        """
        Private slot to handle the delete selected watch expressions context
        menu entry.
        """
        idxList = []
        for index in self.selectedIndexes():
            sindex = self.__toSourceIndex(index)
            if sindex.isValid() and index.column() == 0:
                idxList.append(sindex)
        self.__model.deleteWatchPoints(idxList)

    def __showBackMenu(self):
        """
        Private slot to handle the aboutToShow signal of the background menu.
        """
        if self.model().rowCount() == 0:
            self.backMenuActions["EnableAll"].setEnabled(False)
            self.backMenuActions["DisableAll"].setEnabled(False)
            self.backMenuActions["DeleteAll"].setEnabled(False)
        else:
            self.backMenuActions["EnableAll"].setEnabled(True)
            self.backMenuActions["DisableAll"].setEnabled(True)
            self.backMenuActions["DeleteAll"].setEnabled(True)

    def __getSelectedItemsCount(self):
        """
        Private method to get the count of items selected.
        
        @return count of items selected (integer)
        """
        count = len(self.selectedIndexes()) // (self.__model.columnCount() - 1)
        # column count is 1 greater than selectable
        return count
    
    def __configure(self):
        """
        Private method to open the configuration dialog.
        """
        e5App().getObject("UserInterface")\
            .showPreferences("debuggerGeneralPage")
Exemplo n.º 25
0
class TileStampsDock(QDockWidget):
    setStamp = pyqtSignal(TileStamp)

    def __init__(self, stampManager, parent=None):
        super().__init__(parent)

        self.mTileStampManager = stampManager
        self.mTileStampModel = stampManager.tileStampModel()
        self.mProxyModel = QSortFilterProxyModel(self.mTileStampModel)
        self.mFilterEdit = QLineEdit(self)
        self.mNewStamp = QAction(self)
        self.mAddVariation = QAction(self)
        self.mDuplicate = QAction(self)
        self.mDelete = QAction(self)
        self.mChooseFolder = QAction(self)

        self.setObjectName("TileStampsDock")
        self.mProxyModel.setSortLocaleAware(True)
        self.mProxyModel.setSortCaseSensitivity(Qt.CaseInsensitive)
        self.mProxyModel.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.mProxyModel.setSourceModel(self.mTileStampModel)
        self.mProxyModel.sort(0)
        self.mTileStampView = TileStampView(self)
        self.mTileStampView.setModel(self.mProxyModel)
        self.mTileStampView.setVerticalScrollMode(
            QAbstractItemView.ScrollPerPixel)
        self.mTileStampView.header().setStretchLastSection(False)
        self.mTileStampView.header().setSectionResizeMode(
            0, QHeaderView.Stretch)
        self.mTileStampView.header().setSectionResizeMode(
            1, QHeaderView.ResizeToContents)
        self.mTileStampView.setContextMenuPolicy(Qt.CustomContextMenu)
        self.mTileStampView.customContextMenuRequested.connect(
            self.showContextMenu)
        self.mNewStamp.setIcon(QIcon(":images/16x16/document-new.png"))
        self.mAddVariation.setIcon(QIcon(":/images/16x16/add.png"))
        self.mDuplicate.setIcon(QIcon(":/images/16x16/stock-duplicate-16.png"))
        self.mDelete.setIcon(QIcon(":images/16x16/edit-delete.png"))
        self.mChooseFolder.setIcon(QIcon(":images/16x16/document-open.png"))
        Utils.setThemeIcon(self.mNewStamp, "document-new")
        Utils.setThemeIcon(self.mAddVariation, "add")
        Utils.setThemeIcon(self.mDelete, "edit-delete")
        Utils.setThemeIcon(self.mChooseFolder, "document-open")

        self.mFilterEdit.setClearButtonEnabled(True)
        self.mFilterEdit.textChanged.connect(
            self.mProxyModel.setFilterFixedString)
        self.mTileStampModel.stampRenamed.connect(self.ensureStampVisible)
        self.mNewStamp.triggered.connect(self.newStamp)
        self.mAddVariation.triggered.connect(self.addVariation)
        self.mDuplicate.triggered.connect(self.duplicate)
        self.mDelete.triggered.connect(self.delete_)
        self.mChooseFolder.triggered.connect(self.chooseFolder)
        self.mDuplicate.setEnabled(False)
        self.mDelete.setEnabled(False)
        self.mAddVariation.setEnabled(False)
        widget = QWidget(self)
        layout = QVBoxLayout(widget)
        layout.setContentsMargins(5, 5, 5, 5)

        buttonContainer = QToolBar()
        buttonContainer.setFloatable(False)
        buttonContainer.setMovable(False)
        buttonContainer.setIconSize(QSize(16, 16))
        buttonContainer.addAction(self.mNewStamp)
        buttonContainer.addAction(self.mAddVariation)
        buttonContainer.addAction(self.mDuplicate)
        buttonContainer.addAction(self.mDelete)
        stretch = QWidget()
        stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
        buttonContainer.addWidget(stretch)
        buttonContainer.addAction(self.mChooseFolder)
        listAndToolBar = QVBoxLayout()
        listAndToolBar.setSpacing(0)
        listAndToolBar.addWidget(self.mFilterEdit)
        listAndToolBar.addWidget(self.mTileStampView)
        listAndToolBar.addWidget(buttonContainer)
        layout.addLayout(listAndToolBar)
        selectionModel = self.mTileStampView.selectionModel()
        selectionModel.currentRowChanged.connect(self.currentRowChanged)
        self.setWidget(widget)
        self.retranslateUi()

    def changeEvent(self, e):
        super().changeEvent(e)
        x = e.type()
        if x == QEvent.LanguageChange:
            self.retranslateUi()
        else:
            pass

    def keyPressEvent(self, event):
        x = event.key()
        if x == Qt.Key_Delete or x == Qt.Key_Backspace:
            self.delete_()
            return

        super().keyPressEvent(event)

    def currentRowChanged(self, index):
        sourceIndex = self.mProxyModel.mapToSource(index)
        isStamp = self.mTileStampModel.isStamp(sourceIndex)
        self.mDuplicate.setEnabled(isStamp)
        self.mDelete.setEnabled(sourceIndex.isValid())
        self.mAddVariation.setEnabled(isStamp)
        if (isStamp):
            self.setStamp.emit(self.mTileStampModel.stampAt(sourceIndex))
        else:
            variation = self.mTileStampModel.variationAt(sourceIndex)
            if variation:
                # single variation clicked, use it specifically
                self.setStamp.emit(TileStamp(Map(variation.map)))

    def showContextMenu(self, pos):
        index = self.mTileStampView.indexAt(pos)
        if (not index.isValid()):
            return
        menu = QMenu()
        sourceIndex = self.mProxyModel.mapToSource(index)
        if (self.mTileStampModel.isStamp(sourceIndex)):
            addStampVariation = QAction(self.mAddVariation.icon(),
                                        self.mAddVariation.text(), menu)
            deleteStamp = QAction(self.mDelete.icon(), self.tr("Delete Stamp"),
                                  menu)
            deleteStamp.triggered.connect(self.delete_)
            addStampVariation.triggered.connect(self.addVariation)
            menu.addAction(addStampVariation)
            menu.addSeparator()
            menu.addAction(deleteStamp)
        else:
            removeVariation = QAction(QIcon(":/images/16x16/remove.png"),
                                      self.tr("Remove Variation"), menu)
            Utils.setThemeIcon(removeVariation, "remove")
            removeVariation.triggered.connect(self.delete_)
            menu.addAction(removeVariation)

        menu.exec(self.mTileStampView.viewport().mapToGlobal(pos))

    def newStamp(self):
        stamp = self.mTileStampManager.createStamp()
        if (self.isVisible() and not stamp.isEmpty()):
            stampIndex = self.mTileStampModel.index(stamp)
            if (stampIndex.isValid()):
                viewIndex = self.mProxyModel.mapFromSource(stampIndex)
                self.mTileStampView.setCurrentIndex(viewIndex)
                self.mTileStampView.edit(viewIndex)

    def delete_(self):
        index = self.mTileStampView.currentIndex()
        if (not index.isValid()):
            return
        sourceIndex = self.mProxyModel.mapToSource(index)
        self.mTileStampModel.removeRow(sourceIndex.row(), sourceIndex.parent())

    def duplicate(self):
        index = self.mTileStampView.currentIndex()
        if (not index.isValid()):
            return
        sourceIndex = self.mProxyModel.mapToSource(index)
        if (not self.mTileStampModel.isStamp(sourceIndex)):
            return
        stamp = self.mTileStampModel.stampAt = TileStamp(sourceIndex)
        self.mTileStampModel.addStamp(stamp.clone())

    def addVariation(self):
        index = self.mTileStampView.currentIndex()
        if (not index.isValid()):
            return
        sourceIndex = self.mProxyModel.mapToSource(index)
        if (not self.mTileStampModel.isStamp(sourceIndex)):
            return
        stamp = self.mTileStampModel.stampAt(sourceIndex)
        self.mTileStampManager.addVariation(stamp)

    def chooseFolder(self):
        prefs = Preferences.instance()
        stampsDirectory = prefs.stampsDirectory()
        stampsDirectory = QFileDialog.getExistingDirectory(
            self.window(), self.tr("Choose the Stamps Folder"),
            stampsDirectory)
        if (not stampsDirectory.isEmpty()):
            prefs.setStampsDirectory(stampsDirectory)

    def ensureStampVisible(self, stamp):
        stampIndex = self.mTileStampModel.index(stamp)
        if (stampIndex.isValid()):
            self.mTileStampView.scrollTo(
                self.mProxyModel.mapFromSource(stampIndex))

    def retranslateUi(self):
        self.setWindowTitle(self.tr("Tile Stamps"))
        self.mNewStamp.setText(self.tr("Add New Stamp"))
        self.mAddVariation.setText(self.tr("Add Variation"))
        self.mDuplicate.setText(self.tr("Duplicate Stamp"))
        self.mDelete.setText(self.tr("Delete Selected"))
        self.mChooseFolder.setText(self.tr("Set Stamps Folder"))
        self.mFilterEdit.setPlaceholderText(self.tr("Filter"))
Exemplo n.º 26
0
class GuiPropertyDock():
    '''
    GuiPropertyDock
    '''
    dock_name = "properties-dock"
    dock_displayed_name = _("Properties")

    def __init__(self):
        '''
        Constructor
        '''
        super(GuiPropertyDock, self).__init__()
        self.widget = None
        self.core_part = None  # CorePropertyDock
        self._sheet_id = None
        self.tree_sheet = None

    @property
    def sheet_id(self):
        return self._sheet_id

    @sheet_id.setter
    def sheet_id(self, sheet_id):
        if self._sheet_id == sheet_id:
            pass
        self._sheet_id = sheet_id
        if self.sheet_id is not None:
            self.tree_sheet = gui_cfg.core.tree_sheet_manager.get_tree_sheet_from_sheet_id(
                self.sheet_id)
            self.core_part = self.tree_sheet.get_instance_of(self.dock_name)
            self.core_part.sheet_id = sheet_id

    def get_widget(self):

        if self.widget is None:
            self.widget = QWidget()
            self.ui = properties_dock_ui.Ui_PropertiesDock()
            self.ui.setupUi(self.widget)

            if self.tree_sheet is not None and self.core_part is not None:
                table_model = self.core_part.property_table_model

                # filter :
                self.filter = QSortFilterProxyModel(self.widget)
                self.filter.setFilterKeyColumn(-1)
                self.filter.setFilterCaseSensitivity(False)
                self.filter.setSourceModel(table_model)

                # model :
                self.ui.tableView.setModel(self.filter)

                # connect :
                self.ui.addPropButton.clicked.connect(self.add_property_row)
                self.ui.removePropButton.clicked.connect(
                    self.remove_property_row)
                self.ui.filterLineEdit.textChanged.connect(
                    self.filter.setFilterFixedString)
                self.ui.tableView.clicked.connect(self.set_current_row)

            self.widget.gui_part = self
        return self.widget

    @pyqtSlot()
    def add_property_row(self):
        index = self.filter.mapToSource(self.ui.tableView.currentIndex())
        self.core_part.add_property_row(index)

    @pyqtSlot()
    def remove_property_row(self):
        index = self.filter.mapToSource(self.ui.tableView.currentIndex())
        self.core_part.remove_property_row(index)

    @pyqtSlot('QModelIndex')
    def set_current_row(self, model_index):
        self.ui.tableView.setCurrentIndex(model_index)
Exemplo n.º 27
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)
Exemplo n.º 28
0
class SearchFileWidget(QWidget):

    language_filter_change = pyqtSignal(list)

    def __init__(self):
        QWidget.__init__(self)

        self._refreshing = False

        self.fileModel = None
        self.proxyFileModel = None
        self.videoModel = None

        self._state = None

        self.timeLastSearch = QTime.currentTime()

        self.ui = Ui_SearchFileWidget()
        self.setup_ui()

    def set_state(self, state):
        self._state = state
        self._state.login_status_changed.connect(self.on_login_state_changed)
        self._state.interface_language_changed.connect(
            self.on_interface_language_changed)

    def get_state(self):
        return self._state

    def setup_ui(self):
        self.ui.setupUi(self)
        settings = QSettings()

        self.ui.splitter.setSizes([600, 1000])
        self.ui.splitter.setChildrenCollapsible(False)

        # Set up folder view

        lastDir = settings.value("mainwindow/workingDirectory",
                                 QDir.homePath())
        log.debug('Current directory: {currentDir}'.format(currentDir=lastDir))

        self.fileModel = QFileSystemModel(self)
        self.fileModel.setFilter(QDir.AllDirs | QDir.Dirs | QDir.Drives
                                 | QDir.NoDotAndDotDot | QDir.Readable
                                 | QDir.Executable | QDir.Writable)
        self.fileModel.iconProvider().setOptions(
            QFileIconProvider.DontUseCustomDirectoryIcons)
        self.fileModel.setRootPath(QDir.rootPath())
        self.fileModel.directoryLoaded.connect(self.onFileModelDirectoryLoaded)

        self.proxyFileModel = QSortFilterProxyModel(self)
        self.proxyFileModel.setSortRole(Qt.DisplayRole)
        self.proxyFileModel.setSourceModel(self.fileModel)
        self.proxyFileModel.sort(0, Qt.AscendingOrder)
        self.proxyFileModel.setSortCaseSensitivity(Qt.CaseInsensitive)
        self.ui.folderView.setModel(self.proxyFileModel)

        self.ui.folderView.setHeaderHidden(True)
        self.ui.folderView.hideColumn(3)
        self.ui.folderView.hideColumn(2)
        self.ui.folderView.hideColumn(1)

        index = self.fileModel.index(lastDir)
        proxyIndex = self.proxyFileModel.mapFromSource(index)
        self.ui.folderView.scrollTo(proxyIndex)

        self.ui.folderView.expanded.connect(self.onFolderViewExpanded)
        self.ui.folderView.clicked.connect(self.onFolderTreeClicked)
        self.ui.buttonFind.clicked.connect(self.onButtonFind)
        self.ui.buttonRefresh.clicked.connect(self.onButtonRefresh)

        # Set up introduction
        self.showInstructions()

        # Set up video view
        self.ui.filterLanguageForVideo.set_unknown_text(_('All languages'))
        self.ui.filterLanguageForVideo.selected_language_changed.connect(
            self.on_language_combobox_filter_change)
        # self.ui.filterLanguageForVideo.selected_language_changed.connect(self.onFilterLanguageVideo)

        self.videoModel = VideoModel(self)
        self.ui.videoView.setHeaderHidden(True)
        self.ui.videoView.setModel(self.videoModel)
        self.ui.videoView.activated.connect(self.onClickVideoTreeView)
        self.ui.videoView.clicked.connect(self.onClickVideoTreeView)
        self.ui.videoView.customContextMenuRequested.connect(self.onContext)
        self.videoModel.dataChanged.connect(self.subtitlesCheckedChanged)
        self.language_filter_change.connect(
            self.videoModel.on_filter_languages_change)

        self.ui.buttonSearchSelectVideos.clicked.connect(
            self.onButtonSearchSelectVideos)
        self.ui.buttonSearchSelectFolder.clicked.connect(
            self.onButtonSearchSelectFolder)
        self.ui.buttonDownload.clicked.connect(self.onButtonDownload)
        self.ui.buttonPlay.clicked.connect(self.onButtonPlay)
        self.ui.buttonIMDB.clicked.connect(self.onViewOnlineInfo)
        self.ui.videoView.setContextMenuPolicy(Qt.CustomContextMenu)

        # Drag and Drop files to the videoView enabled
        self.ui.videoView.__class__.dragEnterEvent = self.dragEnterEvent
        self.ui.videoView.__class__.dragMoveEvent = self.dragEnterEvent
        self.ui.videoView.__class__.dropEvent = self.dropEvent
        self.ui.videoView.setAcceptDrops(1)

        # FIXME: ok to drop this connect?
        # self.ui.videoView.clicked.connect(self.onClickMovieTreeView)

        self.retranslate()

    def retranslate(self):
        introduction = '<p align="center"><h2>{title}</h2></p>' \
            '<p><b>{tab1header}</b><br/>{tab1content}</p>' \
            '<p><b>{tab2header}</b><br/>{tab2content}</p>'\
            '<p><b>{tab3header}</b><br/>{tab3content}</p>'.format(
                title=_('How To Use {title}').format(title=PROJECT_TITLE),
                tab1header=_('1st Tab:'),
                tab2header=_('2nd Tab:'),
                tab3header=_('3rd Tab:'),
                tab1content=_('Select, from the Folder Tree on the left, the folder which contains the videos '
                              'that need subtitles. {project} will then try to automatically find available '
                              'subtitles.').format(project=PROJECT_TITLE),
                tab2content=_('If you don\'t have the videos in your machine, you can search subtitles by '
                              'introducing the title/name of the video.').format(project=PROJECT_TITLE),
                tab3content=_('If you have found some subtitle somewhere else that is not in {project}\'s database, '
                              'please upload those subtitles so next users will be able to '
                              'find them more easily.').format(project=PROJECT_TITLE))
        self.ui.introductionHelp.setHtml(introduction)

    @pyqtSlot(Language)
    def on_interface_language_changed(self, language):
        self.ui.retranslateUi(self)
        self.retranslate()

    @pyqtSlot(str)
    def onFileModelDirectoryLoaded(self, path):
        settings = QSettings()
        lastDir = settings.value('mainwindow/workingDirectory',
                                 QDir.homePath())
        qDirLastDir = QDir(lastDir)
        qDirLastDir.cdUp()
        if qDirLastDir.path() == path:
            index = self.fileModel.index(lastDir)
            proxyIndex = self.proxyFileModel.mapFromSource(index)
            self.ui.folderView.scrollTo(proxyIndex)
            self.ui.folderView.setCurrentIndex(proxyIndex)

    @pyqtSlot(int, str)
    def on_login_state_changed(self, state, message):
        log.debug(
            'on_login_state_changed(state={state}, message={message}'.format(
                state=state, message=message))
        if state in (State.LOGIN_STATUS_LOGGED_OUT, State.LOGIN_STATUS_BUSY):
            self.ui.buttonSearchSelectFolder.setEnabled(False)
            self.ui.buttonSearchSelectVideos.setEnabled(False)
            self.ui.buttonFind.setEnabled(False)
        elif state == State.LOGIN_STATUS_LOGGED_IN:
            self.ui.buttonSearchSelectFolder.setEnabled(True)
            self.ui.buttonSearchSelectVideos.setEnabled(True)
            self.ui.buttonFind.setEnabled(
                self.get_current_selected_folder() is not None)
        else:
            log.warning('unknown state')

    @pyqtSlot(Language)
    def on_language_combobox_filter_change(self, language):
        if language.is_generic():
            self.language_filter_change.emit(
                self.get_state().get_permanent_language_filter())
        else:
            self.language_filter_change.emit([language])

    def on_permanent_language_filter_change(self, languages):
        selected_language = self.ui.filterLanguageForVideo.get_selected_language(
        )
        if selected_language.is_generic():
            self.language_filter_change.emit(languages)

    @pyqtSlot()
    def subtitlesCheckedChanged(self):
        subs = self.videoModel.get_checked_subtitles()
        if subs:
            self.ui.buttonDownload.setEnabled(True)
        else:
            self.ui.buttonDownload.setEnabled(False)

    def showInstructions(self):
        self.ui.stackedSearchResult.setCurrentWidget(self.ui.pageIntroduction)

    def hideInstructions(self):
        self.ui.stackedSearchResult.setCurrentWidget(self.ui.pageSearchResult)

    @pyqtSlot(QModelIndex)
    def onFolderTreeClicked(self, proxyIndex):
        """What to do when a Folder in the tree is clicked"""
        if not proxyIndex.isValid():
            return

        index = self.proxyFileModel.mapToSource(proxyIndex)
        settings = QSettings()
        folder_path = self.fileModel.filePath(index)
        settings.setValue('mainwindow/workingDirectory', folder_path)
        # self.ui.buttonFind.setEnabled(self.get_state().)

    def get_current_selected_folder(self):
        proxyIndex = self.ui.folderView.currentIndex()
        index = self.proxyFileModel.mapToSource(proxyIndex)
        folder_path = self.fileModel.filePath(index)
        if not folder_path:
            return None
        return folder_path

    def get_current_selected_item_videomodel(self):
        current_index = self.ui.videoView.currentIndex()
        return self.videoModel.getSelectedItem(current_index)

    @pyqtSlot()
    def onButtonFind(self):
        now = QTime.currentTime()
        if now < self.timeLastSearch.addMSecs(500):
            return
        folder_path = self.get_current_selected_folder()

        settings = QSettings()
        settings.setValue('mainwindow/workingDirectory', folder_path)
        self.search_videos([folder_path])

        self.timeLastSearch = QTime.currentTime()

    @pyqtSlot()
    def onButtonRefresh(self):
        currentPath = self.get_current_selected_folder()
        if not currentPath:
            settings = QSettings()
            currentPath = settings.value('mainwindow/workingDirectory',
                                         QDir.homePath())

        self._refreshing = True

        self.ui.folderView.collapseAll()

        currentPath = self.get_current_selected_folder()
        if not currentPath:
            settings = QSettings()
            currentPath = settings.value('mainwindow/workingDirectory',
                                         QDir.homePath())

        index = self.fileModel.index(currentPath)

        self.ui.folderView.scrollTo(self.proxyFileModel.mapFromSource(index))

    @pyqtSlot(QModelIndex)
    def onFolderViewExpanded(self, proxyIndex):
        if self._refreshing:
            expandedPath = self.fileModel.filePath(
                self.proxyFileModel.mapToSource(proxyIndex))
            if expandedPath == QDir.rootPath():
                currentPath = self.get_current_selected_folder()
                if not currentPath:
                    settings = QSettings()
                    currentPath = settings.value('mainwindow/workingDirectory',
                                                 QDir.homePath())

                index = self.fileModel.index(currentPath)

                self.ui.folderView.scrollTo(
                    self.proxyFileModel.mapFromSource(index))
                self._refreshing = False

    @pyqtSlot()
    def onButtonSearchSelectFolder(self):
        settings = QSettings()
        path = settings.value('mainwindow/workingDirectory', QDir.homePath())
        folder_path = QFileDialog.getExistingDirectory(
            self, _('Select the directory that contains your videos'), path)
        if folder_path:
            settings.setValue('mainwindow/workingDirectory', folder_path)
            self.search_videos([folder_path])

    @pyqtSlot()
    def onButtonSearchSelectVideos(self):
        settings = QSettings()
        currentDir = settings.value('mainwindow/workingDirectory',
                                    QDir.homePath())
        fileNames, t = QFileDialog.getOpenFileNames(
            self, _('Select the video(s) that need subtitles'), currentDir,
            SELECT_VIDEOS)
        if fileNames:
            settings.setValue('mainwindow/workingDirectory',
                              QFileInfo(fileNames[0]).absolutePath())
            self.search_videos(fileNames)

    def search_videos(self, paths):
        if not self.get_state().connected():
            QMessageBox.about(
                self, _("Error"),
                _('You are not connected to the server. Please reconnect first.'
                  ))
            return
        self.ui.buttonFind.setEnabled(False)
        self._search_videos_raw(paths)
        self.ui.buttonFind.setEnabled(True)

    def _search_videos_raw(self, paths):
        # FIXME: must pass mainwindow as argument to ProgressCallbackWidget
        callback = ProgressCallbackWidget(self)
        callback.set_title_text(_("Scanning..."))
        callback.set_label_text(_("Scanning files"))
        callback.set_finished_text(_("Scanning finished"))
        callback.set_block(True)

        try:
            local_videos, local_subs = scan_videopaths(paths,
                                                       callback=callback,
                                                       recursive=True)
        except OSError:
            callback.cancel()
            QMessageBox.warning(self, _('Error'),
                                _('Some directories are not accessible.'))

        if callback.canceled():
            return

        callback.finish()

        log.debug("Videos found: %s" % local_videos)
        log.debug("Subtitles found: %s" % local_subs)
        self.hideInstructions()

        QCoreApplication.processEvents()

        if not local_videos:
            QMessageBox.about(self, _("Scan Results"),
                              _("No video has been found!"))
            return

        total = len(local_videos)

        # FIXME: must pass mainwindow as argument to ProgressCallbackWidget
        # callback = ProgressCallbackWidget(self)
        # callback.set_title_text(_("Asking Server..."))
        # callback.set_label_text(_("Searching subtitles..."))
        # callback.set_updated_text(_("Searching subtitles ( %d / %d )"))
        # callback.set_finished_text(_("Search finished"))
        callback.set_block(True)
        callback.set_range(0, total)

        callback.show()

        callback.set_range(0, 2)

        download_callback = callback.get_child_progress(0, 1)
        # videoSearchResults = self.get_state().get_OSDBServer().SearchSubtitles("", videos_piece)
        remote_subs = self.get_state().get_OSDBServer().search_videos(
            videos=local_videos, callback=download_callback)

        self.videoModel.set_videos(local_videos)
        # self.onFilterLanguageVideo(self.ui.filterLanguageForVideo.get_selected_language())

        if remote_subs is None:
            QMessageBox.about(
                self, _("Error"),
                _("Error contacting the server. Please try again later"))
        callback.finish()

        # TODO: CHECK if our local subtitles are already in the server, otherwise suggest to upload
        # self.OSDBServer.CheckSubHash(sub_hashes)

    @pyqtSlot()
    def onButtonPlay(self):
        settings = QSettings()
        programPath = settings.value('options/VideoPlayerPath', '')
        parameters = settings.value('options/VideoPlayerParameters', '')
        if programPath == '':
            QMessageBox.about(
                self, _('Error'),
                _('No default video player has been defined in Settings.'))
            return

        selected_subtitle = self.get_current_selected_item_videomodel()
        if isinstance(selected_subtitle, SubtitleFileNetwork):
            selected_subtitle = selected_subtitle.get_subtitles()[0]

        if isinstance(selected_subtitle, LocalSubtitleFile):
            subtitle_file_path = selected_subtitle.get_filepath()
        elif isinstance(selected_subtitle, RemoteSubtitleFile):
            subtitle_file_path = QDir.temp().absoluteFilePath(
                'subdownloader.tmp.srt')
            log.debug(
                'Temporary subtitle will be downloaded into: {temp_path}'.
                format(temp_path=subtitle_file_path))
            # FIXME: must pass mainwindow as argument to ProgressCallbackWidget
            callback = ProgressCallbackWidget(self)
            callback.set_title_text(_('Playing video + sub'))
            callback.set_label_text(_('Downloading files...'))
            callback.set_finished_text(_('Downloading finished'))
            callback.set_block(True)
            callback.set_range(0, 100)
            callback.show()
            try:
                subtitle_stream = selected_subtitle.download(
                    self.get_state().get_OSDBServer(), callback=callback)
            except ProviderConnectionError:
                log.debug('Unable to download subtitle "{}"'.format(
                    selected_subtitle.get_filename()),
                          exc_info=True)
                QMessageBox.about(
                    self, _('Error'),
                    _('Unable to download subtitle "{subtitle}"').format(
                        subtitle=selected_subtitle.get_filename()))
                callback.finish()
                return
            callback.finish()
            write_stream(subtitle_stream, subtitle_file_path)

        video = selected_subtitle.get_parent().get_parent().get_parent()

        def windows_escape(text):
            return '"{text}"'.format(text=text.replace('"', '\\"'))

        params = [windows_escape(programPath)]

        for param in parameters.split(' '):
            param = param.format(video.get_filepath(), subtitle_file_path)
            if platform.system() in ('Windows', 'Microsoft'):
                param = windows_escape(param)
            params.append(param)

        pid = None
        log.info('Running this command: {params}'.format(params=params))
        try:
            log.debug('Trying os.spawnvpe ...')
            pid = os.spawnvpe(os.P_NOWAIT, programPath, params, os.environ)
            log.debug('... SUCCESS. pid={pid}'.format(pid=pid))
        except AttributeError:
            log.debug('... FAILED', exc_info=True)
        except Exception as e:
            log.debug('... FAILED', exc_info=True)
        if pid is None:
            try:
                log.debug('Trying os.fork ...')
                pid = os.fork()
                if not pid:
                    log.debug('... SUCCESS. pid={pid}'.format(pid=pid))
                    os.execvpe(os.P_NOWAIT, programPath, params, os.environ)
            except:
                log.debug('... FAIL', exc_info=True)
        if pid is None:
            QMessageBox.about(self, _('Error'),
                              _('Unable to launch videoplayer'))

    @pyqtSlot(QModelIndex)
    def onClickVideoTreeView(self, index):
        data_item = self.videoModel.getSelectedItem(index)

        if isinstance(data_item, SubtitleFile):
            self.ui.buttonPlay.setEnabled(True)
        else:
            self.ui.buttonPlay.setEnabled(False)

        if isinstance(data_item, VideoFile):
            video = data_item
            if True:  # video.getMovieInfo():
                self.ui.buttonIMDB.setEnabled(True)
                self.ui.buttonIMDB.setIcon(QIcon(':/images/info.png'))
                self.ui.buttonIMDB.setText(_('Movie Info'))
        elif isinstance(data_item, RemoteSubtitleFile):
            self.ui.buttonIMDB.setEnabled(True)
            self.ui.buttonIMDB.setIcon(
                QIcon(':/images/sites/opensubtitles.png'))
            self.ui.buttonIMDB.setText(_('Subtitle Info'))
        else:
            self.ui.buttonIMDB.setEnabled(False)

    def onContext(self, point):
        # FIXME: code duplication with Main.onContext and/or SearchNameWidget and/or SearchFileWidget
        menu = QMenu('Menu', self)

        listview = self.ui.videoView

        index = listview.currentIndex()
        data_item = listview.model().getSelectedItem(index)
        if data_item is not None:
            if isinstance(data_item, VideoFile):
                video = data_item
                movie_info = video.getMovieInfo()
                if movie_info:
                    subWebsiteAction = QAction(QIcon(":/images/info.png"),
                                               _("View IMDB info"), self)
                    subWebsiteAction.triggered.connect(self.onViewOnlineInfo)
                else:
                    subWebsiteAction = QAction(QIcon(":/images/info.png"),
                                               _("Set IMDB info..."), self)
                    subWebsiteAction.triggered.connect(self.onSetIMDBInfo)
                menu.addAction(subWebsiteAction)
            elif isinstance(data_item, SubtitleFile):
                downloadAction = QAction(QIcon(":/images/download.png"),
                                         _("Download"), self)
                # Video tab, TODO:Replace me with a enum

                downloadAction.triggered.connect(self.onButtonDownload)
                playAction = QAction(QIcon(":/images/play.png"),
                                     _("Play video + subtitle"), self)
                playAction.triggered.connect(self.onButtonPlay)
                menu.addAction(playAction)

                subWebsiteAction = QAction(
                    QIcon(":/images/sites/opensubtitles.png"),
                    _("View online info"), self)

                menu.addAction(downloadAction)
                subWebsiteAction.triggered.connect(self.onViewOnlineInfo)
                menu.addAction(subWebsiteAction)
            elif isinstance(data_item, Movie):
                subWebsiteAction = QAction(QIcon(":/images/info.png"),
                                           _("View IMDB info"), self)
                subWebsiteAction.triggered.connect(self.onViewOnlineInfo)
                menu.addAction(subWebsiteAction)

        # Show the context menu.
        menu.exec_(listview.mapToGlobal(point))

    def onButtonDownload(self):
        # We download the subtitle in the same folder than the video
        subs = self.videoModel.get_checked_subtitles()
        replace_all = False
        skip_all = False
        if not subs:
            QMessageBox.about(self, _("Error"),
                              _("No subtitles selected to be downloaded"))
            return
        total_subs = len(subs)
        answer = None
        success_downloaded = 0

        # FIXME: must pass mainwindow as argument to ProgressCallbackWidget
        callback = ProgressCallbackWidget(self)
        callback.set_title_text(_("Downloading..."))
        callback.set_label_text(_("Downloading files..."))
        callback.set_updated_text(_("Downloading subtitle {0} ({1}/{2})"))
        callback.set_finished_text(
            _("{0} from {1} subtitles downloaded successfully"))
        callback.set_block(True)
        callback.set_range(0, total_subs)

        callback.show()

        for i, sub in enumerate(subs):
            if callback.canceled():
                break
            destinationPath = self.get_state().getDownloadPath(self, sub)
            if not destinationPath:
                break
            log.debug("Trying to download subtitle '%s'" % destinationPath)
            callback.update(i,
                            QFileInfo(destinationPath).fileName(), i + 1,
                            total_subs)

            # Check if we have write permissions, otherwise show warning window
            while True:
                # If the file and the folder don't have write access.
                if not QFileInfo(
                        destinationPath).isWritable() and not QFileInfo(
                            QFileInfo(destinationPath).absoluteDir().path()
                        ).isWritable():
                    warningBox = QMessageBox(
                        _("Error write permission"),
                        _("%s cannot be saved.\nCheck that the folder exists and you have write-access permissions."
                          ) % destinationPath, QMessageBox.Warning,
                        QMessageBox.Retry | QMessageBox.Default,
                        QMessageBox.Discard | QMessageBox.Escape,
                        QMessageBox.NoButton, self)

                    saveAsButton = warningBox.addButton(
                        _("Save as..."), QMessageBox.ActionRole)
                    answer = warningBox.exec_()
                    if answer == QMessageBox.Retry:
                        continue
                    elif answer == QMessageBox.Discard:
                        break  # Let's get out from the While true
                    # If we choose the SAVE AS
                    elif answer == QMessageBox.NoButton:
                        fileName, t = QFileDialog.getSaveFileName(
                            self, _("Save subtitle as..."), destinationPath,
                            'All (*.*)')
                        if fileName:
                            destinationPath = fileName
                else:  # If we have write access we leave the while loop.
                    break

            # If we have chosen Discard subtitle button.
            if answer == QMessageBox.Discard:
                continue  # Continue the next subtitle

            optionWhereToDownload = QSettings().value(
                "options/whereToDownload", "SAME_FOLDER")
            # Check if doesn't exists already, otherwise show fileExistsBox
            # dialog
            if QFileInfo(destinationPath).exists(
            ) and not replace_all and not skip_all and optionWhereToDownload != "ASK_FOLDER":
                # The "remote filename" below is actually not the real filename. Real name could be confusing
                # since we always rename downloaded sub to match movie
                # filename.
                fileExistsBox = QMessageBox(
                    QMessageBox.Warning, _("File already exists"),
                    _("Local: {local}\n\nRemote: {remote}\n\nHow would you like to proceed?"
                      ).format(local=destinationPath,
                               remote=QFileInfo(destinationPath).fileName()),
                    QMessageBox.NoButton, self)
                skipButton = fileExistsBox.addButton(_("Skip"),
                                                     QMessageBox.ActionRole)
                #                    skipAllButton = fileExistsBox.addButton(_("Skip all"), QMessageBox.ActionRole)
                replaceButton = fileExistsBox.addButton(
                    _("Replace"), QMessageBox.ActionRole)
                replaceAllButton = fileExistsBox.addButton(
                    _("Replace all"), QMessageBox.ActionRole)
                saveAsButton = fileExistsBox.addButton(_("Save as..."),
                                                       QMessageBox.ActionRole)
                cancelButton = fileExistsBox.addButton(_("Cancel"),
                                                       QMessageBox.ActionRole)
                fileExistsBox.exec_()
                answer = fileExistsBox.clickedButton()
                if answer == replaceAllButton:
                    # Don't ask us again (for this batch of files)
                    replace_all = True
                elif answer == saveAsButton:
                    # We will find a uniqiue filename and suggest this to user.
                    # add .<lang> to (inside) the filename. If that is not enough, start adding numbers.
                    # There should also be a preferences setting "Autorename
                    # files" or similar ( =never ask) FIXME
                    suggBaseName, suggFileExt = os.path.splitext(
                        destinationPath)
                    fNameCtr = 0  # Counter used to generate a unique filename
                    suggestedFileName = suggBaseName + '.' + \
                        sub.get_language().xxx() + suggFileExt
                    while (os.path.exists(suggestedFileName)):
                        fNameCtr += 1
                        suggestedFileName = suggBaseName + '.' + \
                            sub.get_language().xxx() + '-' + \
                            str(fNameCtr) + suggFileExt
                    fileName, t = QFileDialog.getSaveFileName(
                        None, _("Save subtitle as..."), suggestedFileName,
                        'All (*.*)')
                    if fileName:
                        destinationPath = fileName
                    else:
                        # Skip this particular file if no filename chosen
                        continue
                elif answer == skipButton:
                    continue  # Skip this particular file
#                    elif answer == skipAllButton:
#                        count += percentage
#                        skip_all = True # Skip all files already downloaded
#                        continue
                elif answer == cancelButton:
                    break  # Break out of DL loop - cancel was pushed
            QCoreApplication.processEvents()
            # FIXME: redundant update?
            callback.update(i,
                            QFileInfo(destinationPath).fileName(), i + 1,
                            total_subs)
            try:
                if not skip_all:
                    log.debug("Downloading subtitle '%s'" % destinationPath)
                    download_callback = ProgressCallback()  # FIXME
                    data_stream = sub.download(
                        provider_instance=self.get_state().get_OSDBServer(),
                        callback=download_callback,
                    )
                    write_stream(data_stream, destinationPath)
            except Exception as e:
                log.exception('Unable to Download subtitle {}'.format(
                    sub.get_filename()))
                QMessageBox.about(
                    self, _("Error"),
                    _("Unable to download subtitle %s") % sub.get_filename())
        callback.finish(success_downloaded, total_subs)

    def onViewOnlineInfo(self):
        # FIXME: code duplication with Main.onContext and/or SearchNameWidget and/or SearchFileWidget
        # Tab for SearchByHash TODO:replace this 0 by an ENUM value
        listview = self.ui.videoView
        index = listview.currentIndex()
        data_item = self.videoModel.getSelectedItem(index)

        if isinstance(data_item, VideoFile):
            video = data_item
            movie_info = video.getMovieInfo()
            if movie_info:
                imdb = movie_info["IDMovieImdb"]
                if imdb:
                    webbrowser.open("http://www.imdb.com/title/tt%s" % imdb,
                                    new=2,
                                    autoraise=1)

        elif isinstance(data_item, RemoteSubtitleFile):
            sub = data_item
            webbrowser.open(sub.get_link(), new=2, autoraise=1)

        elif isinstance(data_item, Movie):
            movie = data_item
            imdb = movie.IMDBId
            if imdb:
                webbrowser.open("http://www.imdb.com/title/tt%s" % imdb,
                                new=2,
                                autoraise=1)

    @pyqtSlot()
    def onSetIMDBInfo(self):
        #FIXME: DUPLICATED WITH SEARCHNAMEWIDGET
        QMessageBox.about(self, _("Info"), "Not implemented yet. Sorry...")
Exemplo n.º 29
0
class E5NetworkMonitor(QDialog, Ui_E5NetworkMonitor):
    """
    Class implementing a network monitor dialog.
    """
    _monitor = None
    
    @classmethod
    def instance(cls, networkAccessManager):
        """
        Class method to get a reference to our singleton.
        
        @param networkAccessManager reference to the network access manager
            (QNetworkAccessManager)
        @return reference to the network monitor singleton (E5NetworkMonitor)
        """
        if cls._monitor is None:
            cls._monitor = E5NetworkMonitor(networkAccessManager)
        
        return cls._monitor
    
    @classmethod
    def closeMonitor(cls):
        """
        Class method to close the monitor dialog.
        """
        if cls._monitor is not None:
            cls._monitor.close()
    
    def __init__(self, networkAccessManager, parent=None):
        """
        Constructor
        
        @param networkAccessManager reference to the network access manager
            (QNetworkAccessManager)
        @param parent reference to the parent widget (QWidget)
        """
        super(E5NetworkMonitor, self).__init__(parent)
        self.setupUi(self)
        
        self.__requestHeaders = QStandardItemModel(self)
        self.__requestHeaders.setHorizontalHeaderLabels(
            [self.tr("Name"), self.tr("Value")])
        self.requestHeadersList.setModel(self.__requestHeaders)
        self.requestHeadersList.horizontalHeader().setStretchLastSection(True)
        self.requestHeadersList.doubleClicked.connect(self.__showHeaderDetails)
        
        self.__replyHeaders = QStandardItemModel(self)
        self.__replyHeaders.setHorizontalHeaderLabels(
            [self.tr("Name"), self.tr("Value")])
        self.responseHeadersList.setModel(self.__replyHeaders)
        self.responseHeadersList.horizontalHeader().setStretchLastSection(True)
        self.responseHeadersList.doubleClicked.connect(
            self.__showHeaderDetails)
        
        self.requestsList.horizontalHeader().setStretchLastSection(True)
        self.requestsList.verticalHeader().setMinimumSectionSize(-1)
        
        self.__proxyModel = QSortFilterProxyModel(self)
        self.__proxyModel.setFilterKeyColumn(-1)
        self.searchEdit.textChanged.connect(
            self.__proxyModel.setFilterFixedString)
        
        self.removeButton.clicked.connect(self.requestsList.removeSelected)
        self.removeAllButton.clicked.connect(self.requestsList.removeAll)
        
        self.__model = E5RequestModel(networkAccessManager, self)
        self.__proxyModel.setSourceModel(self.__model)
        self.requestsList.setModel(self.__proxyModel)
        self.__proxyModel.rowsInserted.connect(
            self.requestsList.scrollToBottom)
        self.requestsList.selectionModel()\
            .currentChanged[QModelIndex, QModelIndex]\
            .connect(self.__currentChanged)
        
        fm = self.fontMetrics()
        em = fm.width("m")
        self.requestsList.horizontalHeader().resizeSection(0, em * 5)
        self.requestsList.horizontalHeader().resizeSection(1, em * 20)
        self.requestsList.horizontalHeader().resizeSection(3, em * 5)
        self.requestsList.horizontalHeader().resizeSection(4, em * 15)
        
        self.__headersDlg = None
    
    def closeEvent(self, evt):
        """
        Protected method called upon closing the dialog.
        
        @param evt reference to the close event object (QCloseEvent)
        """
        self.__class__._monitor = None
        super(E5NetworkMonitor, self).closeEvent(evt)
    
    def reject(self):
        """
        Public slot to close the dialog with a Reject status.
        """
        self.__class__._monitor = None
        super(E5NetworkMonitor, self).reject()
    
    def __currentChanged(self, current, previous):
        """
        Private slot to handle a change of the current index.
        
        @param current new current index (QModelIndex)
        @param previous old current index (QModelIndex)
        """
        self.__requestHeaders.setRowCount(0)
        self.__replyHeaders.setRowCount(0)
        
        if not current.isValid():
            return
        
        row = self.__proxyModel.mapToSource(current).row()
        
        req = self.__model.requests[row].request
        
        for header in req.rawHeaderList():
            self.__requestHeaders.insertRows(0, 1, QModelIndex())
            self.__requestHeaders.setData(
                self.__requestHeaders.index(0, 0),
                str(header, "utf-8"))
            self.__requestHeaders.setData(
                self.__requestHeaders.index(0, 1),
                str(req.rawHeader(header), "utf-8"))
            self.__requestHeaders.item(0, 0).setFlags(
                Qt.ItemIsSelectable | Qt.ItemIsEnabled)
            self.__requestHeaders.item(0, 1).setFlags(
                Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        
        for header in self.__model.requests[row].replyHeaders:
            self.__replyHeaders.insertRows(0, 1, QModelIndex())
            self.__replyHeaders.setData(
                self.__replyHeaders.index(0, 0),
                header[0])
            self.__replyHeaders.setData(
                self.__replyHeaders.index(0, 1),
                header[1])
            self.__replyHeaders.item(0, 0).setFlags(
                Qt.ItemIsSelectable | Qt.ItemIsEnabled)
            self.__replyHeaders.item(0, 1).setFlags(
                Qt.ItemIsSelectable | Qt.ItemIsEnabled)
    
    def __showHeaderDetails(self, index):
        """
        Private slot to show a dialog with the header details.
        
        @param index index of the entry to show (QModelIndex)
        """
        if not index.isValid():
            return
        
        headerList = self.sender()
        if headerList is None:
            return
        
        row = index.row()
        name = headerList.model().data(headerList.model().index(row, 0))
        value = headerList.model().data(headerList.model().index(row, 1))
        if self.__headersDlg is None:
            from .E5NetworkHeaderDetailsDialog import \
                E5NetworkHeaderDetailsDialog
            self.__headersDlg = E5NetworkHeaderDetailsDialog(self)
        self.__headersDlg.setData(name, value)
        self.__headersDlg.show()
Exemplo n.º 30
0
class ListWidget(QWidget):
    deviceSelected = pyqtSignal(TasmotaDevice)
    openRulesEditor = pyqtSignal()
    openConsole = pyqtSignal()
    openTelemetry = pyqtSignal()
    openWebUI = pyqtSignal()

    def __init__(self, parent, *args, **kwargs):
        super(ListWidget, self).__init__(*args, **kwargs)
        self.setWindowTitle("Devices list")
        self.setWindowState(Qt.WindowMaximized)
        self.setLayout(VLayout(margin=0, spacing=0))

        self.mqtt = parent.mqtt
        self.env = parent.env

        self.device = None
        self.idx = None

        self.nam = QNetworkAccessManager()
        self.backup = bytes()

        self.settings = QSettings("{}/TDM/tdm.cfg".format(QDir.homePath()), QSettings.IniFormat)
        views_order = self.settings.value("views_order", [])

        self.views = {}
        self.settings.beginGroup("Views")
        views = self.settings.childKeys()
        if views and views_order:
            for view in views_order.split(";"):
                view_list = self.settings.value(view).split(";")
                self.views[view] = base_view + view_list
        else:
            self.views = default_views
        self.settings.endGroup()

        self.tb = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon)
        self.tb_relays = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonIconOnly)
        # self.tb_filter = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon)
        self.tb_views = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon)

        self.pwm_sliders = []

        self.layout().addWidget(self.tb)
        self.layout().addWidget(self.tb_relays)
        # self.layout().addWidget(self.tb_filter)

        self.device_list = TableView()
        self.device_list.setIconSize(QSize(24, 24))
        self.model = parent.device_model
        self.model.setupColumns(self.views["Home"])

        self.sorted_device_model = QSortFilterProxyModel()
        self.sorted_device_model.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.sorted_device_model.setSourceModel(parent.device_model)
        self.sorted_device_model.setSortRole(Qt.InitialSortOrderRole)
        self.sorted_device_model.setFilterKeyColumn(-1)

        self.device_list.setModel(self.sorted_device_model)
        self.device_list.setupView(self.views["Home"])
        self.device_list.setSortingEnabled(True)
        self.device_list.setWordWrap(True)
        self.device_list.setItemDelegate(DeviceDelegate())
        self.device_list.sortByColumn(self.model.columnIndex("FriendlyName"), Qt.AscendingOrder)
        self.device_list.setContextMenuPolicy(Qt.CustomContextMenu)
        self.device_list.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
        self.layout().addWidget(self.device_list)

        self.layout().addWidget(self.tb_views)

        self.device_list.clicked.connect(self.select_device)
        self.device_list.customContextMenuRequested.connect(self.show_list_ctx_menu)

        self.ctx_menu = QMenu()
        self.ctx_menu_relays = None

        self.create_actions()
        self.create_view_buttons()
        # self.create_view_filter()

        self.device_list.doubleClicked.connect(lambda: self.openConsole.emit())

    def create_actions(self):
        self.ctx_menu_cfg = QMenu("Configure")
        self.ctx_menu_cfg.setIcon(QIcon("GUI/icons/settings.png"))
        self.ctx_menu_cfg.addAction("Module", self.configureModule)
        self.ctx_menu_cfg.addAction("GPIO", self.configureGPIO)
        self.ctx_menu_cfg.addAction("Template", self.configureTemplate)
        # self.ctx_menu_cfg.addAction("Wifi", self.ctx_menu_teleperiod)
        # self.ctx_menu_cfg.addAction("Time", self.cfgTime.emit)
        # self.ctx_menu_cfg.addAction("MQTT", self.ctx_menu_teleperiod)

        # self.ctx_menu_cfg.addAction("Logging", self.ctx_menu_teleperiod)

        self.ctx_menu.addMenu(self.ctx_menu_cfg)
        self.ctx_menu.addSeparator()

        self.ctx_menu.addAction(QIcon("GUI/icons/refresh.png"), "Refresh", self.ctx_menu_refresh)

        self.ctx_menu.addSeparator()
        self.ctx_menu.addAction(QIcon("GUI/icons/clear.png"), "Clear retained", self.ctx_menu_clear_retained)
        self.ctx_menu.addAction("Clear Backlog", self.ctx_menu_clear_backlog)
        self.ctx_menu.addSeparator()
        self.ctx_menu.addAction(QIcon("GUI/icons/copy.png"), "Copy", self.ctx_menu_copy)
        self.ctx_menu.addSeparator()
        self.ctx_menu.addAction(QIcon("GUI/icons/restart.png"), "Restart", self.ctx_menu_restart)
        self.ctx_menu.addAction(QIcon(), "Reset", self.ctx_menu_reset)
        self.ctx_menu.addSeparator()
        self.ctx_menu.addAction(QIcon("GUI/icons/delete.png"), "Delete", self.ctx_menu_delete_device)

        console = self.tb.addAction(QIcon("GUI/icons/console.png"), "Console", self.openConsole.emit)
        console.setShortcut("Ctrl+E")

        rules = self.tb.addAction(QIcon("GUI/icons/rules.png"), "Rules", self.openRulesEditor.emit)
        rules.setShortcut("Ctrl+R")

        self.tb.addAction(QIcon("GUI/icons/timers.png"), "Timers", self.configureTimers)

        buttons = self.tb.addAction(QIcon("GUI/icons/buttons.png"), "Buttons", self.configureButtons)
        buttons.setShortcut("Ctrl+B")

        switches = self.tb.addAction(QIcon("GUI/icons/switches.png"), "Switches", self.configureSwitches)
        switches.setShortcut("Ctrl+S")

        power = self.tb.addAction(QIcon("GUI/icons/power.png"), "Power", self.configurePower)
        power.setShortcut("Ctrl+P")

        # setopts = self.tb.addAction(QIcon("GUI/icons/setoptions.png"), "SetOptions", self.configureSO)
        # setopts.setShortcut("Ctrl+S")

        self.tb.addSpacer()

        telemetry = self.tb.addAction(QIcon("GUI/icons/telemetry.png"), "Telemetry", self.openTelemetry.emit)
        telemetry.setShortcut("Ctrl+T")

        webui = self.tb.addAction(QIcon("GUI/icons/web.png"), "WebUI", self.openWebUI.emit)
        webui.setShortcut("Ctrl+U")

        # self.tb.addAction(QIcon(), "Multi Command", self.ctx_menu_webui)

        self.agAllPower = QActionGroup(self)
        self.agAllPower.addAction(QIcon("GUI/icons/P_ON.png"), "All ON")
        self.agAllPower.addAction(QIcon("GUI/icons/P_OFF.png"), "All OFF")
        self.agAllPower.setEnabled(False)
        self.agAllPower.setExclusive(False)
        self.agAllPower.triggered.connect(self.toggle_power_all)
        self.tb_relays.addActions(self.agAllPower.actions())

        self.agRelays = QActionGroup(self)
        self.agRelays.setVisible(False)
        self.agRelays.setExclusive(False)

        for a in range(1, 9):
            act = QAction(QIcon("GUI/icons/P{}_OFF.png".format(a)), "")
            act.setShortcut("F{}".format(a))
            self.agRelays.addAction(act)

        self.agRelays.triggered.connect(self.toggle_power)
        self.tb_relays.addActions(self.agRelays.actions())

        self.tb_relays.addSeparator()
        self.actColor = self.tb_relays.addAction(QIcon("GUI/icons/color.png"), "Color", self.set_color)
        self.actColor.setEnabled(False)

        self.actChannels = self.tb_relays.addAction(QIcon("GUI/icons/sliders.png"), "Channels")
        self.actChannels.setEnabled(False)
        self.mChannels = QMenu()
        self.actChannels.setMenu(self.mChannels)
        self.tb_relays.widgetForAction(self.actChannels).setPopupMode(QToolButton.InstantPopup)

    def create_view_buttons(self):
        self.tb_views.addWidget(QLabel("View mode: "))
        ag_views = QActionGroup(self)
        ag_views.setExclusive(True)
        for v in self.views.keys():
            a = QAction(v)
            a.triggered.connect(self.change_view)
            a.setCheckable(True)
            ag_views.addAction(a)
        self.tb_views.addActions(ag_views.actions())
        ag_views.actions()[0].setChecked(True)

        stretch = QWidget()
        stretch.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum))
        self.tb_views.addWidget(stretch)
        # actEditView = self.tb_views.addAction("Edit views...")

    # def create_view_filter(self):
    #     # self.tb_filter.addWidget(QLabel("Show devices: "))
    #     # self.cbxLWT = QComboBox()
    #     # self.cbxLWT.addItems(["All", "Online"d, "Offline"])
    #     # self.cbxLWT.currentTextChanged.connect(self.build_filter_regex)
    #     # self.tb_filter.addWidget(self.cbxLWT)
    #
    #     self.tb_filter.addWidget(QLabel(" Search: "))
    #     self.leSearch = QLineEdit()
    #     self.leSearch.setClearButtonEnabled(True)
    #     self.leSearch.textChanged.connect(self.build_filter_regex)
    #     self.tb_filter.addWidget(self.leSearch)
    #
    # def build_filter_regex(self, txt):
    #     query = self.leSearch.text()
    #     # if self.cbxLWT.currentText() != "All":
    #     #     query = "{}|{}".format(self.cbxLWT.currentText(), query)
    #     self.sorted_device_model.setFilterRegExp(query)

    def change_view(self, a=None):
        view = self.views[self.sender().text()]
        self.model.setupColumns(view)
        self.device_list.setupView(view)

    def ctx_menu_copy(self):
        if self.idx:
            string = dumps(self.model.data(self.idx))
            if string.startswith('"') and string.endswith('"'):
                string = string[1:-1]
            QApplication.clipboard().setText(string)

    def ctx_menu_clear_retained(self):
        if self.device:
            relays = self.device.power()
            if relays and len(relays.keys()) > 0:
                for r in relays.keys():
                    self.mqtt.publish(self.device.cmnd_topic(r), retain=True)
            QMessageBox.information(self, "Clear retained", "Cleared retained messages.")

    def ctx_menu_clear_backlog(self):
        if self.device:
            self.mqtt.publish(self.device.cmnd_topic("backlog"), "")
            QMessageBox.information(self, "Clear Backlog", "Backlog cleared.")

    def ctx_menu_restart(self):
        if self.device:
            self.mqtt.publish(self.device.cmnd_topic("restart"), payload="1")
            for k in list(self.device.power().keys()):
                self.device.p.pop(k)

    def ctx_menu_reset(self):
        if self.device:
            reset, ok = QInputDialog.getItem(self, "Reset device and restart", "Select reset mode", resets, editable=False)
            if ok:
                self.mqtt.publish(self.device.cmnd_topic("reset"), payload=reset.split(":")[0])
                for k in list(self.device.power().keys()):
                    self.device.p.pop(k)

    def ctx_menu_refresh(self):
        if self.device:
            for k in list(self.device.power().keys()):
                self.device.p.pop(k)

            for c in initial_commands():
                cmd, payload = c
                cmd = self.device.cmnd_topic(cmd)
                self.mqtt.publish(cmd, payload, 1)

    def ctx_menu_delete_device(self):
        if self.device:
            if QMessageBox.question(self, "Confirm", "Do you want to remove the following device?\n'{}' ({})"
                    .format(self.device.p['FriendlyName1'], self.device.p['Topic'])) == QMessageBox.Yes:
                self.model.deleteDevice(self.idx)

    def ctx_menu_teleperiod(self):
        if self.device:
            teleperiod, ok = QInputDialog.getInt(self, "Set telemetry period", "Input 1 to reset to default\n[Min: 10, Max: 3600]", self.device.p['TelePeriod'], 1, 3600)
            if ok:
                if teleperiod != 1 and teleperiod < 10:
                    teleperiod = 10
            self.mqtt.publish(self.device.cmnd_topic("teleperiod"), teleperiod)

    def ctx_menu_config_backup(self):
        if self.device:
            self.backup = bytes()
            self.dl = self.nam.get(QNetworkRequest(QUrl("http://{}/dl".format(self.device.p['IPAddress']))))
            self.dl.readyRead.connect(self.get_dump)
            self.dl.finished.connect(self.save_dump)

    def ctx_menu_ota_set_url(self):
        if self.device:
            url, ok = QInputDialog.getText(self, "Set OTA URL", '100 chars max. Set to "1" to reset to default.', text=self.device.p['OtaUrl'])
            if ok:
                self.mqtt.publish(self.device.cmnd_topic("otaurl"), payload=url)

    def ctx_menu_ota_set_upgrade(self):
        if self.device:
            if QMessageBox.question(self, "OTA Upgrade", "Are you sure to OTA upgrade from\n{}".format(self.device.p['OtaUrl']), QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes:
                self.mqtt.publish(self.device.cmnd_topic("upgrade"), payload="1")

    def show_list_ctx_menu(self, at):
        self.select_device(self.device_list.indexAt(at))
        self.ctx_menu.popup(self.device_list.viewport().mapToGlobal(at))

    def select_device(self, idx):
        self.idx = self.sorted_device_model.mapToSource(idx)
        self.device = self.model.deviceAtRow(self.idx.row())
        self.deviceSelected.emit(self.device)

        relays = self.device.power()

        self.agAllPower.setEnabled(len(relays) >= 1)

        for i, a in enumerate(self.agRelays.actions()):
            a.setVisible(len(relays) > 1 and i < len(relays))

        color = self.device.color().get("Color", False)
        has_color = bool(color)
        self.actColor.setEnabled(has_color and not self.device.setoption(68))

        self.actChannels.setEnabled(has_color)

        if has_color:
            self.actChannels.menu().clear()

            max_val = 100
            if self.device.setoption(15) == 0:
                max_val = 1023

            for k, v in self.device.pwm().items():
                channel = SliderAction(self, k)
                channel.slider.setMaximum(max_val)
                channel.slider.setValue(int(v))
                self.mChannels.addAction(channel)
                channel.slider.valueChanged.connect(self.set_channel)

            dimmer = self.device.color().get("Dimmer")
            if dimmer:
                saDimmer = SliderAction(self, "Dimmer")
                saDimmer.slider.setValue(int(dimmer))
                self.mChannels.addAction(saDimmer)
                saDimmer.slider.valueChanged.connect(self.set_channel)

    def toggle_power(self, action):
        if self.device:
            idx = self.agRelays.actions().index(action)
            relay = list(self.device.power().keys())[idx]
            self.mqtt.publish(self.device.cmnd_topic(relay), "toggle")

    def toggle_power_all(self, action):
        if self.device:
            idx = self.agAllPower.actions().index(action)
            for r in self.device.power().keys():
                self.mqtt.publish(self.device.cmnd_topic(r), str(not bool(idx)))

    def set_color(self):
        if self.device:
            color = self.device.color().get("Color")
            if color:
                dlg = QColorDialog()
                new_color = dlg.getColor(QColor("#{}".format(color)))
                if new_color.isValid():
                    new_color = new_color.name()
                    if new_color != color:
                        self.mqtt.publish(self.device.cmnd_topic("color"), new_color)

    def set_channel(self, value=0):
        cmd = self.sender().objectName()

        if self.device:
            self.mqtt.publish(self.device.cmnd_topic(cmd), str(value))

    def configureSO(self):
        if self.device:
            dlg = SetOptionsDialog(self.device)
            dlg.sendCommand.connect(self.mqtt.publish)
            dlg.exec_()

    def configureModule(self):
        if self.device:
            dlg = ModuleDialog(self.device)
            dlg.sendCommand.connect(self.mqtt.publish)
            dlg.exec_()

    def configureGPIO(self):
        if self.device:
            dlg = GPIODialog(self.device)
            dlg.sendCommand.connect(self.mqtt.publish)
            dlg.exec_()

    def configureTemplate(self):
        if self.device:
            dlg = TemplateDialog(self.device)
            dlg.sendCommand.connect(self.mqtt.publish)
            dlg.exec_()

    def configureTimers(self):
        if self.device:
            self.mqtt.publish(self.device.cmnd_topic("timers"))
            timers = TimersDialog(self.device)
            self.mqtt.messageSignal.connect(timers.parseMessage)
            timers.sendCommand.connect(self.mqtt.publish)
            timers.exec_()

    def configureButtons(self):
        if self.device:
            backlog = []
            buttons = ButtonsDialog(self.device)
            if buttons.exec_() == QDialog.Accepted:
                for c, cw in buttons.command_widgets.items():
                    current_value = self.device.p.get(c)
                    new_value = ""

                    if isinstance(cw.input, SpinBox):
                        new_value = cw.input.value()

                    if isinstance(cw.input, QComboBox):
                        new_value = cw.input.currentIndex()

                    if current_value != new_value:
                        backlog.append("{} {}".format(c, new_value))

                for so, sow in buttons.setoption_widgets.items():
                    current_value = self.device.setoption(so)
                    new_value = -1

                    if isinstance(sow.input, SpinBox):
                        new_value = sow.input.value()

                    if isinstance(sow.input, QComboBox):
                        new_value = sow.input.currentIndex()

                    if current_value != new_value:
                        backlog.append("SetOption{} {}".format(so, new_value))

                if backlog:
                    backlog.append("status 3")
                    self.mqtt.publish(self.device.cmnd_topic("backlog"), "; ".join(backlog))

    def configureSwitches(self):
        if self.device:
            backlog = []
            switches = SwitchesDialog(self.device)
            if switches.exec_() == QDialog.Accepted:
                for c, cw in switches.command_widgets.items():
                    current_value = self.device.p.get(c)
                    new_value = ""

                    if isinstance(cw.input, SpinBox):
                        new_value = cw.input.value()

                    if isinstance(cw.input, QComboBox):
                        new_value = cw.input.currentIndex()

                    if current_value != new_value:
                        backlog.append("{} {}".format(c, new_value))

                for so, sow in switches.setoption_widgets.items():
                    current_value = self.device.setoption(so)
                    new_value = -1

                    if isinstance(sow.input, SpinBox):
                        new_value = sow.input.value()

                    if isinstance(sow.input, QComboBox):
                        new_value = sow.input.currentIndex()

                    if current_value != new_value:
                        backlog.append("SetOption{} {}".format(so, new_value))

                for sw, sw_mode in enumerate(self.device.p['SwitchMode']):
                    new_value = switches.sm.inputs[sw].currentIndex()

                    if sw_mode != new_value:
                        backlog.append("switchmode{} {}".format(sw+1, new_value))

                if backlog:
                    backlog.append("status")
                    backlog.append("status 3")
                self.mqtt.publish(self.device.cmnd_topic("backlog"), "; ".join(backlog))

    def configurePower(self):
        if self.device:
            backlog = []
            power = PowerDialog(self.device)
            if power.exec_() == QDialog.Accepted:
                for c, cw in power.command_widgets.items():
                    current_value = self.device.p.get(c)
                    new_value = ""

                    if isinstance(cw.input, SpinBox):
                        new_value = cw.input.value()

                    if isinstance(cw.input, QComboBox):
                        new_value = cw.input.currentIndex()

                    if current_value != new_value:
                        backlog.append("{} {}".format(c, new_value))

                for so, sow in power.setoption_widgets.items():
                    new_value = -1

                    if isinstance(sow.input, SpinBox):
                        new_value = sow.input.value()

                    if isinstance(sow.input, QComboBox):
                        new_value = sow.input.currentIndex()

                    if new_value != self.device.setoption(so):
                        backlog.append("SetOption{} {}".format(so, new_value))

                new_interlock_value = power.ci.input.currentData()
                new_interlock_grps = " ".join([grp.text().replace(" ", "") for grp in power.ci.groups]).rstrip()

                if new_interlock_value != self.device.p.get("Interlock", "OFF"):
                    backlog.append("interlock {}".format(new_interlock_value))

                if new_interlock_grps != self.device.p.get("Groups", ""):
                    backlog.append("interlock {}".format(new_interlock_grps))

                for i, pt in enumerate(power.cpt.inputs):
                    ptime = "PulseTime{}".format(i+1)
                    current_ptime = self.device.p.get(ptime)
                    if current_ptime:
                        current_value = list(current_ptime.keys())[0]
                        new_value = str(pt.value())

                        if new_value != current_value:
                            backlog.append("{} {}".format(ptime, new_value))

                if backlog:
                    backlog.append("status")
                    backlog.append("status 3")
                    self.mqtt.publish(self.device.cmnd_topic("backlog"), "; ".join(backlog))

    def get_dump(self):
        self.backup += self.dl.readAll()

    def save_dump(self):
        fname = self.dl.header(QNetworkRequest.ContentDispositionHeader)
        if fname:
            fname = fname.split('=')[1]
            save_file = QFileDialog.getSaveFileName(self, "Save config backup", "{}/TDM/{}".format(QDir.homePath(), fname))[0]
            if save_file:
                with open(save_file, "wb") as f:
                    f.write(self.backup)

    def check_fulltopic(self, fulltopic):
        fulltopic += "/" if not fulltopic.endswith('/') else ''
        return "%prefix%" in fulltopic and "%topic%" in fulltopic

    def closeEvent(self, event):
        event.ignore()
Exemplo n.º 31
0
class BreakPointViewer(QTreeView):
    """
    Class implementing the Breakpoint viewer widget.
    
    Breakpoints will be shown with all their details. They can be modified
    through the context menu of this widget.
    
    @signal sourceFile(str, int) emitted to show the source of a breakpoint
    """
    sourceFile = pyqtSignal(str, int)
    
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent the parent (QWidget)
        """
        super(BreakPointViewer, self).__init__(parent)
        self.setObjectName("BreakPointViewer")
        
        self.__model = None
        
        self.setItemsExpandable(False)
        self.setRootIsDecorated(False)
        self.setAlternatingRowColors(True)
        self.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        
        self.setWindowTitle(self.tr("Breakpoints"))
        
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.__showContextMenu)
        self.doubleClicked.connect(self.__doubleClicked)
        
        self.__createPopupMenus()
        
        self.condHistory = []
        self.fnHistory = []
        self.fnHistory.append('')
        
    def setModel(self, model):
        """
        Public slot to set the breakpoint model.
        
        @param model reference to the breakpoint model (BreakPointModel)
        """
        self.__model = model
        
        self.sortingModel = QSortFilterProxyModel()
        self.sortingModel.setDynamicSortFilter(True)
        self.sortingModel.setSourceModel(self.__model)
        super(BreakPointViewer, self).setModel(self.sortingModel)
        
        header = self.header()
        header.setSortIndicator(0, Qt.AscendingOrder)
        header.setSortIndicatorShown(True)
        if qVersion() >= "5.0.0":
            header.setSectionsClickable(True)
        else:
            header.setClickable(True)
        
        self.setSortingEnabled(True)
        
        self.__layoutDisplay()
        
    def __layoutDisplay(self):
        """
        Private slot to perform a layout operation.
        """
        self.__resizeColumns()
        self.__resort()
        
    def __resizeColumns(self):
        """
        Private slot to resize the view when items get added, edited or
        deleted.
        """
        self.header().resizeSections(QHeaderView.ResizeToContents)
        self.header().setStretchLastSection(True)
    
    def __resort(self):
        """
        Private slot to resort the tree.
        """
        self.model().sort(self.header().sortIndicatorSection(),
                          self.header().sortIndicatorOrder())
        
    def __toSourceIndex(self, index):
        """
        Private slot to convert an index to a source index.
        
        @param index index to be converted (QModelIndex)
        @return mapped index (QModelIndex)
        """
        return self.sortingModel.mapToSource(index)
        
    def __fromSourceIndex(self, sindex):
        """
        Private slot to convert a source index to an index.
        
        @param sindex source index to be converted (QModelIndex)
        @return mapped index (QModelIndex)
        """
        return self.sortingModel.mapFromSource(sindex)
        
    def __setRowSelected(self, index, selected=True):
        """
        Private slot to select a complete row.
        
        @param index index determining the row to be selected (QModelIndex)
        @param selected flag indicating the action (bool)
        """
        if not index.isValid():
            return
        
        if selected:
            flags = QItemSelectionModel.SelectionFlags(
                QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
        else:
            flags = QItemSelectionModel.SelectionFlags(
                QItemSelectionModel.Deselect | QItemSelectionModel.Rows)
        self.selectionModel().select(index, flags)
        
    def __createPopupMenus(self):
        """
        Private method to generate the popup menus.
        """
        self.menu = QMenu()
        self.menu.addAction(self.tr("Add"), self.__addBreak)
        self.menu.addAction(self.tr("Edit..."), self.__editBreak)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Enable"), self.__enableBreak)
        self.menu.addAction(self.tr("Enable all"), self.__enableAllBreaks)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Disable"), self.__disableBreak)
        self.menu.addAction(self.tr("Disable all"),
                            self.__disableAllBreaks)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Delete"), self.__deleteBreak)
        self.menu.addAction(self.tr("Delete all"), self.__deleteAllBreaks)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Goto"), self.__showSource)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Configure..."), self.__configure)

        self.backMenuActions = {}
        self.backMenu = QMenu()
        self.backMenu.addAction(self.tr("Add"), self.__addBreak)
        self.backMenuActions["EnableAll"] = \
            self.backMenu.addAction(self.tr("Enable all"),
                                    self.__enableAllBreaks)
        self.backMenuActions["DisableAll"] = \
            self.backMenu.addAction(self.tr("Disable all"),
                                    self.__disableAllBreaks)
        self.backMenuActions["DeleteAll"] = \
            self.backMenu.addAction(self.tr("Delete all"),
                                    self.__deleteAllBreaks)
        self.backMenu.aboutToShow.connect(self.__showBackMenu)
        self.backMenu.addSeparator()
        self.backMenu.addAction(self.tr("Configure..."), self.__configure)

        self.multiMenu = QMenu()
        self.multiMenu.addAction(self.tr("Add"), self.__addBreak)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Enable selected"),
                                 self.__enableSelectedBreaks)
        self.multiMenu.addAction(self.tr("Enable all"),
                                 self.__enableAllBreaks)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Disable selected"),
                                 self.__disableSelectedBreaks)
        self.multiMenu.addAction(self.tr("Disable all"),
                                 self.__disableAllBreaks)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Delete selected"),
                                 self.__deleteSelectedBreaks)
        self.multiMenu.addAction(self.tr("Delete all"),
                                 self.__deleteAllBreaks)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction(self.tr("Configure..."), self.__configure)
    
    def __showContextMenu(self, coord):
        """
        Private slot to show the context menu.
        
        @param coord the position of the mouse pointer (QPoint)
        """
        cnt = self.__getSelectedItemsCount()
        if cnt <= 1:
            index = self.indexAt(coord)
            if index.isValid():
                cnt = 1
                self.__setRowSelected(index)
        coord = self.mapToGlobal(coord)
        if cnt > 1:
            self.multiMenu.popup(coord)
        elif cnt == 1:
            self.menu.popup(coord)
        else:
            self.backMenu.popup(coord)
    
    def __clearSelection(self):
        """
        Private slot to clear the selection.
        """
        for index in self.selectedIndexes():
            self.__setRowSelected(index, False)
            
    def __addBreak(self):
        """
        Private slot to handle the add breakpoint context menu entry.
        """
        from .EditBreakpointDialog import EditBreakpointDialog
        
        dlg = EditBreakpointDialog((self.fnHistory[0], None), None,
                                   self.condHistory, self, modal=1,
                                   addMode=1, filenameHistory=self.fnHistory)
        if dlg.exec_() == QDialog.Accepted:
            fn, line, cond, temp, enabled, count = dlg.getAddData()
            if fn is not None:
                if fn in self.fnHistory:
                    self.fnHistory.remove(fn)
                self.fnHistory.insert(0, fn)
            
            if cond:
                if cond in self.condHistory:
                    self.condHistory.remove(cond)
                self.condHistory.insert(0, cond)
            
            self.__model.addBreakPoint(fn, line, (cond, temp, enabled, count))
            self.__resizeColumns()
            self.__resort()
    
    def __doubleClicked(self, index):
        """
        Private slot to handle the double clicked signal.
        
        @param index index of the entry that was double clicked (QModelIndex)
        """
        if index.isValid():
            self.__editBreakpoint(index)

    def __editBreak(self):
        """
        Private slot to handle the edit breakpoint context menu entry.
        """
        index = self.currentIndex()
        if index.isValid():
            self.__editBreakpoint(index)
    
    def __editBreakpoint(self, index):
        """
        Private slot to edit a breakpoint.
        
        @param index index of breakpoint to be edited (QModelIndex)
        """
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            bp = self.__model.getBreakPointByIndex(sindex)
            if not bp:
                return
            
            fn, line, cond, temp, enabled, count = bp[:6]
            
            from .EditBreakpointDialog import EditBreakpointDialog
            dlg = EditBreakpointDialog(
                (fn, line), (cond, temp, enabled, count),
                self.condHistory, self, modal=True)
            if dlg.exec_() == QDialog.Accepted:
                cond, temp, enabled, count = dlg.getData()
                if cond:
                    if cond in self.condHistory:
                        self.condHistory.remove(cond)
                    self.condHistory.insert(0, cond)
                
                self.__model.setBreakPointByIndex(
                    sindex, fn, line, (cond, temp, enabled, count))
                self.__resizeColumns()
                self.__resort()

    def __setBpEnabled(self, index, enabled):
        """
        Private method to set the enabled status of a breakpoint.
        
        @param index index of breakpoint to be enabled/disabled (QModelIndex)
        @param enabled flag indicating the enabled status to be set (boolean)
        """
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            self.__model.setBreakPointEnabledByIndex(sindex, enabled)
        
    def __enableBreak(self):
        """
        Private slot to handle the enable breakpoint context menu entry.
        """
        index = self.currentIndex()
        self.__setBpEnabled(index, True)
        self.__resizeColumns()
        self.__resort()

    def __enableAllBreaks(self):
        """
        Private slot to handle the enable all breakpoints context menu entry.
        """
        index = self.model().index(0, 0)
        while index.isValid():
            self.__setBpEnabled(index, True)
            index = self.indexBelow(index)
        self.__resizeColumns()
        self.__resort()

    def __enableSelectedBreaks(self):
        """
        Private slot to handle the enable selected breakpoints context menu
        entry.
        """
        for index in self.selectedIndexes():
            if index.column() == 0:
                self.__setBpEnabled(index, True)
        self.__resizeColumns()
        self.__resort()

    def __disableBreak(self):
        """
        Private slot to handle the disable breakpoint context menu entry.
        """
        index = self.currentIndex()
        self.__setBpEnabled(index, False)
        self.__resizeColumns()
        self.__resort()

    def __disableAllBreaks(self):
        """
        Private slot to handle the disable all breakpoints context menu entry.
        """
        index = self.model().index(0, 0)
        while index.isValid():
            self.__setBpEnabled(index, False)
            index = self.indexBelow(index)
        self.__resizeColumns()
        self.__resort()

    def __disableSelectedBreaks(self):
        """
        Private slot to handle the disable selected breakpoints context menu
        entry.
        """
        for index in self.selectedIndexes():
            if index.column() == 0:
                self.__setBpEnabled(index, False)
        self.__resizeColumns()
        self.__resort()

    def __deleteBreak(self):
        """
        Private slot to handle the delete breakpoint context menu entry.
        """
        index = self.currentIndex()
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            self.__model.deleteBreakPointByIndex(sindex)
        
    def __deleteAllBreaks(self):
        """
        Private slot to handle the delete all breakpoints context menu entry.
        """
        self.__model.deleteAll()

    def __deleteSelectedBreaks(self):
        """
        Private slot to handle the delete selected breakpoints context menu
        entry.
        """
        idxList = []
        for index in self.selectedIndexes():
            sindex = self.__toSourceIndex(index)
            if sindex.isValid() and index.column() == 0:
                idxList.append(sindex)
        self.__model.deleteBreakPoints(idxList)

    def __showSource(self):
        """
        Private slot to handle the goto context menu entry.
        """
        index = self.currentIndex()
        sindex = self.__toSourceIndex(index)
        bp = self.__model.getBreakPointByIndex(sindex)
        if not bp:
            return
        
        fn, line = bp[:2]
        self.sourceFile.emit(fn, line)
    
    def highlightBreakpoint(self, fn, lineno):
        """
        Public slot to handle the clientLine signal.
        
        @param fn filename of the breakpoint (string)
        @param lineno line number of the breakpoint (integer)
        """
        sindex = self.__model.getBreakPointIndex(fn, lineno)
        if sindex.isValid():
            return
        
        index = self.__fromSourceIndex(sindex)
        if index.isValid():
            self.__clearSelection()
            self.__setRowSelected(index, True)
    
    def handleResetUI(self):
        """
        Public slot to reset the breakpoint viewer.
        """
        self.__clearSelection()
    
    def __showBackMenu(self):
        """
        Private slot to handle the aboutToShow signal of the background menu.
        """
        if self.model().rowCount() == 0:
            self.backMenuActions["EnableAll"].setEnabled(False)
            self.backMenuActions["DisableAll"].setEnabled(False)
            self.backMenuActions["DeleteAll"].setEnabled(False)
        else:
            self.backMenuActions["EnableAll"].setEnabled(True)
            self.backMenuActions["DisableAll"].setEnabled(True)
            self.backMenuActions["DeleteAll"].setEnabled(True)

    def __getSelectedItemsCount(self):
        """
        Private method to get the count of items selected.
        
        @return count of items selected (integer)
        """
        count = len(self.selectedIndexes()) // (self.__model.columnCount() - 1)
        # column count is 1 greater than selectable
        return count
    
    def __configure(self):
        """
        Private method to open the configuration dialog.
        """
        e5App().getObject("UserInterface").showPreferences(
            "debuggerGeneralPage")
Exemplo n.º 32
0
class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        self.setAttribute(Qt.WA_QuitOnClose)
        self.setAttribute(Qt.WA_DeleteOnClose)

        # create instance variables
        # ui
        self.ui = uic.loadUi('mainwindow.ui', self)

        # report manager
        self._reportManager = ReportManager(parent=self)
        self._archiveManager = ArchiveManager(parent=self, arcPath=arc_path)

        # report engines
        self._xlsxEngine = XlsxEngine(parent=self)
        self._reportManager.setEngine(self._xlsxEngine)
        # self._printEngine = PrintEngine(parent=self)
        # self._reportManager.setEngine(self._printEngine)

        # persistence engine
        # self._persistenceEngine = CsvEngine(parent=self)
        # self._persistenceEngine = SqliteEngine(parent=self)
        self._persistenceEngine = MysqlEngine(parent=self,
                                              dbItemClass=BillItem)

        # facades
        self._persistenceFacade = PersistenceFacade(
            parent=self, persistenceEngine=self._persistenceEngine)
        self._uiFacade = UiFacade(parent=self,
                                  reportManager=self._reportManager,
                                  archiveManager=self._archiveManager)

        # models
        # domain
        self._modelDomain = DomainModel(
            parent=self, persistenceFacade=self._persistenceFacade)

        # bill list + search proxy
        # TODO: use settings to set icon
        self._modelBillList = BillTableModel(
            parent=self,
            domainModel=self._modelDomain,
            docIcon=QPixmap('./icons/doc.png', 'PNG').scaled(22, 22),
            rightIcon=QPixmap('./icons/right.png', 'PNG').scaled(22, 22))
        self._modelBillSearchProxy = BillSearchProxyModel(parent=self)
        self._modelBillSearchProxy.setSourceModel(self._modelBillList)

        # bill plan + search proxy
        self._modelBillPlan = BillPlanModel(parent=self,
                                            domainModel=self._modelDomain)
        self._modelPlanSearchProxy = QSortFilterProxyModel(parent=self)
        self._modelPlanSearchProxy.setSourceModel(self._modelBillPlan)

        # orders + search proxy
        self._modelOrderList = OrderTableModel(
            parent=self,
            domainModel=self._modelDomain,
            docIcon=QPixmap('./icons/doc.png', 'PNG').scaled(22, 22),
            rightIcon=QPixmap('./icons/right.png', 'PNG').scaled(22, 22))
        self._modelOrderSearchProxy = QSortFilterProxyModel(parent=self)
        self._modelOrderSearchProxy.setSourceModel(self._modelOrderList)

        # connect ui facade to models
        self._uiFacade.setDomainModel(self._modelDomain)
        self._uiFacade.setBillModel(self._modelBillSearchProxy)
        self._uiFacade.setPlanModel(self._modelPlanSearchProxy)
        self._uiFacade.setOrderModel(self._modelOrderSearchProxy)

        # actions
        self.actRefresh = QAction('Обновить', self)
        self.actAddBillRecord = QAction('Добавить счёт...', self)
        self.actEditBillRecord = QAction('Изменить счёт...', self)
        self.actDeleteBillRecord = QAction('Удалить счёт...', self)
        self.actPrint = QAction('Распечатать...', self)
        self.actOpenDictEditor = QAction('Словари', self)
        self.actMakeBillFromOrder = QAction('Создать счёт...', self)
        self.actAddOrderRecord = QAction('Добавить заказ...', self)
        self.actEditOrderRecord = QAction('Изменить заказ...', self)
        self.actViewBillStats = QAction('Статистика по счетам...', self)
        self.actDeleteOrderRecord = QAction('Удалить заказ...', self)

    def buildWeekSelectionCombo(self):
        # TODO if more settings is needed, move all settings-related code to a separate class
        year, week, day = datetime.datetime.now().isocalendar()
        week_list = list()
        for i in range(1, isoweek.Week.last_week_of_year(year).week + 1):
            w = isoweek.Week(year, i)
            week_list.append(
                str(i) + ': ' + str(w.monday().strftime('%d.%m')) + '-' +
                str(w.friday().strftime('%d.%m')))

        self.ui.comboWeek.addItems(week_list)

        # TODO read settings
        if os.path.isfile('settings.ini'):
            with open('settings.ini', mode='tr') as f:
                line = f.readline()
            index = int(line.split('=')[1])
        else:
            index = week

        self.ui.comboWeek.setCurrentIndex(index - 1)
        self._modelBillPlan.updateHeader(index)

    def initApp(self):
        # TODO: extract methods
        # init instances
        # self._persistenceEngine.initEngine(fileName='ref/1.csv')
        # self._persistenceEngine.initEngine(fileName='sqlite3.db')
        self._persistenceEngine.initEngine()
        self._persistenceFacade.initFacade()
        # self._uiFacade.initFacade()
        self._modelDomain.initModel()

        self.show()
        if not self._uiFacade.requestLogin():
            sys.exit(5)

        self.setWindowTitle('Планировщик счетов, пользователь: ' +
                            self._modelDomain.getLoggedUserName())

        self._modelDomain.buildPlanData()
        self._modelBillList.initModel()
        self._modelBillPlan.initModel()
        self._modelOrderList.initModel()

        # init UI
        # bill list table
        self.ui.tableBill: QTableView
        self.ui.tableBill.setModel(self._modelBillSearchProxy)
        self.ui.tableBill.setSelectionMode(QAbstractItemView.SingleSelection)
        self.ui.tableBill.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.ui.tableBill.setEditTriggers(QAbstractItemView.NoEditTriggers)
        # draw delegates
        # self.ui.tableBill.setItemDelegateForRow(0, TableRowDelegate(self.ui.tableBill))
        # self.ui.tableBill.setHorizontalHeader(SectionHeaderView(Qt.Horizontal, parent=self.ui.tableBill))
        # formatting
        self.ui.tableBill.horizontalHeader().setDefaultAlignment(
            Qt.AlignCenter)
        self.ui.tableBill.horizontalHeader().setHighlightSections(False)
        self.ui.tableBill.horizontalHeader().setFixedHeight(24)
        self.ui.tableBill.horizontalHeader().setStretchLastSection(True)
        self.ui.tableBill.horizontalHeader().setStyleSheet(
            'QHeaderView::section {'
            '    padding: 4px;'
            '    border-style: none;'
            '    border-color: #000000;'
            '    border-bottom: 1px solid #000000;'
            '    border-right: 1px solid #000000;'
            '}'
            'QHeaderView::section:horizontal {'
            '    border-right: 1px solid #000000'
            '}')
        # self.ui.tableBill.horizontalHeader().setAutoFillBackground(False)
        self.ui.tableBill.verticalHeader().setVisible(False)
        # self.ui.tableBill.verticalHeader().setDefaultSectionSize(40)
        self.ui.tableBill.setWordWrap(True)
        self.ui.tableBill.resizeRowsToContents()
        self.ui.tableBill.setStyleSheet('QTableView { gridline-color : black}')
        self.hideBillTableColumns()
        # self.ui.tableBill.setSpan(0, 0, 1, 3)

        # bill plan table
        self.ui.tablePlan.setModel(self._modelPlanSearchProxy)
        self.ui.tablePlan.setSelectionMode(QAbstractItemView.NoSelection)
        self.ui.tablePlan.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.ui.tablePlan.setEditTriggers(QAbstractItemView.SelectedClicked)
        self.ui.tablePlan.horizontalHeader().setDefaultAlignment(
            Qt.AlignCenter)
        self.ui.tablePlan.horizontalHeader().setHighlightSections(False)
        self.ui.tablePlan.horizontalHeader().setFixedHeight(24)
        self.ui.tablePlan.horizontalHeader().setStretchLastSection(True)
        self.ui.tablePlan.horizontalHeader().setStyleSheet(
            'QHeaderView::section {'
            '    padding: 4px;'
            '    border-style: none;'
            '    border-color: #000000;'
            '    border-bottom: 1px solid #000000;'
            '    border-right: 1px solid #000000;'
            '}'
            'QHeaderView::section:horizontal {'
            '    border-right: 1px solid #000000'
            '}')
        self.ui.tablePlan.verticalHeader().setVisible(False)
        self.ui.tablePlan.hideColumn(0)
        # self.ui.tablePlan.hideColumn(3)
        self.ui.tablePlan.hideColumn(4)
        # self.ui.tablePlan.verticalHeader().setDefaultSectionSize(40)
        self.ui.tablePlan.setWordWrap(True)
        self.ui.tablePlan.resizeRowsToContents()
        # self.ui.tablePlan.setSpan(0, 0, 1, 3)
        self.ui.tablePlan.setStyleSheet(
            'QTableView { gridline-color : black }')
        # self.ui.tablePlan.setItemDelegateForRow(0, TableRowDelegate(self.ui.tablePlan))

        # bill order table
        self.ui.tableOrder: QTableView
        self.ui.tableOrder.setModel(self._modelOrderSearchProxy)
        self.ui.tableOrder.setSelectionMode(QAbstractItemView.SingleSelection)
        self.ui.tableOrder.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.ui.tableOrder.setEditTriggers(QAbstractItemView.NoEditTriggers)
        # formatting
        self.ui.tableOrder.horizontalHeader().setDefaultAlignment(
            Qt.AlignCenter)
        self.ui.tableOrder.horizontalHeader().setHighlightSections(False)
        self.ui.tableOrder.horizontalHeader().setFixedHeight(24)
        self.ui.tableOrder.horizontalHeader().setStretchLastSection(True)
        self.ui.tableOrder.horizontalHeader().setStyleSheet(
            'QHeaderView::section {'
            '    padding: 4px;'
            '    border-style: none;'
            '    border-color: #000000;'
            '    border-bottom: 1px solid #000000;'
            '    border-right: 1px solid #000000;'
            '}'
            'QHeaderView::section:horizontal {'
            '    border-right: 1px solid #000000'
            '}')
        self.ui.tableOrder.verticalHeader().setVisible(False)
        self.ui.tableOrder.setWordWrap(True)
        self.ui.tableOrder.resizeRowsToContents()
        self.ui.tableOrder.setStyleSheet(
            'QTableView { gridline-color : black}')

        # setup filter widgets
        self.ui.comboProjectFilter.setModel(self._modelDomain.dicts['project'])
        self.ui.comboStatusFilter.setModel(self._modelDomain.dicts['status'])
        self.ui.comboPriorityFilter.setModel(
            self._modelDomain.dicts['priority'])
        self.ui.comboShipmentFilter.setModel(
            self._modelDomain.dicts['shipment'])
        self.ui.dateFromFilter.setDate(
            QDate.fromString(self._modelDomain.getEarliestBillDate(),
                             'dd.MM.yyyy'))
        self.ui.dateUntilFilter.setDate(QDate.currentDate())

        # self.btnRefresh.setVisible(False)

        self.buildWeekSelectionCombo()

        # create actions
        self.initActions()

        # setup ui widget signals
        # buttons
        self.ui.btnRefresh.clicked.connect(self.onBtnRefreshClicked)
        self.ui.btnAddBill.clicked.connect(self.onBtnAddBillClicked)
        self.ui.btnEditBill.clicked.connect(self.onBtnEditBillClicked)
        self.ui.btnDeleteBill.clicked.connect(self.onBtnDeleteBillClicked)
        self.ui.btnPrint.clicked.connect(self.onBtnPrintClicked)
        self.ui.btnDictEditor.clicked.connect(self.onBtnDictEditorClicked)
        self.ui.btnMakeBillFromOrder.clicked.connect(
            self.onBtnMakeBillFromOrderClicked)
        self.ui.btnAddOrder.clicked.connect(self.onBtnAddOrderClicked)
        self.ui.btnEditOrder.clicked.connect(self.onBtnEditOrderClicked)
        self.ui.btnDelOrder.clicked.connect(self.onBtnDelOrderClicked)
        self.ui.btnBillStats.clicked.connect(self.onBtnBillStatsClicked)

        # table widgets
        self.ui.tableBill.doubleClicked.connect(self.onTableBillDoubleClicked)
        self.ui.tableBill.clicked.connect(self.onTableBillClicked)
        self.ui.tableOrder.doubleClicked.connect(
            self.onTableOrderDoubleClicked)
        self.ui.tableOrder.clicked.connect(self.onTableOrderClicked)
        self.ui.tabWidget.currentChanged.connect(self.onTabBarCurrentChanged)

        # model signals
        self._modelDomain.beginClearModel.connect(
            self._modelBillList.beginClearModel)
        self._modelDomain.beginClearModel.connect(
            self._modelBillPlan.beginClearModel)
        self._modelDomain.beginClearModel.connect(
            self._modelOrderList.beginClearModel)
        self._modelDomain.endClearModel.connect(
            self._modelBillList.endClearModel)
        self._modelDomain.endClearModel.connect(
            self._modelBillPlan.endClearModel)
        self._modelDomain.endClearModel.connect(
            self._modelOrderList.endClearModel)

        # totals update
        self._uiFacade.totalsChanged.connect(self.updateTotals)

        # search widgets
        self.ui.comboWeek.currentIndexChanged.connect(
            self.onComboWeekCurrentIndexChanged)
        self.ui.editSearch.textChanged.connect(self.setSearchFilter)
        self.ui.comboProjectFilter.currentIndexChanged.connect(
            self.setSearchFilter)
        self.ui.comboStatusFilter.currentIndexChanged.connect(
            self.setSearchFilter)
        self.ui.comboPriorityFilter.currentIndexChanged.connect(
            self.setSearchFilter)
        self.ui.comboShipmentFilter.currentIndexChanged.connect(
            self.setSearchFilter)
        self.ui.dateFromFilter.dateChanged.connect(self.setSearchFilter)
        self.ui.dateUntilFilter.dateChanged.connect(self.setSearchFilter)

        self.setSearchFilter()

        # widget tweaks
        # self.ui.btnRefresh.setVisible(False)
        self.updateTotals()

        self.prepareUi(self._modelDomain.getLoggedUserLevel())

    def initActions(self):
        self.actRefresh.setShortcut('Ctrl+R')
        self.actRefresh.setStatusTip('Обновить данные')
        self.actRefresh.triggered.connect(self.procActRefresh)

        self.actAddBillRecord.setShortcut('Ctrl+A')
        self.actAddBillRecord.setStatusTip('Добавить новый счёт')
        self.actAddBillRecord.triggered.connect(self.procActAddBillRecord)

        # self.actEditBillRecord.setShortcut('Ctrl+A')
        self.actEditBillRecord.setStatusTip('Добавить новый счёт')
        self.actEditBillRecord.triggered.connect(self.procActEditRecord)

        # self.actDeleteBillRecord.setShortcut('Ctrl+A')
        self.actDeleteBillRecord.setStatusTip('Добавить новый счёт')
        self.actDeleteBillRecord.triggered.connect(self.procActDeleteRecord)

        self.actPrint.setStatusTip('Напечатать текущую таблицу')
        self.actPrint.triggered.connect(self.procActPrint)

        self.actOpenDictEditor.setStatusTip('Открыть редактор словарей')
        self.actOpenDictEditor.triggered.connect(self.procActOpenDictEditor)

        self.actMakeBillFromOrder.setStatusTip('Создать счёт из заказа')
        self.actMakeBillFromOrder.triggered.connect(
            self.procActMakeBillFromOrder)

        self.actAddOrderRecord.setStatusTip('Добавить заказ')
        self.actAddOrderRecord.triggered.connect(self.procActAddOrderRecord)

        self.actEditOrderRecord.setStatusTip('Изменить заказ')
        self.actEditOrderRecord.triggered.connect(self.procActEditOrderRecord)

        self.actViewBillStats.setStatusTip(
            'Посмотреть статистику по счетам...')
        self.actViewBillStats.triggered.connect(self.procActViewBillStats)

        self.actDeleteOrderRecord.setStatusTip('Перенести заказ в архив...')
        self.actDeleteOrderRecord.triggered.connect(
            self.procActDeleteOrderRecord)

    def refreshView(self):
        screenRect = QApplication.desktop().screenGeometry()

        # if self.ui.tabWidget.currentIndex() == 0:
        tbwidth = screenRect.width() - 50
        self.ui.tableBill.setColumnWidth(0, tbwidth * 0.03)
        self.ui.tableBill.setColumnWidth(1, tbwidth * 0.05)
        self.ui.tableBill.setColumnWidth(2, tbwidth * 0.07)
        self.ui.tableBill.setColumnWidth(3, tbwidth * 0.05)  # +0.01
        self.ui.tableBill.setColumnWidth(4, tbwidth * 0.08)
        self.ui.tableBill.setColumnWidth(5, tbwidth * 0.05)
        self.ui.tableBill.setColumnWidth(6, tbwidth * 0.06)
        self.ui.tableBill.setColumnWidth(7, tbwidth * 0.20)  # +0.01
        self.ui.tableBill.setColumnWidth(8, tbwidth * 0.06)
        self.ui.tableBill.setColumnWidth(9, tbwidth * 0.06)
        self.ui.tableBill.setColumnWidth(10, tbwidth * 0.055)
        self.ui.tableBill.setColumnWidth(11, tbwidth * 0.055)
        self.ui.tableBill.setColumnWidth(12, tbwidth * 0.06)
        self.ui.tableBill.setColumnWidth(13, tbwidth * 0.035)
        # self.ui.tableBill.setColumnWidth(14, tbwidth * 0.03)
        self.ui.tableBill.setColumnWidth(15, tbwidth * 0.01)
        self.ui.tableBill.setColumnWidth(16, tbwidth * 0.01)
        self.ui.tableBill.setColumnWidth(17, tbwidth * 0.01)  # +0.01

        # elif self.ui.tabWidget.currentIndex() == 1:
        tpwidth = screenRect.width() - 45
        # 1 2 3 5 .. week count - 1
        # self.ui.tablePlan.setColumnWidth(0, tpwidth * 0.035)
        self.ui.tablePlan.setColumnWidth(1, tpwidth * 0.13)
        self.ui.tablePlan.setColumnWidth(2, tpwidth * 0.05)
        self.ui.tablePlan.setColumnWidth(3, tpwidth * 0.10)
        # self.ui.tablePlan.setColumnWidth(4, tpwidth * 0.06)
        self.ui.tablePlan.setColumnWidth(5, tpwidth * 0.09)
        self.ui.tablePlan.setColumnWidth(6, tpwidth * 0.09)
        self.ui.tablePlan.setColumnWidth(7, tpwidth * 0.09)
        self.ui.tablePlan.setColumnWidth(8, tpwidth * 0.09)
        self.ui.tablePlan.setColumnWidth(9, tpwidth * 0.09)
        self.ui.tablePlan.setColumnWidth(10, tpwidth * 0.09)
        self.ui.tablePlan.setColumnWidth(11, tpwidth * 0.09)
        self.ui.tablePlan.setColumnWidth(12, tpwidth * 0.09)

        # elif self.ui.tabWidget.currentIndex() == 2:
        towidth = screenRect.width() - 45
        self.ui.tableOrder.setColumnWidth(0, towidth * 0.02)
        self.ui.tableOrder.setColumnWidth(1, towidth * 0.30)
        self.ui.tableOrder.setColumnWidth(2, towidth * 0.20)
        self.ui.tableOrder.setColumnWidth(3, towidth * 0.06)
        self.ui.tableOrder.setColumnWidth(4, towidth * 0.05)
        self.ui.tableOrder.setColumnWidth(5, towidth * 0.06)
        self.ui.tableOrder.setColumnWidth(6, towidth * 0.06)
        self.ui.tableOrder.setColumnWidth(7, towidth * 0.07)
        self.ui.tableOrder.setColumnWidth(8, towidth * 0.07)
        self.ui.tableOrder.setColumnWidth(9, towidth * 0.07)
        self.ui.tableOrder.setColumnWidth(10, towidth * 0.02)
        self.ui.tableOrder.setColumnWidth(11, towidth * 0.01)

    def prepareUi(self, level):
        if level == 1:
            self.ui.tabWidget.setCurrentIndex(0)
            index = self._modelBillSearchProxy.mapFromSource(
                self._modelBillList.index(
                    self._modelBillList.rowCount(QModelIndex()) - 1, 0))
            self.ui.tableBill.scrollTo(index, QAbstractItemView.EnsureVisible)
            self.ui.tableBill.selectionModel().setCurrentIndex(
                index, QItemSelectionModel.Select
                | QItemSelectionModel.Rows)
        elif level == 2 or level == 3:
            print('approver + user')
            self.ui.tabWidget.setCurrentIndex(2)
            self.ui.tabWidget.removeTab(0)
            self.ui.tabWidget.removeTab(0)
            self.ui.btnMakeBillFromOrder.hide()
            self.ui.btnDictEditor.hide()
            self.ui.btnPrint.hide()
            self.ui.btnRefresh.hide()
            self.ui.btnBillStats.hide()

    def hideBillTableColumns(self):
        # self.ui.tableBill.hideColumn(11)
        self.ui.tableBill.hideColumn(14)

    def hideOrderTableColumns(self):
        self.ui.tableOrder.hideColumn(3)

    # ui events
    def onBtnRefreshClicked(self):
        self.actRefresh.trigger()

    def onBtnAddBillClicked(self):
        self.actAddBillRecord.trigger()

    def onBtnEditBillClicked(self):
        self.actEditBillRecord.trigger()

    def onBtnDeleteBillClicked(self):
        self.actDeleteBillRecord.trigger()

    def onBtnDictEditorClicked(self):
        self.actOpenDictEditor.trigger()

    def onBtnPrintClicked(self):
        self.actPrint.trigger()

    def onBtnMakeBillFromOrderClicked(self):
        self.actMakeBillFromOrder.trigger()

    def onBtnBillStatsClicked(self):
        self.actViewBillStats.trigger()

    def onBtnAddOrderClicked(self):
        self.actAddOrderRecord.trigger()

    def onBtnEditOrderClicked(self):
        self.actEditOrderRecord.trigger()

    def onBtnDelOrderClicked(self):
        self.actDeleteOrderRecord.trigger()

    def onTableBillClicked(self, index):
        col = index.column()
        if col == self._modelBillList.ColumnDoc:
            doc = index.data(Qt.EditRole)
            if doc:
                subprocess.Popen(r'explorer /select,"' +
                                 index.data(Qt.EditRole).replace("/", "\\") +
                                 '"')
        elif col == self._modelBillList.ColumnOrder:
            orderId = index.data(const.RoleOrderId)
            if not orderId:
                return
            self.ui.tabWidget.setCurrentIndex(2)
            rowToSelect = self._modelOrderList.getRowById(orderId)
            indexToSelect = self._modelOrderSearchProxy.mapFromSource(
                self._modelOrderList.index(rowToSelect, 0, QModelIndex()))
            self.ui.tableOrder.selectRow(indexToSelect.row())

    def onTableBillDoubleClicked(self, index):
        self.actEditBillRecord.trigger()

    def onTableOrderClicked(self, index):
        col = index.column()
        if col == self._modelOrderList.ColumnBill:
            billId = self._modelDomain.getBillIdForOrderId(
                index.data(const.RoleNodeId))
            if billId:
                self.ui.tabWidget.setCurrentIndex(0)
                rowToSelect = self._modelBillList.getRowById(billId)
                indexToSelect = self._modelBillSearchProxy.mapFromSource(
                    self._modelBillList.index(rowToSelect, 0, QModelIndex()))
                self.ui.tableBill.selectRow(indexToSelect.row())
        if col == self._modelOrderList.ColumnDoc:
            doc = index.data(Qt.EditRole)
            if doc:
                subprocess.Popen(r'explorer /select,"' +
                                 index.data(Qt.EditRole).replace("/", "\\") +
                                 '"')

    def onTableOrderDoubleClicked(self, index):
        self.actEditOrderRecord.trigger()

    def onTabBarCurrentChanged(self, index):
        if index == 1:
            self._modelDomain.buildPlanData()

    def onComboWeekCurrentIndexChanged(self, index):
        self._modelBillPlan.updateHeader(index + 1)

    # misc events
    def resizeEvent(self, event):
        # print('resize event')
        self.refreshView()
        # self.ui.tableBill.resizeRowsToContents()
        # self.ui.tablePlan.resizeRowsToContents()
        self.ui.tableOrder.resizeRowsToContents()

    def closeEvent(self, *args, **kwargs):
        # TODO error handling on saving before exiting
        self._uiFacade.requestExit(self.ui.comboWeek.currentIndex())
        super(MainWindow, self).closeEvent(*args, **kwargs)

    # action processing
    # send user commands to the ui facade: (command, parameters (like indexes, etc.))
    def procActRefresh(self):
        print('act refresh trigger')

        try:
            if self._modelDomain.savePlanData():
                print('plan data saved')
            else:
                QMessageBox.information(
                    self, 'Ошибка',
                    'Ошибка подключения к БД при попытке отбновления данных.\nОбратитесь к разработчику.'
                )
        except Exception as ex:
            print(ex)

        self._modelDomain.clearModel()

        self._modelDomain.initModel()
        self._modelBillList.initModel()
        self._modelBillPlan.initModel()
        self._modelOrderList.initModel()

        self._uiFacade.requestRefresh()
        self.refreshView()
        self.ui.tableBill.resizeRowsToContents()
        self.ui.tablePlan.resizeRowsToContents()
        self.ui.tableOrder.resizeRowsToContents()
        self.hideBillTableColumns()
        self.hideOrderTableColumns()

    def procActAddBillRecord(self):
        row = self._uiFacade.requestAddBillRecord()

        if row is not None:
            index = self._modelBillSearchProxy.mapFromSource(
                self._modelBillList.index(row, 0))
            self.ui.tableBill.scrollTo(index)
            self.ui.tableBill.selectionModel().setCurrentIndex(
                index, QItemSelectionModel.Select
                | QItemSelectionModel.Rows)

    def procActEditRecord(self):
        if not self.ui.tableBill.selectionModel().hasSelection():
            QMessageBox.information(self, 'Ошибка',
                                    'Изменить: пожалуйста, выберите запись.')
            return

        selectedIndex = self.ui.tableBill.selectionModel().selectedIndexes()[0]
        self._uiFacade.requestEditBillRecord(
            self._modelBillSearchProxy.mapToSource(selectedIndex))

    def procActDeleteRecord(self):
        # print('act delete record trigger')
        if not self.ui.tableBill.selectionModel().hasSelection():
            QMessageBox.information(self, 'Ошибка',
                                    'Удалить: пожалуйста, выберите запись.')
            return

        selectedIndex: QModelIndex = self.ui.tableBill.selectionModel(
        ).selectedIndexes()[0]
        # TODO: HACK -- set active to false before deleting a bill
        self._modelBillList.setData(selectedIndex, 2, Qt.CheckStateRole)
        self._uiFacade.requestDeleteRecord(
            self._modelBillSearchProxy.mapToSource(selectedIndex))

    def procActMakeBillFromOrder(self):
        if not self.ui.tableOrder.selectionModel().hasSelection():
            QMessageBox.information(
                self, 'Ошибка', 'Выберите запись о заказе для создания счёта.')
            return

        selectedIndex: QModelIndex = self.ui.tableOrder.selectionModel(
        ).selectedIndexes()[0]
        row = self._uiFacade.requestMakeBillFromOrder(
            self._modelOrderSearchProxy.mapToSource(selectedIndex))

        if row is not None:
            self.ui.tabWidget.setCurrentIndex(0)
            index = self._modelBillSearchProxy.mapFromSource(
                self._modelBillList.index(row, 0))
            self.ui.tableBill.scrollTo(index)
            self.ui.tableBill.selectionModel().setCurrentIndex(
                index, QItemSelectionModel.Select
                | QItemSelectionModel.Rows)

    def procActAddOrderRecord(self):
        row = self._uiFacade.requestAddOrderRecord()

        if row is not None:
            index = self._modelOrderSearchProxy.mapFromSource(
                self._modelOrderList.index(row, 0))
            self.ui.tableOrder.scrollTo(index)
            self.ui.tableOrder.selectionModel().setCurrentIndex(
                index, QItemSelectionModel.Select
                | QItemSelectionModel.Rows)

    def procActEditOrderRecord(self):
        if not self.ui.tableOrder.selectionModel().hasSelection():
            QMessageBox.information(self, 'Ошибка',
                                    'Изменить: пожалуйста, выберите запись.')
            return

        selectedIndex = self.ui.tableOrder.selectionModel().selectedIndexes(
        )[0]
        self._uiFacade.requestEditOrderRecord(
            self._modelOrderSearchProxy.mapToSource(selectedIndex))

    def procActDeleteOrderRecord(self):
        if not self.ui.tableOrder.selectionModel().hasSelection():
            QMessageBox.information(
                self, 'Ошибка',
                'Перенести в архив: пожалуйста, выберите запись.')
            return

        selectedIndex = self.ui.tableOrder.selectionModel().selectedIndexes(
        )[0]
        self._uiFacade.requestDeleteOrderRecord(
            self._modelOrderSearchProxy.mapToSource(selectedIndex))

    def procActPrint(self):
        self._uiFacade.requestPrint(self.ui.tabWidget.currentIndex(),
                                    self._modelDomain.getBillTotals())

    def setSearchFilter(self, dummy=0):
        self._modelBillSearchProxy.filterString = self.ui.editSearch.text()
        self._modelBillSearchProxy.filterProject = self.ui.comboProjectFilter.currentData(
            const.RoleNodeId)
        self._modelBillSearchProxy.filterStatus = self.ui.comboStatusFilter.currentData(
            const.RoleNodeId)
        self._modelBillSearchProxy.filterPriority = self.ui.comboPriorityFilter.currentData(
            const.RoleNodeId)
        self._modelBillSearchProxy.filterShipment = self.ui.comboShipmentFilter.currentData(
            const.RoleNodeId)
        self._modelBillSearchProxy.filterFromDate = self.ui.dateFromFilter.date(
        )
        self._modelBillSearchProxy.filterUntilDate = self.ui.dateUntilFilter.date(
        )
        self._modelBillSearchProxy.invalidate()

        # self._modelPlanSearchProxy.setFilterWildcard(self.ui.editSearch.text())
        # self._modelPlanSearchProxy.invalidate()
        #
        # self._modelOrderSearchProxy.setFilterWildcard(self.ui.editSearch.text())
        # self._modelPlanSearchProxy.invalidate()

        self.hideBillTableColumns()
        self.hideOrderTableColumns()
        self.refreshView()

    def procActOpenDictEditor(self):
        self._uiFacade.requestOpenDictEditor()

    def procActViewBillStats(self):
        self._uiFacade.requestViewBillStats()

    @pyqtSlot()
    def updateTotals(self):
        print("update totals")
        p, r, t = self._modelDomain.getBillTotals()
        self.ui.lblTotal.setText(
            f'Оплачено:  <span style="background-color:#92D050">{f"{p/100:,.2f}".replace(",", " ")}</span><br>'
            f'Осталось:  <span style="background-color:#FF6767">{f"{r/100:,.2f}".replace(",", " ")}</span><br>'
            f'Всего:  {f"{t/100:,.2f}".replace(",", " ")}')