def generate_model(self) -> Tuple[QStandardItemModel, QModelIndex]: """Generate a Qt Model based on the project structure.""" model = QStandardItemModel() root = model.invisibleRootItem() # TODO: Add these icon resources to library or something so they are not loaded every time dgs_ico = QIcon('ui/assets/DGSIcon.xpm') flt_ico = QIcon('ui/assets/flight_icon.png') prj_header = QStandardItem( dgs_ico, "{name}: {path}".format(name=self.name, path=self.projectdir)) prj_header.setEditable(False) fli_header = QStandardItem(flt_ico, "Flights") fli_header.setEditable(False) # TODO: Add a human readable identifier to flights first_flight = None for uid, flight in self.flights.items(): fli_item = QStandardItem(flt_ico, "Flight: {}".format(flight.name)) if first_flight is None: first_flight = fli_item fli_item.setToolTip("UUID: {}".format(uid)) fli_item.setEditable(False) fli_item.setData(flight, QtCore.Qt.UserRole) gps_path, gps_uid = flight.gps_file gps = QStandardItem("GPS: {}".format(gps_uid)) gps.setToolTip("File Path: {}".format(gps_path)) gps.setEditable(False) gps.setData(gps_uid) # For future use grav_path, grav_uid = flight.gravity_file if grav_path is not None: _, grav_fname = os.path.split(grav_path) else: grav_fname = '<None>' grav = QStandardItem("Gravity: {}".format(grav_fname)) grav.setToolTip("File Path: {}".format(grav_path)) grav.setEditable(False) grav.setData(grav_uid) # For future use fli_item.appendRow(gps) fli_item.appendRow(grav) for line in flight: line_item = QStandardItem("Line {}:{}".format( line.start, line.end)) line_item.setEditable(False) fli_item.appendRow(line_item) fli_header.appendRow(fli_item) prj_header.appendRow(fli_header) root.appendRow(prj_header) self.log.debug("Tree Model generated") first_index = model.indexFromItem(first_flight) return model, first_index
class ListView(QListView): def __init__(self, *args, **kwargs): super(ListView, self).__init__(*args, **kwargs) # 模型 self._model = QStandardItemModel(self) self.setModel(self._model) # 循环生成10个自定义控件 for i in range(10): item = QStandardItem() self._model.appendRow(item) # 添加item # 得到索引 index = self._model.indexFromItem(item) widget = CustomWidget(str(i)) item.setSizeHint(widget.sizeHint()) # 主要是调整item的高度 # 设置自定义的widget self.setIndexWidget(index, widget)
def updateImagesList(self): index, selected = self.getSelected() model = QStandardItemModel(self.imagesList) self.primaryImage.clear() selectedIndex = None for i in self.project["images"]: item = QStandardItem(i) item.setEditable(False) model.appendRow(item) self.primaryImage.addItem(i) if i == selected: selectedIndex = model.indexFromItem(item) self.imagesList.setModel(model) if selectedIndex != None: self.imagesList.setCurrentIndex(selectedIndex) if "primary" in self.project: self.primaryImage.setCurrentText( self.project["images"][self.project["primary"]])
class DataChangeUI(object): def __init__(self, window, uaclient, hub_manager): self.window = window self.uaclient = uaclient # FIXME IoT stuff self.hub_manager = hub_manager self._subhandler = DataChangeHandler(self.hub_manager) self._subscribed_nodes = [] self.model = QStandardItemModel() self.window.ui.subView.setModel(self.model) self.window.ui.subView.horizontalHeader().setSectionResizeMode(1) self.window.ui.actionSubscribeDataChange.triggered.connect(self._subscribe) self.window.ui.actionUnsubscribeDataChange.triggered.connect(self._unsubscribe) # populate contextual menu self.window.ui.treeView.addAction(self.window.ui.actionSubscribeDataChange) self.window.ui.treeView.addAction(self.window.ui.actionUnsubscribeDataChange) # handle subscriptions self._subhandler.data_change_fired.connect(self._update_subscription_model, type=Qt.QueuedConnection) def clear(self): self._subscribed_nodes = [] self.model.clear() def _subscribe(self): node = self.window.get_current_node() if node is None: return if node in self._subscribed_nodes: print("allready subscribed to node: ", node) return self.model.setHorizontalHeaderLabels(["DisplayName", "Value", "Timestamp"]) row = [QStandardItem(node.display_name), QStandardItem("No Data yet"), QStandardItem("")] row[0].setData(node) self.model.appendRow(row) self._subscribed_nodes.append(node) self.window.ui.subDockWidget.raise_() try: self.uaclient.subscribe_datachange(node, self._subhandler) except Exception as ex: self.window.show_error(ex) idx = self.model.indexFromItem(row[0]) self.model.takeRow(idx.row()) def _unsubscribe(self): node = self.window.get_current_node() if node is None: return self.uaclient.unsubscribe_datachange(node) self._subscribed_nodes.remove(node) i = 0 while self.model.item(i): item = self.model.item(i) if item.data() == node: self.model.removeRow(i) i += 1 def _update_subscription_model(self, node, value, timestamp): i = 0 while self.model.item(i): item = self.model.item(i) if item.data() == node: it = self.model.item(i, 1) it.setText(value) it_ts = self.model.item(i, 2) it_ts.setText(timestamp) i += 1
class DataChangeUI(object): def __init__(self, window, opc_ua_client, view): self.window = window self.opc_ua_client = opc_ua_client self._subhandler = DataChangeHandler() self.subscribed_nodes = [] self.view = view self.model = QStandardItemModel() self.model.setHorizontalHeaderLabels(["ObjectName", "VariableName", "Value", "Timestamp"]) self.view.setModel(self.model) # handle subscriptions self._subhandler.data_change_fired.connect(self._update_subscription_model, type=Qt.QueuedConnection) # accept drops self.model.canDropMimeData = self.canDropMimeData self.model.dropMimeData = self.dropMimeData # Context menu self.view.setContextMenuPolicy(Qt.CustomContextMenu) self.view.customContextMenuRequested.connect(self.showContextMenu) actionDeleteMonitoredItem = QAction("Delete Monitored Item", self.model) actionDeleteMonitoredItem.triggered.connect(self._delete_monitored_item_context) self._contextMenu = QMenu() self._contextMenu.addAction(actionDeleteMonitoredItem) self.view.selectionModel().selectionChanged.connect(self.highlight_node) def highlight_node(self, selection): if isinstance(selection, QItemSelection): if not selection.indexes(): # no selection return idx = self.view.currentIndex() idx = idx.siblingAtColumn(0) it = self.model.itemFromIndex(idx) if not it: return node = it.data() self.window.tree_ui.expand_to_node(node) def canDropMimeData(self, mdata, action, row, column, parent): node = self.opc_ua_client.client.get_node(mdata.text()) if node.get_node_class() == ua.NodeClass.Variable: return True return False def dropMimeData(self, mdata, action, row, column, parent): node = self.opc_ua_client.client.get_node(mdata.text()) self.window.add_monitored_item(node) return True def showContextMenu(self, position): item = self.get_current_item() if item: self._contextMenu.exec_(self.view.viewport().mapToGlobal(position)) def get_current_item(self, col_idx=0): idx = self.view.currentIndex() idx = idx.siblingAtColumn(col_idx) return self.model.itemFromIndex(idx) def _delete_monitored_item_context(self): it = self.get_current_item() if it: index = self.window.ui.tabWidget.currentIndex() self.delete_monitored_item(index, it.data()) def clear(self): self.subscribed_nodes = [] # remove all rows but not header self.model.removeRows(0, self.model.rowCount()) def show_error(self, *args): self.window.show_error(*args) def create_subscription(self): self.opc_ua_client.create_subscription(self._subhandler) def delete_subscription(self, index): self.opc_ua_client.delete_subscription(index) @trycatchslot def add_monitored_item(self, index, node): variable_name = node.get_display_name().Text descriptions = node.get_references(ua.ObjectIds.Aggregates, ua.BrowseDirection.Inverse, ua.NodeClass.Object, True) parent_node = node while not descriptions: parent_node = parent_node.get_parent() descriptions = parent_node.get_references(ua.ObjectIds.Aggregates, ua.BrowseDirection.Inverse, ua.NodeClass.Object, True) parent_nodeid = descriptions[0].NodeId if parent_nodeid in self.opc_ua_client.custom_objects: custom_type = self.opc_ua_client.custom_objects[parent_nodeid] icon = get_icon(custom_type) else: icon = "icons/object.svg" row = [QStandardItem(QIcon(icon), descriptions[0].DisplayName.Text), QStandardItem(variable_name), QStandardItem("No Data yet"), QStandardItem("")] row[0].setData(node) self.model.appendRow(row) self.subscribed_nodes.append(node) try: self.opc_ua_client.create_monitored_items(node, index) row[0].setData(self.window.get_monitored_item_tooltip(), Qt.ToolTipRole) self.model.sort(0, Qt.AscendingOrder) self._color_rows() except Exception as ex: self.window.show_error(ex) idx = self.model.indexFromItem(row[0]) self.model.takeRow(idx.row()) self.subscribed_nodes.remove(node) raise def _color_rows(self): n_rows = self.model.rowCount() if n_rows > 1: change_color = False for row in range(1, n_rows): prev_object_name = self.model.data(self.model.index(row - 1, 0)) curr_object_name = self.model.data(self.model.index(row, 0)) if curr_object_name != prev_object_name: change_color = not change_color if change_color: color = QColor(240, 240, 240) # grey else: color = QColor(255, 255, 255) # white for col in range(self.model.columnCount()): item = self.model.item(row, col) item.setData(QBrush(color), Qt.BackgroundRole) @trycatchslot def delete_monitored_item(self, index, node=None): if not isinstance(node, Node): node = self.window.get_current_node() if node is None: return self.opc_ua_client.remove_monitored_item(node, index) self.subscribed_nodes.remove(node) i = 0 while self.model.item(i): item = self.model.item(i) if item.data() == node: self.model.removeRow(i) i += 1 def _update_subscription_model(self, node, value, status_code, timestamp): i = 0 while self.model.item(i): item = self.model.item(i) if item.data() == node: it = self.model.item(i, 2) it.setText(value) if status_code == ua.StatusCodes.Good: it.setForeground(QBrush(QColor("green"))) elif status_code == ua.StatusCodes.Uncertain: it.setForeground(QBrush(QColor("yellow"))) else: # StatusCode = Bad: it.setForeground(QBrush(QColor("red"))) it_ts = self.model.item(i, 3) it_ts.setText(timestamp) i += 1
class IsosViewer(QMainWindow, mageiaSyncUI.Ui_mainWindow): # Display the main window def __init__(self, parent=None): super(IsosViewer, self).__init__(parent) self.setupUi(self) self.connectActions() self.IprogressBar.setMinimum(0) self.IprogressBar.setMaximum(100) self.IprogressBar.setValue(0) self.IprogressBar.setEnabled(False) self.selectAllState=True self.stop.setEnabled(False) self.destination='' self.rsyncThread = mageiaSyncExt.syncThread(self) # create a thread to launch rsync self.rsyncThread.progressSignal.connect(self.setProgress) self.rsyncThread.speedSignal.connect(self.setSpeed) self.rsyncThread.sizeSignal.connect(self.setSize) self.rsyncThread.remainSignal.connect(self.setRemain) self.rsyncThread.endSignal.connect(self.syncEnd) self.rsyncThread.lvM.connect(self.lvMessage) self.rsyncThread.checkSignal.connect(self.checks) self.checkThreads=[] # A list of thread for each iso # Model for list view in a table self.model = QStandardItemModel(0, 6, self) headers=[self.tr("Directory"),self.tr("Name"),self.tr("Size"),self.tr("Date"),"SHA1","MD5"] i=0 for label in headers: self.model.setHeaderData(i, QtCore.Qt.Horizontal,label ) i+=1 # settings for the list view self.localList.setModel(self.model) self.localList.setColumnWidth(0,220) self.localList.setColumnWidth(1,220) self.localList.horizontalHeader().setStretchLastSection(True) # settings for local iso names management self.localListNames=[] def multiSelect(self): # allows to select multiple lines in remote ISOs list self.listIsos.setSelectionMode(2) def add(self, iso): # Add an remote ISO in list self.listIsos.addItem(iso) def localAdd(self, path,iso,isoSize): # Add an entry in local ISOs list, with indications about checking itemPath=QStandardItem(path) itemIso=QStandardItem(iso) if isoSize==0: itemSize=QStandardItem('--') else: formatedSize='{:n}'.format(isoSize).replace(","," ") itemSize=QStandardItem(formatedSize) itemSize.setTextAlignment(QtCore.Qt.AlignVCenter|QtCore.Qt.AlignHCenter) itemDate=QStandardItem("--/--/--") itemDate.setTextAlignment(QtCore.Qt.AlignVCenter|QtCore.Qt.AlignHCenter) itemCheck1=QStandardItem("--") itemCheck1.setTextAlignment(QtCore.Qt.AlignVCenter|QtCore.Qt.AlignHCenter) itemCheck5=QStandardItem("--") itemCheck5.setTextAlignment(QtCore.Qt.AlignVCenter|QtCore.Qt.AlignHCenter) self.model.appendRow([itemPath,itemIso,itemSize,itemDate, itemCheck1, itemCheck5,]) self.localListNames.append([path,iso]) def setProgress(self, value): # Update the progress bar self.IprogressBar.setValue(value) def setSpeed(self, value): # Update the speed field self.speedLCD.display(value) def setSize(self, size): # Update the size field self.Lsize.setText(size+self.tr(" bytes")) def setRemain(self,remainTime): content=QtCore.QTime.fromString(remainTime,"h:mm:ss") self.timeRemaining.setTime(content) def manualChecks(self): for iso in self.listIsos.selectedItems(): path,name=iso.text().split('/') try: # Look for ISO in local list item=self.model.findItems(name,QtCore.Qt.MatchExactly,1)[0] except: # Remote ISO is not yet in local directory. We add it in localList and create the directory self.localAdd(path,name,0) basedir=QtCore.QDir(self.destination) basedir.mkdir(path) item=self.model.findItems(name,QtCore.Qt.MatchExactly,1)[0] row=self.model.indexFromItem(item).row() self.checks(row) def checks(self,isoIndex): # processes a checking for each iso # launches a thread for each iso newThread=mageiaSyncExt.checkThread(self) self.checkThreads.append(newThread) self.checkThreads[-1].setup(self.destination, self.model.data(self.model.index(isoIndex,0)) , self.model.data(self.model.index(isoIndex,1)), isoIndex) self.checkThreads[-1].md5Signal.connect(self.md5Check) self.checkThreads[-1].sha1Signal.connect(self.sha1Check) self.checkThreads[-1].dateSignal.connect(self.dateCheck) self.checkThreads[-1].sizeFinalSignal.connect(self.sizeUpdate) self.checkThreads[-1].checkStartSignal.connect(self.checkStart) self.checkThreads[-1].start() def checkStart(self,isoIndex): # the function indicates that checking is in progress # the hundred contains index of the value to check, the minor value contains the row col=(int)(isoIndex/100) row=isoIndex-col*100 self.model.setData(self.model.index(row, col, QtCore.QModelIndex()), self.tr("Checking")) def md5Check(self,check): if check>=128: val=self.tr("OK") row=check-128 else: val=self.tr("Failed") row=check self.model.setData(self.model.index(row, 5, QtCore.QModelIndex()), val) def sha1Check(self,check): if check>=128: val=self.tr("OK") row=check-128 else: val=self.tr("Failed") row=check self.model.setData(self.model.index(row, 4, QtCore.QModelIndex()), val) def dateCheck(self,check): if check>=128: val=self.tr("OK") row=check-128 else: val=self.tr("Failed") row=check self.model.setData(self.model.index(row, 3, QtCore.QModelIndex()), val) def sizeUpdate(self,signal,isoSize): col=(int)(signal/100) row=signal-col*100 self.model.setData(self.model.index(row, col, QtCore.QModelIndex()), isoSize) def syncEnd(self, rc): if rc==1: self.lvMessage(self.tr("Command rsync not found")) elif rc==2: self.lvMessage(self.tr("Error in rsync parameters")) elif rc==3: self.lvMessage(self.tr("Unknown error in rsync")) self.IprogressBar.setEnabled(False) self.syncGo.setEnabled(True) self.listIsos.setEnabled(True) self.selectAll.setEnabled(True) self.stop.setEnabled(False) def prefsInit(self): # Load the parameters at first params=QtCore.QSettings("Mageia","mageiaSync") paramRelease="" try: paramRelease=params.value("release", type="QString") # the parameters already initialised? except: pass if paramRelease =="": # Values are not yet set self.pd0=prefsDialog0() self.pd0.user.setFocus() answer=self.pd0.exec_() if answer: # Update params self.user=self.pd0.user.text() self.password=self.pd0.password.text() self.location=self.pd0.location.text() params=QtCore.QSettings("Mageia","mageiaSync") params.setValue("user",self.user) params.setValue("password",self.password) params.setValue("location",self.location) else: pass # answer=QDialogButtonBox(QDialogButtonBox.Ok) # the user must set values or default values self.pd0.close() self.pd=prefsDialog() if self.password !="": code,list=mageiaSyncExt.findRelease('rsync://'+self.user+'@bcd.mageia.org/isos/',self.password) if code==0: for item in list: self.pd.release.addItem(item) self.pd.password.setText(self.password) self.pd.user.setText(self.user) self.pd.location.setText(self.location) self.pd.selectDest.setText(QtCore.QDir.currentPath()) self.pd.release.setFocus() answer=self.pd.exec_() if answer: # Update params self.user=self.pd.user.text() self.password=self.pd.password.text() self.location=self.pd.location.text() params=QtCore.QSettings("Mageia","mageiaSync") self.release= self.pd.release.currentText() self.destination=self.pd.selectDest.text() self.bwl=self.pd.bwl.value() params.setValue("release", self.release) params.setValue("user",self.user) params.setValue("password",self.password) params.setValue("location",self.location) params.setValue("destination",self.destination) params.setValue("bwl",str(self.bwl)) else: pass # answer=QDialogButtonBox(QDialogButtonBox.Ok) print(self.tr("the user must set values or default values")) self.pd.close() else: self.release=params.value("release", type="QString") self.user=params.value("user", type="QString") self.location=params.value("location", type="QString") self.password=params.value("password", type="QString") self.destination=params.value("destination", type="QString") self.bwl=params.value("bwl",type=int) self.localDirLabel.setText(self.tr("Local directory: ")+self.destination) if self.location !="": self.remoteDirLabel.setText(self.tr("Remote directory: ")+self.location) def selectDestination(self): # dialog box to select the destination (local directory) directory = QFileDialog.getExistingDirectory(self, self.tr('Select a directory'),'~/') isosSync.destination = directory self.pd.selectDest.setText(isosSync.destination) def selectAllIsos(self): # Select or unselect the ISOs in remote list if self.selectAllState : for i in range(self.listIsos.count()): self.listIsos.item(i).setSelected(True) self.selectAll.setText(self.tr("Unselect &All")) else: for i in range(self.listIsos.count()): self.listIsos.item(i).setSelected(False) self.selectAll.setText(self.tr("Select &All")) self.selectAllState=not self.selectAllState def connectActions(self): self.actionQuit.triggered.connect(app.quit) self.quit.clicked.connect(app.quit) self.actionRename.triggered.connect(self.rename) self.actionUpdate.triggered.connect(self.updateList) self.actionCheck.triggered.connect(self.manualChecks) self.actionPreferences.triggered.connect(self.prefs) self.syncGo.clicked.connect(self.launchSync) self.selectAll.clicked.connect(self.selectAllIsos) def updateList(self): # From the menu entry self.lw = LogWindow() self.lw.show() self.listIsos.clear() self.model.removeRows(0,self.model.rowCount()) if self.location == "" : self.nameWithPath='rsync://'+self.user+'@bcd.mageia.org/isos/'+self.release+'/' # print self.nameWithPath else: self.nameWithPath=self.location+'/' self.lvMessage(self.tr("Source: ")+self.nameWithPath) self.fillList = mageiaSyncExt.findIsos() self.fillList.setup(self.nameWithPath, self.password,self.destination) self.fillList.endSignal.connect(self.closeFill) self.fillList.start() # Reset the button self.selectAll.setText(self.tr("Select &All")) self.selectAllState=True def lvMessage( self,message): # Add a line in the logview self.lvText.append(message) def renameDir(self): # Choose the directory where isos are stored directory = QFileDialog.getExistingDirectory(self, self.tr('Select a directory'),self.destination) self.rd.chooseDir.setText(directory) def rename(self): # rename old isos and directories to a new release self.rd=renameDialog() loc=[] loc=self.location.split('/') self.rd.oldRelease.setText(loc[-1]) self.rd.chooseDir.setText(self.destination) answer=self.rd.exec_() if answer: returnMsg=mageiaSyncExt.rename(self.rd.chooseDir.text(),self.rd.oldRelease.text(),str(self.rd.newRelease.text())) self.lvMessage(returnMsg) self.rd.close() def prefs(self): # From the menu entry self.pd=prefsDialog() self.pd.release.addItem(self.release) self.pd.password.setText(self.password) self.pd.user.setText(self.user) self.pd.location.setText(self.location) self.pd.selectDest.setText(self.destination) self.pd.bwl.setValue(self.bwl) params=QtCore.QSettings("Mageia","mageiaSync") answer=self.pd.exec_() if answer: params.setValue("release", self.pd.release.currentText()) params.setValue("user",self.pd.user.text()) params.setValue("password",self.pd.password.text()) params.setValue("location",self.pd.location.text()) params.setValue("destination",self.pd.selectDest.text()) params.setValue("bwl",str(self.pd.bwl.value())) self.prefsInit() self.pd.close() def launchSync(self): self.IprogressBar.setEnabled(True) self.stop.setEnabled(True) self.syncGo.setEnabled(False) self.listIsos.setEnabled(False) self.selectAll.setEnabled(False) # Connect the button Stop self.stop.clicked.connect(self.stopSync) self.rsyncThread.params(self.password, self.bwl) for iso in self.listIsos.selectedItems(): path,name=iso.text().split('/') try: # Look for ISO in local list item=self.model.findItems(name,QtCore.Qt.MatchExactly,1)[0] except: # Remote ISO is not yet in local directory. We add it in localList and create the directory self.localAdd(path,name,0) basedir=QtCore.QDir(self.destination) basedir.mkdir(path) item=self.model.findItems(name,QtCore.Qt.MatchExactly,1)[0] row=self.model.indexFromItem(item).row() if self.location == "" : self.nameWithPath='rsync://'+self.user+'@bcd.mageia.org/isos/'+self.release+'/'+path else: self.nameWithPath=self.location+path if (not str(path).endswith('/')): self.nameWithPath+='/' self.rsyncThread.setup(self.nameWithPath, self.destination+'/'+path+'/',row) self.rsyncThread.start() # start the thread # Pour les tests uniquement #rsync://[email protected]/isos/$release/ #self.nameWithPath='rsync://ftp5.gwdg.de/pub/linux/mageia/iso/4.1/Mageia-4.1-LiveCD-GNOME-en-i586-CD/' def closeFill(self,code): if code==0: # list returned list=self.fillList.getList() for iso in list: self.add(iso) elif code==1: self.lvMessage(self.tr("Command rsync not found")) elif code==2: self.lvMessage(self.tr("Error in rsync parameters")) elif code==3: self.lvMessage(self.tr("Unknown error in rsync")) list=self.fillList.getList() list=self.fillList.getLocal() for path,iso,isoSize in list: self.localAdd(path,iso, isoSize) self.fillList.quit() self.lw.hide() def stopSync(self): self.rsyncThread.stop() self.IprogressBar.setEnabled(False) self.stop.setEnabled(False) self.syncGo.setEnabled(True) self.listIsos.setEnabled(True) self.selectAll.setEnabled(True) def main(self): self.show() # Load or look for intitial parameters self.prefsInit() # look for Isos list and add it to the isoSync list. Update preferences self.updateList() self.multiSelect() def close(self): self.rsyncThread.stop() exit(0)
class ApiListPresenter: model: QStandardItemModel view: QListView is_drop_operation: bool = False dropped_row: int = -1 def __init__(self, parent_view): self.view = parent_view.lst_http_requests self.parent_view = parent_view self.assertion_result_presenter = AssertionResultPresenter( self.parent_view) self.app_state_interactor = AppStateInteractor() self.model = QStandardItemModel() self.view.setModel(self.model) self.view.setItemDelegate(ApiCallItemDelegate()) self.view.selectionModel().currentChanged.connect( self.on_list_item_selected) self.view.setContextMenuPolicy(Qt.CustomContextMenu) self.view.customContextMenuRequested.connect(self.show_context_menu) self.view.drop_event_signal.connect(self.on_drop_event) self.view.model().rowsRemoved.connect(self.onRowsRemoved) http_exchange_signals.request_started.connect(self.on_request_started) # Context Menu setup duplicate_action = QAction("Duplicate", self.view) duplicate_action.triggered.connect(self._duplicate_request) remove_action = QAction("Delete", self.view) remove_action.triggered.connect(self.on_remove_selected_item) toggle_action = QAction("Toggle Enable/Disable", self.view) toggle_action.triggered.connect(self.on_toggle_request_status) self.menu = QMenu() self.menu.addAction(duplicate_action) self.menu.addAction(remove_action) self.menu.addAction(toggle_action) self.separator_menu = QMenu() self.separator_menu.addAction(remove_action) app_settings.app_data_writer.signals.api_call_added.connect( self.add_request_widget) app_settings.app_data_writer.signals.api_call_updated.connect( self.refresh_selected_item) app_settings.app_data_writer.signals.multiple_api_calls_added.connect( self.refresh_multiple_items) app_settings.app_data_writer.signals.selected_tag_changed.connect( self.refresh) def on_toggle_request_status(self): selected_model_index: QModelIndex = self.index_at_selected_row() if not selected_model_index: return api_call_id = selected_model_index.data(API_ID_ROLE) api_call: ApiCall = app_settings.app_data_cache.get_api_call( api_call_id) api_call.enabled = not api_call.enabled api_call_interactor.update_api_call(api_call_id, api_call) def on_drop_event(self, model_index: QModelIndex): self.is_drop_operation = True self.dropped_row = model_index.row() def show_context_menu(self, position): index: QModelIndex = self.view.indexAt(position) if not index.isValid(): return selected_api_call: ApiCall = index.data(API_CALL_ROLE) global_position = self.view.viewport().mapToGlobal(position) if selected_api_call.is_separator: self.separator_menu.exec_(global_position) else: self.menu.exec_(global_position) def selectPreviousApiCall(self): selected_index = self.index_at_selected_row() if not selected_index: return selected_row = selected_index.row() previous_row = selected_row - 1 if previous_row >= 0: previous_item: QStandardItem = self.model.item(previous_row) self.view.setCurrentIndex(previous_item.index()) def selectNextApiCall(self): selected_index = self.index_at_selected_row() if not selected_index: return selected_row = selected_index.row() next_row = selected_row + 1 total_rows = self.model.rowCount() if next_row < total_rows: next_item: QStandardItem = self.model.item(next_row) self.view.setCurrentIndex(next_item.index()) def onRowsRemoved(self): if self.is_drop_operation: self.is_drop_operation = False if self.dropped_row < 0 or self.dropped_row >= self.model.rowCount( ): self.dropped_row = self.model.rowCount() - 1 current_item = self.model.item(self.dropped_row) current_index = self.model.indexFromItem(current_item) api_call = current_item.data(API_CALL_ROLE) before = self.dropped_row - 1 prev_sequence_number = 0 if before >= 0: prev_api_call = self.model.item(before).data(API_CALL_ROLE) prev_sequence_number = prev_api_call.sequence_number total_rows = self.model.rowCount() after = self.dropped_row + 1 if after >= total_rows: next_sequence_number = self.app_state_interactor.update_sequence_number( ) else: next_api_call = self.model.item(after).data(API_CALL_ROLE) next_sequence_number = next_api_call.sequence_number new_sequence_number = prev_sequence_number + ( next_sequence_number - prev_sequence_number) / 2 api_call.sequence_number = new_sequence_number logging.info( f"API Call: {api_call.id} - " f"New Sequence {new_sequence_number} - " f"Moved between {prev_sequence_number} and {next_sequence_number}" ) api_call_interactor.update_api_call(api_call.id, api_call) self.view.setCurrentIndex(current_index) def add_request_widget(self, api_call_id, api_call: ApiCall, select_item=True): logging.info( f"Adding new item with id {api_call_id} to requests list - {api_call}" ) item = QStandardItem(api_call.title) item.setData(api_call, API_CALL_ROLE) item.setData(QVariant(api_call.id), API_ID_ROLE) item.setToolTip(api_call.description) item.setDragEnabled(True) item.setDropEnabled(False) self.model.appendRow(item) if select_item: index = self.model.indexFromItem(item) self.view.setCurrentIndex(index) def run_all_api_calls(self): total_rows = self.model.rowCount() logging.debug("** Running multiple API calls: {}".format(total_rows)) for n in range(total_rows): api_call = self.model.item(n).data(API_CALL_ROLE) logging.debug("** Multiple APIs: API Call {}".format(api_call.id)) if api_call.enabled: rest_api_interactor.make_http_call(api_call) def on_remove_selected_item(self): selected_model_index: QModelIndex = self.index_at_selected_row() if not selected_model_index: return row_to_remove = selected_model_index.row() api_call: ApiCall = selected_model_index.data(API_CALL_ROLE) # Migration api_call_interactor.remove_api_call([api_call.id]) previous_row = row_to_remove - 1 if previous_row >= 0: previous_item: QStandardItem = self.model.item(previous_row) self.view.setCurrentIndex(previous_item.index()) def on_list_item_selected(self, current: QModelIndex): if not current: return selected_api_call: ApiCall = current.data(API_CALL_ROLE) logging.info(f"List Item Selected: {selected_api_call.id}") if not selected_api_call.is_separator: api_call_interactor.update_selected_api_call(selected_api_call.id) def on_request_started(self, _, api_call_id): logging.info(f"Request started for {api_call_id}") api_call_row = self.__row_for_api_call(api_call_id) item_running = self.model.item(api_call_row) index_running = self.model.indexFromItem(item_running) self.view.setCurrentIndex(index_running) def refresh_selected_item(self, api_call_id): api_call_row = self.__row_for_api_call(api_call_id) if api_call_row != -1: api_call = app_settings.app_data_cache.get_api_call(api_call_id) logging.info(f"Refreshing row {api_call_row} -> {api_call.id}") self.model.item(api_call_row).setData(api_call, API_CALL_ROLE) def refresh_multiple_items(self, doc_ids: List[str], api_calls: List[ApiCall]): assert len(doc_ids) == len(api_calls) for doc_id, api_call in zip(doc_ids, api_calls): self.add_request_widget(doc_id, api_call, select_item=False) def on_search_query(self, search_query): app_settings.app_data_cache.update_search_query(search_query) self.refresh() def refresh(self): """Re-renders this list view and selects the first item""" self.model.clear() app_state = app_settings.app_data_cache.get_app_state() api_calls = app_settings.app_data_cache.filter_api_calls( search_query=app_state.selected_search, search_tag=app_state.selected_tag) for api in api_calls: self.add_request_widget(api.id, api, select_item=False) if len(api_calls) > 0: first_item: QStandardItem = self.model.item(0) self.view.setCurrentIndex(first_item.index()) def index_at_selected_row(self): selected_model: QItemSelectionModel = self.view.selectionModel() if not selected_model.hasSelection(): return None return selected_model.currentIndex() def _duplicate_request(self): selected_index = self.index_at_selected_row() if not selected_index: return api_call = selected_index.data(API_CALL_ROLE) duplicate_api_call = copy.deepcopy(api_call) duplicate_api_call.title = f"{duplicate_api_call.title} Duplicate" duplicate_api_call.last_response_code = 0 duplicate_api_call.sequence_number = self.app_state_interactor.update_sequence_number( ) # Migration api_call_interactor.add_api_call(duplicate_api_call) def __row_for_api_call(self, api_call_id): api_call_row = next( (n for n in range(self.model.rowCount()) if self.model.item(n).data(API_ID_ROLE) == api_call_id), -1) return api_call_row
class Tree(QTreeView): def __init__(self, parent=None): super(Tree, self).__init__(parent) self.tmodel = QStandardItemModel() self.tmodel.setHorizontalHeaderLabels(['name']) self.setModel(self.tmodel) self.setUniformRowHeights(True) self.setHeaderHidden(True) self.setFixedWidth(300) # self.storage = get_storage() # self.storage = smanager.get_storage() # self.tree = self.storage.get_tree() self.select_cb = None self.current_uuid = None # текущий uuid элемента self.current_index = None # элемент с флагом current = True - для автоматического выбора(modelIndex) self.expand_indexes = [] # список элементов, которые необходимо раскрыть #--- dnd self.setDragEnabled(True) # self.setDragDropMode(QAbstractItemView.DragOnly) # self.setDragDropMode(QAbstractItemView.DragDrop) # self.setDragDropMode(QAbstractItemView.InternalMove) self.viewport().setAcceptDrops(True) # self.setDropIndicatorShown(True) self.modal_icons = None self.copy_node_uuid = None #--- menu self.__make_cmenu() # sevents.eon("storage_opened", self.__remake_tree) sevents.eon("project_updated", self.__update_tree) # dbus.eon(dbus.STORAGE_OPENED, self.__on_storage_opened) storage.s_opened.connect(self.__on_storage_opened) #--- signals self.pressed.connect(self.__on_select) self.expanded.connect(self.__on_expanded) self.collapsed.connect(self.__on_collapsed) # self.__remake_tree() def __on_storage_opened(self): log.debug("__on_storage_opened") self.__remake_tree() def dropEvent(self, ev): index = self.indexAt(ev.pos()) uuid = index.data(Qt.UserRole + 1) ev.accept() self.storage.pmanager.move_node(self.current_uuid, uuid) self.storage.update_project_file() def __remake_tree(self): log.debug("remake tree") self.tree = storage.get_tree() # print(self.storage) self.current_uuid = None # текущий uuid элемента self.current_index = None # элемент с флагом current = True - для автоматического выбора(modelIndex) self.expand_indexes = [] # список элементов, которые необходимо раскрыть # print(self.storage.project_path) self.__update_tree() def __update_tree(self): log.debug("__update_tree") self.current_index = None self.tmodel.clear() self.__make_tree() def update_tree(self): self.__update_tree() def __make_cmenu(self): """контекстное меню""" self.setContextMenuPolicy(Qt.ActionsContextMenu) create_new_root = QAction("Новая корневая запись", self) create_new_parent = QAction("Новая запись для данного элемента", self) create_new_level = QAction("Новая запись такого же уровня", self) edit_name = QAction("Изменить название", self) edit_icon = QAction("Изменить иконки", self) show_info = QAction("Информация", self) # act_copy = QAction("Копировать", self) # act_paste = QAction("Вставить", self) move_up = QAction("Выше", self) move_down = QAction("Ниже", self) remove_item = QAction("Удалить запись(ветку)", self) separator1 = QAction(self) separator1.setSeparator(True) separator2 = QAction(self) separator2.setSeparator(True) separator3 = QAction(self) separator3.setSeparator(True) self.addAction(create_new_root) self.addAction(create_new_parent) self.addAction(create_new_level) self.addAction(separator1) self.addAction(edit_name) self.addAction(edit_icon) self.addAction(show_info) # self.addAction(act_copy) # self.addAction(act_paste) self.addAction(separator2) self.addAction(move_up) self.addAction(move_down) self.addAction(separator3) self.addAction(remove_item) # self.addSeparetor() create_new_root.setIcon(qicon("filesystems", "folder_blue.png")) create_new_parent.setIcon(qicon("filesystems", "folder_green.png")) create_new_level.setIcon(qicon("filesystems", "folder_orange.png")) edit_name.setIcon(qicon("actions", "edit.png")) edit_icon.setIcon(qicon("actions", "frame_image.png")) show_info.setIcon(qicon("actions", "kdeprint_printer_infos.png")) # act_copy.setIcon(qicon("actions", "editcopy.png")) remove_item.setIcon(qicon("actions", "remove.png")) move_up.setIcon(qicon("actions", "arrow_up.png")) move_down.setIcon(qicon("actions", "arrow_down.png")) create_new_root.triggered.connect(self.__act_create_new_root) create_new_parent.triggered.connect(self.__act_create_new_parent) create_new_level.triggered.connect(self.__act_create_new_level) edit_name.triggered.connect(self.__act_edit_name) edit_icon.triggered.connect(self.__act_edit_icon) show_info.triggered.connect(self.__act_show_info) # act_copy.triggered.connect(self.__act_copy) # act_paste.triggered.connect(self.__act_paste) move_up.triggered.connect(self.__act_move_up) move_down.triggered.connect(self.__act_move_down) remove_item.triggered.connect(self.__act_remove_item) def __make_tree(self): """строим дерево""" log.debug("__make_tree") self.blockSignals(True) #--- все элементы корня root_items = self.tree.get_nodes_level(1) root_items.sort(key=lambda node: node.tree_lk) #--- запуск обхода дерева(рекурсия) for item in root_items: self.__wnode(item, self.tmodel) #--- выбираем элемент у которого флаг current if self.current_index: self.setCurrentIndex(self.current_index) self.__on_select(self.current_index) #--- если текущего нет - первый else: index = self.tmodel.index(0, 0) self.setCurrentIndex(index) self.__on_select(index) #--- разворачиваем элементы, у которых стоит флаг expanded for i in self.expand_indexes: self.expand(i) self.expand_indexes = [] self.blockSignals(False) def __wnode(self, node, parent): """ рекурсивный обход элементов и добавление их на форму """ # if node.ntype == "f": # # icon = QIcon(get_icon_path("document-properties.png")) # icon = qicon("empty.png") # elif node.ntype == "d": # icon = qicon("folder.png") # # icon = QIcon(get_icon_path("document-open.png")) # # icon = QIcon(get_icon_path("document-open.png")) # else: # icon = QIcon(get_icon_path("list-remove.png")) if node.ipack and node.icon: icon = QIcon(get_icon_path(node.ipack, node.icon)) else: icon = QIcon(get_icon_path("empty.svg")) row = QStandardItem(node.name) # элемент строки row.setIcon(icon) # icon row.setEditable(False) # editable - false row.setData(node.uuid, Qt.UserRole+1) parent.appendRow(row) # добавляем if node.current: self.current_index = self.tmodel.indexFromItem(row) self.current_uuid = node.uuid if node.expanded: index = self.tmodel.indexFromItem(row) self.expand_indexes.append(index) # print(index) #--- ищем всех деток на уровень ниже(не дальше) # child_items = [node for node in simple_obj_list # if (node["tree_lk"] > node_lk) and (node["tree_rk"] < node_rk) and(node["tree_level"] == node_level+1)] # child_items = self.tree.get_childrens(node) child_items = self.tree.find_childrens(node.uuid) # child_items = node.childrens #--- для каждого из деток вызываем рекурсию for node in child_items: self.__wnode(node, row) #--- user actions --------------------------------------------------------- def __on_select(self, index): """ событие от дерева о выбранном элементе прилетает сразу при построении дерева причём current_uuid == uuid """ log.debug("__on_select: {}".format(index)) #--- get selected uuid uuid = index.data(Qt.UserRole + 1) storage.select_node(uuid) # #--- get node from storage # node = smanager.storage.get_node(uuid) # # #--- set current node + emit # smanager.storage.set_current_node(node) # # # if uuid == self.current_uuid: # return False # self.current_uuid = uuid # # #--- update tree node(in project.json) # smanager.storage.project.set_current_flag(node.uuid) def __on_expanded(self, model_index): """раскрытие узла""" uuid = model_index.data(Qt.UserRole + 1) storage.pmanager.set_node_expanded(uuid, True) def __on_collapsed(self, model_index): """закрытие узла""" uuid = model_index.data(Qt.UserRole + 1) storage.pmanager.set_node_expanded(uuid, False) def __act_create_new_root(self): """запрос на создание ноды от корня""" actions.show_modal_create_node(None) def __act_create_new_parent(self): """выбранная нода - является родительской""" actions.show_modal_create_node(self.current_uuid) def __act_create_new_level(self): """выбранная нода - находится у родителя - пока отключено!!!!!""" parent_pnode = storage.pmanager.find_parent_node(self.current_uuid) #--- если родитель - корень, то вызываем модал как и у __act_create_new_root parent_uuid = parent_pnode.uuid if parent_pnode.tree_lk > 0 else None actions.show_modal_create_node(parent_uuid) def __act_remove_item(self): """удаление ноды""" actions.show_modal_remove_node() def __act_edit_name(self): """редактирование названия""" actions.show_modal_edit_name() def __act_edit_icon(self): """редактирование иконки""" # events.show_edit_icon(self.current_uuid) self.modal_icons = ModalIcons(self.__on_icon_selected, parent=self) self.modal_icons.show() def __act_show_info(self): """информация о ноде""" actions.show_modal_node_info() # def __act_copy(self): # """копирование ветки""" # # log.debug("copy node: " + self.current_uuid) # self.copy_node_uuid = self.current_uuid # # # def __act_paste(self): # log.debug(self.current_uuid) # log.debug(self.copy_node_uuid) # # self.storage.copy_node(self.copy_node_uuid, self.current_uuid) def __act_move_up(self): result = storage.pmanager.move_node_up(self.current_uuid) if result: storage.update_project_file() def __act_move_down(self): result = storage.pmanager.move_node_down(self.current_uuid) if result: storage.update_project_file() #--- user actions --------------------------------------------------------- def __on_icon_selected(self, ipack, icon): node = storage.pnode node.ipack = ipack node.icon = icon self.modal_icons.set_close() self.modal_icons = None storage.update_project_file()
class Intercept_Queue: def __init__(self, size=100): self.size = size self.packet_list_model = QStandardItemModel() self.field_list_model = QStandardItemModel() self.packet_list = [] self.lock = Lock() # Populates the gui with packet information. def populate_packet_area(self): print('Populating to GUI...', flush=True) """ Populate Parent Node """ layer_dict = self.packet_list[-1].layer_dict parent = QStandardItem(QIcon(arrow), "Frame {}: {}".format(id_generator(size=3), ', '.join(self.packet_list[-1].layers) )) parent.setEditable(False) self.packet_list_model.appendRow(parent) """ Populate Children of Parent """ for layer in self.packet_list[-1].layers: display_layer = layer if layer in layer_dict: display_layer = layer_dict[layer] if "src" in self.packet_list[-1].fields[layer] and "dst" in self.packet_list[-1].fields[layer]: src = self.packet_list[-1].fields[layer]["src"] dst = self.packet_list[-1].fields[layer]["dst"] child = QStandardItem(QIcon(circle), "{}, Src: {}, Dst: {}".format(display_layer, src, dst)) elif "sport" in self.packet_list[-1].fields[layer] and "dport" in self.packet_list[-1].fields[layer]: src = self.packet_list[-1].fields[layer]["sport"] dst = self.packet_list[-1].fields[layer]["dport"] child = QStandardItem(QIcon(circle), "{}, Src Port: {}, Dst Port: {}".format(display_layer, src, dst)) else: child = QStandardItem(QIcon(circle), "{}".format(display_layer)) child.setEditable(False) self.packet_list_model.itemFromIndex(self.packet_list_model.indexFromItem(parent)).appendRow(child) # Populates the field area with fields from the selected layer. def populate_field_area(self, packet_idx, layer_idx): self.field_list_model.removeRows(0,self.field_list_model.rowCount()) layer = self.packet_list[packet_idx].get_layer(layer_idx) fields = self.packet_list[packet_idx].fields[layer] for k,v in fields.items(): self.field_list_model.appendRow(QStandardItem("".join("{}:{}".format(k,v)) )) # Insert a packet into the list of packets. def put(self, packet): with self.lock: if self.size > len(self.packet_list): self.packet_list.append(packet) self.populate_packet_area() # Get a packet from the list of packets. def get(self): with self.lock: packet = self.packet_list[0] del self.packet_list[0] self.packet_list_model.removeRow(0) return packet
class TurnoverDialog(QDialog, Ui_TurnoverDialog): """ Dialog used for the display of metabolite turnover This dialog shows the turnover of a specific metabolite from a given solution. The turnover is displayed in two forms: 1) A map showing the active reactions 2) A table listing the individual contributions of the reactions """ def __init__(self, parent=None): super(TurnoverDialog, self).__init__(parent) self.setupUi(self) self.setWindowFlags(QtCore.Qt.Window) # Store input self.metabolite = None self.solution = None self.temp_file = os.path.join(tempfile.gettempdir(), "{0!s}.html".format(uuid.uuid4())) # Setup data structures self.datatable = QStandardItemModel(self) self.datatable.setHorizontalHeaderLabels(("%", "Rate") + ReactionBaseTable.header) self.dataView.setModel(self.datatable) self.dataView.setEditTriggers(QAbstractItemView.NoEditTriggers) self.mapView.settings().setAttribute( QWebEngineSettings.LocalContentCanAccessFileUrls, True) # Connect signals/slots self.finished.connect(self.save_settings) self.finished.connect(self.delete_temp_file) # Restore settings self._restore_settings() def set_solution(self, solution, metabolite): self.metabolite = metabolite self.solution = solution if metabolite and solution: rates = get_rates(solution.fluxes, metabolite) self._refresh_map(rates) self._populate_tree(rates) self.setWindowTitle("{0!s} turnover".format(metabolite.id)) def _refresh_map(self, rates): """ Refresh the map being displayed Generate a map from the set solution and metabolite. Only display those reactions that are active in the set solution if hide_active is set. Returns ------- None """ # Generate escher turnover map builder = escher.Builder(map_json=setup_turnover_map( self.metabolite, rates), reaction_data=self.solution.fluxes.to_dict()) html = builder._get_html(**ESCHER_OPTIONS_LOCAL) # As Qt does not allow the loading of local files # from html set via the setHtml method, write map # to file and read it back to webview with open(self.temp_file, "w") as ofile: ofile.write(replace_css_paths(html)) self.mapView.load( QtCore.QUrl("file:///" + self.temp_file.replace("\\", "/"))) def _populate_tree(self, rates): """ Populate the datamodel from reactions Show the reactions participating in the metabolism of the specific metabolite. Returns ------- None """ # Delete old entries self.datatable.setRowCount(0) # Setup items consuming, producing, inactive = QStandardItem("Consuming"), QStandardItem("Producing"), \ QStandardItem("Inactive") turnover = sum(v for v in rates.values() if v > 0.) for reaction, rate in sorted(rates.items(), key=lambda x: abs(x[1]), reverse=True): row = [] try: row.append(QStandardItem('{:.2%}'.format(abs(rate / turnover)))) except ZeroDivisionError: row.append(QStandardItem("0")) finally: row.append(QStandardItem(str(rate))) row.extend(ReactionBaseTable.row_from_item(reaction)) if rate > 0: producing.appendRow(row) elif rate < 0: consuming.appendRow(row) else: inactive.appendRow(row) # Add nodes root = self.datatable.invisibleRootItem() for i, item in enumerate((producing, consuming, inactive)): item.setText(item.text() + " ({0!s})".format(item.rowCount())) root.setChild(i, item) # Expand consuming/producing node for item in (consuming, producing): self.dataView.expand(self.datatable.indexFromItem(item)) def _restore_settings(self): """ Restore dialog geometry from setting Returns ------- """ with Settings(group=self.__class__.__name__) as settings: restore_state(self.dataView.header(), settings.value("TableHeader")) restore_geometry(self, settings.value("DialogGeometry")) restore_state(self.splitter, settings.value("SplitterState")) def save_settings(self): """ Store dialog geometry in settings Returns ------- None """ with Settings(group=self.__class__.__name__) as settings: settings.setValue("SplitterState", self.splitter.saveState()) settings.setValue("TableHeader", self.dataView.header().saveState()) settings.setValue("DialogGeometry", self.saveGeometry()) def delete_temp_file(self): """ Delete temporary file created when displaying map Returns ------- """ try: os.remove(self.temp_file) except: pass
class DataChangeUI(object): def __init__(self, window, uaclient): self.window = window self.uaclient = uaclient self._subhandler = DataChangeHandler() self._subscribed_nodes = [] self.model = QStandardItemModel() self.window.ui.subView.setModel(self.model) self.window.ui.subView.horizontalHeader().setSectionResizeMode(1) self.window.ui.actionSubscribeDataChange.triggered.connect(self._subscribe) self.window.ui.actionUnsubscribeDataChange.triggered.connect(self._unsubscribe) # populate contextual menu self.window.ui.treeView.addAction(self.window.ui.actionSubscribeDataChange) self.window.ui.treeView.addAction(self.window.ui.actionUnsubscribeDataChange) # handle subscriptions self._subhandler.data_change_fired.connect(self._update_subscription_model, type=Qt.QueuedConnection) # accept drops self.model.canDropMimeData = self.canDropMimeData self.model.dropMimeData = self.dropMimeData def canDropMimeData(self, mdata, action, row, column, parent): return True def dropMimeData(self, mdata, action, row, column, parent): node = self.uaclient.client.get_node(mdata.text()) self._subscribe(node) return True def clear(self): self._subscribed_nodes = [] self.model.clear() def show_error(self, *args): self.window.show_error(*args) @trycatchslot def _subscribe(self, node=None): if not isinstance(node, Node): node = self.window.get_current_node() if node is None: return if node in self._subscribed_nodes: logger.warning("allready subscribed to node: %s ", node) return self.model.setHorizontalHeaderLabels(["DisplayName", "Value", "Timestamp"]) text = str(node.get_display_name().Text) row = [QStandardItem(text), QStandardItem("No Data yet"), QStandardItem("")] row[0].setData(node) self.model.appendRow(row) self._subscribed_nodes.append(node) self.window.ui.subDockWidget.raise_() try: self.uaclient.subscribe_datachange(node, self._subhandler) except Exception as ex: self.window.show_error(ex) idx = self.model.indexFromItem(row[0]) self.model.takeRow(idx.row()) raise @trycatchslot def _unsubscribe(self): node = self.window.get_current_node() if node is None: return self.uaclient.unsubscribe_datachange(node) self._subscribed_nodes.remove(node) i = 0 while self.model.item(i): item = self.model.item(i) if item.data() == node: self.model.removeRow(i) i += 1 def _update_subscription_model(self, node, value, timestamp): i = 0 while self.model.item(i): item = self.model.item(i) if item.data() == node: it = self.model.item(i, 1) it.setText(value) it_ts = self.model.item(i, 2) it_ts.setText(timestamp) i += 1
class TreeView(QTreeView): def __init__(self, *args, **kwargs): super(TreeView, self).__init__(*args, **kwargs) self._initModel() self._initSignals() def _initModel(self): """设置目录树Model""" self._dmodel = QStandardItemModel(self) self._fmodel = SortFilterModel(self) self._fmodel.setSourceModel(self._dmodel) self.setModel(self._fmodel) def _initSignals(self): Signals.itemJumped.connect(self.onItemJumped) Signals.filterChanged.connect(self._fmodel.setFilterRegExp) self.clicked.connect(self.onClicked) self.doubleClicked.connect(self.onDoubleClicked) def rootItem(self): """得到根节点Item""" return self._dmodel.invisibleRootItem() def findItems(self, name): """根据名字查找item :param name: """ return self._dmodel.findItems(name) def onItemJumped(self, name): items = self.findItems(name) if not items: return index = self._fmodel.mapFromSource(self._dmodel.indexFromItem( items[0])) self.setCurrentIndex(index) self.expand(index) # # 显示readme Signals.urlLoaded.emit(name) def listSubDir(self, pitem, path): """遍历子目录 :param item: 上级Item :param path: 目录 """ paths = os.listdir(path) files = [] for name in paths: spath = os.path.join(path, name) if not os.path.isfile(spath): continue spath = os.path.splitext(spath) if len(spath) == 0: continue if spath[1] == '.py' and spath[0].endswith('__init__') == False: files.append(name) # 已经存在的item existsItems = [pitem.child(i).text() for i in range(pitem.rowCount())] for name in files: if name in existsItems: continue file = os.path.join(path, name).replace('\\', '/') item = QStandardItem(name) # 添加自定义的数据 item.setData(False, Constants.RoleRoot) # 不是根目录 item.setData(file, Constants.RolePath) try: item.setData( open(file, 'rb').read().decode(errors='ignore'), Constants.RoleCode) except Exception as e: AppLog.warn('read file({}) code error: {}'.format( file, str(e))) pitem.appendRow(item) def initCatalog(self): """初始化本地仓库结构树 """ AppLog.debug('') if not os.path.exists(Constants.DirProjects): return pitem = self._dmodel.invisibleRootItem() # 只遍历根目录 for name in os.listdir(Constants.DirProjects): file = os.path.join(Constants.DirProjects, name).replace('\\', '/') if os.path.isfile(file): # 跳过文件 continue if name.startswith( '.') or name == 'Donate' or name == 'Test': # 不显示.开头的文件夹 continue items = self.findItems(name) if items: item = items[0] else: item = QStandardItem(name) # 添加自定义的数据 # 用于绘制进度条的item标识 item.setData(True, Constants.RoleRoot) # 目录或者文件的绝对路径 item.setData( os.path.abspath(os.path.join(Constants.DirProjects, name)), Constants.RolePath) pitem.appendRow(item) # 遍历子目录 self.listSubDir(item, file) # 排序 self._fmodel.sort(0, Qt.AscendingOrder) def onClicked(self, modelIndex): """Item单击 :param modelIndex: 此处是代理模型中的QModelIndex, 并不是真实的 """ root = modelIndex.data(Constants.RoleRoot) path = modelIndex.data(Constants.RolePath) code = modelIndex.data(Constants.RoleCode) AppLog.debug('is root: {}'.format(root)) AppLog.debug('path: {}'.format(path)) if not root and os.path.isfile(path) and code: # 右侧显示代码 Signals.showCoded.emit(code) if root and os.path.isdir(path): if self.isExpanded(modelIndex): self.collapse(modelIndex) else: self.expand(modelIndex) # 显示readme Signals.showReadmed.emit(os.path.join(path, 'README.md')) def onDoubleClicked(self, modelIndex): """Item双击 :param modelIndex: 此处是代理模型中的QModelIndex, 并不是真实的 """ root = modelIndex.data(Constants.RoleRoot) path = modelIndex.data(Constants.RolePath) AppLog.debug('is root: {}'.format(root)) AppLog.debug('path: {}'.format(path)) if not root and os.path.isfile(path): # 运行代码 Signals.runExampled.emit(path) # if root and os.path.isdir(path): # # 显示readme # Signals.showReadmed.emit(os.path.join(path, 'README.md')) def enterEvent(self, event): super(TreeView, self).enterEvent(event) # 鼠标进入显示滚动条 self.verticalScrollBar().setVisible(True) def leaveEvent(self, event): super(TreeView, self).leaveEvent(event) # 鼠标离开隐藏滚动条 self.verticalScrollBar().setVisible(False)
class MyWindow(QMainWindow): def __init__(self, parent=None): super(MyWindow, self).__init__(parent) #uic.loadUi('gui_template.ui',self) try: self.path_home = os.path.expanduser("~\\Desktop\\") except Exception: self.path_home = "" for curren_dir in ["interim_image"]: if os.path.exists(curren_dir): if os.path.isdir(curren_dir): print(curren_dir + " is here") else: try: os.mkdir(curren_dir) except OSError: print("Error generate dir " + curren_dir) else: try: os.mkdir(curren_dir) except OSError: print("Error generate dir " + curren_dir) # Load last path for files try: with open('last_path.json', "r") as f: path_dict = {i: j for i, j in json.load(f).items()} self.path_image = path_dict["path_image"] self.path_dir_images = path_dict["path_dir_images"] self.path_dir_dirs = path_dict["path_dir_dirs"] self.path_excel = path_dict["path_excel"] self.path_excels = path_dict["path_excels"] self.path_vez_excel = path_dict["path_vez_excel"] self.path_ro_excel = path_dict["path_ro_excel"] self.path_ro_word = path_dict["path_ro_word"] except Exception: self.path_image = self.path_home self.path_dir_images = self.path_home self.path_dir_dirs = self.path_home self.path_excel = self.path_home self.path_excels = self.path_home self.path_vez_excel = self.path_home self.path_ro_excel = self.path_home self.path_ro_word = self.path_home self.lv_c = 5 self.t_c = 0.5 self.imnam = "слои" desktop = QApplication.desktop() wd = desktop.width() hg = desktop.height() ww = 1000 wh = 500 if ww > wd: ww = int(0.7 * wd) if wh > hg: wh = int(0.7 * hg) x = (wd - ww) // 2 y = (hg - wh) // 2 self.setGeometry(x, y, ww, wh) topVBoxLayout = QVBoxLayout(self) topVBoxLayout.setContentsMargins(0, 0, 0, 0) SPFrame = QFrame() SPFrame.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) SPFrame.setMinimumSize(QSize(150, 100)) self.ImFrame = QFrame() self.ImFrame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) #self.ImFrame.setMinimumSize(QSize(300, 100)) TbFrame = QFrame() TbFrame.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) TbFrame.setMinimumSize(QSize(0, 100)) RFrame = QFrame() RFrame.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) RFrame.setMinimumSize(QSize(0, 100)) self.listView = CustomList() #QListView() self.listView.clicked.connect(self.OpenPict) self.select_able = True self.listView.setcloseEditorSignal(self.Rename) #self.listView.editingFinished.connect(self.Rename) #self.listView.edit.connect(self.Rename) BoxLayout1 = QVBoxLayout() self.progress_bar = QProgressBar() self.progress_bar.setAlignment(Qt.AlignHCenter) BoxLayout1.addWidget(self.progress_bar) BoxLayout1.addWidget(self.listView) SPFrame.setLayout(BoxLayout1) self.Table = CustomTable() #QTableWidget() self.Table.setcloseEditorSignal( lambda: self.WriteTable(who_edited="table")) #self.Table.itemChanged(lambda:print("change2")) self.Table.cellClicked[int, int].connect(self.PosChangedCell) self.Table.setColumnCount(3) self.Table.setHorizontalHeaderLabels(["p", "h", "d"]) self.Table.setRowCount(30) self.Table.setColumnWidth(0, 50) self.Table.setColumnWidth(1, 50) self.Table.setColumnWidth(2, 50) self.Table.setItemDelegate(DownloadDelegate(self)) BoxLayout2 = QHBoxLayout() BoxLayout2.addWidget(self.Table) TbFrame.setLayout(BoxLayout2) self.pe1 = QLabel("Рэ1=") self.pe2 = QLabel("Рэ2=") self.pe = QLabel("Рэ=") lb1 = QLabel("Длинна вертикального") lb2 = QLabel("заземлителя lв, м") lb3 = QLabel("Глубина заложения") lb4 = QLabel("вертикального заземлителя t, м") lb5 = QLabel("Эквивалентное сопротивление") lb6 = QLabel("Двухслойная модель") lb7 = QLabel("Однослойная модель") lb8 = QLabel("Шаблон искомого изображения") lb9 = QLabel("Название проекта") lb10 = QLabel("Название ВЛ") self.vl = QDoubleSpinBox() self.vl.setValue(self.lv_c) self.vl.valueChanged.connect(self.WriteTable) self.t = QDoubleSpinBox() self.t.setValue(self.t_c) self.t.valueChanged.connect(self.WriteTable) self.pe1_le = QLineEdit() self.pe2_le = QLineEdit() self.pe_le = QLineEdit() self.ImageName = QLineEdit() self.ImageName.setText(self.imnam) self.NPrj = QLineEdit() self.NVL = QLineEdit() self.ImageName.editingFinished.connect(self.ReadImName) spacerItem = QSpacerItem(2, 20, QSizePolicy.Minimum, QSizePolicy.Expanding) BoxLayout3 = QVBoxLayout() BoxLayout4 = QHBoxLayout() BoxLayout5 = QHBoxLayout() BoxLayout6 = QHBoxLayout() BoxLayout3.addWidget(lb8) BoxLayout3.addWidget(self.ImageName) BoxLayout3.addWidget(lb9) BoxLayout3.addWidget(self.NPrj) BoxLayout3.addWidget(lb10) BoxLayout3.addWidget(self.NVL) BoxLayout3.addWidget(lb1) BoxLayout3.addWidget(lb2) BoxLayout3.addWidget(self.vl) BoxLayout3.addWidget(lb3) BoxLayout3.addWidget(lb4) BoxLayout3.addWidget(self.t) BoxLayout3.addWidget(lb5) BoxLayout3.addWidget(lb6) BoxLayout4.addWidget(self.pe1) BoxLayout4.addWidget(self.pe1_le) BoxLayout3.addLayout(BoxLayout4) BoxLayout5.addWidget(self.pe2) BoxLayout5.addWidget(self.pe2_le) BoxLayout3.addLayout(BoxLayout5) BoxLayout3.addWidget(lb7) BoxLayout6.addWidget(self.pe) BoxLayout6.addWidget(self.pe_le) BoxLayout3.addLayout(BoxLayout6) BoxLayout3.addItem(spacerItem) RFrame.setLayout(BoxLayout3) Splitter1 = QSplitter(Qt.Horizontal) Splitter1.addWidget(SPFrame) Splitter1.addWidget(self.ImFrame) Splitter1.addWidget(TbFrame) Splitter1.addWidget(RFrame) Splitter1.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) Splitter1.setStretchFactor(1, 4) Splitter1.setStretchFactor(1, 1) topVBoxLayout.addWidget(Splitter1) self.central_widget = QWidget() self.central_widget.setLayout(topVBoxLayout) self.setCentralWidget(self.central_widget) #self.resize() #self.central_widget.show() self.PaintForm = MyFrame(False, parent=self.ImFrame) self.statusBar() menubar = self.menuBar() exitAction = QAction('&Выход', self) #QIcon('exit.png'), exitAction.setShortcut('Ctrl+Q') exitAction.setStatusTip('Выход и программы') exitAction.triggered.connect(lambda: self.closeEvent(QCloseEvent())) Open0Action = QAction('&Открыть изображение', self) #QIcon('exit.png'), Open0Action.setShortcut('Ctrl+1') Open0Action.setStatusTip( 'Результаты ВЭЗ предоставлены одиночными изображениями') Open0Action.triggered.connect(self.Op0) Open1Action = QAction('&Открыть папку изображений', self) #QIcon('exit.png'), Open1Action.setShortcut('Ctrl+2') Open1Action.setStatusTip( 'Результаты ВЭЗ предоставлены одиночными изображениями') Open1Action.triggered.connect(lambda: self.Op12(0)) Open2Action = QAction('&Открыть папку каталогов', self) #QIcon('exit.png'), Open2Action.setShortcut('Ctrl+3') Open2Action.setStatusTip('Результаты ВЭЗ предоставлены набором файлов') Open2Action.triggered.connect(lambda: self.Op12(1)) Open3Action = QAction('&Открыть файл Ecxel', self) #QIcon('exit.png'), Open3Action.setShortcut('Ctrl+4') Open3Action.setStatusTip('Результаты ВЭЗ файле Ecxel') Open3Action.triggered.connect(self.Op3) Open4Action = QAction('&Открыть файлы Ecxel', self) #QIcon('exit.png'), Open4Action.setShortcut('Ctrl+5') Open4Action.setStatusTip('Результаты ВЭЗ в нескольких файлах Ecxel') Open4Action.triggered.connect(self.Op4) NewPick = QAction("Добавить точку", self) NewPick.setShortcut('Ctrl+A') NewPick.setStatusTip('Добавить новую точку') NewPick.triggered.connect(self.NewPick) DelPick = QAction("Удалить точку", self) DelPick.setShortcut('Ctrl+D') DelPick.setStatusTip('Удалить точку из списка') DelPick.triggered.connect(self.DelPick) ReRead = QAction("Прочитать изображение", self) ReRead.setShortcut('Ctrl+R') ReRead.setStatusTip('Принудительно считывает информацию с изображения') ReRead.triggered.connect(self.ReReadImage) Select = QAction("Выбрать все точки", self) Select.setShortcut('Ctrl+W') Select.setStatusTip('Выбираем все точки из списка') Select.triggered.connect(self.select) ClearTable = QAction("Очистить таблицу", self) ClearTable.setShortcut('Ctrl+Shift+D') ClearTable.setStatusTip('Очищает текущую таблицу') ClearTable.triggered.connect(self.ClearTable) SetLvCheck = QAction("Установить lв выбранным точкам", self) SetLvCheck.setShortcut('Ctrl+E') SetLvCheck.setStatusTip('Изменяет lв выбраных точек на текущее') SetLvCheck.triggered.connect(self.SetLvCheck) SetTCheck = QAction("Установить t выбранным точкам", self) SetTCheck.setShortcut('Ctrl+T') SetTCheck.setStatusTip('Изменяет t выбраных точек на текущее') SetTCheck.triggered.connect(self.SetTCheck) Save1VEZExcel = QAction("Сохранить ВЭЗ в Excel", self) Save1VEZExcel.setShortcut('Ctrl+6') Save1VEZExcel.setStatusTip('Сохраняет выбраные ВЭЗ в Excel файл') Save1VEZExcel.triggered.connect(lambda: self.Save1VEZExcel(1)) Save2VEZExcel = QAction("Сохранить Pэ в Excel", self) Save2VEZExcel.setShortcut('Ctrl+7') Save2VEZExcel.setStatusTip('Сохраняет выбраные Рэ в Excel файл') Save2VEZExcel.triggered.connect(lambda: self.Save1VEZExcel(2)) Save3VEZExcel = QAction("Сохранить Pэ в Word", self) Save3VEZExcel.setShortcut('Ctrl+8') Save3VEZExcel.setStatusTip('Сохраняет выбраные Рэ в Word файл') Save3VEZExcel.triggered.connect(lambda: self.Save1VEZExcel(3)) EditWord = QAction("Редактировать шаблон Word", self) EditWord.setShortcut('Ctrl+G') EditWord.setStatusTip('Запускает окно для редактирования шаблона Word') EditWord.triggered.connect(self.EditWord) zoomIn = QAction("Увеличить изображение", self) zoomIn.setShortcut('Ctrl++') zoomIn.setStatusTip('Увеличивает открытое изображение') zoomIn.triggered.connect(self.PaintForm.zoomIn) zoomOut = QAction("Уменьшить изображение", self) zoomOut.setShortcut('Ctrl+-') zoomOut.setStatusTip('Уменьшает открытое изображение') zoomOut.triggered.connect(self.PaintForm.zoomOut) Rotate1 = QAction("Повернуть изображение по ч.с", self) Rotate1.setShortcut('Ctrl+Shift++') Rotate1.setStatusTip('Поворачивает изображение по часовой стрелке') Rotate1.triggered.connect(lambda: self.PaintForm.Rotate(1)) Rotate2 = QAction("Повернуть изображение против ч.с", self) Rotate2.setShortcut('Ctrl+Shift+-') Rotate2.setStatusTip( 'Поворачивает изображение ппротив часовой стрелке') Rotate2.triggered.connect(lambda: self.PaintForm.Rotate(-1)) NormalSize = QAction("Вернуть исходный размер", self) NormalSize.setShortcut('Ctrl+F') NormalSize.setStatusTip('Вернуть исходный размер изображения') NormalSize.triggered.connect(self.PaintForm.normalSize) fileMenu = menubar.addMenu('&Файл') fileMenu.addAction(Open0Action) fileMenu.addAction(Open1Action) fileMenu.addAction(Open2Action) fileMenu.addAction(Open3Action) fileMenu.addAction(Open4Action) fileMenu.addAction(exitAction) editMenu = menubar.addMenu('&Правка') editMenu.addAction(NewPick) editMenu.addAction(DelPick) editMenu.addAction(ReRead) editMenu.addAction(Select) editMenu.addAction(SetLvCheck) editMenu.addAction(SetTCheck) editMenu.addAction(ClearTable) imageMenu = menubar.addMenu('&Изображение') imageMenu.addAction(zoomIn) imageMenu.addAction(zoomOut) imageMenu.addAction(Rotate1) imageMenu.addAction(Rotate2) imageMenu.addAction(NormalSize) reportMenu = menubar.addMenu('&Отчёт') reportMenu.addAction(Save1VEZExcel) reportMenu.addAction(Save2VEZExcel) reportMenu.addAction(Save3VEZExcel) reportMenu.addAction(EditWord) self.setWindowTitle('VEZRead') self.setWindowIcon(QIcon('images\\op1.png')) self.d = {} self.sp_m = [] self.file_path = [] self.file_name = [] self.arr = [] self.stst = [] self.arr_lv = [] self.arr_t = [] self.adres = None self.ski = QStandardItemModel() self.listView.setModel(self.ski) self.listView.pressed.connect(self.presseditem) self.sel = True self.block = True self.back_colors = ((255, 255, 255, 255), (255, 0, 0, 50), (255, 255, 0, 50), (0, 255, 0, 50)) self.pos_changed_cell = (0, 0) #self.NewPick() def PosChangedCell(self, i, j): self.pos_changed_cell = (i, j) def Zap(self): self.sel = True self.adres = None self.d = {} self.sp_m = [] self.ski = QStandardItemModel() for i in range(len(self.file_name)): self.sp_m.append( QStandardItem(QIcon('images\\op1.png'), self.file_name[i])) check = (Qt.Checked if self.sel else Qt.Unchecked) self.sp_m[i].setCheckable(True) self.sp_m[i].setCheckState(check) self.ski.appendRow(self.sp_m[i]) self.d[QPersistentModelIndex(self.sp_m[i].index())] = i self.listView.setModel(self.ski) def Op0(self): try: p = QFileDialog.getOpenFileNames(self, 'Открыть файлы', self.path_image, "*.png *.jpg *.bmp") if p == ([], ''): raise Exception("string index out of range") self.file_path = [] self.file_name = [] for i in p[0]: fname, pr = os.path.splitext(os.path.split(i)[1]) self.file_path.append(i) self.file_name.append(fname) self.Zap() self.arr = [None for j in self.file_name] self.stst = [None for j in self.file_name] self.arr_lv = [self.lv_c for j in self.file_name] self.arr_t = [self.t_c for j in self.file_name] self.path_image = os.path.dirname(p[0][0]) self.Run_Tread() except Exception as ex: if str(ex) != "string index out of range": ems = QErrorMessage(self) ems.setWindowTitle('Возникла ошибка') ems.showMessage('При открытии возникла ошибка (' + str(ex) + ').') def Tread_signal_handler(self, signal): if signal[:5] == "Start": self.select_able = False elif signal[:4] == "Work": a = signal.index("iter_") b = signal.index("color_") i = int(signal[a + 5:b - 1]) color = int(signal[b + 6:]) self.progress_bar.setValue(i) self.sp_m[i].setBackground(QBrush( QColor(*self.back_colors[color]))) elif signal[:3] == "End": self.select_able = True self.progress_bar.reset() #self.sp_m[i].setData(self.sp_m[i].data()) #self.listView.dataChanged(self.sp_m[i].index(),self.sp_m[i].index()) #self.sp_m[i].setText("aaaa") def Run_Tread(self): self.progress_bar.setRange(0, len(self.file_path)) self.potokRasch = Calc_Tread(self.arr, self.stst, self.file_path) self.potokRasch.mysignal.connect(self.Tread_signal_handler, Qt.QueuedConnection) if not self.potokRasch.isRunning(): self.potokRasch.start() #QFileDialog.getSaveFileName def Op12(self, n): try: p = "" p = QFileDialog.getExistingDirectory( self, 'Открыть папку', self.path_dir_dirs if n == 1 else self.path_dir_images) if p == "": raise Exception("string index out of range") if n == 1: self.file_path, self.file_name = self.Files( p, n, namef=self.ImageName.text()) self.path_dir_dirs = p else: self.file_path, self.file_name = self.Files(p, n) self.path_dir_images = p #print(self.file_path) self.Zap() self.arr = [None for j in self.file_name] self.stst = [None for j in self.file_name] ## self.arr_lv = [self.lv_c for j in self.file_name] self.arr_t = [self.t_c for j in self.file_name] self.Run_Tread() """ def ClosePotok(self): self.potokRasch.stop() self.potokRasch.mysignal.disconnect() del self.potokRasch """ except Exception as ex: if str(ex) != "string index out of range": ems = QErrorMessage(self) ems.setWindowTitle('Возникла ошибка') ems.showMessage('При открытии возникла ошибка (' + str(ex) + ').') def Op3(self): try: p = QFileDialog.getOpenFileNames(self, 'Открыть файлы', self.path_excel, "*.xlsx *.xls") if p == ([], ''): raise Exception("string index out of range") self.file_path = [] self.file_name = [] self.arr_lv = [] self.arr_t = [] self.arr = [] for i in p[0]: N, nam, lv, t = xlsx.OpenFile(i) self.arr += N self.stst += [imagescan.define_state(a) for a in N] self.file_name += nam self.file_path += [None for j in nam] self.arr_lv += [j if j != None else self.lv_c for j in lv] self.arr_t += [j if j != None else self.t_c for j in t] self.path_excel = os.path.dirname(p[0][0]) self.Zap() for i in range(len(self.arr)): color = imagescan.common_state(self.stst[i]) self.sp_m[i].setBackground( QBrush(QColor(*self.back_colors[color]))) except Exception as ex: if str(ex) != "string index out of range": ems = QErrorMessage(self) ems.setWindowTitle('Возникла ошибка') ems.showMessage('При открытии возникла ошибка (' + str(ex) + ').') def Op4(self): try: p = "" p = QFileDialog.getExistingDirectory( self, 'Открыть папку', self.path_excels) # Обрати внимание на последний элемент if p == "": raise Exception("string index out of range") file_path, n = self.Files(p, 2) self.file_path = [] self.file_name = [] self.arr_lv = [] self.arr_t = [] self.arr = [] for i in file_path: N, nam, lv, t = xlsx.OpenFile(i) self.arr += N self.stst += [imagescan.define_state(a) for a in N] self.file_name += nam self.file_path += [None for j in nam] self.arr_lv += [j if j != None else self.lv_c for j in lv] self.arr_t += [j if j != None else self.t_c for j in t] self.path_excels = p self.Zap() for i in range(len(self.arr)): color = imagescan.common_state(self.stst[i]) self.sp_m[i].setBackground( QBrush(QColor(*self.back_colors[color]))) except Exception as ex: if str(ex) != "string index out of range": ems = QErrorMessage(self) ems.setWindowTitle('Возникла ошибка') ems.showMessage('При открытии возникла ошибка (' + str(ex) + ').') #os.path.exists(file_path) #os.path.isfile def Files(self, pt, on, namef=None): p = [] n = [] r1 = set([".png", ".jpg", ".bmp"]) r2 = set([".xls", ".xlsx"]) r = [r1, r1, r2, r2] for root, dirs, files in os.walk(pt + '/'): if on == 0 or on == 2: for filename in files: fname, pr = os.path.splitext(filename) if pr in r[on]: n.append(fname) p.append(pt + "/" + filename) else: for Dir in dirs: for root, dirs, files1 in os.walk(pt + '/' + Dir + '/'): for f in files1: fname, pr = os.path.splitext(f) if fname == namef and pr in r[on]: n.append(Dir) p.append(pt + '/' + Dir + '/' + f) break break return p, n def OpenPict(self, modelindex): if self.select_able: modelindex = QPersistentModelIndex(modelindex) ind = self.d[modelindex] #print(self.arr[ind]) self.PaintForm.Update(self.ImFrame.size()) p = self.file_path[ind] if p != None: self.PaintForm.open(p) if self.arr[ind] == None: try: arr, st_arr = imagescan.Scan(p) except Exception: arr = None st_arr = None self.arr[ind] = arr self.stst[ind] = st_arr else: arr = self.arr[ind] stst = self.stst[ind] else: self.PaintForm.open("images\\image_error_full.png") arr = self.arr[ind] stst = self.stst[ind] ScrollBar = self.PaintForm.scrollArea.horizontalScrollBar() ScrollBar.setValue(ScrollBar.maximum()) self.block = False self.vl.setValue(self.arr_lv[ind]) self.t.setValue(self.arr_t[ind]) self.block = True #print(self.arr[ind]) #print("-"*10) self.WriteArrTable(arr, stst=stst) try: pe1, pe2, pe = roo.RschRoo(arr, self.vl.value(), self.t.value()) self.pe1_le.setText(str(round(pe1, 2)) + " Ом") self.pe2_le.setText(str(round(pe2, 2)) + " Ом") self.pe_le.setText(str(round(pe, 2)) + " Ом") except Exception: self.pe1_le.setText("") self.pe2_le.setText("") self.pe_le.setText("") def WriteArrTable(self, arr, stst=None): try: self.Table.setRowCount(0) self.Table.setRowCount(30 if 30 > len(arr) else len(arr)) #print(arr, stst) for i in range(len(arr)): for j in range(3): a = QTableWidgetItem(str(arr[i][j])) self.Table.setItem(i, j, a) #print("trtrt",stst) for i in range(len(stst)): for j in range(3): tb_item = self.Table.item(i, j) if stst is not None: if tb_item is None: tb_item = QTableWidgetItem("") self.Table.setItem(i, j, tb_item) tb_item.setBackground( QBrush(QColor(*self.back_colors[stst[i][j][0]]))) except Exception as ex: print(ex) self.Table.setRowCount(0) self.Table.setRowCount(30) def resizeEvent(self, event): self.PaintForm.Update(self.ImFrame.size()) def ReadTable(self): b = 0 c = ("", "", "") N = [] for i in range(30): try: a1 = self.Table.item(i, 0).text().replace(",", ".") a1 = float(a1) if a1 != '' else a1 except Exception: a1 = "" try: a2 = self.Table.item(i, 1).text().replace(",", ".") a2 = float(a2) if a2 != '' else a2 except Exception: a2 = "" try: a3 = self.Table.item(i, 2).text().replace(",", ".") a3 = float(a3) if a3 != '' else a3 except Exception: a3 = "" N.append([a1, a2, a3]) if str(a1) + str(a2) + str(a3) != "": b = i c = (a1, a2, a3) if str(c[0]) != "" and str(c[1]) != "": return N[:b + 2] else: return N[:b + 1] def NewPick(self): self.sp_m.append(QStandardItem(QIcon('images\\op1.png'), "Точка")) i = len(self.sp_m) - 1 check = (Qt.Checked if True else Qt.Unchecked) self.sp_m[i].setCheckable(True) self.sp_m[i].setCheckState(check) self.ski.appendRow(self.sp_m[i]) self.d[QPersistentModelIndex(self.sp_m[i].index())] = i self.file_name.append("Точка") self.file_path.append(None) self.arr.append([]) self.stst.append([]) self.arr_lv.append(self.lv_c) self.arr_t.append(self.t_c) def DelPick(self): if self.adres in self.d: self.ski.removeRow(self.adres.row()) self.listView.clearSelection() del self.d[self.adres] self.adres = None #print("test") def presseditem(self, modelindex): self.adres = QPersistentModelIndex(modelindex) def Rename(self): ind = self.d[self.adres] try: modelindex = QModelIndex(self.adres) self.file_name[ind] = self.ski.itemFromIndex(modelindex).text() except Exception as ex: print(ex) def set_table_color(self, ind): for i in range(len(self.stst[ind])): for j in range(3): #print(self.stst[ind][i][j]) tb_item = self.Table.item(i, j) if tb_item is None: tb_item = QTableWidgetItem("") self.Table.setItem(i, j, tb_item) tb_item.setBackground( QBrush(QColor(*self.back_colors[self.stst[ind][i][j][0]]))) for i in range(len(self.stst[ind]), 30): for j in range(3): #print(self.stst[ind][i][j]) tb_item = self.Table.item(i, j) if tb_item is not None: tb_item.setBackground(QBrush(QColor(*self.back_colors[0]))) def WriteTable(self, who_edited=None): if self.block: arr = self.ReadTable() #print(arr) stst = imagescan.define_state(arr) if self.adres != None: ind = self.d[self.adres] self.arr[ind] = arr new_stst = imagescan.new_state(self.stst[ind], stst, *self.pos_changed_cell) self.stst[ind] = new_stst self.arr_lv[ind] = self.vl.value() self.arr_t[ind] = self.t.value() if who_edited == "table": i, j = self.pos_changed_cell self.set_table_color(ind) color = imagescan.common_state(self.stst[ind]) self.sp_m[ind].setBackground( QBrush(QColor(*self.back_colors[color]))) #print("change1", i,j) try: pe1, pe2, pe = roo.RschRoo(arr, self.vl.value(), self.t.value()) self.pe1_le.setText(str(round(pe1, 2)) + " Ом") self.pe2_le.setText(str(round(pe2, 2)) + " Ом") self.pe_le.setText(str(round(pe, 2)) + " Ом") except Exception: self.pe1_le.setText("") self.pe2_le.setText("") self.pe_le.setText("") def ReReadImage(self): if self.adres != None: ind = self.d[self.adres] p = self.file_path[ind] if p != None: try: arr, st_arr = imagescan.Scan(p) self.arr[ind] = arr self.stst[ind] = st_arr self.WriteArrTable(arr, stst=st_arr) self.WriteTable() color = imagescan.common_state(self.stst[ind]) self.sp_m[ind].setBackground( QBrush(QColor(*self.back_colors[color]))) except Exception: 1 def ClearTable(self): if self.adres != None: ind = self.d[self.adres] self.arr[ind] = [] self.Table.setRowCount(0) self.Table.setRowCount(30) def select(self): i = 0 self.sel = not self.sel while self.ski.item(i): item = self.ski.item(i) item.setCheckState(Qt.Checked if self.sel else Qt.Unchecked) i += 1 def SetLvCheck(self): i = 0 while self.ski.item(i): item = self.ski.item(i) if item.checkState(): modelindex = QPersistentModelIndex( self.ski.indexFromItem(item)) ind = self.d[modelindex] self.arr_lv[ind] = self.vl.value() i += 1 def SetTCheck(self): i = 0 while self.ski.item(i): item = self.ski.item(i) if item.checkState(): modelindex = QPersistentModelIndex( self.ski.indexFromItem(item)) ind = self.d[modelindex] self.arr_t[ind] = self.t.value() i += 1 def ReadImName(self): self.imnam = self.ImageName.text() def Save1VEZExcel(self, trig=1): i = 0 N = [] nm = [] lv = [] t = [] Pe1 = [] Pe2 = [] Pe = [] current_path = { 1: self.path_vez_excel, 2: self.path_ro_excel, 3: self.path_ro_word }[trig] try: fname = QFileDialog.getSaveFileName( self, 'Сохранить файл', current_path, '*.xlsx;;*.xls' if trig != 3 else '*.docx')[0] # Обрати внимание на последний элемент while self.ski.item(i): item = self.ski.item(i) if item.checkState(): modelindex = QPersistentModelIndex( self.ski.indexFromItem(item)) ind = self.d[modelindex] p = self.file_path[ind] if p != None: if self.arr[ind] == None: try: arr, st_arr = imagescan.Scan(p) except Exception: arr = None else: arr = self.arr[ind] else: arr = self.arr[ind] if arr != None and arr != []: if trig == 1: N.append(arr) nm.append(self.file_name[ind]) lv.append(self.arr_lv[ind] if self.arr_lv[ind] != self.lv_c else None) t.append(self.arr_t[ind] if self.arr_t[ind] != self.t_c else None) elif trig == 2 or trig == 3: try: pe1, pe2, pe = roo.RschRoo( arr, self.arr_lv[ind], self.arr_t[ind]) except Exception: 1 else: nm.append(self.file_name[ind]) lv.append(self.arr_lv[ind]) t.append(self.arr_t[ind]) Pe1.append(round(pe1, 2)) Pe2.append(round(pe2, 2)) Pe.append(round(pe, 2)) i += 1 if trig == 1: if fname != "": self.path_vez_excel = os.path.dirname(fname) xlsx.SaveFile1(fname, nm, N, lv, t) elif trig == 2: if fname != "": self.path_ro_excel = os.path.dirname(fname) xlsx.SaveFile2(fname, nm, Pe1, Pe2, Pe, lv, t) elif trig == 3: if fname != "": self.path_ro_word = os.path.dirname(fname) Word.Word(fname, nm, Pe, lv, t, self.NPrj.text(), self.NVL.text()) except Exception as ex: if str(ex) != "string index out of range": ems = QErrorMessage(self) ems.setWindowTitle('Возникла ошибка') ems.showMessage('Не получилось сгенерировать массив точек. ' + str(ex)) else: mes = QMessageBox.information(self, 'Генерация массива точек', 'Операция прошла успешно.', buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok) def closeEvent(self, event): Message = QMessageBox(QMessageBox.Question, 'Выход из программы', "Вы дейстивлеьно хотите выйти?", parent=self) Message.addButton('Да', QMessageBox.YesRole) Message.addButton('Нет', QMessageBox.NoRole) #Message.addButton('Сохранить', QMessageBox.ActionRole) reply = Message.exec() if reply == 0: # Save last path for files with open('last_path.json', "w", encoding="utf8") as f: json.dump( { "path_image": self.path_image, "path_dir_images": self.path_dir_images, "path_dir_dirs": self.path_dir_dirs, "path_excel": self.path_excel, "path_excels": self.path_excels, "path_vez_excel": self.path_vez_excel, "path_ro_excel": self.path_ro_excel, "path_ro_word": self.path_ro_word }, f, indent=4) qApp.quit() elif reply == 1: event.ignore() def EditWord(self): try: win32api.ShellExecute(0, 'open', "Word_template\\Template.docx", '', '', 1) except Exception as ex: if str( ex ) == "(2, 'ShellExecute', 'Не удается найти указанный файл.')": d = Document() d.save("Word_template\\Template.docx") else: print(ex)
class StepListView: def __init__(self, main_window): self.main_window = main_window self.lst_steps: CustomStepsListView = self.main_window.lst_steps self.controller = StepListController(self, self.main_window.world) # menu delete_action = QAction("Delete", self.lst_steps) delete_action.triggered.connect(self.on_delete_selected_item) self.menu = QMenu() self.menu.addAction(delete_action) # setup model self.model = QStandardItemModel() self.lst_steps.setModel(self.model) self.lst_steps.setItemDelegate(StepItemDelegate()) # ui events self.lst_steps.selectionModel().currentChanged.connect( self.on_step_selected) self.lst_steps.setContextMenuPolicy(Qt.CustomContextMenu) self.lst_steps.customContextMenuRequested.connect( self.on_display_context_menu) self.lst_steps.dropEventSignal.connect(self.on_drop_event) def on_drop_event(self, model_index: QModelIndex): selected_model_indexes = self.lst_steps.selectedIndexes() self.delete_steps_by_indexes(selected_model_indexes, delete_from_db=False) def step_with_order(order): step_entity = self.model.item(order).data(STEP_LIST_OBJECT_ROLE) step_entity.order = order return step_entity steps = [step_with_order(n) for n in range(self.model.rowCount())] self.controller.update_multiple_steps(steps) def get_step_entity_at_index(self, model_index): return model_index.data(STEP_LIST_OBJECT_ROLE) def select_step_at_index(self, model_index): self.lst_steps.setCurrentIndex(model_index) def indexes_for_selected_rows(self): return self.lst_steps.selectedIndexes() def delete_steps_by_indexes(self, model_indexes, delete_from_db=True): for items in reversed(sorted(model_indexes)): if delete_from_db: step_entity: StepEntity = self.get_step_entity_at_index(items) self.controller.delete_step(step_entity) self.model.takeRow(items.row()) def on_delete_selected_item(self): selected_model_indexes = self.indexes_for_selected_rows() if not selected_model_indexes: return self.delete_steps_by_indexes(selected_model_indexes) before_first_row_to_delete = selected_model_indexes[0].row() - 1 if before_first_row_to_delete >= 0: previous_item: QStandardItem = self.model.item( before_first_row_to_delete) if previous_item: self.select_step_at_index(previous_item.index()) def on_display_context_menu(self, position): index: QModelIndex = self.lst_steps.indexAt(position) if not index.isValid(): return global_position = self.lst_steps.viewport().mapToGlobal(position) self.menu.exec_(global_position) def clear_steps(self): self.model.clear() def update_steps(self, steps): for step in steps: self.add_step_widget(step, select_item=False) def select_step_at(self, position): first_item: QStandardItem = self.model.item(position) self.lst_steps.setCurrentIndex(first_item.index()) def add_step_widget(self, step: StepEntity, select_item=True): logging.info("Adding a new widget for {}".format(step)) step_item = QStandardItem("({}) {}".format(step.step_type.value, step.title)) step_item.setData(step, STEP_LIST_OBJECT_ROLE) step_item.setData(QVariant(step.id), STEP_LIST_ID_ROLE) step_item.setDragEnabled(True) step_item.setDropEnabled(False) self.model.appendRow(step_item) if select_item: index = self.model.indexFromItem(step_item) self.lst_steps.setCurrentIndex(index) def on_step_selected(self, current: QModelIndex): if not current: return selected_step_id = current.data(STEP_LIST_ID_ROLE) self.controller.trigger_step_selected(selected_step_id)
class QWTable(QTableView): def __init__(self, parent=None): QTableView.__init__(self, parent) self._name = self.__class__.__name__ icon.set_icons() self.is_connected_item_changed = False self.model = QStandardItemModel() self.set_selection_mode() self.fill_table_model() # defines self.model self.setModel(self.model) self.connect_item_selected(self.on_item_selected) self.clicked.connect(self.on_click) self.doubleClicked.connect(self.on_double_click) #self.connect_item_changed(self.on_item_changed) self.set_style() #def __del__(self): # QTableView.__del__(self) - it does not have __del__ def set_selection_mode(self, smode=QAbstractItemView.ExtendedSelection): logger.debug('Set selection mode: %s' % smode) self.setSelectionMode(smode) def connect_item_changed(self, recipient): self.model.itemChanged.connect(recipient) self.is_connected_item_changed = True def disconnect_item_changed(self, recipient): if self.is_connected_item_changed: self.model.itemChanged.disconnect(recipient) self.is_connected_item_changed = False def connect_item_selected(self, recipient): self.selectionModel().currentChanged[QModelIndex, QModelIndex].connect(recipient) def disconnect_item_selected(self, recipient): self.selectionModel().currentChanged[QModelIndex, QModelIndex].disconnect(recipient) def set_style(self): self.setStyleSheet("QTableView::item:hover{background-color:#00FFAA;}") def fill_table_model(self): self.clear_model() self.model.setHorizontalHeaderLabels( ['col0', 'col1', 'col2', 'col3', 'col4']) self.model.setVerticalHeaderLabels(['row0', 'row1', 'row2', 'row3']) for row in range(0, 4): for col in range(0, 6): item = QStandardItem("itemA %d %d" % (row, col)) item.setIcon(icon.icon_table) item.setCheckable(True) self.model.setItem(row, col, item) if col == 2: item.setIcon(icon.icon_folder_closed) if col == 3: item.setText('Some text') #self.model.appendRow(item) def clear_model(self): rows, cols = self.model.rowCount(), self.model.columnCount() self.model.removeRows(0, rows) self.model.removeColumns(0, cols) def selected_indexes(self): return self.selectedIndexes() def selected_items(self): indexes = self.selectedIndexes() return [self.model.itemFromIndex(i) for i in self.selectedIndexes()] def getFullNameFromItem(self, item): #item = self.model.itemFromIndex(ind) ind = self.model.indexFromItem(item) return self.getFullNameFromIndex(ind) def getFullNameFromIndex(self, ind): item = self.model.itemFromIndex(ind) if item is None: return None self._full_name = item.text() self._getFullName(ind) return self._full_name def _getFullName(self, ind): ind_par = self.model.parent(ind) if (ind_par.column() == -1): item = self.model.itemFromIndex(ind) self.full_name = '/' + self._full_name #logger.debug('Item full name:' + self._full_name) return self._full_name else: item_par = self.model.itemFromIndex(ind_par) self._full_name = item_par.text() + '/' + self._full_name self._getFullName(ind_par) def closeEvent(self, event): # if the x is clicked logger.debug('closeEvent') def on_click(self, index): item = self.model.itemFromIndex(index) msg = 'on_click: item in row:%02d text: %s' % (index.row(), item.text()) logger.debug(msg) def on_double_click(self, index): item = self.model.itemFromIndex(index) msg = 'on_double_click: item in row:%02d text: %s' % (index.row(), item.text()) logger.debug(msg) def on_item_selected(self, ind_sel, ind_desel): item = self.model.itemFromIndex(ind_sel) logger.debug('on_item_selected: "%s" is selected' % (item.text() if item is not None else None)) def on_item_changed(self, item): state = ['UNCHECKED', 'TRISTATE', 'CHECKED'][item.checkState()] logger.debug('abstract on_item_changed: "%s" at state %s' % (self.getFullNameFromItem(item), state)) def process_selected_items(self): selitems = self.selected_items() msg = '%d Selected items:' % len(selitems) for i in selitems: msg += '\n %s' % i.text() logger.info(msg) def key_usage(self): return 'Keys:'\ '\n ESC - exit'\ '\n S - show selected items'\ '\n' def keyPressEvent(self, e): logger.info('keyPressEvent, key=', e.key()) if e.key() == Qt.Key_Escape: self.close() elif e.key() == Qt.Key_S: self.process_selected_items() else: logger.info(self.key_usage())
class Window(QMainWindow): def __init__(self): QMainWindow.__init__(self) self.ui = Ui_MainWindow() self.ui.setupUi(self) #fix stuff imposible to do in qtdesigner #remove dock titlebar for addressbar w = QWidget() self.ui.addrDockWidget.setTitleBarWidget(w) #tabify some docks self.tabifyDockWidget(self.ui.evDockWidget, self.ui.subDockWidget) self.tabifyDockWidget(self.ui.subDockWidget, self.ui.refDockWidget) # init widgets self.ui.statusBar.hide() self.ui.addrComboBox.insertItem(-1, "opc.tcp://localhost:4841/") self.ui.addrComboBox.insertItem( 1, "opc.tcp://localhost:53530/OPCUA/SimulationServer/") self.ui.addrComboBox.insertItem(1, "opc.tcp://10.0.5.15:49320/") self.attr_model = QStandardItemModel() self.refs_model = QStandardItemModel() self.sub_model = QStandardItemModel() self.ui.attrView.setModel(self.attr_model) self.ui.refView.setModel(self.refs_model) self.ui.subView.setModel(self.sub_model) self.model = MyModel(self) self.model.clear() self.model.error.connect(self.show_error) self.ui.treeView.setModel(self.model) self.ui.treeView.setUniformRowHeights(True) self.ui.treeView.setSelectionBehavior(QAbstractItemView.SelectRows) self.uaclient = UaClient() self.ui.connectButton.clicked.connect(self._connect) self.ui.disconnectButton.clicked.connect(self._disconnect) self.ui.treeView.activated.connect(self._show_attrs_and_refs) self.ui.treeView.clicked.connect(self._show_attrs_and_refs) self.ui.treeView.expanded.connect(self._fit) self.ui.actionSubscribeDataChange.triggered.connect(self._subscribe) self.ui.actionSubscribeEvent.triggered.connect(self._subscribeEvent) self.ui.actionConnect.triggered.connect(self._connect) self.ui.actionDisconnect.triggered.connect(self._disconnect) self.ui.attrRefreshButton.clicked.connect(self.show_attrs) # context menu self.ui.treeView.addAction(self.ui.actionSubscribeDataChange) self.ui.treeView.addAction(self.ui.actionSubscribeEvent) self.ui.treeView.addAction(self.ui.actionUnsubscribe) # handle subscriptions self._subhandler = SubHandler(self.sub_model) def show_error(self, msg, level=1): print("showing error: ", msg, level) self.ui.statusBar.show() self.ui.statusBar.setStyleSheet( "QStatusBar { background-color : red; color : black; }") #self.ui.statusBar.clear() self.ui.statusBar.showMessage(str(msg)) QTimer.singleShot(1500, self.ui.statusBar.hide) def _fit(self, idx): self.ui.treeView.resizeColumnToContents(0) def _subscribeEvent(self): self.showError("Not Implemented") def _subscribe(self): idx = self.ui.treeView.currentIndex() it = self.model.itemFromIndex(idx) if not id: self.show_error("No item currently selected") node = it.data() attrs = self.uaclient.get_node_attrs(node) self.sub_model.setHorizontalHeaderLabels( ["DisplayName", "Browse Name", 'NodeId', "Value"]) item = QStandardItem(attrs[1]) self.sub_model.appendRow( [item, QStandardItem(attrs[2]), QStandardItem(attrs[3])]) try: # FIXME use handle to unsubscribe!!! handle = self.uaclient.subscribe(node, self._subhandler) except Exception as ex: self.show_error(ex) idx = self.sub_model.indexFromItem(item) self.sub_model.takeRow(idx.row()) def unsubscribe(self): idx = self.ui.treeView.currentIndex() it = self.model.itemFromIndex(idx) if not id: print("No item currently selected") node = it.data() self.uaclient.unsubscribe(node) def _show_attrs_and_refs(self, idx): node = self.get_current_node(idx) if node: self._show_attrs(node) self._show_refs(node) def get_current_node(self, idx=None): if idx is None: idx = self.ui.treeView.currentIndex() it = self.model.itemFromIndex(idx) if not it: return None node = it.data() if not node: print("No node for item:", it, it.text()) return None return node def show_attrs(self): node = self.get_current_node() if node: self._show_attrs(node) else: self.attr_model.clear() def _show_refs(self, node): self.refs_model.clear() self.refs_model.setHorizontalHeaderLabels( ['ReferenceType', 'NodeId', "BrowseName", "TypeDefinition"]) try: refs = self.uaclient.get_all_refs(node) except Exception as ex: self.show_error(ex) raise for ref in refs: self.refs_model.appendRow([ QStandardItem(str(ref.ReferenceTypeId)), QStandardItem(str(ref.NodeId)), QStandardItem(str(ref.BrowseName)), QStandardItem(str(ref.TypeDefinition)) ]) self.ui.refView.resizeColumnToContents(0) self.ui.refView.resizeColumnToContents(1) self.ui.refView.resizeColumnToContents(2) self.ui.refView.resizeColumnToContents(3) def _show_attrs(self, node): try: attrs = self.uaclient.get_all_attrs(node) except Exception as ex: self.show_error(ex) raise self.attr_model.clear() self.attr_model.setHorizontalHeaderLabels(['Attribute', 'Value']) for k, v in attrs.items(): self.attr_model.appendRow( [QStandardItem(k), QStandardItem(str(v))]) self.ui.attrView.resizeColumnToContents(0) self.ui.attrView.resizeColumnToContents(1) def _connect(self): uri = self.ui.addrComboBox.currentText() try: self.uaclient.connect(uri) except Exception as ex: self.show_error(ex) raise self.model.client = self.uaclient self.model.clear() self.model.add_item(self.uaclient.get_root_attrs()) self.ui.treeView.resizeColumnToContents(0) self.ui.treeView.resizeColumnToContents(1) self.ui.treeView.resizeColumnToContents(2) def _disconnect(self): try: self.uaclient.disconnect() except Exception as ex: self.show_error(ex) raise finally: self.model.clear() self.sub_model.clear() self.model.client = None def closeEvent(self, event): self._disconnect() event.accept()
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 PinStatusWidget(GalacteekTab): COL_TS = 0 COL_QUEUE = 1 COL_PATH = 2 COL_STATUS = 3 COL_PROGRESS = 4 COL_CTRL = 5 def __init__(self, gWindow, **kw): super(PinStatusWidget, self).__init__(gWindow, **kw) self.tree = QTreeView() self.tree.setObjectName('pinStatusWidget') self.boxLayout = QVBoxLayout() self.boxLayout.addWidget(self.tree) self.ctrlLayout = QHBoxLayout() self.btnPin = QPushButton(iPin()) self.pathLabel = QLabel(iCidOrPath()) self.pathEdit = QLineEdit() self.ctrlLayout.addWidget(self.pathLabel) self.ctrlLayout.addWidget(self.pathEdit) self.ctrlLayout.addWidget(self.btnPin) self.vLayout.addLayout(self.ctrlLayout) self.vLayout.addLayout(self.boxLayout) self.app.ipfsCtx.pinItemStatusChanged.connect(self.onPinStatusChanged) self.app.ipfsCtx.pinFinished.connect(self.onPinFinished) self.app.ipfsCtx.pinItemRemoved.connect(self.onItemRemoved) self.pathEdit.returnPressed.connect(self.onPathEntered) self.btnPin.clicked.connect(self.onPathEntered) self.model = QStandardItemModel() self.model.setHorizontalHeaderLabels( ['TS', iQueue(), iPath(), iStatus(), iNodesProcessed(), '']) self.tree.setSortingEnabled(True) self.tree.setModel(self.model) self.tree.sortByColumn(self.COL_TS, Qt.DescendingOrder) for col in [self.COL_QUEUE, self.COL_PATH, self.COL_PROGRESS]: self.tree.header().setSectionResizeMode( col, QHeaderView.ResizeToContents) self.tree.hideColumn(self.COL_TS) def resort(self): self.model.sort(self.COL_TS, Qt.DescendingOrder) def onPathEntered(self): text = self.pathEdit.text() self.pathEdit.clear() path = IPFSPath(text) if path.valid: ensure(self.app.ipfsCtx.pinner.queue(path.objPath, True, None)) else: messageBox(iInvalidInput()) def removeItem(self, path): modelSearch(self.model, search=path, columns=[self.COL_PATH], delete=True) def onItemRemoved(self, qname, path): self.removeItem(path) def getIndexFromPath(self, path): idxList = self.model.match(self.model.index(0, self.COL_PATH), PinObjectPathRole, path, 1, Qt.MatchFixedString | Qt.MatchWrap) if len(idxList) > 0: return idxList.pop() def findPinItems(self, path): idx = self.getIndexFromPath(path) if not idx: return None itemP = self.model.itemFromIndex(idx) if not itemP: return None idxQueue = self.model.index(itemP.row(), self.COL_QUEUE, itemP.index().parent()) idxProgress = self.model.index(itemP.row(), self.COL_PROGRESS, itemP.index().parent()) idxStatus = self.model.index(itemP.row(), self.COL_STATUS, itemP.index().parent()) idxC = self.model.index(itemP.row(), self.COL_CTRL, itemP.index().parent()) cancelButton = self.tree.indexWidget(idxC) return { 'itemPath': itemP, 'itemQname': self.model.itemFromIndex(idxQueue), 'itemProgress': self.model.itemFromIndex(idxProgress), 'itemStatus': self.model.itemFromIndex(idxStatus), 'cancelButton': cancelButton } def updatePinStatus(self, path, status, progress): idx = self.getIndexFromPath(path) if not idx: return try: itemPath = self.model.itemFromIndex(idx) if itemPath and time.time() - itemPath.lastProgressUpdate < 5: return itemProgress = self.model.itemFromIndex( self.model.index(idx.row(), self.COL_PROGRESS, idx.parent())) itemStatus = self.model.itemFromIndex( self.model.index(idx.row(), self.COL_STATUS, idx.parent())) itemStatus.setText(status) itemProgress.setText(progress) itemPath.lastProgressUpdate = time.time() except: pass def onPinFinished(self, path): items = self.findPinItems(path) if items: items['itemStatus'].setText(iPinned()) items['itemProgress'].setText('OK') color1 = QBrush(QColor('#4a9ea1')) color2 = QBrush(QColor('#66a56e')) for item in [ items['itemQname'], items['itemPath'], items['itemStatus'], items['itemProgress'] ]: item.setBackground(color1) for item in [items['itemStatus'], items['itemProgress']]: item.setBackground(color2) if items['cancelButton']: items['cancelButton'].setEnabled(False) self.resort() self.purgeFinishedItems() def purgeFinishedItems(self): maxFinished = 16 ret = modelSearch(self.model, search=iPinned(), columns=[self.COL_STATUS]) if len(ret) > maxFinished: rows = [] for idx in ret: item = self.model.itemFromIndex(idx) if not item: continue rows.append(item.row()) try: for row in list(sorted(rows))[int(maxFinished / 2):]: self.model.removeRow(row) except: pass async def onCancel(self, qname, path, *a): self.removeItem(path) await self.app.ipfsCtx.pinner.cancel(qname, path) def onPinStatusChanged(self, qname, path, statusInfo): nodesProcessed = statusInfo['status'].get('Progress', iUnknown()) idx = self.getIndexFromPath(path) if not idx: # Register it btnCancel = QToolButton() btnCancel.setIcon(getIcon('cancel.png')) btnCancel.setText(iCancel()) btnCancel.clicked.connect(partialEnsure(self.onCancel, qname, path)) btnCancel.setFixedWidth(140) displayPath = path if len(displayPath) > 64: displayPath = displayPath[0:64] + ' ..' itemTs = UneditableItem(str(statusInfo['ts_queued'])) itemQ = UneditableItem(qname) itemP = UneditableItem(displayPath) itemP.setData(path, PinObjectPathRole) itemP.setToolTip(path) itemP.lastProgressUpdate = time.time() itemStatus = UneditableItem(iPinning()) itemProgress = UneditableItem(str(nodesProcessed)) itemC = UneditableItem('') self.model.invisibleRootItem().appendRow( [itemTs, itemQ, itemP, itemStatus, itemProgress, itemC]) idx = self.model.indexFromItem(itemC) self.tree.setIndexWidget(idx, btnCancel) self.resort() else: self.updatePinStatus(path, iPinning(), str(nodesProcessed))
class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self, parent=None): super().__init__(parent) self.setupUi(self) self.theModel = QStandardItemModel() theItemDelegate = RixJsonItemDelegate() self.theModel.setHorizontalHeaderItem(0, QStandardItem('Key')) self.theModel.setHorizontalHeaderItem(1, QStandardItem('Value')) self.theModel.setHorizontalHeaderItem(2, QStandardItem('Type')) # 设置TREE VIEW使用的模型为StandarItemModel self.treeView.setModel(theModel) # 设置Tree View使用的委托为自定义的委托 self.treeView.setItemDelegate(theItemDelegate) self.theModel.itemChanged.connect(self.treeDataChanged) self.openButton.clicked.connect(self.openFile) self.saveButton.clicked.connect(self.saveFile) self.saveAnotherFile.clicked.connect(self.saveAnotherFile) self.menuEdit.triggered.connect(self.onMenuActionTrigger) self.menuFile.triggered.connect(self.onMenuActionTrigger) self.treeView.setContextMenuPolicy(Qt.CustomContextMenu) self.treeView.CustomContextMenuRequested.connect(self.showTreeViewMenu) self.menuEdit.aboutToShow.connect(self.aboutToShowEditMenu) def treeDataChanged(self, item: 'QStandardItem'): s = QStack() if item.column() == 0: i = self.theModel.indexFromItem(item) else: i = self.theModel.indexFromItem(item).siblingAtColumn(0) while i.isValid(): o = DataManager.get_instance().getCurrentJsonObject() p = None while not s.isEmpty(): index = s.pop() p = o o = DataManager.get_instance().getChild(index.row()) def openFile(self): pass def saveFile(self): pass def saveAnotherFile(self): pass def onMenuActionTrigger(self, action: 'QAction'): pass def showTreeViewMenu(self, point: 'QPoint'): self.menuEdit.exec(QCursor.pos()) def aboutToShowEditMenu(self): index = self.treeView.selectionModel.currentIndex() self.actionAddChild.setVisible( index.isValid() & (index.siblingAtColumn(2).data(Qt.DisplayRole))) def updateTreeModel(self): pass def addRixJsonItem(self, asChild=False, key="key", value="value", type=str): index = self.treeView.selectionModel().currentIndex() if not index.isValid(): o = DataManager.get_instance().getCurrentJsonObject() o.setType(type) o.addChild(new_o) # ???? self.updateTreeModel() self.textBrowser.setText( DataManager.get_instance().getCurrentJsonObject()) DataManager.get_instance().setDirty(True) self.setWindowTitle("RixJsonEditor | " + DataManager.get_instance().getFileName() + "*") return item = self.theModel.itemFromIndex(index) s = QStack() if item.column() == 0: i = self.theModel.indexFromItem(item) else: i = self.theModel.indexFromItem(item).siblingAtColumn(0) while i.isValid: s.push(i) i = i.parent() o = DataManager.get_instance().getCurrentJsonObject() p = None while not s.isEmpty(): index = s.pop() p = o o = DataManager.get_instance().getChild(index.row()) if asChild: o.addChild(new_o) else: if p != None: os = p.getChildren() pos = index.row() os.insert(os.begin() + pos + 1, new_o) # ??? self.updateTreeModel() self.textBrowser.setText( DataManager.get_instance().getCurrentJsonObject()) DataManager.get_instance().setDirty(True) self.setWindowTitle("RixJsonEditor | " + DataManager.get_instance().getFileName() + "*") def deleteRixJsonItem(self): index = self.treeView.selectionModel().currentIndex() if not index.isValid(): return item = self.theModel.itemFromIndex(index) s = QStack() if item.column() == 0: i = self.theModel.indexFromItem(item) else: i = self.theModel.indexFromItem(item).siblingAtColumn(0) while i.isValid(): s.push(i) i = i.parent() o = DataManager.get_instance().getCurrentJsonObject() p = None while not s.isEmpty(): index = s.pop() p = o o = DataManager.get_instance().getChild(index.row()) if p != None: os = p.getChildren() os.erase(os.begin() + index.row()) self.updateTreeModel() self.textBrowser.setText( DataManager.get_instance().getCurrentJsonObject()) DataManager.get_instance().setDirty(True) self.setWindowTitle("RixJsonEditor | " + DataManager.get_instance().getFileName() + "*") def expandAll(self): self.treeView.expandAll def collapseAll(self): self.treeView.collapseAll()
class NamespaceWidget(QObject): error = pyqtSignal(Exception) def __init__(self, view): QObject.__init__(self, view) self.view = view self.model = QStandardItemModel() self.view.setModel(self.model) delegate = MyDelegate(self.view, self) delegate.error.connect(self.error.emit) self.view.setItemDelegate(delegate) self.node = None self.view.header().setSectionResizeMode(1) self.addNamespaceAction = QAction("Add Namespace", self.model) self.addNamespaceAction.triggered.connect(self.add_namespace) self.removeNamespaceAction = QAction("Remove Namespace", self.model) self.removeNamespaceAction.triggered.connect(self.remove_namespace) self.view.setContextMenuPolicy(Qt.CustomContextMenu) self.view.customContextMenuRequested.connect(self.showContextMenu) self._contextMenu = QMenu() self._contextMenu.addAction(self.addNamespaceAction) self._contextMenu.addAction(self.removeNamespaceAction) @trycatchslot def add_namespace(self): uries = self.node.get_value() newidx = len(uries) it = self.model.item(0, 0) uri_it = QStandardItem("") it.appendRow([QStandardItem(), QStandardItem(str(newidx)), uri_it]) idx = self.model.indexFromItem(uri_it) self.view.edit(idx) @trycatchslot def remove_namespace(self): idx = self.view.currentIndex() if not idx.isValid() or idx == self.model.item(0, 0): logger.warning("No valid item selected to remove") idx = idx.sibling(idx.row(), 2) item = self.model.itemFromIndex(idx) uri = item.text() uries = self.node.get_value() uries.remove(uri) logger.info("Writting namespace array: %s", uries) self.node.set_value(uries) self.reload() def set_node(self, node): self.model.clear() self.node = node self.show_array() def reload(self): self.set_node(self.node) def show_array(self): self.model.setHorizontalHeaderLabels(['Browse Name', 'Index', 'Value']) name_item = QStandardItem(self.node.get_browse_name().Name) self.model.appendRow([name_item, QStandardItem(""), QStandardItem()]) it = self.model.item(0, 0) val = self.node.get_value() for idx, url in enumerate(val): it.appendRow([QStandardItem(), QStandardItem(str(idx)), QStandardItem(url)]) self.view.expandAll() def clear(self): self.model.clear() def showContextMenu(self, position): self.removeNamespaceAction.setEnabled(False) idx = self.view.currentIndex() if not idx.isValid(): return if idx.parent().isValid() and idx.row() >= 1: self.removeNamespaceAction.setEnabled(True) self._contextMenu.exec_(self.view.viewport().mapToGlobal(position))
class ClientGui(QWidget): def __init__(self): self.service_msg_deq = [] # очередь сервисных сообщений, очищать! self.icon_user = QIcon("user.svg") self.icon_new_msg = QIcon("message.svg") super().__init__() self.get_login_dialog() def initUI(self): # Кнопки: добавить/удалить контакты в контакт лист self.button_add_contact = QPushButton('add', self) self.button_add_contact.clicked.connect(self.add_contact) self.button_del_contact = QPushButton('del', self) self.button_del_contact.clicked.connect(self.del_contact) self.button_settings = QPushButton('men', self) self.button_settings.setEnabled(False) # не работает self.button_connect = QPushButton('conn', self) self.button_connect.setEnabled(False) # не работает self.box_button = QHBoxLayout() self.box_button.addWidget(self.button_add_contact) self.box_button.addWidget(self.button_del_contact) self.box_button.addWidget(self.button_settings) self.box_button.addWidget(self.button_connect) # создаю модель для листа контактов, подключаю отображение cl = bd_client_app.BDContacts().get_contacts() self.model_cl = QStandardItemModel() for user in cl: row = QStandardItem(self.icon_user, user) self.model_cl.appendRow(row) self.contact_list = QListView() self.contact_list.setModel(self.model_cl) self.contact_list.setSelectionMode(QListView.SingleSelection) self.contact_list.setEditTriggers(QListView.NoEditTriggers) self.contact_list.clicked.connect(self.select_conlist) # строка и кнопка отправки сообщений qButton = QPushButton('>>', self) qButton.clicked.connect(self.send_click) self.sendBox = QLineEdit(self) self.sendBox.returnPressed.connect(self.send_click) self.messageBox = QStackedWidget() # два словаря, в первом: логин ключ виджет значение, второй наоборот self.messageBox_dict_ctw = {} self.messageBox_dict_wtc = {} for user in cl: self.messageBox_dict_ctw[user] = QListWidget() self.messageBox_dict_wtc[self.messageBox_dict_ctw[user]] = user self.messageBox.addWidget(self.messageBox_dict_ctw[user]) grid = QGridLayout() # строка, столбец, растянуть на строк, растянуть на столбцов grid.addWidget(self.contact_list, 0, 0, 2, 3) grid.addLayout(self.box_button, 2, 0) grid.addWidget(self.messageBox, 0, 3, 2, 3) grid.addWidget(self.sendBox, 2, 3, 1, 2) grid.addWidget(qButton, 2, 5) grid.setSpacing(5) grid.setColumnMinimumWidth(3, 200) grid.setColumnStretch(3, 10) self.setLayout(grid) self.resize(800, 300) self.center() self.setWindowTitle('Avocado') self.setWindowIcon(QIcon('icon.svg')) self.show() def initThreads(self): self.print_thread = ClientThreads(self.client, self) self.print_thread.print_signal.connect(self.add_message) self.print_thread.start() def get_login_dialog(self): text, ok = QInputDialog.getText(self, 'Login', 'Connect with login:') self.login_name = str(text) if ok: self.service_msg_deq.clear() # жду свой ответ self.init_client() self.initThreads() # while not self.service_msg_deq: # print(self.service_msg_deq) # pass # жду ответ # if self.service_msg_deq[0] is True: time.sleep(1) self.initUI() # else: # self.exit() else: self.exit() def init_client(self): self.client = client.Client(self.login_name, "localhost", 7777) self.client.start_th_gui_client() def center(self): # центрирую окно qr = self.frameGeometry() cp = QDesktopWidget().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft()) @pyqtSlot() def send_click(self): text_to_send = self.sendBox.text() if text_to_send.rstrip(): self.messageBox.currentWidget().addItem("<< " + text_to_send) self.client.inp_queue.put(text_to_send) self.sendBox.clear() @pyqtSlot(QModelIndex) def select_conlist(self, curr): self.messageBox.setCurrentIndex(curr.row()) self.model_cl.itemFromIndex(curr).setIcon(self.icon_user) self.client.to_user = self.messageBox_dict_wtc[ self.messageBox.currentWidget()] @pyqtSlot(tuple) def add_message(self, message): msg = message[0] from_u = message[1] try: client_widget = self.messageBox_dict_ctw[from_u] except KeyError: mesg_con_log.error("Message from user from not in contact list:") mesg_con_log.error("%s, %s" % (from_u, msg)) else: client_widget.addItem(">> " + msg) message_from = self.model_cl.findItems(from_u)[0] if self.contact_list.currentIndex() != self.model_cl.indexFromItem( message_from): message_from.setIcon(self.icon_new_msg) @pyqtSlot() def del_contact(self): user = self.client.to_user self.client.inp_queue.put("del_contact " + user) self.messageBox.removeWidget(self.messageBox_dict_ctw[user]) self.model_cl.takeRow( self.model_cl.indexFromItem( self.model_cl.findItems(user)[0]).row()) @pyqtSlot() def add_contact(self): user = self.sendBox.text() self.service_msg_deq.clear() # жду свой ответ self.client.inp_queue.put("add_contact " + user) while not self.service_msg_deq: pass # жду ответ if self.service_msg_deq[0] is True: row = QStandardItem(self.icon_user, user) self.model_cl.appendRow(row) self.messageBox_dict_ctw[user] = QListWidget() self.messageBox_dict_wtc[self.messageBox_dict_ctw[user]] = user self.messageBox.addWidget(self.messageBox_dict_ctw[user]) else: pass self.sendBox.clear()
class ConfigManagerDialog(Ui_MainWindow, QMainWindow): def __init__(self, roamapp, parent=None): super(ConfigManagerDialog, self).__init__(parent) self.setupUi(self) self.setWindowTitle("Roam Config Manager") self.bar = roam.messagebaritems.MessageBar(self) self.roamapp = roamapp self.reloadingProject = False self.loadedProject = None self.projectpath = None # Nope! self.projectwidget.roamapp = roamapp self.projectwidget.bar = self.bar self.treemodel = QStandardItemModel() self.projectList.setModel(self.treemodel) self.projectList.setHeaderHidden(True) self.projectList.selectionModel().currentChanged.connect( self.nodeselected) self.projectwidget.adjustSize() self.setWindowFlags(Qt.Window) self.projectwidget.projectupdated.connect(self.projectupdated) self.projectwidget.projectloaded.connect(self.projectLoaded) self.projectwidget.setaboutinfo() self.projectwidget.projects_page.projectlocationchanged.connect( self.loadprojects) self.setuprootitems() ConfigEvents.deleteForm.connect(self.delete_form) def closeEvent(self, closeevent): self.save_page_config() closeevent.accept() def raiseerror(self, *exinfo): self.bar.pushError(*exinfo) def setuprootitems(self): rootitem = self.treemodel.invisibleRootItem() self.roamnode = RoamNode() rootitem.appendRow(self.roamnode) self.datanode = DataNode(folder=None) rootitem.appendRow(self.datanode) self.projectsnode = ProjectsNode(folder=None) rootitem.appendRow(self.projectsnode) self.publishnode = PublishNode(folder=None) rootitem.appendRow(self.publishnode) self.pluginsnode = PluginsNode() pluginpath = os.path.join(self.roamapp.apppath, "plugins") rootitem.appendRow(self.pluginsnode) self.pluginsnode.add_plugin_paths([pluginpath]) def delete_form(self): index = self.projectList.currentIndex() node = index.data(Qt.UserRole) if not node.type() == Treenode.FormNode: return title, removemessage = node.removemessage delete = node.canremove if node.canremove and removemessage: button = QMessageBox.warning(self, title, removemessage, QMessageBox.Yes | QMessageBox.No) delete = button == QMessageBox.Yes if delete: parentindex = index.parent() newindex = self.treemodel.index(index.row(), 0, parentindex) if parentindex.isValid(): parent = parentindex.data(Qt.UserRole) parent.delete(index.row()) print(parentindex) self.projectList.setCurrentIndex(parentindex) def delete_project(self): index = self.projectList.currentIndex() node = index.data(Qt.UserRole) if node.type() == Treenode.ProjectNode: self.projectwidget._closeqgisproject() title, removemessage = node.removemessage delete = node.canremove if node.canremove and removemessage: button = QMessageBox.warning(self, title, removemessage, QMessageBox.Yes | QMessageBox.No) delete = button == QMessageBox.Yes if delete: parentindex = index.parent() newindex = self.treemodel.index(index.row(), 0, parentindex) if parentindex.isValid(): parent = parentindex.data(Qt.UserRole) parent.delete(index.row()) self.projectList.setCurrentIndex(newindex) def addprojectfolders(self, folders): self.projectwidget.projects_page.setprojectfolders(folders) @property def active_project_folder(self): return self.projectpath @active_project_folder.setter def active_project_folder(self, value): self.projectpath = value pass def loadprojects(self, projectpath): self.active_project_folder = projectpath projects = roam.project.getProjects([projectpath]) self.projectsnode.loadprojects(projects, projectsbase=projectpath) index = self.treemodel.indexFromItem(self.projectsnode) self.projectList.setCurrentIndex(index) self.projectList.expand(index) def projectLoaded(self, project): roam.utils.info("Project loaded: {}".format(project.name)) node = self.projectsnode.find_by_name(project.name) node.create_children() self.projectwidget.setpage(node.page, node, refreshingProject=True) self.reloadingProject = False self.loadedProject = project self.projectList.setExpanded(node.index(), True) def nodeselected(self, index, last, reloadProject=False): node = index.data(Qt.UserRole) if node is None: return project = node.project if project: if not project.requires_upgrade: validateresults = list(project.validate()) if validateresults: text = "Here are some reasons we found: \n\n" for message in validateresults: text += "- {} \n".format(message) self.projectwidget.reasons_label.setText(text) return if node.nodetype == Treenode.ProjectNode and reloadProject: self.reloadingProject = True self.projectwidget.setproject(project) return if project and self.loadedProject != project: # Only load the project if it's different the current one. if self.loadedProject: lastnode = self.projectsnode.find_by_name( self.loadedProject.name) self.projectList.setExpanded(lastnode.index(), False) # We are closing the open project at this point. Watch for null ref after this. self.projectwidget.setproject(project) self.projectwidget.setpage(node.page, node) return else: self.projectwidget.setpage(node.page, node) if node.nodetype == Treenode.AddNew: try: item = node.additem() except ValueError: return newindex = self.treemodel.indexFromItem(item) self.projectList.setCurrentIndex(newindex) return def save_page_config(self): """ Save the current page config """ self.projectwidget.savePage(closing=True) def projectupdated(self, project): node = self.projectsnode.find_by_name(project.name) self.projectList.selectionModel().select( node.index(), QItemSelectionModel.ClearAndSelect) self.nodeselected(node.index(), None, reloadProject=True)
class BlockView(QWidget): selectionChanged = pyqtSignal(int, int, str) addAnalyzerTrigger = pyqtSignal(str) def __init__(self, parent, cols: dict, blocks: [Block], undoStack: QUndoStack): super(BlockView, self).__init__(parent=parent) self.undoStack = undoStack self.buffer = None self.cols = cols self.blocks = blocks self.tree = TreeView(self) self.model = QStandardItemModel() self.tree.setModel(self.model) self.entry = QLineEdit(self) self.entry.setPlaceholderText('Search for ...') self.entry.setClearButtonEnabled(True) vbox = QVBoxLayout() vbox.addWidget(self.entry) vbox.addWidget(self.tree) self.setLayout(vbox) self.tree.selectionModel().selectionChanged.connect( self.selectionChangedEvent) self.tree.doubleClicked.connect(self.moduleModify) self.entry.textChanged.connect(self.searchTree) self.hiddenRows = {} self.create_ui() def clearHidden(self): if self.entry.text().strip() == '': for key, rows in self.hiddenRows.items(): for row in rows: self.tree.setRowHidden(row, key, False) self.hiddenRows = {} def searchTree(self): text = self.entry.text().strip() if text == '': self.clearHidden() return pattern = re.compile(text, re.IGNORECASE) field_pattern = re.compile(text, re.IGNORECASE) for row in range(self.model.rowCount()): item = self.model.item(row, 0) index = self.model.indexFromItem(item) block = self.blocks[index.row()] if item.hasChildren(): for itemRow in range(item.rowCount()): fields = block.get_register_fields(itemRow) for itemCol in range(item.columnCount()): child = item.child(itemRow, itemCol) if pattern.search(child.text()): self.tree.setRowHidden(itemRow, index, False) break if itemCol == item.columnCount() - 1: if self.search_fields(fields, field_pattern): self.tree.setRowHidden(itemRow, index, False) break else: self.hiddenRows.setdefault(index, []) self.hiddenRows[index].append(itemRow) self.tree.setRowHidden(itemRow, index, True) def search_fields(self, fields, pattern): for field in fields: for value in field.values(): # if type(value) is int: # value = str(value) if pattern.findall(value): return True return False def dataBeforeChangedEvent(self, index: QModelIndex, new: str, old: str): if not index.isValid(): return row = index.row() col = index.column() cmd = DataChanged(widget=self.model, newtext=new, oldtext=old, index=index, description=f'Table Data changed at ({row}, {col})') self.undoStack.push(cmd) def create_ui(self): self.create_actions() self.create_cols() self.create_rows() self.tree.setCurrentIndex(self.model.index(0, 0)) def create_rows(self, blocks: [dict] = None): if blocks is not None: self.blocks = blocks self.model.removeRows(0, self.model.rowCount()) for block in self.blocks: block.setDisplayItem() root = block.getDisplayItem() for register in block: root[0].appendRow([ QStandardItem(register.get(col)) for col in self.cols.keys() ]) self.model.appendRow(root) def create_cols(self): self.model.setHorizontalHeaderLabels(self.cols.keys()) for col, config in enumerate(self.cols.values()): widget = config.get('widget', None) width = config.get('width', None) if width: self.tree.setColumnWidth(col, width) if widget == "list": items = config.get('items', []) delegate = ListViewDelegate(self.tree, items) elif widget == 'textEdit': delegate = TextEditDelegate(self.tree) else: validator = config.get('type', None) items = config.get('items', None) delegate = LineEditDelegate(self.tree, items, validator, minValue=config.get('minValue', 0), maxValue=config.get( 'maxValue', 31)) self.tree.setItemDelegateForColumn(col, delegate) delegate.dataBeforeChanged.connect(self.dataBeforeChangedEvent) def selectionChangedEvent(self): # self.blockSignals(True) index = self.tree.currentIndex() if not index.isValid(): # print(index.row(), index.column(), 'is not exist') return try: if not index.model().hasChildren(index): block_index = index.parent() if not block_index.isValid(): return block_index = block_index.row() block = self.blocks[block_index] index = index.row() register = block.get_register(index) if register is None: return self.selectionChanged.emit( block_index, index, caption_formatter.format(Module=block, Register=register)) except Exception: traceback.print_exc(file=sys.stdout) def getRowValues(self, items=None): if items is None: items = self.tree.selectedIndexes() values = {} for item, col in zip(items, self.cols.keys()): values[col] = item.data() return values def iterItemPerSelectedRow(self) -> (int, QModelIndex): indexes = self.tree.selectedIndexes() col_length = len(self.cols.keys()) for i in range(0, len(indexes), col_length): index = indexes[i] yield index.row(), index # yield indexes[i].row() def contextMenuEvent(self, event: QContextMenuEvent): menu = QMenu(self.tree) actions = {} for each_menu in register_contextmenu: label = each_menu.get('label') icon = each_menu.get('icon') buffer = each_menu.get('buffer', False) action = menu.addAction(label) if buffer and self.buffer is None: action.setEnabled(False) sc = each_menu.get('shortcut', None) if sc: action.setShortcut(sc) if icon: action.setIcon(qta.icon(icon)) actions[action] = getattr(self, each_menu.get('action')) action = menu.exec_(self.mapToGlobal(event.pos())) func = actions.get(action) if callable(func): func() def create_actions(self): for config in register_contextmenu: sc = config.get('shortcut', None) if sc is None: continue label = config.get('label') qaction = QAction(label, self.tree) # func = actions.pop() func = getattr(self, config.get('action')) qaction.setShortcut(sc) qaction.setShortcutContext(Qt.WidgetWithChildrenShortcut) qaction.triggered.connect(func) self.addAction(qaction) def append_new(self): row = self.tree.currentRow new = Register(deepcopy(new_reg)) index = self.tree.currentIndex() if not index.isValid(): return if index.data(Qt.UserRole) == 'block': return if index.column() != 0: index = index.sibling(index.row(), 0) try: offset = int(index.data(), 16) + 4 new['Offset'] = "0x{0:04X}".format(offset) self.insert_row( row + 1, [new], ) except ValueError: MessageBox.showError( self, "The Offset value of this row in not valid\n", GUI_NAME) def prepend_new(self): index = self.tree.currentIndex() if not index.isValid(): return if index.data(Qt.UserRole) == 'block': return if index.column() != 0: index = index.sibling(index.row(), 0) row = self.tree.currentRow new = Register(deepcopy(new_reg)) try: offset = int(index.data(), 16) - 4 if offset < 0: offset = 0 new['Offset'] = "0x{0:04X}".format(offset) self.insert_row( row, [new], ) except ValueError: MessageBox.showError( self, "The Offset value of this row in not valid\n", GUI_NAME) def append_copy(self): if self.buffer is None: return index = self.tree.currentIndex() if index.data(Qt.UserRole) == 'block': items = self.buffer.get('blocks') else: items = self.buffer.get('registers') if not items: return news = [] for item in items: news.append(item.copy()) row = self.tree.currentRow self.insert_row( row + 1, news, ) def prepend_copy(self): if self.buffer is None: return row = self.tree.currentRow news = [] for register in self.buffer: news.append(register.copy()) self.insert_row( row, news, ) def insert_row(self, row, items): root = self.model.itemFromIndex(self.tree.currentIndex()).parent() if root is None: root = self.model block = self.blocks is_root = True else: block = self.blocks[root.row()].registers is_root = False for item in reversed(items): cmd = TreeInsertCommand(widget=root, block=block, row=row, items=item, cols=self.cols, description='tree insertion', is_root=is_root) self.undoStack.push(cmd) def copy(self): try: values = {'blocks': [], 'registers': []} for row, index in self.iterItemPerSelectedRow(): if index.data(Qt.UserRole) == 'block': values['blocks'].append(self.blocks[row].copy()) else: register = self.blocks[index.parent().row()].get_register( row) values['registers'].append(register.copy()) if not values: self.buffer = None else: self.buffer = values except Exception as e: print(traceback.format_exc()) def moduleModify(self, index: QModelIndex): if not index.isValid(): return item = self.model.itemFromIndex(index) if item.data(Qt.UserRole) == 'block': block = self.blocks[index.row()] dialog = InputDialog(parent=self, title=GUI_NAME, inputs=block_columns, values=block) info, save = dialog.get() if save: block.update(info) block.viewUpdate() def cut(self): self.copy() self.remove() def remove(self): if not self.blocks: return items = {} for row, index in self.iterItemPerSelectedRow(): item = self.model.itemFromIndex(index) if not index.isValid(): continue if item.data(Qt.UserRole) == 'block': if row >= len(self.blocks): continue items[row] = [self.model, self.blocks] else: index = index.parent() items[row] = [ self.model.itemFromIndex(index), self.blocks[index.row()].registers ] if not items: return for row in sorted(items.keys(), reverse=True): cmd = TreeRemoveCommand(widget=items[row][0], row=row, description='remove', block=items[row][1]) self.undoStack.push(cmd) def setFocus(self): self.tree.setFocus() def saveChanges(self): for row in range(self.model.rowCount()): item = self.model.item(row, 0) block = self.blocks[row] if item.hasChildren(): for itemRow in range(item.rowCount()): register = block.get_register(itemRow) for col, header in enumerate(self.cols.keys()): child = item.child(itemRow, col) register[header] = child.text() def clearSelection(self): self.tree.clearSelection() def addAnalyzer(self): index = self.tree.currentIndex() if not index.isValid(): return if index.data(Qt.UserRole) == 'block': return block_index = index.parent().row() address, _ = self.blocks[block_index].get_address_space(index.row()) self.addAnalyzerTrigger.emit(address)
class NamespaceWidget(QObject): error = pyqtSignal(Exception) def __init__(self, view): QObject.__init__(self, view) self.view = view self.model = QStandardItemModel() self.view.setModel(self.model) delegate = MyDelegate(self.view, self) delegate.error.connect(self.error.emit) self.view.setItemDelegate(delegate) self.node = None self.view.header().setSectionResizeMode(1) self.addNamespaceAction = QAction("Add Namespace", self.model) self.addNamespaceAction.triggered.connect(self.add_namespace) self.removeNamespaceAction = QAction("Remove Namespace", self.model) self.removeNamespaceAction.triggered.connect(self.remove_namespace) self.view.setContextMenuPolicy(Qt.CustomContextMenu) self.view.customContextMenuRequested.connect(self.showContextMenu) self._contextMenu = QMenu() self._contextMenu.addAction(self.addNamespaceAction) self._contextMenu.addAction(self.removeNamespaceAction) @trycatchslot def add_namespace(self): uries = self.node.read_value() newidx = len(uries) it = self.model.item(0, 0) uri_it = QStandardItem("") it.appendRow([QStandardItem(), QStandardItem(str(newidx)), uri_it]) idx = self.model.indexFromItem(uri_it) self.view.edit(idx) @trycatchslot def remove_namespace(self): idx = self.view.currentIndex() if not idx.isValid() or idx == self.model.item(0, 0): logger.warning("No valid item selected to remove") idx = idx.sibling(idx.row(), 2) item = self.model.itemFromIndex(idx) uri = item.text() uries = self.node.read_value() uries.remove(uri) logger.info("Writting namespace array: %s", uries) self.node.write_value(uries) self.reload() def set_node(self, node): self.model.clear() self.node = node self.show_array() def reload(self): self.set_node(self.node) def show_array(self): self.model.setHorizontalHeaderLabels(['Browse Name', 'Index', 'Value']) name_item = QStandardItem(self.node.read_browse_name().Name) self.model.appendRow([name_item, QStandardItem(""), QStandardItem()]) it = self.model.item(0, 0) val = self.node.read_value() for idx, url in enumerate(val): it.appendRow([QStandardItem(), QStandardItem(str(idx)), QStandardItem(url)]) self.view.expandAll() def clear(self): self.model.clear() def showContextMenu(self, position): self.removeNamespaceAction.setEnabled(False) idx = self.view.currentIndex() if not idx.isValid(): return if idx.parent().isValid() and idx.row() >= 1: self.removeNamespaceAction.setEnabled(True) self._contextMenu.exec_(self.view.viewport().mapToGlobal(position))
class FileListDisplay(QtWidgets.QListView): """ """ NO_COMPATIBLE_IMAGE_STR = 'No Compatible Images' # settings for text display of selected and unselected lines UNSELECTED_FONT = QFont("Helvetica", 12) UNSELECTED_FONT.setBold(False) font = QFont() SELECTED_FONT = QFont("Helvetica", 14) SELECTED_FONT.setBold(True) #################################################################################################################### def __init__(self, ui: ipview_ui.IPViewWindow): """ """ self.ui = ui super(FileListDisplay, self).__init__() self.model = QStandardItemModel() self.ui.text_list_display.setModel(self.model) # list of QStandardItem, each element represents one text line self.q_item_list = [] self.displayed_item_idx = -1 self.stream_display = StreamDisplay.StreamDisplay(ui=self.ui) #################################################################################################################### def load_directory_button_pushed(self) -> None: """ Slot method for loading directory upon load push button. """ self.clear_list_display() self.stream_display.clear_text() directory = self.ui.directory_display.toPlainText() try: files_for_display = self.ui.app_data.load_directory(directory=directory) except FileNotFoundError as e: print(e) return if len(files_for_display) == 0: item = QStandardItem(FileListDisplay.NO_COMPATIBLE_IMAGE_STR) self.model.appendRow(item) else: for f in files_for_display: # initialize to UNSELECTED item = QStandardItem(f) item.setFont(FileListDisplay.UNSELECTED_FONT) self.q_item_list.append(item) self.model.appendRow(item) # display to scene return #################################################################################################################### def display_next_item(self) -> None: """ Method to display next item. In effect, will change the settings of the old item back to UNCHECKED and will change the settings of the next item to CHECKED. """ if self.displayed_item_idx < len(self.q_item_list) - 1: self.q_item_list[self.displayed_item_idx].setFont(FileListDisplay.UNSELECTED_FONT) self.displayed_item_idx += 1 self.q_item_list[self.displayed_item_idx].setFont(FileListDisplay.SELECTED_FONT) self.__keep_current_item_in_view() return #################################################################################################################### def display_previous_item(self) -> None: """ Method to display next item. In effect, will change the settings of the old item back to UNCHECKED and will change the settings of the previous item to CHECKED. """ if self.displayed_item_idx > 0: self.q_item_list[self.displayed_item_idx].setFont(FileListDisplay.UNSELECTED_FONT) self.displayed_item_idx -= 1 self.q_item_list[self.displayed_item_idx].setFont(FileListDisplay.SELECTED_FONT) self.__keep_current_item_in_view() return #################################################################################################################### def clear_list_display(self) -> None: """ Method to clear text display. """ self.model.clear() self.q_item_list = [] self.displayed_item_idx = -1 return #################################################################################################################### def __keep_current_item_in_view(self) -> None: """ Method to auto-scroll the display box to keep the current item in view. """ try: index = self.model.indexFromItem(self.q_item_list[self.displayed_item_idx]) except IndexError: return self.ui.text_list_display.scrollTo(index, QtWidgets.QAbstractItemView.EnsureVisible) return
class DataChangeUI(object): def __init__(self, window, sub_window): self.window = window self.sub_window = sub_window self._subhandler = DataChangeHandler() self._subscribed_nodes = [] #self._datachange_sub = None self._datachange_sub = { } #key: subscriptionId, value: subscription class self._subs_dc = {} #key: nodeId, value: (handler,subscriptionId) '''self.model = QStandardItemModel() self.monitored_item_model = QStandardItemModel() self.window.ui.subView.setModel(self.model) self.window.ui.subView.horizontalHeader().setSectionResizeMode(1) self.window.ui.monItemView.setModel(self.monitored_item_model) self.window.ui.monItemView.horizontalHeader().setSectionResizeMode(1)''' self.model = QStandardItemModel() self.monitored_item_model = QStandardItemModel() self.sub_window.ui.subView.setModel(self.model) self.sub_window.ui.subView.horizontalHeader().setSectionResizeMode(1) self.sub_window.ui.monItemView.setModel(self.monitored_item_model) self.sub_window.ui.monItemView.horizontalHeader().setSectionResizeMode( 1) self.pub_interval = 500 self.lifetime_count = 10000 self.max_keep_alive_count = 3000 self.max_notifications_per_publish = 10000 self.priority = 0 self.subscription_id = None self.sampling_interval = 500 self.monitoring_mode = ua.MonitoringMode.Reporting self.queue_size = 1 self.deadband_value = 0 self.discard_oldest = True self.deadband_type = None self.window.ui.actionCreate_subscription.triggered.connect( lambda: self.show_sub_dialog()) self.window.ui.actionCreate_monitored_item.triggered.connect( lambda: self.show_monitored_item_dialog()) self.window.ui.actionDelete_monitered_item.triggered.connect( lambda: self._unsubscribe()) self.window.ui.actionDelete_subscription.triggered.connect( lambda: self.show_delete_sub_dialog()) # handle subscriptions self._subhandler.data_change_fired.connect( self._update_subscription_model, type=Qt.QueuedConnection) self.numeric_types = [ ua.uatypes.VariantType.Int16, ua.uatypes.VariantType.UInt16, ua.uatypes.VariantType.Int32, ua.uatypes.VariantType.UInt32, ua.uatypes.VariantType.Int64, ua.uatypes.VariantType.UInt64, ua.uatypes.VariantType.Float, ua.uatypes.VariantType.Double ] def show_sub_dialog(self): dia = SubscriptionDialog(self) dia.exec_() def show_monitored_item_dialog(self): dia = MonitoredItemDialog(self) dia.exec_() def show_delete_sub_dialog(self): dia = DeleteSubDialog(self) dia.exec_() def clear(self): self._subscribed_nodes = [] self.model.clear() self.monitored_item_model.clear() def create_subscription(self): try: #if not self._datachange_sub: params = ua.CreateSubscriptionParameters() params.RequestedPublishingInterval = self.pub_interval params.RequestedLifetimeCount = self.lifetime_count params.RequestedMaxKeepAliveCount = self.max_keep_alive_count params.MaxNotificationsPerPublish = self.max_notifications_per_publish params.PublishingEnabled = True params.Priority = self.priority sub = Subscription(self.window.client.uaclient, params, self._subhandler) self._datachange_sub[sub.subscription_id] = sub self.model.setHorizontalHeaderLabels( ["Subscription Id", "Publishing Interval"]) row = [ QStandardItem(str(sub.subscription_id)), QStandardItem(str(sub.parameters.RequestedPublishingInterval)) ] row[0].setData(sub) self.model.appendRow(row) except Exception as ex: self.window.log_window.ui.logTextEdit.append(str(ex)) raise #modelidx = self.model.indexFromItem(row[0]) #self.model.takeRow(idx.row()) def _subscribe(self, node=None): if not isinstance(node, Node): node = self.window.get_current_node() if node is None: return if node.get_node_class() != ua.NodeClass.Variable: self.window.log_window.ui.logTextEdit.append( "Select a variable node") return if node in self._subscribed_nodes: self.window.log_window.ui.logTextEdit.append( "already subscribed to node: %s " % node) return self.monitored_item_model.setHorizontalHeaderLabels( ["DisplayName", "Value", "Timestamp", "Subscription Id"]) text = str(node.get_display_name().Text) row = [ QStandardItem(text), QStandardItem("No Data yet"), QStandardItem(""), QStandardItem(str(self.subscription_id)) ] row[0].setData(node) self.monitored_item_model.appendRow(row) self._subscribed_nodes.append(node) try: mir = self._datachange_sub[ self.subscription_id]._make_monitored_item_request( node, ua.AttributeIds.Value, None, self.queue_size) #mfilter, queue size mir.RequestedParameters.DiscardOldest = self.discard_oldest mir.RequestedParameters.SamplingInterval = self.sampling_interval mir.MonitoringMode = self.monitoring_mode print(mir.MonitoringMode) mod_filter = ua.DataChangeFilter() mod_filter.Trigger = ua.DataChangeTrigger( 1) # send notification when status or value change if self.deadband_type != None: if self.deadband_type == 1: if node.get_data_type_as_variant_type( ) in self.numeric_types: mod_filter.DeadbandType = self.deadband_type #1 assoluta , 2 percentage mod_filter.DeadbandValue = self.deadband_value else: self.window.log_window.ui.logTextEdit.append( "filter must be used for numeric data type") elif self.deadband_type == 2: properties = node.get_properties() EURange = False for p in properties: browse_name = p.get_browse_name().Name if browse_name == "EURange": EURange = True if node.get_type_definition( ).Identifier == ua.object_ids.ObjectIds.AnalogItemType and EURange == True: if node.get_data_type_as_variant_type( ) in self.numeric_types: mod_filter.DeadbandType = self.deadband_type #1 assoluta , 2 percentage mod_filter.DeadbandValue = self.deadband_value else: self.window.log_window.ui.logTextEdit.append( "filter must be used for numeric data type") else: self.window.log_window.ui.logTextEdit.append( "percentage deadband must be applied to AnalagoItemType with EUrange" ) mir.RequestedParameters.Filter = mod_filter handle = self._datachange_sub[ self.subscription_id].create_monitored_items([mir]) self._subs_dc[node.nodeid] = (handle[0], self.subscription_id) except Exception as ex: self.window.log_window.ui.logTextEdit.append(str(ex)) idx = self.monitored_item_model.indexFromItem(row[0]) self.monitored_item_model.takeRow(idx.row()) def _unsubscribe(self, node=None): try: if node is None: node = self.window.get_current_node() if node is None: return sub_id = self._subs_dc[node.nodeid][1] handle = self._subs_dc[node.nodeid][0] self._datachange_sub[sub_id].unsubscribe(handle) self._subscribed_nodes.remove(node) i = 0 while self.monitored_item_model.item(i): item = self.monitored_item_model.item(i) if item.data() == node: self.monitored_item_model.removeRow(i) i += 1 except Exception as ex: self.window.log_window.ui.logTextEdit.append(str(ex)) def _update_subscription_model(self, node, value, timestamp): i = 0 while self.monitored_item_model.item(i): item = self.monitored_item_model.item(i) if item.data() == node: it = self.monitored_item_model.item(i, 1) it.setText(value) it_ts = self.monitored_item_model.item(i, 2) it_ts.setText(timestamp) i += 1 def delete_subscription(self, subscription_id): try: for k, v in self._subs_dc.items(): if v[1] == subscription_id: node = self.get_node(k) if node is not None: self._unsubscribe(node=node) sub = self._datachange_sub[subscription_id].delete() self._datachange_sub.pop(subscription_id) i = 0 while self.model.item(i): item = self.model.item(i) if item.data().subscription_id == subscription_id: self.model.removeRow(i) i += 1 except Exception as ex: self.window.log_window.ui.logTextEdit.append(str(ex)) def get_node(self, node_id): for node in self._subscribed_nodes: if node != None: if node.nodeid == node_id: return node return None
class WadTreeView(Base, Form): def __init__(self, root, controller, parent=None): super(self.__class__, self).__init__(parent) self.setupUi(self) add_widget(root, self, 'WAD_TREE') self.wads = controller.models.wads self.categories = controller.models.categories self.controller = controller self.loaded_wads = {} self.loaded_categories = {} self.pending_children = {} self.finished_loading = {'categories': False, 'wads': False} self.id_item_mapping = {} self.wadtree = self.findChild(QTreeView, 'wadtree') self.wadtree.dropEvent = self.dropEvent self.wadtree.setDragEnabled(True) self.wadtree.viewport().setAcceptDrops(True) self.wadtree.setDropIndicatorShown(True) self.wadtree.setDragDropMode(QAbstractItemView.DragDrop) self.wadtree.setDefaultDropAction(Qt.MoveAction) self.wadtree.setContextMenuPolicy(Qt.CustomContextMenu) self.wadtree.customContextMenuRequested.connect(self.open_menu) self.wadtree_model = QStandardItemModel() self.wadtree.setModel(self.wadtree_model) self.wadtree.selectionModel().selectionChanged.connect( self.select_tree_index) self.wadtree_model.itemChanged.connect(self.change_category_text) self.root = self.wadtree_model.invisibleRootItem() def dropEvent(self, e): from_index = e.source().currentIndex() from_item = self.wadtree_model.itemFromIndex(from_index) from_parent = from_item.parent() or self.root from_row = from_item.row() to_index = self.wadtree.indexAt(e.pos()) to_item = self.wadtree_model.itemFromIndex(to_index) or self.root to_parent = to_item.parent() or self.root to_row = to_item.row() take_from = from_parent.takeRow(from_row) sharing_parents = from_parent == to_parent adjust = { QAbstractItemView.AboveItem: (-1) if sharing_parents and from_row < to_row else 0, QAbstractItemView.BelowItem: (0) if sharing_parents and from_row < to_row else 1 } self.categories.remove_child(from_parent.data(ID_ROLE), from_item.data(ID_ROLE)) indicator = self.wadtree.dropIndicatorPosition() if indicator == QAbstractItemView.OnItem: self.categories.add_child(to_item.data(ID_ROLE), from_item.data(ID_ROLE)) to_item.appendRow(take_from) elif indicator == QAbstractItemView.AboveItem: self.categories.insert_child(to_parent.data(ID_ROLE), from_item.data(ID_ROLE), to_row + adjust[indicator]) to_parent.insertRow(to_row + adjust[indicator], take_from) elif indicator == QAbstractItemView.BelowItem: self.categories.insert_child(to_parent.data(ID_ROLE), from_item.data(ID_ROLE), to_row + adjust[indicator]) to_parent.insertRow(to_row + adjust[indicator], take_from) elif indicator == QAbstractItemView.OnViewport: self.categories.add_child(to_parent.data(ID_ROLE), from_item.data(ID_ROLE)) to_parent.appendRow(take_from) self.categories.save() def change_category_text(self, item): if all(self.finished_loading.values()): item_text = item.text() if item.data(TYPE_ROLE) == 'categories' and self.categories.find( item.data(ID_ROLE)).name != item_text: self.controller.edit_category(item.data(ID_ROLE), name=item_text) def select_tree_index(self, selection): if len(selection.indexes()) == 0: return index = selection.indexes()[0] item = self.wadtree_model.itemFromIndex(index) if item.data(TYPE_ROLE) == 'wads': self.wads.select_wad(item.data(ID_ROLE)) def remove_wad(self, item): self.wads.remove(item.data(ID_ROLE)) def wad_removed(self, data): item = self.id_item_mapping[data.id] parent = item.parent() or self.root self.categories.remove_child(parent.data(ID_ROLE), item.data(ID_ROLE)) parent.removeRow(item.row()) self.categories.save() def import_wad(self, id, parent_name): data = self.wads.find(id) item = self.make_tree_item(data) parent = self.categories.find_by(name=parent_name) parent_item = None if not parent: parent_id = self.categories.create(name=parent_name, children=[]) parent_item = self.make_tree_item(self.categories.find(parent_id)) self.root.appendRow(parent_item) self.categories.add_child(self.root.data(ID_ROLE), parent_item.data(ID_ROLE)) else: parent_item = self.id_item_mapping[parent.id] index = self.wadtree_model.indexFromItem(parent_item) self.wadtree.expand(index) self.categories.add_child(parent_item.data(ID_ROLE), data.id) parent_item.appendRow(item) self.categories.save() def new_category(self, index): item = self.wadtree_model.itemFromIndex(index) or self.root if item.data(TYPE_ROLE) == 'wads': item = self.wadtree_model.itemFromIndex( index.parent()) or self.root self.categories.new(item.data(ID_ROLE)) def category_added(self, parent_id, data): item = self.make_tree_item(data) self.id_item_mapping[parent_id].appendRow(item) def remove_category(self, id): parent = self.id_item_mapping[id].parent() or self.root self.categories.remove(id, parent.data(ID_ROLE)) def category_removed(self, parent_id, id): parent = self.id_item_mapping[parent_id] item = self.id_item_mapping.pop(id) item_row = item.row() for row in reversed(range(item.rowCount())): child = item.takeChild(row) parent.insertRow(item_row + 1, child) parent.removeRow(item_row) def open_menu(self, pos): index = self.wadtree.indexAt(pos) entries = [('Add Category', lambda *_: self.new_category(index))] if index.isValid(): item = self.wadtree_model.itemFromIndex(index) model = getattr(self, item.data(TYPE_ROLE).lower()) data = model.find(item.data(ID_ROLE)) def remove_wad(): self.remove_wad(item) def remove_category(): self.remove_category(data.id) entries_by_model = { 'categories': ('Remove category ({})'.format(data.name), remove_category), 'wads': ('Remove ({})'.format(data.display_name), remove_wad) } entries.append(entries_by_model[item.data(TYPE_ROLE).lower()]) execute_menu = make_context_menu(self, entries) execute_menu(pos) def appendWad(self, data): if data.id in self.pending_children: from_item = self.pending_children.pop(data.id) item = self.make_tree_item(data, from_item=from_item) else: self.loaded_wads[data.id] = data def appendCategory(self, data): is_root = data.is_root item = None if is_root: item = self.make_tree_item(data, self.root) self.root = item elif data.id in self.pending_children: from_item = self.pending_children.pop(data.id) item = self.make_tree_item(data, from_item=from_item) else: item = self.make_tree_item(data) self.loaded_categories[data.id] = item for child_id in data.children: child_item = None if child_id in self.loaded_wads: wad = self.loaded_wads.pop(child_id) child_item = self.make_tree_item(wad) child_item.setFlags(TREE_WAD_FLAGS) elif child_id in self.loaded_categories: child_item = self.loaded_categories.pop(child_id) else: child_item = self.make_tree_item(Pending(id=child_id)) self.pending_children[child_id] = child_item item.appendRow(child_item) if data.id not in self.loaded_categories: index = self.wadtree_model.indexFromItem(item) self.wadtree.expand(index) def finish_loading(self, model_type): self.finished_loading[model_type] = True if not all(self.finished_loading.values()): return wads_missing_categories = False root_id = self.root.data(ID_ROLE) root_category = self.categories.find(root_id) for wad in self.loaded_wads.values(): if wad.id in self.pending_children: item = self.pending_children.pop(wad.id) self.make_tree_item(wad, from_item=item) else: item = self.make_tree_item(wad) root_category.add_child(wad.id) self.root.appendRow(item) wads_missing_categories = True for category in self.loaded_categories.values(): root_category.add_child(category.id) self.root.appendRow(category) wads_missing_categories = True if wads_missing_categories: self.categories.save() for pending_child in self.pending_children.values(): parent = (pending_child.parent() or self.root) parent_category = self.categories.find(parent.data(ID_ROLE)) parent_category.remove_child(pending_child.data(ID_ROLE)) parent.removeRow(pending_child.row()) self.categories.save() def make_tree_item(self, data, from_item=None): item_type = data.model_type make_item = { 'categories': make_category_item, 'wads': lambda data, item: make_wad_item(data, TREE_WAD_FLAGS, item), 'pending': make_pending_item } item = make_item[item_type](data, from_item) self.id_item_mapping[data.id] = item return item
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)
def __init__(self, app: QApplication): super().__init__() Settings.load() self.setWindowTitle(APP_NAME) self.app = app app.setApplicationName(APP_NAME) app.setApplicationDisplayName(APP_NAME) self.preview_image = QLabel('No items to preview') self.preview_image.setFixedHeight(USABLE_HEIGHT) self.props_empty = False self.save_image_button = QPushButton('Save image') self.save_image_button.setDisabled(True) self.tree_view = ItemView() self.items = self.create_model() self.item_data = [] self.tree_view.setRootIsDecorated(False) # self.tree_view.setAlternatingRowColors(True) self.tree_view.setModel(self.items) self.tree_view.clicked.connect(self.tree_view_clicked) self.tree_view.rowMoved.connect(self.tree_view_reorder) self.tree_view.setDragEnabled(True) self.tree_view.setDragDropMode(QTreeView.InternalMove) self.tree_view.setItemsExpandable(False) self.tree_view.setDragDropOverwriteMode(False) root = QVBoxLayout() items_layout = QHBoxLayout() group = QGroupBox('Source items:') layout = QVBoxLayout() layout.addWidget(self.tree_view) buttons = QHBoxLayout() add_menu = QMenu() add_menu.addAction(Text.get_add_add_action(self)) add_menu.addAction(Image.get_add_add_action(self)) add_menu.addAction(Barcode.get_add_add_action(self)) add_menu.addAction(QrCode.get_add_add_action(self)) add_menu.addSeparator() add_menu.addAction(Spacing.get_add_add_action(self)) add_button = QPushButton('Add') add_button.setMenu(add_menu) buttons.addWidget(add_button) #buttons.addWidget(add_text_button) #buttons.addWidget(add_barcode_button) buttons.addStretch() buttons.setSpacing(1) b_down = QPushButton('⬇︎') b_down.clicked.connect(lambda _: self.move_item(1)) b_up = QPushButton('⬆︎') b_up.clicked.connect(lambda _: self.move_item(-1)) b_delete = QPushButton('Delete') b_delete.clicked.connect(self.delete_item) b_clone = QPushButton('Copy') b_clone.clicked.connect(self.on_clone) buttons.addWidget(b_clone) buttons.addSpacing(10) buttons.addWidget(b_up) buttons.addWidget(b_down) buttons.addSpacing(10) buttons.addWidget(b_delete) layout.addLayout(buttons) group.setLayout(layout) items_layout.addWidget(group) self.property_group = QGroupBox('Item properties:') self.props_layout = QVBoxLayout() self.property_group.setLayout(self.props_layout) self.props_current = QLabel() self.props_layout.addWidget(self.props_current) self.save_printable_button = QPushButton('Save Changes') self.save_printable_button.clicked.connect(self.save_props) self.props_layout.addWidget(self.save_printable_button) self.update_props() items_layout.addWidget(self.property_group) root.addLayout(items_layout) group = QGroupBox('Preview:') layout = QVBoxLayout() preview_wrapper = QScrollArea(self) #prev_layout = QHBoxLayout() preview_wrapper.setWidget(self.preview_image) preview_wrapper.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) preview_wrapper.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) preview_wrapper.setWidgetResizable(True) preview_wrapper.setFixedHeight(USABLE_HEIGHT) layout.addWidget(preview_wrapper) # layout.addWidget(self.save_image_button) group.setLayout(layout) group.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)) root.addWidget(group) self.printer_select = QComboBox(self) fs_model = QFileSystemModel(self) #model_proxy = QSortFilterProxyModel(self) #model_proxy.setSourceModel(fs_model) # fs_model.setNameFilters(['tty.PT-P3*']) potential_printer = None printers = QStandardItemModel() #for p in QDir('/dev').entryList(['tty*'], QDir.System, QDir.Name): # if p.startswith('tty.'): for p in LabelMaker.list_serial_ports(): # pprint(p.__dict__) item = [QStandardItem(p.name), QStandardItem(p.device)] print(p.name) printers.appendRow(item) ''' item = QStandardItem('/dev/' + p) printers.appendRow(item) if p.startswith('tty.PT-P3'): potential_printer = item ''' #print(printers.entryList()) #model_proxy.setRecursiveFilteringEnabled(True) #model_proxy.setFilterKeyColumn(0) fs_model.setRootPath('/dev/') #/Users/nilsmasen') fs_model.setFilter(QDir.System) dev_index = fs_model.index('/dev') #proxy_dev = model_proxy.mapFromSource(dev_index) self.printer_select.setModel(printers) if potential_printer is not None: index = printers.indexFromItem(potential_printer) self.printer_select.setCurrentIndex(index.row()) #printer_select.setRootModelIndex(dev_index) #printer_select.setRootIndex(dev_index) #printer_select.setExpanded(dev_index, True) #model_proxy.setFilterWildcard('tty*') bottom_box = QGroupBox('Print label: ') bottom_bar = QHBoxLayout() bottom_bar.addWidget(QLabel('Print device:')) bottom_bar.addWidget(self.printer_select) bottom_bar.addStretch() # /dev/tty.PT-P300BT0607-Serial print_button = QPushButton('Print') print_button.setFixedWidth(100) bottom_bar.addWidget(print_button) print_button.clicked.connect(self.print_clicked) bottom_box.setLayout(bottom_bar) root.addWidget(bottom_box) root_widget = QWidget() root_widget.setFixedWidth(800) root_widget.setLayout(root) self.setCentralWidget(root_widget) menu = QMenuBar() self.setMenuBar(menu) menu.setWindowTitle(APP_NAME) tools_menu = menu.addMenu('Python') prefs = QAction('&Preferences', self) prefs.triggered.connect(self.on_prefs) tools_menu.addAction(prefs) about = QAction('About ' + APP_NAME, self) about.triggered.connect(self.on_prefs) about.setMenuRole(QAction.AboutRole) tools_menu.addAction(about) file_menu = menu.addMenu('Label') act_new = QAction('&New', self) act_new.triggered.connect(self.on_new) file_menu.addAction(act_new) file_menu.addSeparator() act_open = QAction('&Open', self) act_open.triggered.connect(self.on_open) act_open.setShortcut(QKeySequence('Ctrl+O')) file_menu.addAction(act_open) file_menu.addSeparator() act_save = QAction('&Save', self) act_save.triggered.connect(self.on_save) act_save.setShortcut(QKeySequence("Ctrl+S")) file_menu.addAction(act_save) act_save_as = QAction('Save &as...', self) act_save_as.triggered.connect(self.on_save_as) act_save_as.setShortcut(QKeySequence("Ctrl+Shift+S")) file_menu.addAction(act_save_as) file_menu.addSeparator() file_menu.addAction(QAction('&Export image', self))
class DataChangeUI(object): def __init__(self, window, uaclient, hub_manager): self.window = window self.uaclient = uaclient # FIXME IoT stuff self.hub_manager = hub_manager self._subhandler = DataChangeHandler(self.hub_manager) self._subscribed_nodes = [] self.model = QStandardItemModel() self.window.ui.subView.setModel(self.model) self.window.ui.subView.horizontalHeader().setSectionResizeMode(1) self.window.ui.actionSubscribeDataChange.triggered.connect( self._subscribe) self.window.ui.actionUnsubscribeDataChange.triggered.connect( self._unsubscribe) # populate contextual menu self.window.ui.treeView.addAction( self.window.ui.actionSubscribeDataChange) self.window.ui.treeView.addAction( self.window.ui.actionUnsubscribeDataChange) # handle subscriptions self._subhandler.data_change_fired.connect( self._update_subscription_model, type=Qt.QueuedConnection) def clear(self): self._subscribed_nodes = [] self.model.clear() def _subscribe(self): node = self.window.get_current_node() if node is None: return if node in self._subscribed_nodes: print("allready subscribed to node: ", node) return self.model.setHorizontalHeaderLabels( ["DisplayName", "Value", "Timestamp"]) row = [ QStandardItem(node.display_name), QStandardItem("No Data yet"), QStandardItem("") ] row[0].setData(node) self.model.appendRow(row) self._subscribed_nodes.append(node) self.window.ui.subDockWidget.raise_() try: self.uaclient.subscribe_datachange(node, self._subhandler) except Exception as ex: self.window.show_error(ex) idx = self.model.indexFromItem(row[0]) self.model.takeRow(idx.row()) def _unsubscribe(self): node = self.window.get_current_node() if node is None: return self.uaclient.unsubscribe_datachange(node) self._subscribed_nodes.remove(node) i = 0 while self.model.item(i): item = self.model.item(i) if item.data() == node: self.model.removeRow(i) i += 1 def _update_subscription_model(self, node, value, timestamp): i = 0 while self.model.item(i): item = self.model.item(i) if item.data() == node: it = self.model.item(i, 1) it.setText(value) it_ts = self.model.item(i, 2) it_ts.setText(timestamp) i += 1
class Window(QMainWindow): """ Defines the look and characteristics of the application's main window. """ title = _("Mu {}").format(__version__) icon = "icon" timer = None usb_checker = None repl = None plotter = None zooms = ("xs", "s", "m", "l", "xl", "xxl", "xxxl") # levels of zoom. zoom_position = 2 # current level of zoom (as position in zooms tuple). _zoom_in = pyqtSignal(str) _zoom_out = pyqtSignal(str) data_received = pyqtSignal(bytes) open_file = pyqtSignal(str) load_theme = pyqtSignal(str) previous_folder = None debug_widths = None def __init__(self, parent=None): super().__init__(parent) # Record pane area to allow reopening where user put it in a session self._debugger_area = 0 self._inspector_area = 0 self._plotter_area = 0 self._repl_area = 0 self._runner_area = 0 def wheelEvent(self, event): """ Trap a CTRL-scroll event so the user is able to zoom in and out. """ modifiers = QApplication.keyboardModifiers() if modifiers == Qt.ControlModifier: zoom = event.angleDelta().y() > 0 if zoom: self.zoom_in() else: self.zoom_out() event.ignore() def set_zoom(self): """ Sets the zoom to current zoom_position level. """ self._zoom_in.emit(self.zooms[self.zoom_position]) def zoom_in(self): """ Handles zooming in. """ self.zoom_position = min(self.zoom_position + 1, len(self.zooms) - 1) self._zoom_in.emit(self.zooms[self.zoom_position]) def zoom_out(self): """ Handles zooming out. """ self.zoom_position = max(self.zoom_position - 1, 0) self._zoom_out.emit(self.zooms[self.zoom_position]) def connect_zoom(self, widget): """ Connects a referenced widget to the zoom related signals and sets the zoom of the widget to the current zoom level. """ self._zoom_in.connect(widget.set_zoom) self._zoom_out.connect(widget.set_zoom) widget.set_zoom(self.zooms[self.zoom_position]) @property def current_tab(self): """ Returns the currently focussed tab. """ return self.tabs.currentWidget() def set_read_only(self, is_readonly): """ Set all tabs read-only. """ self.read_only_tabs = is_readonly for tab in self.widgets: tab.setReadOnly(is_readonly) def get_load_path(self, folder, extensions="*", allow_previous=True): """ Displays a dialog for selecting a file to load. Returns the selected path. Defaults to start in the referenced folder unless a previous folder has been used and the allow_previous flag is True (the default behaviour) """ if allow_previous: open_in = (folder if self.previous_folder is None else self.previous_folder) else: open_in = folder path, _ = QFileDialog.getOpenFileName(self.widget, "Open file", open_in, extensions) logger.debug("Getting load path: {}".format(path)) if allow_previous: self.previous_folder = os.path.dirname(path) return path def get_save_path(self, folder): """ Displays a dialog for selecting a file to save. Returns the selected path. Defaults to start in the referenced folder. """ path, _ = QFileDialog.getSaveFileName( self.widget, "Save file", folder if self.previous_folder is None else self.previous_folder, "Python (*.py);;Other (*.*)", "Python (*.py)", ) self.previous_folder = os.path.dirname(path) # Ensure there's a .py extension if none is provided by the user. # See issue #1571. name, ext = os.path.splitext(os.path.basename(path)) if (not name.startswith(".")) and (not ext): # The file is not a . (dot) file and there's no extension, so add # .py as default. path += ".py" logger.debug("Getting save path: {}".format(path)) return path def get_microbit_path(self, folder): """ Displays a dialog for locating the location of the BBC micro:bit in the host computer's filesystem. Returns the selected path. Defaults to start in the referenced folder. """ path = QFileDialog.getExistingDirectory( self.widget, "Locate BBC micro:bit", folder if self.previous_folder is None else self.previous_folder, QFileDialog.ShowDirsOnly, ) self.previous_folder = os.path.dirname(path) logger.debug("Getting micro:bit path: {}".format(path)) return path def add_tab(self, path, text, api, newline): """ Adds a tab with the referenced path and text to the editor. """ new_tab = EditorPane(path, text, newline) new_tab.connect_margin(self.breakpoint_toggle) new_tab_index = self.tabs.addTab(new_tab, new_tab.label) new_tab.set_api(api) @new_tab.modificationChanged.connect def on_modified(): modified_tab_index = self.tabs.indexOf(new_tab) # Update tab label & window title # Tab dirty indicator is managed in FileTabs.addTab self.tabs.setTabText(modified_tab_index, new_tab.label) self.update_title(new_tab.title) @new_tab.open_file.connect def on_open_file(file): # Bubble the signal up self.open_file.emit(file) new_tab.context_menu.connect(self.on_context_menu) self.tabs.setCurrentIndex(new_tab_index) self.connect_zoom(new_tab) self.set_theme(self.theme) new_tab.setFocus() if self.read_only_tabs: new_tab.setReadOnly(self.read_only_tabs) return new_tab def focus_tab(self, tab): """ Force focus on the referenced tab. """ index = self.tabs.indexOf(tab) self.tabs.setCurrentIndex(index) tab.setFocus() @property def tab_count(self): """ Returns the number of active tabs. """ return self.tabs.count() @property def widgets(self): """ Returns a list of references to the widgets representing tabs in the editor. """ return [self.tabs.widget(i) for i in range(self.tab_count)] @property def modified(self): """ Returns a boolean indication if there are any modified tabs in the editor. """ for widget in self.widgets: if widget.isModified(): return True return False def on_context_menu(self): """ Called when a user right-clicks on an editor pane. If the REPL is active AND there is selected text in the current editor pane, modify the default context menu to include a paste to REPL option. Otherwise, just display the default context menu. """ # Create a standard context menu. menu = self.current_tab.createStandardContextMenu() # Flag to indicate if there is a section of text highlighted. has_selection = any([x > -1 for x in self.current_tab.getSelection()]) # Flag to indicate if the REPL pane is active. has_repl = hasattr(self, "repl") and self.repl # Flag to indicate if the Python process runner pane is active and in # interactive Python3 mode (pasting into other "standard" Python modes # doesn't make sense). has_runner = (hasattr(self, "process_runner") and self.process_runner and self.process_runner.is_interactive) if has_selection and (has_repl or has_runner): # Text selected with REPL/process context, so add the bespoke # "copy to repl" item to the standard context menu to handle this # situation. actions = menu.actions() copy_to_repl = QAction(_("Copy selected text to REPL"), menu) copy_to_repl.triggered.connect(self.copy_to_repl) menu.insertAction(actions[0], copy_to_repl) menu.insertSeparator(actions[0]) # Display menu. menu.exec_(QCursor.pos()) def copy_to_repl(self): """ Copies currently selected text in the editor, into the active REPL widget and sets focus to the REPL widget. The final line pasted into the REPL waits for RETURN to be pressed by the user (this appears to be the default behaviour for pasting into the REPL widget). """ line_from, pos_from, line_to, pos_to = self.current_tab.getSelection() lines = self.current_tab.text().split("\n") if line_from == line_to: # Paste a fragment from an individual line. to_paste = lines[line_from][pos_from:pos_to] else: # Multi-line paste (paste all lines selected). selected = lines[line_from:line_to + 1] # Ensure the correct indentation. # If the first line starts with whitespace, work out the number of # spaces and deduct said number of whitespace from start of all # other lines. That's perhaps the best we can do. if selected and selected[0].startswith(" "): indent = 0 for char in selected[0]: if char.isspace(): indent += 1 else: break selected = [line[indent:] for line in selected] to_paste = "\n".join(selected) logger.info("Pasting to REPL") logger.info("\n" + to_paste) clipboard = QApplication.clipboard() clipboard.setText(to_paste) if hasattr(self, "repl_pane") and self.repl_pane: self.repl_pane.paste() self.repl_pane.setFocus() elif hasattr(self, "process_runner") and self.process_runner: self.process_runner.paste() self.process_runner.setFocus() def on_stdout_write(self, data): """ Called when either a running script or the REPL write to STDOUT. """ self.data_received.emit(data) def add_filesystem(self, home, file_manager, board_name="board"): """ Adds the file system pane to the application. """ self.fs_pane = FileSystemPane(home) @self.fs_pane.open_file.connect def on_open_file(file): # Bubble the signal up self.open_file.emit(file) self.fs = QDockWidget(_("Filesystem on ") + board_name) self.fs.setWidget(self.fs_pane) self.fs.setFeatures(QDockWidget.DockWidgetMovable) self.fs.setAllowedAreas(Qt.BottomDockWidgetArea) self.addDockWidget(Qt.BottomDockWidgetArea, self.fs) self.fs_pane.setFocus() file_manager.on_list_files.connect(self.fs_pane.on_ls) self.fs_pane.list_files.connect(file_manager.ls) self.fs_pane.microbit_fs.put.connect(file_manager.put) self.fs_pane.microbit_fs.delete.connect(file_manager.delete) self.fs_pane.microbit_fs.list_files.connect(file_manager.ls) self.fs_pane.local_fs.get.connect(file_manager.get) self.fs_pane.local_fs.put.connect(file_manager.put) self.fs_pane.local_fs.list_files.connect(file_manager.ls) file_manager.on_put_file.connect(self.fs_pane.microbit_fs.on_put) file_manager.on_delete_file.connect(self.fs_pane.microbit_fs.on_delete) file_manager.on_get_file.connect(self.fs_pane.local_fs.on_get) file_manager.on_list_fail.connect(self.fs_pane.on_ls_fail) file_manager.on_put_fail.connect(self.fs_pane.on_put_fail) file_manager.on_delete_fail.connect(self.fs_pane.on_delete_fail) file_manager.on_get_fail.connect(self.fs_pane.on_get_fail) self.connect_zoom(self.fs_pane) return self.fs_pane def add_micropython_repl(self, name, connection): """ Adds a MicroPython based REPL pane to the application. """ repl_pane = MicroPythonREPLPane(connection) connection.data_received.connect(repl_pane.process_tty_data) self.add_repl(repl_pane, name) def add_micropython_plotter(self, name, connection, data_flood_handler): """ Adds a plotter that reads data from a serial connection. """ plotter_pane = PlotterPane() connection.data_received.connect(plotter_pane.process_tty_data) plotter_pane.data_flood.connect(data_flood_handler) self.add_plotter(plotter_pane, name) def add_python3_plotter(self, mode): """ Add a plotter that reads from either the REPL or a running script. Since this function will only be called when either the REPL or a running script are running (but not at the same time), it'll just grab data emitted by the REPL or script via data_received. """ plotter_pane = PlotterPane() self.data_received.connect(plotter_pane.process_tty_data) plotter_pane.data_flood.connect(mode.on_data_flood) self.add_plotter(plotter_pane, _("Python3 data tuple")) def add_jupyter_repl(self, kernel_manager, kernel_client): """ Adds a Jupyter based REPL pane to the application. """ kernel_manager.kernel.gui = "qt4" kernel_client.start_channels() ipython_widget = JupyterREPLPane() ipython_widget.kernel_manager = kernel_manager ipython_widget.kernel_client = kernel_client ipython_widget.on_append_text.connect(self.on_stdout_write) self.add_repl(ipython_widget, _("Python3 (Jupyter)")) def add_repl(self, repl_pane, name): """ Adds the referenced REPL pane to the application. """ self.repl_pane = repl_pane self.repl = QDockWidget(_("{} REPL").format(name)) self.repl.setWidget(repl_pane) self.repl.setFeatures(QDockWidget.DockWidgetMovable) self.repl.setAllowedAreas(Qt.BottomDockWidgetArea | Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) area = self._repl_area or Qt.BottomDockWidgetArea self.addDockWidget(area, self.repl) self.connect_zoom(self.repl_pane) self.repl_pane.set_theme(self.theme) self.repl_pane.setFocus() def add_plotter(self, plotter_pane, name): """ Adds the referenced plotter pane to the application. """ self.plotter_pane = plotter_pane self.plotter = QDockWidget(_("{} Plotter").format(name)) self.plotter.setWidget(plotter_pane) self.plotter.setFeatures(QDockWidget.DockWidgetMovable) self.plotter.setAllowedAreas(Qt.BottomDockWidgetArea | Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) area = self._plotter_area or Qt.BottomDockWidgetArea self.addDockWidget(area, self.plotter) self.plotter_pane.set_theme(self.theme) self.plotter_pane.setFocus() def add_python3_runner( self, interpreter, script_name, working_directory, interactive=False, debugger=False, command_args=None, envars=None, python_args=None, ): """ Display console output for the interpreter with the referenced pythonpath running the referenced script. The script will be run within the workspace_path directory. If interactive is True (default is False) the Python process will run in interactive mode (dropping the user into the REPL when the script completes). If debugger is True (default is False) the script will be run within a debug runner session. The debugger overrides the interactive flag (you cannot run the debugger in interactive mode). If there is a list of command_args (the default is None) then these will be passed as further arguments into the command run in the new process. If envars is given, these will become part of the environment context of the new chlid process. If python_args is given, these will be passed as arguments to the Python runtime used to launch the child process. """ self.process_runner = PythonProcessPane(self) self.runner = QDockWidget( _("Running: {}").format(os.path.basename(script_name))) self.runner.setWidget(self.process_runner) self.runner.setFeatures(QDockWidget.DockWidgetMovable) self.runner.setAllowedAreas(Qt.BottomDockWidgetArea | Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) self.process_runner.debugger = debugger if debugger: area = self._debugger_area or Qt.BottomDockWidgetArea else: area = self._runner_area or Qt.BottomDockWidgetArea self.addDockWidget(area, self.runner) logger.info( "About to start_process: %r, %r, %r, %r, %r, %r, %r, %r", interpreter, script_name, working_directory, interactive, debugger, command_args, envars, python_args, ) self.process_runner.start_process( interpreter, script_name, working_directory, interactive, debugger, command_args, envars, python_args, ) self.process_runner.setFocus() self.process_runner.on_append_text.connect(self.on_stdout_write) self.connect_zoom(self.process_runner) return self.process_runner def add_debug_inspector(self): """ Display a debug inspector to view the call stack. """ self.debug_inspector = DebugInspector() self.debug_model = QStandardItemModel() self.debug_inspector.setModel(self.debug_model) self.inspector = QDockWidget(_("Debug Inspector")) self.inspector.setWidget(self.debug_inspector) self.inspector.setFeatures(QDockWidget.DockWidgetMovable) self.inspector.setAllowedAreas(Qt.BottomDockWidgetArea | Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) area = self._inspector_area or Qt.RightDockWidgetArea self.addDockWidget(area, self.inspector) self.connect_zoom(self.debug_inspector) # Setup the inspector headers and restore column widths self.debug_model.setHorizontalHeaderLabels([_("Name"), _("Value")]) if self.debug_widths: for col, width in enumerate(self.debug_widths): self.debug_inspector.setColumnWidth(col, width) def update_debug_inspector(self, locals_dict): """ Given the contents of a dict representation of the locals in the current stack frame, update the debug inspector with the new values. """ excluded_names = ["__builtins__", "__debug_code__", "__debug_script__"] names = sorted([x for x in locals_dict if x not in excluded_names]) # Remove rows so we keep the same column layouts if manually set while self.debug_model.rowCount() > 0: self.debug_model.removeRow(0) for name in names: item_to_expand = None try: # DANGER! val = eval(locals_dict[name]) except Exception: val = None if isinstance(val, list): # Show a list consisting of rows of position/value list_item = DebugInspectorItem(name) item_to_expand = list_item for i, i_val in enumerate(val): list_item.appendRow([ DebugInspectorItem(str(i)), DebugInspectorItem(repr(i_val)), ]) self.debug_model.appendRow([ list_item, DebugInspectorItem( _("(A list of {} items.)").format(len(val))), ]) elif isinstance(val, dict): # Show a dict consisting of rows of key/value pairs. dict_item = DebugInspectorItem(name) item_to_expand = dict_item for k, k_val in val.items(): dict_item.appendRow([ DebugInspectorItem(repr(k)), DebugInspectorItem(repr(k_val)), ]) self.debug_model.appendRow([ dict_item, DebugInspectorItem( _("(A dict of {} items.)").format(len(val))), ]) else: self.debug_model.appendRow([ DebugInspectorItem(name), DebugInspectorItem(locals_dict[name]), ]) # Expand dicts/list with names matching old expanded entries if (hasattr(self, "debug_inspector") and name in self.debug_inspector.expanded_dicts and item_to_expand is not None): self.debug_inspector.expand( self.debug_model.indexFromItem(item_to_expand)) def remove_filesystem(self): """ Removes the file system pane from the application. """ if hasattr(self, "fs") and self.fs: self.fs_pane = None self.fs.setParent(None) self.fs.deleteLater() self.fs = None def remove_repl(self): """ Removes the REPL pane from the application. """ if self.repl: self._repl_area = self.dockWidgetArea(self.repl) self.repl_pane = None self.repl.setParent(None) self.repl.deleteLater() self.repl = None def remove_plotter(self): """ Removes the plotter pane from the application. """ if self.plotter: self._plotter_area = self.dockWidgetArea(self.plotter) self.plotter_pane = None self.plotter.setParent(None) self.plotter.deleteLater() self.plotter = None def remove_python_runner(self): """ Removes the runner pane from the application. """ if hasattr(self, "runner") and self.runner: if self.process_runner.debugger: self._debugger_area = self.dockWidgetArea(self.runner) else: self._runner_area = self.dockWidgetArea(self.runner) self.process_runner = None self.runner.setParent(None) self.runner.deleteLater() self.runner = None def remove_debug_inspector(self): """ Removes the debug inspector pane from the application. """ if hasattr(self, "inspector") and self.inspector: width = self.debug_inspector.columnWidth self.debug_widths = width(0), width(1) self._inspector_area = self.dockWidgetArea(self.inspector) self.debug_inspector = None self.debug_model = None self.inspector.setParent(None) self.inspector.deleteLater() self.inspector = None def set_theme(self, theme): """ Sets the theme for the REPL and editor tabs. """ self.theme = theme self.load_theme.emit(theme) if theme == "contrast": new_theme = ContrastTheme new_icon = "theme_day" elif theme == "night": new_theme = NightTheme new_icon = "theme_contrast" else: new_theme = DayTheme new_icon = "theme" for widget in self.widgets: widget.set_theme(new_theme) self.button_bar.slots["theme"].setIcon(load_icon(new_icon)) if hasattr(self, "repl") and self.repl: self.repl_pane.set_theme(theme) if hasattr(self, "plotter") and self.plotter: self.plotter_pane.set_theme(theme) def set_checker_icon(self, icon): """ Set the status icon to use on the check button """ self.button_bar.slots["check"].setIcon(load_icon(icon)) timer = QTimer() @timer.timeout.connect def reset(): self.button_bar.slots["check"].setIcon(load_icon("check.png")) timer.stop() timer.start(500) def show_admin(self, log, settings, packages, mode, device_list): """ Display the administrative dialog with referenced content of the log and settings. Return a dictionary of the settings that may have been changed by the admin dialog. """ admin_box = AdminDialog(self) admin_box.setup(log, settings, packages, mode, device_list) result = admin_box.exec() if result: return admin_box.settings() else: return {} def sync_packages(self, to_remove, to_add): """ Display a modal dialog that indicates the status of the add/remove package management operation. """ package_box = PackageDialog(self) package_box.setup(to_remove, to_add) package_box.exec() def show_message(self, message, information=None, icon=None): """ Displays a modal message to the user. If information is passed in this will be set as the additional informative text in the modal dialog. Since this mechanism will be used mainly for warning users that something is awry the default icon is set to "Warning". It's possible to override the icon to one of the following settings: NoIcon, Question, Information, Warning or Critical. """ message_box = QMessageBox(self) message_box.setText(message) message_box.setWindowTitle("Mu") if information: message_box.setInformativeText(information) if icon and hasattr(message_box, icon): message_box.setIcon(getattr(message_box, icon)) else: message_box.setIcon(message_box.Warning) logger.debug(message) logger.debug(information) message_box.exec() def show_confirmation(self, message, information=None, icon=None): """ Displays a modal message to the user to which they need to confirm or cancel. If information is passed in this will be set as the additional informative text in the modal dialog. Since this mechanism will be used mainly for warning users that something is awry the default icon is set to "Warning". It's possible to override the icon to one of the following settings: NoIcon, Question, Information, Warning or Critical. """ message_box = QMessageBox(self) message_box.setText(message) message_box.setWindowTitle(_("Mu")) if information: message_box.setInformativeText(information) if icon and hasattr(message_box, icon): message_box.setIcon(getattr(message_box, icon)) else: message_box.setIcon(message_box.Warning) message_box.setStandardButtons(message_box.Cancel | message_box.Ok) message_box.setDefaultButton(message_box.Cancel) logger.debug(message) logger.debug(information) return message_box.exec() def update_title(self, filename=None): """ Updates the title bar of the application. If a filename (representing the name of the file currently the focus of the editor) is supplied, append it to the end of the title. """ title = self.title if filename: title += " - " + filename self.setWindowTitle(title) def screen_size(self): """ Returns an (width, height) tuple with the screen geometry. """ screen = QDesktopWidget().screenGeometry() return screen.width(), screen.height() def size_window(self, x=None, y=None, w=None, h=None): """ Makes the editor 80% of the width*height of the screen and centres it when none of x, y, w and h is passed in; otherwise uses the passed in values to position and size the editor window. """ screen_width, screen_height = self.screen_size() w = int(screen_width * 0.8) if w is None else w h = int(screen_height * 0.8) if h is None else h self.resize(w, h) size = self.geometry() x = (screen_width - size.width()) / 2 if x is None else x y = (screen_height - size.height()) / 2 if y is None else y self.move(x, y) def reset_annotations(self): """ Resets the state of annotations on the current tab. """ self.current_tab.reset_annotations() def annotate_code(self, feedback, annotation_type): """ Given a list of annotations about the code in the current tab, add the annotations to the editor window so the user can make appropriate changes. """ self.current_tab.annotate_code(feedback, annotation_type) def show_annotations(self): """ Show the annotations added to the current tab. """ self.current_tab.show_annotations() def setup(self, breakpoint_toggle, theme): """ Sets up the window. Defines the various attributes of the window and defines how the user interface is laid out. """ self.theme = theme self.breakpoint_toggle = breakpoint_toggle # Give the window a default icon, title and minimum size. self.setWindowIcon(load_icon(self.icon)) self.update_title() self.read_only_tabs = False screen_width, screen_height = self.screen_size() self.setMinimumSize(screen_width // 2, screen_height // 2) self.setTabPosition(Qt.AllDockWidgetAreas, QTabWidget.North) self.widget = QWidget() widget_layout = QVBoxLayout() self.widget.setLayout(widget_layout) self.button_bar = ButtonBar(self.widget) self.tabs = FileTabs() self.setCentralWidget(self.tabs) self.status_bar = StatusBar(parent=self) self.setStatusBar(self.status_bar) self.addToolBar(self.button_bar) self.show() def resizeEvent(self, resizeEvent): """ Respond to window getting too small for the button bar to fit well. """ size = resizeEvent.size() self.button_bar.set_responsive_mode(size.width(), size.height()) def select_mode(self, modes, current_mode): """ Display the mode selector dialog and return the result. """ mode_select = ModeSelector(self) mode_select.setup(modes, current_mode) mode_select.exec() try: return mode_select.get_mode() except Exception: return None def change_mode(self, mode): """ Given a an object representing a mode, recreates the button bar with the expected functionality. """ self.button_bar.change_mode(mode) # Update the autocomplete / tooltip APIs for each tab to the new mode. api = mode.api() for widget in self.widgets: widget.set_api(api) def set_usb_checker(self, duration, callback): """ Sets up a timer that polls for USB changes via the "callback" every "duration" seconds. """ self.usb_checker = QTimer() self.usb_checker.timeout.connect(callback) self.usb_checker.start(duration * 1000) def set_timer(self, duration, callback): """ Set a repeating timer to call "callback" every "duration" seconds. """ self.timer = QTimer() self.timer.timeout.connect(callback) self.timer.start(duration * 1000) # Measured in milliseconds. def stop_timer(self): """ Stop the repeating timer. """ if self.timer: self.timer.stop() self.timer = None def connect_tab_rename(self, handler, shortcut): """ Connect the double-click event on a tab and the keyboard shortcut to the referenced handler (causing the Save As dialog). """ self.tabs.shortcut = QShortcut(QKeySequence(shortcut), self) self.tabs.shortcut.activated.connect(handler) self.tabs.tabBarDoubleClicked.connect(handler) def open_directory_from_os(self, path): """ Given the path to a directory, open the OS's built in filesystem explorer for that path. Works with Windows, OSX and Linux. """ if sys.platform == "win32": # Windows os.startfile(path) elif sys.platform == "darwin": # OSX os.system('open "{}"'.format(path)) else: # Assume freedesktop.org on unix-y. os.system('xdg-open "{}"'.format(path)) def connect_find_replace(self, handler, shortcut): """ Create a keyboard shortcut and associate it with a handler for doing a find and replace. """ self.find_replace_shortcut = QShortcut(QKeySequence(shortcut), self) self.find_replace_shortcut.activated.connect(handler) def connect_find_again(self, handlers, shortcut): """ Create keyboard shortcuts and associate them with handlers for doing a find again in forward or backward direction. Any given shortcut will be used for forward find again, while Shift+shortcut will find again backwards. """ forward, backward = handlers self.find_again_shortcut = QShortcut(QKeySequence(shortcut), self) self.find_again_shortcut.activated.connect(forward) backward_shortcut = QKeySequence("Shift+" + shortcut) self.find_again_backward_shortcut = QShortcut(backward_shortcut, self) self.find_again_backward_shortcut.activated.connect(backward) def show_find_replace(self, find, replace, global_replace): """ Display the find/replace dialog. If the dialog's OK button was clicked return a tuple containing the find term, replace term and global replace flag. """ finder = FindReplaceDialog(self) finder.setup(find, replace, global_replace) if finder.exec(): return (finder.find(), finder.replace(), finder.replace_flag()) def replace_text(self, target_text, replace, global_replace): """ Given target_text, replace the first instance after the cursor with "replace". If global_replace is true, replace all instances of "target". Returns the number of times replacement has occurred. """ if not self.current_tab: return 0 if global_replace: counter = 0 found = self.current_tab.findFirst(target_text, False, True, False, False, line=0, index=0) if found: counter += 1 self.current_tab.replace(replace) while self.current_tab.findNext(): self.current_tab.replace(replace) counter += 1 return counter else: found = self.current_tab.findFirst(target_text, False, True, False, True) if found: self.current_tab.replace(replace) return 1 else: return 0 def highlight_text(self, target_text, forward=True): """ Highlight the first match from the current position of the cursor in the current tab for the target_text. Returns True if there's a match. """ if self.current_tab: line = -1 index = -1 if not forward: # Workaround for `findFirst(forward=False)` not advancing # backwards: pass explicit line and index values. line, index, _el, _ei = self.current_tab.getSelection() return self.current_tab.findFirst( target_text, # Text to find, False, # Treat as regular expression True, # Case sensitive search False, # Whole word matches only True, # Wrap search forward=forward, # Forward search line=line, # -1 starts at current position index=index, # -1 starts at current position ) else: return False def connect_toggle_comments(self, handler, shortcut): """ Create a keyboard shortcut and associate it with a handler for toggling comments on highlighted lines. """ self.toggle_comments_shortcut = QShortcut(QKeySequence(shortcut), self) self.toggle_comments_shortcut.activated.connect(handler) def toggle_comments(self): """ Toggle comments on/off for all selected line in the currently active tab. """ if self.current_tab: self.current_tab.toggle_comments() def show_device_selector(self): """ Reveals the device selector in the status bar """ self.status_bar.device_selector.setHidden(False) def hide_device_selector(self): """ Hides the device selector in the status bar """ self.status_bar.device_selector.setHidden(True)
class DataChangeUI(object): def __init__(self, window, uaclient): self.window = window self.uaclient = uaclient self._subhandler = DataChangeHandler() self._subscribed_nodes = [] self.model = QStandardItemModel() self.window.ui.subView.setModel(self.model) self.window.ui.subView.horizontalHeader().setSectionResizeMode(1) self.window.ui.actionSubscribeDataChange.triggered.connect( self._subscribe) self.window.ui.actionUnsubscribeDataChange.triggered.connect( self._unsubscribe) # populate contextual menu self.window.addAction(self.window.ui.actionSubscribeDataChange) self.window.addAction(self.window.ui.actionUnsubscribeDataChange) # handle subscriptions self._subhandler.data_change_fired.connect( self._update_subscription_model, type=Qt.QueuedConnection) # accept drops self.model.canDropMimeData = self.canDropMimeData self.model.dropMimeData = self.dropMimeData def canDropMimeData(self, mdata, action, row, column, parent): return True def dropMimeData(self, mdata, action, row, column, parent): node = self.uaclient.client.get_node(mdata.text()) self._subscribe(node) return True def clear(self): self._subscribed_nodes = [] self.model.clear() def show_error(self, *args): self.window.show_error(*args) @trycatchslot def _subscribe(self, node=None): if not isinstance(node, Node): node = self.window.get_current_node() if node is None: return if node in self._subscribed_nodes: logger.warning("allready subscribed to node: %s ", node) return self.model.setHorizontalHeaderLabels( ["DisplayName", "Value", "Timestamp"]) text = str(node.get_display_name().Text) row = [ QStandardItem(text), QStandardItem("No Data yet"), QStandardItem("") ] row[0].setData(node) self.model.appendRow(row) self._subscribed_nodes.append(node) self.window.ui.subDockWidget.raise_() try: self.uaclient.subscribe_datachange(node, self._subhandler) except Exception as ex: self.window.show_error(ex) idx = self.model.indexFromItem(row[0]) self.model.takeRow(idx.row()) raise @trycatchslot def _unsubscribe(self): node = self.window.get_current_node() if node is None: return self.uaclient.unsubscribe_datachange(node) self._subscribed_nodes.remove(node) i = 0 while self.model.item(i): item = self.model.item(i) if item.data() == node: self.model.removeRow(i) i += 1 def _update_subscription_model(self, node, value, timestamp): i = 0 while self.model.item(i): item = self.model.item(i) if item.data() == node: it = self.model.item(i, 1) it.setText(value) it_ts = self.model.item(i, 2) it_ts.setText(timestamp) i += 1
class CommonListView(QListView): dblAddress = pyqtSignal(int) def __init__(self): super(CommonListView, self).__init__() self.model = QStandardItemModel(self) self.setItemDelegate(HTMLDelegate(self)) self.setModel(self.model) self.addressMap = {} self.resetList = [] self.lockRelease = True self.clickedX = 0 self.lastClickIndex = -1 self.setEditTriggers(QListView.NoEditTriggers) self.setStyleSheet("QListView::item{margin-bottom: 5px; padding:0px}") def getItem(self, row): return self.model.item(row, 0) def getItemFormIndex(self, index): return self.model.item(index.row(), 0) def setSize(self): width_view = 0 for i in range(self.model.rowCount()): item = self.getItem(i) _, end = item.componentRanges[-1] if width_view < end * self.fontMetrics().averageCharWidth(): width_view = end * self.fontMetrics().averageCharWidth() self.setFixedWidth(width_view + self.fontMetrics().averageCharWidth() * 3) self.setFixedHeight( self.sizeHintForRow(1) * self.model.rowCount() + 2 * self.frameWidth() + 10) def highlighRelation(self, text, start, func=None): texts = [text] for regs in relate_registers: if text in regs: texts = regs break if (func is not None) and (func.address in self.addressMap): startItem = self.addressMap[func.address] index = self.model.indexFromItem(startItem) row = index.row() item = self.getItem(row) while hasattr(item, 'func') and item.func == func: if isinstance(item, LocLine): change = item.highlight(texts, 0) else: change = item.highlight(texts, start) if change: self.resetList.append(item) row += 1 item = self.getItem(row) if hasattr(item, 'address'): if item.address > func.maxBound: break else: for i in range(self.model.rowCount()): item = self.getItem(i) if item.isSelectable(): if isinstance(item, LocLine): change = item.highlight(texts, 0) else: if hasattr(item, 'highlight'): change = item.highlight(texts, start) if change: self.resetList.append(item) def mousePressEvent(self, event) -> None: self.lockRelease = False self.clickedX = event.pos().x() for item in self.resetList: if hasattr(item, 'normal'): item.setText(item.normal) self.resetList.clear() super(CommonListView, self).mousePressEvent(event) def mouseReleaseEvent(self, event) -> None: if not self.lockRelease: indexes = self.selectedIndexes() if len(indexes) == 1: index = self.selectedIndexes()[0] item = self.getItemFormIndex(index) self.getClickedIndex(item) super(CommonListView, self).mouseReleaseEvent(event) def getClickedIndex(self, item, highlight=True): import platform if platform.system() == 'Windows': widthChar = self.fontMetrics().averageCharWidth() + 4 self.clickedX -= 5 else: widthChar = self.fontMetrics().averageCharWidth() pos = self.clickedX // widthChar if hasattr(item, 'getIndexByPos'): index = item.getIndexByPos(pos) if index != -1: self.lastClickIndex = index if item.components[index].text != ',': start = 0 if index > 1: start = 2 if highlight: func = None if hasattr(item, 'func'): func = item.func self.highlighRelation(item.components[index].text, start, func) item.selectTextAt(index) self.resetList.append(item) def mouseDoubleClickEvent(self, event) -> None: self.lockRelease = True index = self.selectedIndexes()[0] item = self.getItemFormIndex(index) self.dblAddress.emit(item.address) def focusAddress(self, address, focus=True): if address in self.addressMap: if focus: self.clearAllEffect() item = self.addressMap[address] index = self.model.indexFromItem(item) self.selectionModel().select(index, QItemSelectionModel.Select) if focus: self.scrollTo(index, QAbstractItemView.PositionAtCenter) self.setFocus() def focusItem(self, index): self.clearAllEffect() self.selectionModel().select(index, QItemSelectionModel.Select) self.scrollTo(index, QAbstractItemView.PositionAtCenter) self.setFocus() def clearAllEffect(self): self.clearFocus() self.clearSelection() for item in self.resetList: if hasattr(item, 'normal'): try: item.setText(item.normal) except: pass self.resetList.clear()