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)
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()
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")
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()
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()
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()
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)
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...")
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"))
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)
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
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
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()
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)
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
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())
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)
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
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()
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()
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)
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")
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"))
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)
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)
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...")
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()
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()
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")
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(",", " ")}')