class Demo(QWidget): def __init__(self): super(Demo, self).__init__() self.resize(600, 300) self.model = QDirModel(self) # 1 self.model.setReadOnly(False) self.model.setSorting(QDir.Name | QDir.IgnoreCase) self.tree = QTreeView(self) # 2 self.tree.setModel(self.model) self.tree.clicked.connect(self.show_info) self.index = self.model.index(QDir.currentPath()) self.tree.expand(self.index) self.tree.scrollTo(self.index) self.info_label = QLabel(self) # 3 self.v_layout = QVBoxLayout() self.v_layout.addWidget(self.tree) self.v_layout.addWidget(self.info_label) self.setLayout(self.v_layout) def show_info(self): # 4 index = self.tree.currentIndex() file_name = self.model.fileName(index) file_path = self.model.filePath(index) file_info = 'File Name: {}\nFile Path: {}'.format(file_name, file_path) self.info_label.setText(file_info)
class Packet_Area_Binary_Component(): def __init__(self, proxy_server=Proxy_Server()): self.proxy_server = proxy_server def install_widgets(self, parent=None): self.list = QTreeView(parent) self.list.setModel(self.proxy_server.intercept_queue.packet_list_model) self.list.clicked.connect(self.on_clicked) self.list.show() def on_clicked(self, index): self.list.expand(index)
class Packet_Area_Component(): def __init__(self, proxy_server=Proxy_Server(), field_area=Field_Area_Component()): self.proxy_server = proxy_server self.field_area = field_area def install_widgets(self, parent=None): self.list = QTreeView(parent) self.list.setModel(self.proxy_server.intercept_queue.packet_list_model) self.list.clicked.connect(self.on_clicked) self.list.show() def on_clicked(self, index): self.list.expand(index) item = index.model().itemFromIndex(index) if item.parent() != None: self.proxy_server.intercept_queue.populate_field_area( item.parent().row(), index.row()) self.field_area.set_selected_packet( item.parent().row(), index.row()) # Populate the field area when you click a layer!
class FileTreeView(QWidget): on_menu_select = pyqtSignal(str, str) on_dir_change = pyqtSignal() def __init__(self, parent: Application): super().__init__() self.stopped = threading.Event() self.tree = QTreeView() self.handle = self.Handler(self) self.get_data_file() self.model = QtGui.QStandardItemModel() self.item_construct = {} v_box = QVBoxLayout() v_box.addWidget(self.finder()) v_box.addWidget(self.tree_view()) self.setLayout(v_box) self.watch_dog() parent.app_close.connect(self.exit_push) self.tree.setAlternatingRowColors(True) def exit_push(self): self.stopped.set() def finder(self): w_find = QLineEdit() w_find.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)) w_find.textChanged.connect(self.sort_list) w_find.setPlaceholderText("Search file..") return w_find def sort_list(self, text): list_sort = [] if text == "": self.show_tree(self.list_file) else: for data in self.list_file: if text.lower() in data.name().lower(): list_sort.append(data) self.show_tree(list_sort) def watch_dog(self): watch = ProcessRunnable(target=watch_winform, args=(get_data_folder(), self.handle, self.stopped)) watch.start() class Handler(watchdog.events.PatternMatchingEventHandler): def __init__(self, parent): super().__init__() self.parent = parent def on_created(self, event): print("Watchdog received created event", event.src_path, sep=" : ") asyncio.run( self.parent.file_change('create', event.src_path, event.is_directory)) def on_modified(self, event): print("Watchdog received modified event", event.src_path, sep=" : ") asyncio.run( self.parent.file_change('modify', event.src_path, event.is_directory)) def on_moved(self, event): print("Watchdog received move event", event.src_path, event.dest_path, sep=" : ") asyncio.run( self.parent.file_change('move', event.src_path, event.is_directory, event.dest_path)) def on_deleted(self, event): print("Watchdog received delete event", event.src_path, sep=" : ") asyncio.run( self.parent.file_change('delete', event.src_path, event.is_directory)) async def file_change(self, tpe, old, is_directory, new=""): if tpe == "move": self.import_single(self.model.invisibleRootItem(), new) self.remove_single(old) elif tpe == "delete": self.remove_single(old) elif tpe == "create": self.import_single(self.model.invisibleRootItem(), old) if is_directory: self.on_dir_change.emit() def get_data_file(self): self.list_file = [] self.create_list(get_data_folder()) def create_list(self, dir): lst = os.listdir(path=dir) for f in lst: path = os.path.join(dir, f) file = MyFile() if os.path.isdir(path): file.setParentName(os.path.basename(dir)) file.setParent(dir) file.setName(f) file.setDir(True) self.list_file.append(file) self.create_list(path) else: file.setParentName(os.path.basename(dir)) file.setParent(dir) file.setName(f) file.setDir(False) self.list_file.append(file) def tree_view(self): self.tree.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) self.tree.setContextMenuPolicy(Qt.CustomContextMenu) self.tree.customContextMenuRequested.connect(self.open_menu) self.tree.doubleClicked.connect(self.open_event) # self.model.itemChanged.connect(self.data_change) self.tree.setModel(self.model) self.show_tree(self.list_file) return self.tree def show_tree(self, list_file): self.model.clear() self.model.setHorizontalHeaderLabels(['List API']) self.tree.header().setDefaultSectionSize(180) parent = self.model.invisibleRootItem() self.item_construct = {} self.import_data(parent, list_file) self.tree.expandAll() def import_data_by_path(self, parent, path: list, index): if index < len(path): full = os.sep.join(path[:index + 1]) if full in self.item_construct: item = self.item_construct[full] else: item = QStandardItem(path[index]) if os.path.isfile(os.path.join(get_data_folder(), full)): item.setToolTip( self.read_description( os.path.join(get_data_folder(), full))) item.setEditable(False) if not os.path.isdir(os.path.join(get_data_folder(), full)): item.setIcon(QIcon(get_icon_link("text_snippet.svg"))) else: item.setIcon(QIcon(get_icon_link("folder_yellow.svg"))) item.setData(full) parent.appendRow(item) self.item_construct[full] = item self.import_data_by_path(item, path, index + 1) def read_description(self, path): try: data = json.loads(open(path, encoding='utf-8').read()) json_data = APIData() json_data.construct(data) x = json_data.parseSave().description() if x.isspace() or x == "": return ".." else: return x except Exception as ex: print(ex) return ".." def import_data(self, parent, list_data): for i in list_data: self.import_single(parent, i) def import_single(self, parent, file_path): path = self.path_extract(file_path) self.import_data_by_path(parent, path, 0) def remove_single(self, file_path): path = self.path_extract(file_path) full = os.sep.join(path[:len(path)]) if full in self.item_construct: item = self.item_construct[full] (item.parent() or self.model.invisibleRootItem()).removeRow(item.row()) del self.item_construct[full] def path_extract(self, file_path): if isinstance(file_path, MyFile): path = os.path.join(file_path.parent(), file_path.name()) else: path = file_path path = path.replace(get_data_folder(), "") if path.startswith(os.sep): path = path.replace(os.sep, "", 1) path = path.split(os.sep) return path def open_menu(self, position): indexes = self.tree.selectedIndexes() level = 0 data = "" item = None if len(indexes) > 0: index = indexes[0] item = self.model.itemFromIndex(index) data = item.data() data = os.path.join(get_data_folder(), data) if os.path.isdir(data): level = 1 else: level = 2 menu = QMenu() menu.setStyleSheet(open(get_stylesheet()).read()) rename_action = QAction(QIcon(get_icon_link('edit.svg')), '&Rename', self) rename_action.setStatusTip('Rename') new_action = QAction(QIcon(get_icon_link('create_new_folder.svg')), '&New Folder', self) new_action.setStatusTip('New Folder') refresh_action = QAction(QIcon(get_icon_link('refresh.svg')), '&Refresh', self) refresh_action.setStatusTip('Refresh') delete_action = QAction(QIcon(get_icon_link('delete_forever.svg')), '&Delete', self) delete_action.setStatusTip('Delete') open_action = QAction(QIcon(get_icon_link('open_in_new.svg')), '&Open', self) open_action.setStatusTip('Open file') expand_action = QAction(QIcon(), '&Expand', self) expand_action.setStatusTip('Expand') collapse_action = QAction(QIcon(), '&Collapse', self) collapse_action.setStatusTip('Collapse') duplicate_action = QAction(QIcon(get_icon_link('content_copy.svg')), '&Duplicate', self) duplicate_action.setStatusTip('Duplicate') copy_action = QAction(QIcon(get_icon_link('content_copy.svg')), '&Copy', self) copy_action.setStatusTip('Copy') move_action = QAction(QIcon(get_icon_link('zoom_out_map.svg')), '&Move', self) move_action.setStatusTip('Move') if level == 1: menu.addAction(rename_action) menu.addAction(new_action) menu.addSeparator() menu.addAction(refresh_action) menu.addAction(expand_action) menu.addAction(collapse_action) menu.addSeparator() menu.addAction(delete_action) elif level == 2: menu.addAction(open_action) menu.addAction(new_action) menu.addAction(refresh_action) menu.addSeparator() menu.addAction(rename_action) menu.addAction(duplicate_action) menu.addAction(copy_action) menu.addAction(move_action) menu.addSeparator() menu.addAction(delete_action) else: menu.addAction(new_action) menu.addAction(refresh_action) action = menu.exec_(self.tree.viewport().mapToGlobal(position)) if action == open_action: if data != "": self.on_menu_select.emit("open", data) elif action == refresh_action: self.get_data_file() self.show_tree(self.list_file) elif action == expand_action: if item is not None: self.tree.expand(item.index()) elif action == collapse_action: if item is not None: self.tree.collapse(item.index()) elif action == delete_action: if data != "": msg = QMessageBox() msg.setStyleSheet(open(get_stylesheet()).read()) msg.setIcon(QMessageBox.Warning) msg.setBaseSize(QSize(500, 300)) msg.setText("Delete file.") msg.setInformativeText("Are you sure to detele " + os.path.basename(data) + "?") msg.setWindowTitle("Delete Warning!!!") msg.addButton('Delete', QMessageBox.YesRole) msg.addButton('Move to Trash', QMessageBox.YesRole) msg.addButton('Cancel', QMessageBox.NoRole) rs = msg.exec_() if rs == 0: if os.path.isdir(data): shutil.rmtree(data) else: os.remove(data) elif rs == 1: send2trash(data) elif action == new_action: if data == "": data = get_data_folder() # input_name = QInputDialog() # input_name.setStyleSheet(open(get_stylesheet()).read()) # text, ok = input_name.getText(self, 'New Folder', 'Folder name:') inp = QComboDialog('New Folder', 'Folder name:', QComboDialog.Text) ok = inp.exec_() if ok and inp.select: if os.path.isdir(data): try: os.mkdir(os.path.join(data, inp.select)) except Exception as ex: alert = Alert("Error", "Create folder error", str(ex)) alert.exec_() print(ex) else: new = os.path.join(os.path.dirname(data), inp.select) try: os.mkdir(new) except Exception as ex: alert = Alert("Error", "Create folder error", str(ex)) alert.exec_() print(ex) elif action == rename_action: if data != "": # input_name = QInputDialog() # input_name.setStyleSheet(open(get_stylesheet()).read()) # text, ok = input_name.getText(self, 'Rename file', 'New name:') inp = QComboDialog('Rename file', 'New name:', QComboDialog.Text) ok = inp.exec_() if ok and inp.select: if os.path.isdir(data): new = os.path.join(os.path.dirname(data), inp.select) try: os.rename(data, new) except Exception as ex: alert = Alert("Error", "Rename folder error", str(ex)) alert.exec_() print(ex) else: filename, file_extension = os.path.splitext(data) new = os.path.join(os.path.dirname(data), inp.select + file_extension) try: os.rename(data, new) except Exception as ex: alert = Alert("Error", "Rename file error", str(ex)) alert.exec_() print(ex) elif action == move_action: if data != "": items = get_list_folder(get_data_folder(), get_data_folder()) # # item, ok = QInputDialog.getItem(self, "Select folder dialog", # "Select the destination folder", items, 0, False) inp = QComboDialog("Move", "Select the destination folder", QComboDialog.ComboBox, items) ok = inp.exec_() if ok and inp.select: folder = inp.select if inp.select.startswith(os.sep): folder = inp.select.replace(os.sep, "", 1) new = os.path.join(get_data_folder(), folder, os.path.basename(data)) try: os.rename(data, new) except Exception as ex: alert = Alert("Error", "Move file error", str(ex)) alert.exec_() print(ex) elif action == duplicate_action: if data != "": # input_name = QInputDialog() # input_name.setStyleSheet(open(get_stylesheet()).read()) # text, ok = input_name.getText(self, 'Duplicate file', 'New name:') inp = QComboDialog('Duplicate file', 'New name:', QComboDialog.Text) filename, file_extension = os.path.splitext(data) inp.set_init_text(filename) ok = inp.exec_() if ok and inp.select: new = os.path.join(os.path.dirname(data), inp.select + file_extension) try: copyfile(data, new) except Exception as ex: alert = Alert("Error", "Duplicate file error", str(ex)) alert.exec_() print(ex) elif action == copy_action: if data != "": items = get_list_folder(get_data_folder(), get_data_folder()) inp = QComboDialog("Copy", "Select the destination folder", QComboDialog.ComboBox, items) ok = inp.exec_() # item, ok = QInputDialog.getItem(self, "Select folder dialog", # "Select the destination folder", items, 0, False) if ok and inp.select: folder = inp.select if inp.select.startswith(os.sep): folder = inp.select.replace(os.sep, "", 1) new = os.path.join(get_data_folder(), folder, os.path.basename(data)) try: copyfile(data, new) except Exception as ex: alert = Alert("Error", "Copy file error", str(ex)) alert.exec_() print(ex) def open_event(self, index): item = self.model.itemFromIndex(index) if item is not None: data = item.data() data = os.path.join(get_data_folder(), data) if os.path.isdir(data): level = 1 else: level = 2 if data != "": if level == 2: self.on_menu_select.emit("open", data) elif level == 1: if not self.tree.isExpanded(item.index()): self.tree.collapse(item.index()) else: self.tree.expand(item.index())
class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self._version = "0.1.20" self.setWindowIcon(QIcon("GUI/icons/logo.png")) self.setWindowTitle("Tasmota Device Manager {}".format(self._version)) self.main_splitter = QSplitter() self.devices_splitter = QSplitter(Qt.Vertical) self.mqtt_queue = [] self.devices = {} self.fulltopic_queue = [] old_settings = QSettings() self.settings = QSettings("{}/TDM/tdm.cfg".format(QDir.homePath()), QSettings.IniFormat) self.setMinimumSize(QSize(1280, 800)) for k in old_settings.allKeys(): self.settings.setValue(k, old_settings.value(k)) old_settings.remove(k) self.device_model = TasmotaDevicesModel() self.telemetry_model = TasmotaDevicesTree() self.console_model = ConsoleModel() self.sorted_console_model = QSortFilterProxyModel() self.sorted_console_model.setSourceModel(self.console_model) self.sorted_console_model.setFilterKeyColumn(CnsMdl.FRIENDLY_NAME) self.setup_mqtt() self.setup_telemetry_view() self.setup_main_layout() self.add_devices_tab() self.build_toolbars() self.setStatusBar(QStatusBar()) self.queue_timer = QTimer() self.queue_timer.timeout.connect(self.mqtt_publish_queue) self.queue_timer.start(500) self.auto_timer = QTimer() self.auto_timer.timeout.connect(self.autoupdate) self.load_window_state() if self.settings.value("connect_on_startup", False, bool): self.actToggleConnect.trigger() def setup_main_layout(self): self.mdi = QMdiArea() self.mdi.setActivationOrder(QMdiArea.ActivationHistoryOrder) self.mdi.setViewMode(QMdiArea.TabbedView) self.mdi.setDocumentMode(True) mdi_widget = QWidget() mdi_widget.setLayout(VLayout()) mdi_widget.layout().addWidget(self.mdi) self.devices_splitter.addWidget(mdi_widget) vl_console = VLayout() hl_filter = HLayout() self.cbFilter = QCheckBox("Console filtering") self.cbxFilterDevice = QComboBox() self.cbxFilterDevice.setEnabled(False) self.cbxFilterDevice.setFixedWidth(200) self.cbxFilterDevice.setModel(self.device_model) self.cbxFilterDevice.setModelColumn(DevMdl.FRIENDLY_NAME) hl_filter.addWidgets([self.cbFilter, self.cbxFilterDevice]) hl_filter.addStretch(0) vl_console.addLayout(hl_filter) self.console_view = TableView() self.console_view.setModel(self.console_model) self.console_view.setupColumns(columns_console) self.console_view.setAlternatingRowColors(True) self.console_view.verticalHeader().setDefaultSectionSize(20) self.console_view.setMinimumHeight(200) vl_console.addWidget(self.console_view) console_widget = QWidget() console_widget.setLayout(vl_console) self.devices_splitter.addWidget(console_widget) self.main_splitter.insertWidget(0, self.devices_splitter) self.setCentralWidget(self.main_splitter) self.console_view.clicked.connect(self.select_cons_entry) self.console_view.doubleClicked.connect(self.view_payload) self.cbFilter.toggled.connect(self.toggle_console_filter) self.cbxFilterDevice.currentTextChanged.connect( self.select_console_filter) def setup_telemetry_view(self): tele_widget = QWidget() vl_tele = VLayout() self.tview = QTreeView() self.tview.setMinimumWidth(300) self.tview.setModel(self.telemetry_model) self.tview.setAlternatingRowColors(True) self.tview.setUniformRowHeights(True) self.tview.setIndentation(15) self.tview.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)) self.tview.expandAll() self.tview.resizeColumnToContents(0) vl_tele.addWidget(self.tview) tele_widget.setLayout(vl_tele) self.main_splitter.addWidget(tele_widget) def setup_mqtt(self): self.mqtt = MqttClient() self.mqtt.connecting.connect(self.mqtt_connecting) self.mqtt.connected.connect(self.mqtt_connected) self.mqtt.disconnected.connect(self.mqtt_disconnected) self.mqtt.connectError.connect(self.mqtt_connectError) self.mqtt.messageSignal.connect(self.mqtt_message) def add_devices_tab(self): tabDevicesList = DevicesListWidget(self) self.mdi.addSubWindow(tabDevicesList) tabDevicesList.setWindowState(Qt.WindowMaximized) def load_window_state(self): wndGeometry = self.settings.value('window_geometry') if wndGeometry: self.restoreGeometry(wndGeometry) spltState = self.settings.value('splitter_state') if spltState: self.main_splitter.restoreState(spltState) def build_toolbars(self): main_toolbar = Toolbar(orientation=Qt.Horizontal, iconsize=16, label_position=Qt.ToolButtonTextBesideIcon) main_toolbar.setObjectName("main_toolbar") self.addToolBar(main_toolbar) main_toolbar.addAction(QIcon("./GUI/icons/connections.png"), "Broker", self.setup_broker) self.actToggleConnect = QAction(QIcon("./GUI/icons/disconnect.png"), "MQTT") self.actToggleConnect.setCheckable(True) self.actToggleConnect.toggled.connect(self.toggle_connect) main_toolbar.addAction(self.actToggleConnect) self.actToggleAutoUpdate = QAction(QIcon("./GUI/icons/automatic.png"), "Auto telemetry") self.actToggleAutoUpdate.setCheckable(True) self.actToggleAutoUpdate.toggled.connect(self.toggle_autoupdate) main_toolbar.addAction(self.actToggleAutoUpdate) main_toolbar.addSeparator() main_toolbar.addAction(QIcon("./GUI/icons/bssid.png"), "BSSId", self.bssid) main_toolbar.addAction(QIcon("./GUI/icons/export.png"), "Export list", self.export) def initial_query(self, idx, queued=False): for q in initial_queries: topic = "{}status".format(self.device_model.commandTopic(idx)) if queued: self.mqtt_queue.append([topic, q]) else: self.mqtt.publish(topic, q, 1) self.console_log(topic, "Asked for STATUS {}".format(q), q) def setup_broker(self): brokers_dlg = BrokerDialog() if brokers_dlg.exec_( ) == QDialog.Accepted and self.mqtt.state == self.mqtt.Connected: self.mqtt.disconnect() def toggle_autoupdate(self, state): if state: self.auto_timer.setInterval(5000) self.auto_timer.start() def toggle_connect(self, state): if state and self.mqtt.state == self.mqtt.Disconnected: self.broker_hostname = self.settings.value('hostname', 'localhost') self.broker_port = self.settings.value('port', 1883, int) self.broker_username = self.settings.value('username') self.broker_password = self.settings.value('password') self.mqtt.hostname = self.broker_hostname self.mqtt.port = self.broker_port if self.broker_username: self.mqtt.setAuth(self.broker_username, self.broker_password) self.mqtt.connectToHost() elif not state and self.mqtt.state == self.mqtt.Connected: self.mqtt_disconnect() def autoupdate(self): if self.mqtt.state == self.mqtt.Connected: for d in range(self.device_model.rowCount()): idx = self.device_model.index(d, 0) cmnd = self.device_model.commandTopic(idx) self.mqtt.publish(cmnd + "STATUS", payload=8) def mqtt_connect(self): self.broker_hostname = self.settings.value('hostname', 'localhost') self.broker_port = self.settings.value('port', 1883, int) self.broker_username = self.settings.value('username') self.broker_password = self.settings.value('password') self.mqtt.hostname = self.broker_hostname self.mqtt.port = self.broker_port if self.broker_username: self.mqtt.setAuth(self.broker_username, self.broker_password) if self.mqtt.state == self.mqtt.Disconnected: self.mqtt.connectToHost() def mqtt_disconnect(self): self.mqtt.disconnectFromHost() def mqtt_connecting(self): self.statusBar().showMessage("Connecting to broker") def mqtt_connected(self): self.actToggleConnect.setIcon(QIcon("./GUI/icons/connect.png")) self.statusBar().showMessage("Connected to {}:{} as {}".format( self.broker_hostname, self.broker_port, self.broker_username if self.broker_username else '[anonymous]')) self.mqtt_subscribe() for d in range(self.device_model.rowCount()): idx = self.device_model.index(d, 0) self.initial_query(idx) def mqtt_subscribe(self): main_topics = ["+/stat/+", "+/tele/+", "stat/#", "tele/#"] for d in range(self.device_model.rowCount()): idx = self.device_model.index(d, 0) if not self.device_model.isDefaultTemplate(idx): main_topics.append(self.device_model.commandTopic(idx)) main_topics.append(self.device_model.statTopic(idx)) for t in main_topics: self.mqtt.subscribe(t) def mqtt_publish_queue(self): for q in self.mqtt_queue: t, p = q self.mqtt.publish(t, p) self.mqtt_queue.pop(self.mqtt_queue.index(q)) def mqtt_disconnected(self): self.actToggleConnect.setIcon(QIcon("./GUI/icons/disconnect.png")) self.statusBar().showMessage("Disconnected") def mqtt_connectError(self, rc): reason = { 1: "Incorrect protocol version", 2: "Invalid client identifier", 3: "Server unavailable", 4: "Bad username or password", 5: "Not authorized", } self.statusBar().showMessage("Connection error: {}".format(reason[rc])) self.actToggleConnect.setChecked(False) def mqtt_message(self, topic, msg): found = self.device_model.findDevice(topic) if found.reply == 'LWT': if not msg: msg = "offline" if found.index.isValid(): self.console_log(topic, "LWT update: {}".format(msg), msg) self.device_model.updateValue(found.index, DevMdl.LWT, msg) self.initial_query(found.index, queued=True) elif msg == "Online": self.console_log( topic, "LWT for unknown device '{}'. Asking for FullTopic.". format(found.topic), msg, False) self.mqtt_queue.append( ["cmnd/{}/fulltopic".format(found.topic), ""]) self.mqtt_queue.append( ["{}/cmnd/fulltopic".format(found.topic), ""]) elif found.reply == 'RESULT': try: full_topic = loads(msg).get('FullTopic') new_topic = loads(msg).get('Topic') template_name = loads(msg).get('NAME') ota_url = loads(msg).get('OtaUrl') teleperiod = loads(msg).get('TelePeriod') if full_topic: # TODO: update FullTopic for existing device AFTER the FullTopic changes externally (the message will arrive from new FullTopic) if not found.index.isValid(): self.console_log( topic, "FullTopic for {}".format(found.topic), msg, False) new_idx = self.device_model.addDevice(found.topic, full_topic, lwt='online') tele_idx = self.telemetry_model.addDevice( TasmotaDevice, found.topic) self.telemetry_model.devices[found.topic] = tele_idx #TODO: add QSortFilterProxyModel to telemetry treeview and sort devices after adding self.initial_query(new_idx) self.console_log( topic, "Added {} with fulltopic {}, querying for STATE". format(found.topic, full_topic), msg) self.tview.expand(tele_idx) self.tview.resizeColumnToContents(0) elif new_topic: if found.index.isValid() and found.topic != new_topic: self.console_log( topic, "New topic for {}".format(found.topic), msg) self.device_model.updateValue(found.index, DevMdl.TOPIC, new_topic) tele_idx = self.telemetry_model.devices.get( found.topic) if tele_idx: self.telemetry_model.setDeviceName( tele_idx, new_topic) self.telemetry_model.devices[ new_topic] = self.telemetry_model.devices.pop( found.topic) elif template_name: self.device_model.updateValue( found.index, DevMdl.MODULE, "{} (0)".format(template_name)) elif ota_url: self.device_model.updateValue(found.index, DevMdl.OTA_URL, ota_url) elif teleperiod: self.device_model.updateValue(found.index, DevMdl.TELEPERIOD, teleperiod) except JSONDecodeError as e: self.console_log( topic, "JSON payload decode error. Check error.log for additional info." ) with open("{}/TDM/error.log".format(QDir.homePath()), "a+") as l: l.write("{}\t{}\t{}\t{}\n".format( QDateTime.currentDateTime().toString( "yyyy-MM-dd hh:mm:ss"), topic, msg, e.msg)) elif found.index.isValid(): ok = False try: if msg.startswith("{"): payload = loads(msg) else: payload = msg ok = True except JSONDecodeError as e: self.console_log( topic, "JSON payload decode error. Check error.log for additional info." ) with open("{}/TDM/error.log".format(QDir.homePath()), "a+") as l: l.write("{}\t{}\t{}\t{}\n".format( QDateTime.currentDateTime().toString( "yyyy-MM-dd hh:mm:ss"), topic, msg, e.msg)) if ok: try: if found.reply == 'STATUS': self.console_log(topic, "Received device status", msg) payload = payload['Status'] self.device_model.updateValue( found.index, DevMdl.FRIENDLY_NAME, payload['FriendlyName'][0]) self.telemetry_model.setDeviceFriendlyName( self.telemetry_model.devices[found.topic], payload['FriendlyName'][0]) module = payload['Module'] if module == 0: self.mqtt.publish( self.device_model.commandTopic(found.index) + "template") else: self.device_model.updateValue( found.index, DevMdl.MODULE, modules.get(module, 'Unknown')) self.device_model.updateValue(found.index, DevMdl.MODULE_ID, module) elif found.reply == 'STATUS1': self.console_log(topic, "Received program information", msg) payload = payload['StatusPRM'] self.device_model.updateValue( found.index, DevMdl.RESTART_REASON, payload.get('RestartReason')) self.device_model.updateValue(found.index, DevMdl.OTA_URL, payload.get('OtaUrl')) elif found.reply == 'STATUS2': self.console_log(topic, "Received firmware information", msg) payload = payload['StatusFWR'] self.device_model.updateValue(found.index, DevMdl.FIRMWARE, payload['Version']) self.device_model.updateValue(found.index, DevMdl.CORE, payload['Core']) elif found.reply == 'STATUS3': self.console_log(topic, "Received syslog information", msg) payload = payload['StatusLOG'] self.device_model.updateValue(found.index, DevMdl.TELEPERIOD, payload['TelePeriod']) elif found.reply == 'STATUS5': self.console_log(topic, "Received network status", msg) payload = payload['StatusNET'] self.device_model.updateValue(found.index, DevMdl.MAC, payload['Mac']) self.device_model.updateValue(found.index, DevMdl.IP, payload['IPAddress']) elif found.reply in ('STATE', 'STATUS11'): self.console_log(topic, "Received device state", msg) if found.reply == 'STATUS11': payload = payload['StatusSTS'] self.parse_state(found.index, payload) elif found.reply in ('SENSOR', 'STATUS8'): self.console_log(topic, "Received telemetry", msg) if found.reply == 'STATUS8': payload = payload['StatusSNS'] self.parse_telemetry(found.index, payload) elif found.reply.startswith('POWER'): self.console_log( topic, "Received {} state".format(found.reply), msg) payload = {found.reply: msg} self.parse_power(found.index, payload) except KeyError as k: self.console_log( topic, "JSON key error. Check error.log for additional info.") with open("{}/TDM/error.log".format(QDir.homePath()), "a+") as l: l.write("{}\t{}\t{}\tKeyError: {}\n".format( QDateTime.currentDateTime().toString( "yyyy-MM-dd hh:mm:ss"), topic, payload, k.args[0])) def parse_power(self, index, payload, from_state=False): old = self.device_model.power(index) power = { k: payload[k] for k in payload.keys() if k.startswith("POWER") } # TODO: fix so that number of relays get updated properly after module/no. of relays change needs_update = False if old: # if from_state and len(old) != len(power): # needs_update = True # # else: for k in old.keys(): needs_update |= old[k] != power.get(k, old[k]) if needs_update: break else: needs_update = True if needs_update: self.device_model.updateValue(index, DevMdl.POWER, power) def parse_state(self, index, payload): bssid = payload['Wifi'].get('BSSId') if not bssid: bssid = payload['Wifi'].get('APMac') self.device_model.updateValue(index, DevMdl.BSSID, bssid) self.device_model.updateValue(index, DevMdl.SSID, payload['Wifi']['SSId']) self.device_model.updateValue(index, DevMdl.CHANNEL, payload['Wifi'].get('Channel', "n/a")) self.device_model.updateValue(index, DevMdl.RSSI, payload['Wifi']['RSSI']) self.device_model.updateValue(index, DevMdl.UPTIME, payload['Uptime']) self.device_model.updateValue(index, DevMdl.LOADAVG, payload.get('LoadAvg')) self.device_model.updateValue(index, DevMdl.LINKCOUNT, payload['Wifi'].get('LinkCount', "n/a")) self.device_model.updateValue(index, DevMdl.DOWNTIME, payload['Wifi'].get('Downtime', "n/a")) self.parse_power(index, payload, True) tele_idx = self.telemetry_model.devices.get( self.device_model.topic(index)) if tele_idx: tele_device = self.telemetry_model.getNode(tele_idx) self.telemetry_model.setDeviceFriendlyName( tele_idx, self.device_model.friendly_name(index)) pr = tele_device.provides() for k in pr.keys(): self.telemetry_model.setData(pr[k], payload.get(k)) def parse_telemetry(self, index, payload): device = self.telemetry_model.devices.get( self.device_model.topic(index)) if device: node = self.telemetry_model.getNode(device) time = node.provides()['Time'] if 'Time' in payload: self.telemetry_model.setData(time, payload.pop('Time')) temp_unit = "C" pres_unit = "hPa" if 'TempUnit' in payload: temp_unit = payload.pop('TempUnit') if 'PressureUnit' in payload: pres_unit = payload.pop('PressureUnit') for sensor in sorted(payload.keys()): if sensor == 'DS18x20': for sns_name in payload[sensor].keys(): d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice( DS18x20, payload[sensor][sns_name]['Type'], device) self.telemetry_model.getNode(d).setTempUnit(temp_unit) payload[sensor][sns_name]['Id'] = payload[sensor][ sns_name].pop('Address') pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData( pr[pk], payload[sensor][sns_name].get(pk)) self.tview.expand(d) elif sensor.startswith('DS18B20'): d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice( DS18x20, sensor, device) self.telemetry_model.getNode(d).setTempUnit(temp_unit) pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) if sensor == 'COUNTER': d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice( CounterSns, "Counter", device) pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) else: d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice( sensor_map.get(sensor, Node), sensor, device) pr = self.telemetry_model.getNode(d).provides() if 'Temperature' in pr: self.telemetry_model.getNode(d).setTempUnit(temp_unit) if 'Pressure' in pr or 'SeaPressure' in pr: self.telemetry_model.getNode(d).setPresUnit(pres_unit) for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) # self.tview.resizeColumnToContents(0) def console_log(self, topic, description, payload="", known=True): longest_tp = 0 longest_fn = 0 short_topic = "/".join(topic.split("/")[0:-1]) fname = self.devices.get(short_topic, "") if not fname: device = self.device_model.findDevice(topic) fname = self.device_model.friendly_name(device.index) self.devices.update({short_topic: fname}) self.console_model.addEntry(topic, fname, description, payload, known) if len(topic) > longest_tp: longest_tp = len(topic) self.console_view.resizeColumnToContents(1) if len(fname) > longest_fn: longest_fn = len(fname) self.console_view.resizeColumnToContents(1) def view_payload(self, idx): if self.cbFilter.isChecked(): idx = self.sorted_console_model.mapToSource(idx) row = idx.row() timestamp = self.console_model.data( self.console_model.index(row, CnsMdl.TIMESTAMP)) topic = self.console_model.data( self.console_model.index(row, CnsMdl.TOPIC)) payload = self.console_model.data( self.console_model.index(row, CnsMdl.PAYLOAD)) dlg = PayloadViewDialog(timestamp, topic, payload) dlg.exec_() def select_cons_entry(self, idx): self.cons_idx = idx def export(self): fname, _ = QFileDialog.getSaveFileName(self, "Export device list as...", directory=QDir.homePath(), filter="CSV files (*.csv)") if fname: if not fname.endswith(".csv"): fname += ".csv" with open(fname, "w", encoding='utf8') as f: column_titles = [ 'mac', 'topic', 'friendly_name', 'full_topic', 'cmnd_topic', 'stat_topic', 'tele_topic', 'module', 'module_id', 'firmware', 'core' ] c = csv.writer(f) c.writerow(column_titles) for r in range(self.device_model.rowCount()): d = self.device_model.index(r, 0) c.writerow([ self.device_model.mac(d), self.device_model.topic(d), self.device_model.friendly_name(d), self.device_model.fullTopic(d), self.device_model.commandTopic(d), self.device_model.statTopic(d), self.device_model.teleTopic(d), modules.get(self.device_model.module(d)), self.device_model.module(d), self.device_model.firmware(d), self.device_model.core(d) ]) def bssid(self): BSSIdDialog().exec_() # if dlg.exec_() == QDialog.Accepted: def toggle_console_filter(self, state): self.cbxFilterDevice.setEnabled(state) if state: self.console_view.setModel(self.sorted_console_model) else: self.console_view.setModel(self.console_model) def select_console_filter(self, fname): self.sorted_console_model.setFilterFixedString(fname) def closeEvent(self, e): self.settings.setValue("window_geometry", self.saveGeometry()) self.settings.setValue("splitter_state", self.main_splitter.saveState()) self.settings.sync() e.accept()
class MainWindow(QtWidgets.QMainWindow): Folder = 1 File = 2 Table = 3 LogInfo = 101 LogWarning = 102 LogError = 103 def LoadTableData(self,data,model): try: value = json.loads(data) table = value['table'] model.setColumnCount(len(table)) data = value['data'] model.setRowCount(len(data) + 2) for v in table: model.setHeaderData(v[0],Qt.Horizontal,v[1]) model.setData(model.index(0,v[0]),v[2]) model.setData(model.index(1,v[0]),v[3]) for i in range(0,len(data)): v = data[i] for j in range(0,len(v)): model.setData(model.index(i+2,j),v[j]) model.activeColumn = value['activeColumn'] except Exception as e: pass def AddTreeItem(self,parent,text,type,isexpand = True): if parent == None: texts = text.split('.') if len(texts) > 1: rootItem = self.rootItem for i in range(0,len(texts)-1): t = texts[i] childItem = None for j in range(0,rootItem.rowCount()): childItem = rootItem.child(j,0) if t == childItem.data(): break rootItem = childItem parent = rootItem text = texts[-1] else: parent = self.rootItem lastFolderItem = None for i in range(0,parent.rowCount()): childItem = self.model.itemFromIndex(self.model.index(i,0,parent.index())) if childItem.data() == MainWindow.Folder: lastFolderItem = childItem if text == childItem.text(): return None icon = None if type == MainWindow.Folder: icon = self.iconProvider.icon(QFileIconProvider.Folder) elif type == MainWindow.File: icon = self.iconProvider.icon(QFileIconProvider.File) elif type == MainWindow.Table: icon = self.iconProvider.icon(QFileIconProvider.Desktop) item = QStandardItem(parent) item.setIcon(icon) item.setText(text) item.setData(type) item.setFlags(QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsSelectable) if type == MainWindow.Folder and lastFolderItem != None: parent.insertRow(lastFolderItem.row()+1,item) else: parent.appendRow(item) if isexpand == True: self.tree.expand(parent.index()) return item def SetRootTreeItem(self,text): self.rootItem = QStandardItem() self.rootItem.setIcon(self.iconProvider.icon(QFileIconProvider.Folder)) self.rootItem.setText(text) self.rootItem.setData(MainWindow.Folder) self.model.appendRow(self.rootItem) for i in range(0,self.model.columnCount()): colItem = self.model.itemFromIndex(self.model.index(0,i)) colItem.setFlags(QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsSelectable) def GetTreeItemShortPath(self,item): tempItem = item names = [] while True: names.append(tempItem.text()) tempItem = tempItem.parent() if tempItem == self.rootItem: break return '.'.join(reversed(names)) def OnTreeCustomContextMenuRequested(self,pt): index = self.tree.indexAt(pt); if index.isValid(): item = self.model.itemFromIndex(index) parent = item.parent() if parent != None: item = self.model.itemFromIndex(self.model.index(item.row(),0,parent.index())) def OnAddTreeItem(self,item,type): inputDialog = InputDialog.InputDialog(self) ret = inputDialog.exec_() inputDialog.destroy() if QtWidgets.QDialog.Rejected == ret: return if len(inputDialog.GetTextValue()) == 0: return itemTable = self.AddTreeItem(item,str(inputDialog.GetTextValue()),type) if MainWindow.Table == type: model = GridTableView.TableViewItemModel(2,0) model.setParent(self.tree) itemTable.setData(model,Qt.UserRole+2) cursor = None try: cursor = self.db.cursor() cursor.execute('insert into datas (k, v, t) values (\'{}\', \'{}\', {})'.format(self.GetTreeItemShortPath(itemTable),"None",type)) self.db.commit() except Exception as e: pass finally: cursor.close() def OnAddFolder(index,self = self,item = item): OnAddTreeItem(self,item,MainWindow.Folder) def OnAddFile(index,self = self,item = item): OnAddTreeItem(self,item,MainWindow.File) def OnAddTable(index,self = self,item = item): OnAddTreeItem(self,item,MainWindow.Table) def OnRename(index,self = self,item = item): inputDialog = InputDialog.InputDialog(self,item.text()) ret = inputDialog.exec_() inputDialog.destroy() if QtWidgets.QDialog.Rejected == ret: return text = inputDialog.GetTextValue() if len(text) == 0: return #old_shortpath = self.GetTreeItemShortPath(item) items = [] oldpaths = [] if item.data() == MainWindow.Table: items.append(item) else: def GetAllChildItems(items,item): for i in range(0,item.rowCount()): childItem = item.child(i,0) if childItem.data() != MainWindow.Table: GetAllChildItems(items,childItem) else: items.append(childItem) items.append(item) GetAllChildItems(items,item) for v in items: oldpaths.append(self.GetTreeItemShortPath(v)) item.setText(text) cursor = self.db.cursor() for i in range(0,len(items)): v = items[i] oldpath = oldpaths[i] cursor.execute('update datas set k=? where k=?', (self.GetTreeItemShortPath(v),oldpath)) findTabIndex = False for i in range(0,self.tabWidget.count()): if findTabIndex == True: continue if oldpath == self.tabWidget.tabToolTip(i): findTabIndex = True self.tabWidget.setTabToolTip(i,self.GetTreeItemShortPath(v)) if v == item and item.data() == MainWindow.Table: self.tabWidget.setTabText(i,text) cursor.close() self.db.commit() def OnDelete(index,self = self,item = item): if item == self.rootItem: return deleyeKeys = set() cursor = self.db.cursor() if item.data() == MainWindow.Folder or item.data() == MainWindow.File: cursor.execute('select * from datas') shortpath = self.GetTreeItemShortPath(item) for i in range(0,self.tabWidget.count()): tabText = self.tabWidget.tabToolTip(i) if len(tabText) >= len(shortpath) and tabText[0:len(shortpath)] == shortpath: self.tabWidget.removeTab(i) #if self.OnCloseTab(i) == False: # return def DeleteChildItems(cursor,item): for i in range(0,item.rowCount()): childItem = item.child(i,0) if item.data() != MainWindow.Table: cursor.execute('delete from datas where k=?', (self.GetTreeItemShortPath(childItem),)) DeleteChildItems(cursor,childItem) cursor.execute('delete from datas where k=?', (self.GetTreeItemShortPath(item),)) DeleteChildItems(cursor,item) self.model.removeRow(item.row(),item.parent().index()) elif item.data() == MainWindow.Table: shortpath = self.GetTreeItemShortPath(item) for i in range(0,self.tabWidget.count()): if self.tabWidget.tabToolTip(i) == shortpath: self.tabWidget.removeTab(i) #if self.OnCloseTab(i) == False: # return deleyeKeys.add(shortpath) self.model.removeRow(item.row(),item.parent().index()) for v in deleyeKeys: try: cursor.execute('delete from datas where k=?', (v,)) except Exception as e: pass cursor.close() self.db.commit() action_AddDir = QtWidgets.QAction("添加目录",None,triggered=OnAddFolder) action_AddConfig = QtWidgets.QAction("添加文件",None,triggered=OnAddFile) action_AddTable = QtWidgets.QAction("添加配置表",None,triggered=OnAddTable) action_Rename = QtWidgets.QAction("重命名",None,triggered=OnRename) action_Delete = QtWidgets.QAction("删除",None,triggered=OnDelete) menuTree = QtWidgets.QMenu("menuTree",self.tree) menuTree.addAction(action_AddDir) menuTree.addAction(action_AddConfig) menuTree.addAction(action_AddTable) menuTree.addSeparator() menuTree.addAction(action_Rename) menuTree.addSeparator() menuTree.addAction(action_Delete) if item == self.rootItem: action_Rename.setDisabled(True) if item.data() == MainWindow.Folder: action_AddTable.setDisabled(True) if item == self.rootItem: action_Delete.setDisabled(True) elif item.data() == MainWindow.File: action_AddDir.setDisabled(True) action_AddConfig.setDisabled(True) elif item.data() == MainWindow.Table: action_AddTable.setDisabled(True) action_AddDir.setDisabled(True) action_AddConfig.setDisabled(True) else: return menuTree.exec_(QtGui.QCursor.pos()) menuTree.destroy() def closeEvent(self, event): count = self.tabWidget.count() for i in reversed(range(0,count)): self.OnCloseTab(i) event.accept() def OnPaste(self): tableView = self.tabWidget.currentWidget() if tableView != None: if tableView.IsChanged == True: if QMessageBox.Yes == QMessageBox.information(self,'Save','Do you save changes?',QMessageBox.Yes|QMessageBox.No|QMessageBox.Cancel,QMessageBox.Yes): tableView.Save() fileName,fileType = QtWidgets.QFileDialog.getOpenFileName(self,'Open File','','Excel File(*.xls *.xlsx)') if os.path.exists(fileName) and os.path.isfile(fileName): tableView.Paste(fileName) def OnExport(self): dialog = ExportDialog.ExportDialog(self) dialog.exec_() def DoSave(self,tableView): datas = tableView.Save() if datas != None: tabIndex = self.tabWidget.indexOf(tableView) cursor = None try: cursor = self.db.cursor() k = self.tabWidget.tabToolTip(tabIndex) cursor.execute('select * from datas where k=?', (k,)) values = cursor.fetchall() if len(values) > 0 and values[0][0] == k: cursor.execute('update datas set v=? where k=?', (datas,k)) else: cursor.execute('insert into datas (k, v, t) values (\'{}\', \'{}\', {})', (k,datas,MainWindow.Table)) self.db.commit() except Exception as e: pass finally: if cursor != None: cursor.close() def OnSave(self): tableView = self.tabWidget.currentWidget() if tableView != None and tableView.IsChanged == True: self.DoSave(tableView) tabIndex = self.tabWidget.indexOf(tableView) self.tabWidget.tabBar().setTabTextColor(tabIndex,QColor(0,0,0)) def OnSaveAll(self): for i in range(0,self.tabWidget.count()): tableView = self.tabWidget.widget(i) if tableView.IsChanged == True: self.DoSave(tableView) self.tabWidget.tabBar().setTabTextColor(i,QColor(0,0,0)) def OnUndo(self): tableView = self.tabWidget.currentWidget() if tableView != None: tableView.Undo() def OnRedo(self): tableView = self.tabWidget.currentWidget() if tableView != None: tableView.Redo() def EnableSave(self,enable,tableView): if enable == True: self.tabWidget.tabBar().setTabTextColor(self.tabWidget.indexOf(tableView),QColor(233,21,10)) else: self.tabWidget.tabBar().setTabTextColor(self.tabWidget.indexOf(tableView),QColor(0,0,0)) def OnTreeDoubleClicked(self,index): if index.isValid() == False: return item = self.model.itemFromIndex(index) shortpath = self.GetTreeItemShortPath(item) findTabIndex = -1 if item.data() == MainWindow.Table: for i in range(0,self.tabWidget.count()): if self.tabWidget.tabToolTip(i) == shortpath: findTabIndex = i break if findTabIndex != -1: self.tabWidget.setCurrentIndex(findTabIndex) else: tableView = GridTableView.GridTableView(item.data(Qt.UserRole+2),self.tabWidget) tabIndex = self.tabWidget.addTab(tableView,item.text()) self.tabWidget.setTabToolTip(tabIndex,shortpath) self.tabWidget.setCurrentWidget(tableView) pass pass def OnCloseTab(self,tabId): tableView = self.tabWidget.widget(tabId) if tableView.IsChanged == True: ret = QMessageBox.information(self,'Save','Do you save changes?',QMessageBox.Yes|QMessageBox.No|QMessageBox.Cancel,QMessageBox.Yes) if QMessageBox.Yes == ret: self.DoSave(tableView) elif QMessageBox.Cancel == ret: return False self.tabWidget.removeTab(tabId) return True @property def Settings(self): if self.setting == None: self.setting = {} try: with open("Settings.cfg",'r') as f: self.setting = json.load(f) except IOError as e: pass return self.setting def GetTreeModel(self): return self.model def __del__(self): print('MainWindow.__del__') if self.db!= None: self.db.close() try: with open("Settings.cfg",'w') as f: json.dump(self.setting,f) except IOError as e: pass finally: pass pass def __init__(self): super(MainWindow, self).__init__() uic.loadUi('MainWindow.ui', self) self.db = None self.rootItem = None self.setting = None self.fileSystemWatcher = QFileSystemWatcher() self.oldWindowTitle = self.windowTitle() self.iconProvider = QFileIconProvider() splitterH = QSplitter(self.centralwidget) self.verticalLayout.addWidget(splitterH) self.tree = QTreeView(splitterH) self.model = QStandardItemModel(self.tree) self.model.setHorizontalHeaderLabels(['Name']) self.model.setColumnCount(1) self.tree.setModel(self.model) selectionModel = QItemSelectionModel(self.model) self.tree.setSelectionModel(selectionModel) self.tree.setUniformRowHeights(True) self.tree.header().setStretchLastSection(False) self.tree.viewport().setAttribute(Qt.WA_StaticContents) self.tree.setAttribute(Qt.WA_MacShowFocusRect, False) self.tree.header().setSectionResizeMode(QHeaderView.ResizeToContents) self.tree.header().setStretchLastSection(False); self.tree.setHeaderHidden(True) self.tree.setContextMenuPolicy(Qt.CustomContextMenu) self.tree.customContextMenuRequested['QPoint'].connect(self.OnTreeCustomContextMenuRequested) self.tree.doubleClicked.connect(self.OnTreeDoubleClicked) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(1) self.tree.setSizePolicy(sizePolicy) splitterH.addWidget(self.tree) self.setStatusBar(None) self.tabWidget = QTabWidget(splitterH) self.tabWidget.setTabsClosable(True) self.tabWidget.resize(self.tabWidget.size().width(),self.size().height()/3*1) self.tabWidget.tabCloseRequested['int'].connect(self.OnCloseTab) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(7) self.tabWidget.setSizePolicy(sizePolicy) splitterH.addWidget(self.tabWidget) self.action_Save.setShortcut(Qt.CTRL|Qt.Key_S) self.action_Save.triggered.connect(self.OnSave) self.actionParse_Excel.triggered.connect(self.OnPaste) #self.action_Export_Code.triggered.connect(self.OnExportData) self.actionUndo.setShortcut(Qt.CTRL|Qt.Key_Z) self.actionUndo.triggered.connect(self.OnUndo) self.actionRedo.setShortcut(Qt.CTRL|Qt.Key_Y) self.actionRedo.triggered.connect(self.OnRedo) self.SetRootTreeItem('root') #self.currentZip = '' self.timer = QtCore.QTimer() self.timer.timeout.connect(self.DelayStart) self.timer.start(100) def GetAllDatasFromDB(self): cursor = self.db.cursor() cursor.execute('select * from datas order by k asc') values = cursor.fetchall() cursor.close() return values @property def TreeRootItem(self): return self.rootItem def DelayStart(self): self.timer.stop() self.timer = None sf = SelectFolder.SelectFolder(self) if sf.exec_() == QtWidgets.QDialog.Rejected: self.close() sf.destroy() currentPath = sf.GetFolder() if os.path.exists(currentPath) == False: return self.setWindowTitle(self.oldWindowTitle + ' - ' + currentPath) self.db = sqlite3.connect(currentPath) values = self.GetAllDatasFromDB() for k,v,t in values: item = self.AddTreeItem(None,k, t,False) if t == MainWindow.Table: model = GridTableView.TableViewItemModel(2,0) model.setParent(self.tree) if v != 'None': self.LoadTableData(v,model) item.setData(model,Qt.UserRole+2) self.tree.expand(self.rootItem.index())
class Watch(QWidget): def __init__(self, parent=None): super(QWidget, self).__init__(parent) self.initUI() self.initVariables() def initUI(self): mainlayout = QFormLayout() self.model = CheckableDirModel() #self.model = QFileSystemModel() self.tree = QTreeView() self.tree.setModel(self.model) self.tree.setAnimated(False) self.tree.setIndentation(20) self.tree.setColumnHidden(1, True) self.tree.setColumnHidden(2, True) self.tree.setColumnHidden(3, True) self.tree.setSortingEnabled(False) self.tree.setHeaderHidden(True) self.model.updateCheckBoxSignal.connect(self.updateCheckBoxes) buttonLayout = QHBoxLayout() self.applyButton = QPushButton("Apply", self) self.applyButton.clicked.connect(self.apply) self.applyButton.setEnabled(False) self.resetButton = QPushButton("Reset", self) hspacer = QWidget() hspacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) buttonLayout.addWidget(hspacer) buttonLayout.addWidget(self.applyButton) buttonLayout.addWidget(self.resetButton) vspacer = QWidget() vspacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # Add to layout mainlayout.addRow(self.tree) mainlayout.addRow(vspacer) mainlayout.addRow(buttonLayout) self.setLayout(mainlayout) def initVariables(self): settings = readSettingItems(['Watched']) if 'Watched' in settings.keys(): if len(settings['Watched']): self.watchList = settings['Watched'] if len(self.watchList): for watchItem in self.watchList: tempPath = watchItem[0] tempIndex = self.model.index(tempPath, 0) retData = self.model.filePath(tempIndex) # Set checkbox if len(retData): self.model.setData(tempIndex, watchItem[1], Qt.CheckStateRole) # Expand path while tempIndex.parent().isValid(): tempIndex = tempIndex.parent() self.tree.expand(tempIndex) else: self.watchList = [] def updateCheckBoxes(self, index, value): changeFlag = False fullpath = self.model.filePath(index) newWatchMission = [fullpath, value] if newWatchMission in self.watchList: pass else: if len(self.watchList): tempList = list(filter(lambda x: fullpath in x, self.watchList)) if len(tempList) == 1: tempMissionIndex = self.watchList.index(tempList[0]) if value == 0: self.watchList.pop(tempMissionIndex) else: self.watchList[tempMissionIndex] = newWatchMission elif len(tempList) == 0: if value != 0: self.watchList.append(newWatchMission) changeFlag = True self.applyButton.setEnabled(changeFlag) # to do: update checkboxes states: 0, 1, 2. def apply(self): data = {'Watched': self.watchList } writeSettingItems(data) self.applyButton.setEnabled(False)
class RegisterViewWidget(WidgetBase): def __init__(self, parent: QWidget): super(RegisterViewWidget, self).__init__(parent) self._registers = [] self._running_task: asyncio.Task = None self._visibility_selector = QComboBox(self) self._visibility_selector.addItem("Show all registers", lambda _: True) self._visibility_selector.addItem("Only configuration parameters", lambda r: r.mutable and r.persistent) # noinspection PyUnresolvedReferences self._visibility_selector.currentIndexChanged.connect( lambda _: self._on_visibility_changed()) self._reset_selected_button = make_button( self, "Reset selected", icon_name="clear-symbol", tool_tip=f"Reset the currently selected registers to their default " f"values. The restored values will be committed " f"immediately. This function is available only if a " f"default value is defined. [{RESET_SELECTED_SHORTCUT}]", on_clicked=self._do_reset_selected, ) self._reset_all_button = make_button( self, "Reset all", icon_name="skull-crossbones", tool_tip=f"Reset the all registers to their default " f"values. The restored values will be committed " f"immediately.", on_clicked=self._do_reset_all, ) self._read_selected_button = make_button( self, "Read selected", icon_name="process", tool_tip=f"Read the currently selected registers only " f"[{READ_SELECTED_SHORTCUT}]", on_clicked=self._do_read_selected, ) self._read_all_button = make_button( self, "Read all", icon_name="process-plus", tool_tip="Read all registers from the device", on_clicked=self._do_read_all, ) self._export_button = make_button( self, "Export", icon_name="export", tool_tip="Export configuration parameters", on_clicked=self._do_export, ) self._import_button = make_button( self, "Import", icon_name="import", tool_tip="Import configuration parameters", on_clicked=self._do_import, ) self._expand_all_button = make_button( self, "", icon_name="expand-arrow", tool_tip="Expand all namespaces", on_clicked=lambda: self._tree.expandAll(), ) self._collapse_all_button = make_button( self, "", icon_name="collapse-arrow", tool_tip="Collapse all namespaces", on_clicked=lambda: self._tree.collapseAll(), ) self._status_display = QLabel(self) self._status_display.setWordWrap(True) self._reset_selected_button.setEnabled(False) self._reset_all_button.setEnabled(False) self._read_selected_button.setEnabled(False) self._read_all_button.setEnabled(False) self._export_button.setEnabled(False) self._import_button.setEnabled(False) self._tree = QTreeView(self) self._tree.setVerticalScrollMode(QTreeView.ScrollPerPixel) self._tree.setHorizontalScrollMode(QTreeView.ScrollPerPixel) self._tree.setAnimated(True) self._tree.setSelectionMode(QAbstractItemView.ExtendedSelection) self._tree.setAlternatingRowColors(True) self._tree.setContextMenuPolicy(Qt.ActionsContextMenu) # Not sure about this one. This hardcoded value may look bad on some platforms. self._tree.setIndentation(20) def add_action( callback: typing.Callable[[], None], icon_name: str, name: str, shortcut: typing.Optional[str] = None, ): action = QAction(get_icon(icon_name), name, self) # noinspection PyUnresolvedReferences action.triggered.connect(callback) if shortcut: action.setShortcut(shortcut) action.setAutoRepeat(False) try: action.setShortcutVisibleInContextMenu(True) except AttributeError: pass # This feature is not available in PyQt before 5.10 self._tree.addAction(action) add_action(self._do_read_all, "process-plus", "Read all registers") add_action( self._do_read_selected, "process", "Read selected registers", READ_SELECTED_SHORTCUT, ) add_action( self._do_reset_selected, "clear-symbol", "Reset selected to default", RESET_SELECTED_SHORTCUT, ) self._tree.setItemDelegateForColumn( int(Model.ColumnIndices.VALUE), EditorDelegate(self._tree, self._display_status), ) # It doesn't seem to be explicitly documented, but it seems to be necessary to select either top or bottom # decoration position in order to be able to use center alignment. Left or right positions do not work here. self._tree.setItemDelegateForColumn( int(Model.ColumnIndices.FLAGS), StyleOptionModifyingDelegate( self._tree, decoration_position=QStyleOptionViewItem.Top, # Important decoration_alignment=Qt.AlignCenter, ), ) header: QHeaderView = self._tree.header() header.setSectionResizeMode(QHeaderView.ResizeToContents) header.setStretchLastSection( False) # Horizontal scroll bar doesn't work if this is enabled buttons_layout = QGridLayout() buttons_layout.addWidget(self._read_selected_button, 0, 0) buttons_layout.addWidget(self._reset_selected_button, 0, 2) buttons_layout.addWidget(self._read_all_button, 1, 0) buttons_layout.addWidget(self._reset_all_button, 1, 2) buttons_layout.addWidget(self._import_button, 2, 0) buttons_layout.addWidget(self._export_button, 2, 2) for col in range(3): buttons_layout.setColumnStretch(col, 1) layout = lay_out_vertically( (self._tree, 1), buttons_layout, lay_out_horizontally( self._visibility_selector, (None, 1), self._expand_all_button, self._collapse_all_button, ), self._status_display, ) self.setLayout(layout) def reset(self): self.setup([]) def setup(self, registers: typing.Iterable[Register]): self._registers = list(registers) self._on_visibility_changed() def _replace_model( self, register_visibility_predicate: typing.Callable[[Register], bool]): # Cancel all operations that might be pending on the old model self._cancel_task() old_model = self._tree.model() # Configure the new model filtered_registers = list( filter(register_visibility_predicate, self._registers)) # It is important to set the Tree widget as the parent in order to let the widget take ownership new_model = Model(self._tree, filtered_registers) _logger.info("New model %r", new_model) self._tree.setModel(new_model) # The selection model is implicitly replaced when we replace the model, so it has to be reconfigured self._tree.selectionModel().selectionChanged.connect( lambda *_: self._on_selection_changed()) # TODO: Something fishy is going on. Something keeps the old model alive when we're replacing it. # We could call deleteLater() on it, but it seems dangerous, because if that something ever decided # to refer to that dead model later for any reason, we'll get a rougue dangling pointer access on # our hands. The horror! if old_model is not None: import gc model_referrers = gc.get_referrers(old_model) if len(model_referrers) > 1: _logger.warning( "Extra references to the old model %r: %r", old_model, model_referrers, ) # Update the widget - all root items are expanded by default for row in itertools.count(): index = self._tree.model().index(row, 0) if not index.isValid(): break self._tree.expand(index) self._reset_selected_button.setEnabled(False) self._read_selected_button.setEnabled(False) self._read_all_button.setEnabled(len(filtered_registers) > 0) self._reset_all_button.setEnabled(len(filtered_registers) > 0) self._export_button.setEnabled(len(filtered_registers) > 0) self._import_button.setEnabled(len(filtered_registers) > 0) self._display_status(f"{len(filtered_registers)} registers loaded") def _on_visibility_changed(self): self._replace_model(self._visibility_selector.currentData()) def _on_selection_changed(self): selected = self._get_selected_registers() self._reset_selected_button.setEnabled( any(map(lambda r: r.has_default_value, selected))) self._read_selected_button.setEnabled(len(selected) > 0) def _do_read_selected(self): selected = self._get_selected_registers() if selected: self._read_specific(selected) else: self._display_status("No registers are selected, nothing to read") def _do_reset_selected(self): rv = {} for r in self._get_selected_registers(): if r.has_default_value: rv[r] = r.default_value self._write_specific(rv) def _do_reset_all(self): rv = {} for r in self._registers: if r.has_default_value: rv[r] = r.default_value self._write_specific(rv) def _do_read_all(self): self._read_specific(self._tree.model().registers) def _do_import(self): import_registers(parent=self, registers=self._registers) def _do_export(self): export_registers(parent=self, registers=self._registers) def _read_specific(self, registers: typing.List[Register]): total_registers_read = None def progress_callback(register: Register, current_register_index: int, total_registers: int): nonlocal total_registers_read total_registers_read = total_registers self._display_status( f"Reading {register.name!r} " f"({current_register_index + 1} of {total_registers})") async def executor(): try: _logger.info("Reading registers: %r", [r.name for r in registers]) mod: Model = self._tree.model() await mod.read(registers=registers, progress_callback=progress_callback) except asyncio.CancelledError: self._display_status(f"Read has been cancelled") raise except Exception as ex: _logger.exception("Register read failed") show_error("Read failed", "Could not read registers", repr(ex), self) self._display_status(f"Could not read registers: {ex!r}") else: self._display_status( f"{total_registers_read} registers have been read") self._cancel_task() self._running_task = asyncio.get_event_loop().create_task(executor()) def _write_specific(self, register_value_mapping: typing.Dict[Register, typing.Any]): total_registers_assigned = None def progress_callback(register: Register, current_register_index: int, total_registers: int): nonlocal total_registers_assigned total_registers_assigned = total_registers self._display_status( f"Writing {register.name!r} " f"({current_register_index + 1} of {total_registers})") async def executor(): try: _logger.info( "Writing registers: %r", [r.name for r in register_value_mapping.keys()], ) mod: Model = self._tree.model() await mod.write( register_value_mapping=register_value_mapping, progress_callback=progress_callback, ) except asyncio.CancelledError: self._display_status(f"Write has been cancelled") raise except Exception as ex: _logger.exception("Register write failed") show_error("Write failed", "Could not read registers", repr(ex), self) self._display_status(f"Could not write registers: {ex!r}") else: self._display_status( f"{total_registers_assigned} registers have been written") self._cancel_task() self._running_task = asyncio.get_event_loop().create_task(executor()) def _get_selected_registers(self) -> typing.List[Register]: selected_indexes: typing.List[ QModelIndex] = self._tree.selectedIndexes() selected_registers = set() for si in selected_indexes: r = Model.get_register_from_index(si) if r is not None: selected_registers.add(r) # Beware that sets are not sorted, this may lead to weird user experience when watching the registers # read in a funny order. return list(sorted(selected_registers, key=lambda x: x.name)) def _cancel_task(self): # noinspection PyBroadException try: self._running_task.cancel() except Exception: pass else: _logger.info("A running task had to be cancelled: %r", self._running_task) finally: self._running_task = None def _display_status(self, text=None): self._status_display.setText(text)
class TreeView(QWidget): def __init__(self, table, parent): QWidget.__init__(self, parent) self.window = parent self.tree = QTreeView(self) indent = self.tree.indentation() self.tree.setIndentation(indent / 2) self.model = DataModel(table) self.sorter = sorter = FilterModel(self) sorter.setSourceModel(self.model) self.tree.setModel(sorter) for col in range(3, 9): self.tree.setItemDelegateForColumn(col, PercentDelegate(self)) self.tree.header().setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.tree.header().customContextMenuRequested.connect( self._on_header_menu) self.tree.setSortingEnabled(True) self.tree.setAutoExpandDelay(0) self.tree.resizeColumnToContents(0) self.tree.resizeColumnToContents(NAME_COLUMN) self.tree.expand(self.sorter.index(0, 0)) #self.tree.expandAll() self.tree.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.tree.customContextMenuRequested.connect(self._on_tree_menu) searchbox = QHBoxLayout() self.search = QLineEdit(self) searchbox.addWidget(self.search) self.search_type = QComboBox(self) self.search_type.addItem("Contains", SEARCH_CONTAINS) self.search_type.addItem("Exact", SEARCH_EXACT) self.search_type.addItem("Reg.Exp", SEARCH_REGEXP) searchbox.addWidget(self.search_type) btn = QPushButton("&Search", self) searchbox.addWidget(btn) btn.clicked.connect(self._on_search) btn = QPushButton("&Next", self) searchbox.addWidget(btn) btn.clicked.connect(self._on_search_next) filterbox = QHBoxLayout() label = QLabel("Time Individual", self) filterbox.addWidget(label) self.individual_time = QSpinBox(self) self.individual_time.setMinimum(0) self.individual_time.setMaximum(100) self.individual_time.setSuffix(" %") filterbox.addWidget(self.individual_time) label = QLabel("Alloc Individual", self) filterbox.addWidget(label) self.individual_alloc = QSpinBox(self) self.individual_alloc.setMinimum(0) self.individual_alloc.setMaximum(100) self.individual_alloc.setSuffix(" %") filterbox.addWidget(self.individual_alloc) label = QLabel("Time Inherited", self) filterbox.addWidget(label) self.inherited_time = QSpinBox(self) self.inherited_time.setMinimum(0) self.inherited_time.setMaximum(100) self.inherited_time.setSuffix(" %") filterbox.addWidget(self.inherited_time) label = QLabel("Alloc Inherited", self) filterbox.addWidget(label) self.inherited_alloc = QSpinBox(self) self.inherited_alloc.setMinimum(0) self.inherited_alloc.setMaximum(100) self.inherited_alloc.setSuffix(" %") filterbox.addWidget(self.inherited_alloc) btn = QPushButton("&Filter", self) btn.clicked.connect(self._on_filter) filterbox.addWidget(btn) btn = QPushButton("&Reset", self) filterbox.addWidget(btn) btn.clicked.connect(self._on_reset_filter) vbox = QVBoxLayout() vbox.addLayout(searchbox) vbox.addLayout(filterbox) vbox.addWidget(self.tree) self.setLayout(vbox) self._search_idxs = None self._search_idx_no = 0 def _expand_to(self, idx): idxs = [idx] parent = idx while parent and parent.isValid(): parent = self.sorter.parent(parent) idxs.append(parent) #print(idxs) for idx in reversed(idxs[:-1]): data = self.sorter.data(idx, QtCore.Qt.DisplayRole) #print(data) self.tree.expand(idx) def _on_search(self): text = self.search.text() selected = self.tree.selectedIndexes() # if selected: # start = selected[0] # else: start = self.sorter.index(0, NAME_COLUMN) search_type = self.search_type.currentData() if search_type == SEARCH_EXACT: method = QtCore.Qt.MatchFixedString elif search_type == SEARCH_CONTAINS: method = QtCore.Qt.MatchContains else: method = QtCore.Qt.MatchRegExp self._search_idxs = idxs = self.sorter.search(start, text, search_type) if idxs: self.window.statusBar().showMessage( "Found: {} occurence(s)".format(len(idxs))) self._search_idx_no = 0 idx = idxs[0] self._locate(idx) else: self.window.statusBar().showMessage("Not found") def _locate(self, idx): self.tree.resizeColumnToContents(0) self.tree.resizeColumnToContents(NAME_COLUMN) self._expand_to(idx) self.tree.setCurrentIndex(idx) #self.tree.selectionModel().select(idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Current | QItemSelectionModel.Rows) #self.tree.scrollTo(idx, QAbstractItemView.PositionAtCenter) def _on_search_next(self): if self._search_idxs: n = len(self._search_idxs) self._search_idx_no = (self._search_idx_no + 1) % n idx = self._search_idxs[self._search_idx_no] self.window.statusBar().showMessage("Occurence {} of {}".format( self._search_idx_no, n)) self._locate(idx) else: self.window.statusBar().showMessage("No search results") def _on_filter(self): self.sorter.setFilter(self.search.text(), self.individual_time.value(), self.individual_alloc.value(), self.inherited_time.value(), self.inherited_alloc.value()) def _on_reset_filter(self): self.sorter.reset() def _on_header_menu(self, pos): menu = make_header_menu(self.tree) menu.exec_(self.mapToGlobal(pos)) def _on_tree_menu(self, pos): index = self.tree.indexAt(pos) #print("index: {}".format(index)) if index.isValid(): record = self.sorter.data(index, QtCore.Qt.UserRole + 1) #print("okay?..") #print("context: {}".format(record)) menu = self.window.make_item_menu(self.model, record) menu.exec_(self.tree.viewport().mapToGlobal(pos))
class DebugWindow(QMainWindow): """A main window to edit text and examine the generated token structure. Example:: from PyQt5.Qt import * a=QApplication([]) from parceqt.debug import DebugWindow w = DebugWindow() w.resize(1200,900) w.show() w.set_theme("default", True) from parce.lang.css import * w.set_root_lexicon(Css.root) w.set_text(open("path/to/parce/themes/default.css").read()) In the debug window you can edit the text at the left and directly at the right examine the tree structure. Along the top of the window the path to the token at the current cursor position is displayed, from the root lexicon upto the token, from which the action is displayed. Clicking a button selects the associated range of the context or token in the text view. Clicking an item in the tree also selects that range in the text. Moving the cursor in the text updates the current item in the tree, and the displayed ancestor path. """ show_updated_region_enabled = False def __init__(self, parent=None): super().__init__(parent, windowTitle="parceqt debugger") f = self._updated_format = QTextCharFormat() c = QColor("palegreen") c.setAlpha(64) f.setBackground(c) f = self._currentline_format = QTextCharFormat() f.setProperty(QTextCharFormat.FullWidthSelection, True) self._actions = Actions(self) self._actions.add_menus(self.menuBar()) widget = QWidget(self) self.setCentralWidget(widget) layout = QVBoxLayout(margin=4, spacing=2) widget.setLayout(layout) top_layout = QHBoxLayout(margin=0, spacing=0) self.guessButton = QToolButton(self, clicked=self.guess_root_lexicon, toolTip="Guess Language", icon=self.style().standardIcon( QStyle.SP_BrowserReload)) self.lexiconChooser = LexiconChooser(self) self.ancestorView = AncestorView(self) top_layout.addWidget(self.guessButton) top_layout.addWidget(self.lexiconChooser) top_layout.addWidget(self.ancestorView) top_layout.addStretch(10) layout.addLayout(top_layout) self.guessButton.setFixedHeight( self.lexiconChooser.sizeHint().height()) splitter = QSplitter(self, orientation=Qt.Horizontal) layout.addWidget(splitter, 100) self.textEdit = QPlainTextEdit(lineWrapMode=QPlainTextEdit.NoWrap, cursorWidth=2) self.treeView = QTreeView() splitter.addWidget(self.textEdit) splitter.addWidget(self.treeView) splitter.setStretchFactor(0, 3) splitter.setStretchFactor(1, 2) self.extraSelectionManager = ExtraSelectionManager(self.textEdit) self.document = d = self.textEdit.document() self.textEdit.setDocument(self.document) self.worker = w = parceqt.worker(d) self.builder = b = w.builder() w.debugging = True self.setStatusBar(QStatusBar()) self.create_model() # signal connections self.textEdit.viewport().installEventFilter(self) self.textEdit.installEventFilter(self) self.lexiconChooser.lexicon_changed.connect( self.slot_root_lexicon_changed) self.ancestorView.node_clicked.connect(self.slot_node_clicked) w.started.connect(self.slot_build_started) w.tree_updated.connect(self.slot_build_updated) self.textEdit.cursorPositionChanged.connect( self.slot_cursor_position_changed) self.treeView.clicked.connect(self.slot_item_clicked) self.textEdit.setFocus() self.set_theme() # somewhat larger font by default font = self.textEdit.font() font.setPointSizeF(11) self.textEdit.setFont(font) def create_model(self): """Instantiate a tree model for the tree view.""" m = self.treeView.model() if not m: m = parceqt.treemodel.TreeModel(self.builder.root) m.connect_debugging_builder(self.builder) self.treeView.setModel(m) def delete_model(self): """Delete the model and remove it from the tree.""" m = self.treeView.model() if m: m.disconnect_debugging_builder(self.builder) self.treeView.setModel(None) m.deleteLater() def set_text(self, text): """Set the text in the text edit.""" self.document.setPlainText(text) def set_root_lexicon(self, lexicon): """Set the root lexicon to use.""" self.lexiconChooser.set_root_lexicon(lexicon) def guess_root_lexicon(self): """Again choose the root lexicon based on the text.""" text = self.document.toPlainText() if text: self.set_root_lexicon(parce.find(contents=text)) def open_file(self, filename): """Read a file from disk and guess the language.""" text = read_file(filename) root_lexicon = parce.find(filename=filename, contents=text) self.set_text(text) self.set_root_lexicon(root_lexicon) c = self.textEdit.textCursor() c.setPosition(0) self.textEdit.setTextCursor(c) def set_theme(self, theme="default", adjust_widget=True): """Set the theme to use for the text edit.""" if isinstance(theme, str): theme = parce.theme_by_name(theme) formatter = parceqt.formatter.Formatter(theme) if theme else None if adjust_widget: if formatter: font = formatter.font(self) self.textEdit.setPalette(formatter.palette(self)) else: font = QApplication.font(self) self.textEdit.setPalette(QApplication.palette(self)) font.setPointSizeF(self.textEdit.font().pointSizeF()) # keep size self.textEdit.setFont(font) self.highlight_current_line() h = parceqt.highlighter.SyntaxHighlighter.instance(self.worker) h.set_formatter(formatter) def slot_build_started(self): """Called when the tree builder has started a build.""" self.treeView.setCursor(Qt.BusyCursor) def slot_build_updated(self): """Called when the tree builder has finished a build.""" self.treeView.unsetCursor() self.slot_cursor_position_changed() self.statusBar().showMessage(", ".join( lexicon_names(self.builder.lexicons))) tree = self.worker.get_root() self.lexiconChooser.setToolTip( parceqt.treemodel.TreeModel.node_tooltip(tree)) if self.show_updated_region_enabled: self.show_updated_region() def slot_cursor_position_changed(self): """Called when the text cursor moved.""" tree = self.worker.get_root() if tree: pos = self.textEdit.textCursor().position() doc = parceqt.document.Document(self.document) token = doc.token(pos) if token: self.ancestorView.set_token_path(token) model = self.treeView.model() if model: index = model.get_model_index(token) self.treeView.setCurrentIndex(index) elif tree is not None: self.ancestorView.clear() self.highlight_current_line() def slot_item_clicked(self, index): """Called when a node in the tree view is clicked.""" tree = self.worker.get_root() if tree: model = self.treeView.model() if model: node = self.treeView.model().get_node(index) cursor = self.textEdit.textCursor() cursor.setPosition(node.end) cursor.setPosition(node.pos, QTextCursor.KeepAnchor) self.textEdit.setTextCursor(cursor) self.textEdit.setFocus() def slot_node_clicked(self, node): """Called when a button in the ancestor view is clicked.""" tree = self.worker.get_root() if tree and node.root() is tree: cursor = self.textEdit.textCursor() cursor.setPosition(node.end) cursor.setPosition(node.pos, QTextCursor.KeepAnchor) self.textEdit.setTextCursor(cursor) self.textEdit.setFocus() model = self.treeView.model() if model: index = model.get_model_index(node) self.treeView.expand(index) self.treeView.setCurrentIndex(index) def slot_root_lexicon_changed(self, lexicon): """Called when the root lexicon is changed.""" parceqt.set_root_lexicon(self.document, lexicon) def highlight_current_line(self): """Highlight the current line.""" group = QPalette.Active if self.textEdit.hasFocus( ) else QPalette.Inactive p = self.textEdit.palette() color = p.color(group, QPalette.AlternateBase) self._currentline_format.setBackground(color) if color != p.color(group, QPalette.Base): c = self.textEdit.textCursor() c.clearSelection() self.extraSelectionManager.highlight(self._currentline_format, [c]) else: self.extraSelectionManager.clear(self._currentline_format) def show_updated_region(self): """Highlight the updated region for 2 seconds.""" end = self.builder.end if end >= self.document.characterCount() - 1: end = self.document.characterCount() - 1 if self.builder.start == 0: return c = QTextCursor(self.document) c.setPosition(end) c.setPosition(self.builder.start, QTextCursor.KeepAnchor) self.extraSelectionManager.highlight(self._updated_format, [c], msec=2000) def clear_updated_region(self): self.extraSelectionManager.clear(self._updated_format) def eventFilter(self, obj, ev): """Implemented to support Ctrl+wheel zooming and keybfocus handling.""" if obj == self.textEdit: if ev.type() in (QEvent.FocusIn, QEvent.FocusOut): self.highlight_current_line() else: # viewport if ev.type() == QEvent.Wheel and ev.modifiers( ) == Qt.ControlModifier: if ev.angleDelta().y() > 0: self.textEdit.zoomIn() elif ev.angleDelta().y() < 0: self.textEdit.zoomOut() return True return False
class FileBrowserWidget(QWidget): on_open = pyqtSignal(str) def __init__(self): super().__init__() self.initUI() def initUI(self): self.model = QFileSystemModel() self.rootFolder = '' self.model.setRootPath(self.rootFolder) self.tree = QTreeView() self.tree.setModel(self.model) self.tree.setAnimated(False) self.tree.setIndentation(20) self.tree.setSortingEnabled(True) self.tree.sortByColumn(0, 0) self.tree.setColumnWidth(0, 200) self.tree.setDragEnabled(True) self.tree.setWindowTitle("Dir View") self.tree.resize(640, 480) self.tree.doubleClicked.connect(self.onDblClick) self.tree.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.tree.customContextMenuRequested.connect( self.onCustomContextMenuRequested) windowLayout = QVBoxLayout() windowLayout.addWidget(self.tree) windowLayout.setContentsMargins(0, 0, 0, 0) self.setLayout(windowLayout) def onCustomContextMenuRequested(self, point): index = self.tree.indexAt(point) selectedFile = None selectedFolder = None ctx = QMenu("Context menu", self) if index.isValid(): file = self.model.fileInfo(index) selectedFile = file.absoluteFilePath() selectedFolder = selectedFile if file.isDir( ) else file.absolutePath() if file.isDir(): ctx.addAction( "Open in file manager", lambda: QDesktopServices.openUrl( QUrl.fromLocalFile(selectedFile))) if not file.isDir(): for wndTyp, meta in WindowTypes.types: text = 'Open with ' + meta.get('displayName', meta['name']) print(wndTyp, meta) ctx.addAction( QAction(text, self, statusTip=text, triggered=lambda dummy, meta=meta: navigate( "WINDOW", "Type=" + meta['name'], "FileName=" + selectedFile))) ctx.addSeparator() ctx.addAction("Set root folder ...", lambda: self.selectRootFolder(preselect=selectedFolder)) ctx.exec(self.tree.viewport().mapToGlobal(point)) def selectRootFolder(self, preselect=None): if preselect == None: preselect = self.rootFolder dir = QFileDialog.getExistingDirectory(self, "Set root folder", preselect) if dir != None: self.setRoot(dir) def setRoot(self, dir): self.rootFolder = dir self.model.setRootPath(dir) self.tree.setRootIndex(self.model.index(dir)) def onDblClick(self, index): if index.isValid(): file = self.model.fileInfo(index) if not file.isDir(): navigate("OPEN", "FileName=" + file.absoluteFilePath()) def saveState(self): if self.tree.currentIndex().isValid(): info = self.model.fileInfo(self.tree.currentIndex()) return {"sel": info.absoluteFilePath(), "root": self.rootFolder} def restoreState(self, state): try: self.setRoot(state["root"]) except: pass try: idx = self.model.index(state["sel"]) if idx.isValid(): self.tree.expand(idx) self.tree.setCurrentIndex(idx) self.tree.scrollTo(idx, QAbstractItemView.PositionAtCenter) except: pass
class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self._version = "0.1.11" self.setWindowIcon(QIcon("GUI/icons/logo.png")) self.setWindowTitle("Tasmota Device Manager {}".format(self._version)) self.main_splitter = QSplitter() self.devices_splitter = QSplitter(Qt.Vertical) self.fulltopic_queue = [] self.settings = QSettings() self.setMinimumSize(QSize(1280,800)) self.device_model = TasmotaDevicesModel() self.telemetry_model = TasmotaDevicesTree() self.console_model = ConsoleModel() self.sorted_console_model = QSortFilterProxyModel() self.sorted_console_model.setSourceModel(self.console_model) self.sorted_console_model.setFilterKeyColumn(CnsMdl.FRIENDLY_NAME) self.setup_mqtt() self.setup_telemetry_view() self.setup_main_layout() self.add_devices_tab() self.build_toolbars() self.setStatusBar(QStatusBar()) self.queue_timer = QTimer() self.queue_timer.setSingleShot(True) self.queue_timer.timeout.connect(self.mqtt_ask_for_fulltopic) self.build_cons_ctx_menu() self.load_window_state() def setup_main_layout(self): self.mdi = QMdiArea() self.mdi.setActivationOrder(QMdiArea.ActivationHistoryOrder) self.mdi.setViewMode(QMdiArea.TabbedView) self.mdi.setDocumentMode(True) mdi_widget = QWidget() mdi_widget.setLayout(VLayout()) mdi_widget.layout().addWidget(self.mdi) self.devices_splitter.addWidget(mdi_widget) vl_console = VLayout() self.console_view = TableView() self.console_view.setModel(self.sorted_console_model) self.console_view.setupColumns(columns_console) self.console_view.setAlternatingRowColors(True) self.console_view.setSortingEnabled(True) self.console_view.sortByColumn(CnsMdl.TIMESTAMP, Qt.DescendingOrder) self.console_view.verticalHeader().setDefaultSectionSize(20) self.console_view.setMinimumHeight(200) self.console_view.setContextMenuPolicy(Qt.CustomContextMenu) vl_console.addWidget(self.console_view) console_widget = QWidget() console_widget.setLayout(vl_console) self.devices_splitter.addWidget(console_widget) self.main_splitter.insertWidget(0, self.devices_splitter) self.setCentralWidget(self.main_splitter) self.console_view.clicked.connect(self.select_cons_entry) self.console_view.doubleClicked.connect(self.view_payload) self.console_view.customContextMenuRequested.connect(self.show_cons_ctx_menu) def setup_telemetry_view(self): tele_widget = QWidget() vl_tele = VLayout() self.tview = QTreeView() self.tview.setMinimumWidth(300) self.tview.setModel(self.telemetry_model) self.tview.setAlternatingRowColors(True) self.tview.setUniformRowHeights(True) self.tview.setIndentation(15) self.tview.setSizePolicy(QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)) self.tview.expandAll() self.tview.resizeColumnToContents(0) vl_tele.addWidget(self.tview) tele_widget.setLayout(vl_tele) self.main_splitter.addWidget(tele_widget) def setup_mqtt(self): self.mqtt = MqttClient() self.mqtt.connecting.connect(self.mqtt_connecting) self.mqtt.connected.connect(self.mqtt_connected) self.mqtt.disconnected.connect(self.mqtt_disconnected) self.mqtt.connectError.connect(self.mqtt_connectError) self.mqtt.messageSignal.connect(self.mqtt_message) def add_devices_tab(self): tabDevicesList = DevicesListWidget(self) self.mdi.addSubWindow(tabDevicesList) tabDevicesList.setWindowState(Qt.WindowMaximized) def load_window_state(self): wndGeometry = self.settings.value('window_geometry') if wndGeometry: self.restoreGeometry(wndGeometry) spltState = self.settings.value('splitter_state') if spltState: self.main_splitter.restoreState(spltState) def build_toolbars(self): main_toolbar = Toolbar(orientation=Qt.Horizontal, iconsize=32, label_position=Qt.ToolButtonIconOnly) main_toolbar.setObjectName("main_toolbar") self.addToolBar(main_toolbar) main_toolbar.addAction(QIcon("./GUI/icons/connections.png"), "Configure MQTT broker", self.setup_broker) agBroker = QActionGroup(self) agBroker.setExclusive(True) self.actConnect = CheckableAction(QIcon("./GUI/icons/connect.png"), "Connect to the broker", agBroker) self.actDisconnect = CheckableAction(QIcon("./GUI/icons/disconnect.png"), "Disconnect from broker", agBroker) self.actDisconnect.setChecked(True) self.actConnect.triggered.connect(self.mqtt_connect) self.actDisconnect.triggered.connect(self.mqtt_disconnect) main_toolbar.addActions(agBroker.actions()) main_toolbar.addSeparator() def initial_query(self, idx): for q in initial_queries: topic = "{}status".format(self.device_model.commandTopic(idx)) self.mqtt.publish(topic, q) q = q if q else '' self.console_log(topic, "Asked for STATUS {}".format(q), q) def setup_broker(self): brokers_dlg = BrokerDialog() if brokers_dlg.exec_() == QDialog.Accepted and self.mqtt.state == self.mqtt.Connected: self.mqtt.disconnect() def mqtt_connect(self): self.broker_hostname = self.settings.value('hostname', 'localhost') self.broker_port = self.settings.value('port', 1883, int) self.broker_username = self.settings.value('username') self.broker_password = self.settings.value('password') self.mqtt.hostname = self.broker_hostname self.mqtt.port = self.broker_port if self.broker_username: self.mqtt.setAuth(self.broker_username, self.broker_password) if self.mqtt.state == self.mqtt.Disconnected: self.mqtt.connectToHost() def mqtt_disconnect(self): self.mqtt.disconnectFromHost() def mqtt_connecting(self): self.statusBar().showMessage("Connecting to broker") def mqtt_connected(self): self.statusBar().showMessage("Connected to {}:{} as {}".format(self.broker_hostname, self.broker_port, self.broker_username if self.broker_username else '[anonymous]')) self.mqtt_subscribe() for d in range(self.device_model.rowCount()): idx = self.device_model.index(d, 0) self.initial_query(idx) def mqtt_subscribe(self): main_topics = ["+/stat/+", "+/tele/+", "stat/#", "tele/#"] for d in range(self.device_model.rowCount()): idx = self.device_model.index(d, 0) if not self.device_model.isDefaultTemplate(idx): main_topics.append(self.device_model.commandTopic(idx)) main_topics.append(self.device_model.statTopic(idx)) for t in main_topics: self.mqtt.subscribe(t) def mqtt_ask_for_fulltopic(self): for i in range(len(self.fulltopic_queue)): self.mqtt.publish(self.fulltopic_queue.pop(0)) def mqtt_disconnected(self): self.statusBar().showMessage("Disconnected") def mqtt_connectError(self, rc): reason = { 1: "Incorrect protocol version", 2: "Invalid client identifier", 3: "Server unavailable", 4: "Bad username or password", 5: "Not authorized", } self.statusBar().showMessage("Connection error: {}".format(reason[rc])) self.actDisconnect.setChecked(True) def mqtt_message(self, topic, msg): found = self.device_model.findDevice(topic) if found.reply == 'LWT': if not msg: msg = "offline" if found.index.isValid(): self.console_log(topic, "LWT update: {}".format(msg), msg) self.device_model.updateValue(found.index, DevMdl.LWT, msg) elif msg == "Online": self.console_log(topic, "LWT for unknown device '{}'. Asking for FullTopic.".format(found.topic), msg, False) self.fulltopic_queue.append("cmnd/{}/fulltopic".format(found.topic)) self.fulltopic_queue.append("{}/cmnd/fulltopic".format(found.topic)) self.queue_timer.start(1500) elif found.reply == 'RESULT': full_topic = loads(msg).get('FullTopic') new_topic = loads(msg).get('Topic') template_name = loads(msg).get('NAME') if full_topic: # TODO: update FullTopic for existing device AFTER the FullTopic changes externally (the message will arrive from new FullTopic) if not found.index.isValid(): self.console_log(topic, "FullTopic for {}".format(found.topic), msg, False) new_idx = self.device_model.addDevice(found.topic, full_topic, lwt='online') tele_idx = self.telemetry_model.addDevice(TasmotaDevice, found.topic) self.telemetry_model.devices[found.topic] = tele_idx #TODO: add QSortFilterProxyModel to telemetry treeview and sort devices after adding self.initial_query(new_idx) self.console_log(topic, "Added {} with fulltopic {}, querying for STATE".format(found.topic, full_topic), msg) self.tview.expand(tele_idx) self.tview.resizeColumnToContents(0) if new_topic: if found.index.isValid() and found.topic != new_topic: self.console_log(topic, "New topic for {}".format(found.topic), msg) self.device_model.updateValue(found.index, DevMdl.TOPIC, new_topic) tele_idx = self.telemetry_model.devices.get(found.topic) if tele_idx: self.telemetry_model.setDeviceName(tele_idx, new_topic) self.telemetry_model.devices[new_topic] = self.telemetry_model.devices.pop(found.topic) if template_name: self.device_model.updateValue(found.index, DevMdl.MODULE, template_name) elif found.index.isValid(): if found.reply == 'STATUS': self.console_log(topic, "Received device status", msg) payload = loads(msg)['Status'] self.device_model.updateValue(found.index, DevMdl.FRIENDLY_NAME, payload['FriendlyName'][0]) self.telemetry_model.setDeviceFriendlyName(self.telemetry_model.devices[found.topic], payload['FriendlyName'][0]) self.tview.resizeColumnToContents(0) module = payload['Module'] if module == '0': self.mqtt.publish(self.device_model.commandTopic(found.index)+"template") else: self.device_model.updateValue(found.index, DevMdl.MODULE, module) elif found.reply == 'STATUS1': self.console_log(topic, "Received program information", msg) payload = loads(msg)['StatusPRM'] self.device_model.updateValue(found.index, DevMdl.RESTART_REASON, payload['RestartReason']) elif found.reply == 'STATUS2': self.console_log(topic, "Received firmware information", msg) payload = loads(msg)['StatusFWR'] self.device_model.updateValue(found.index, DevMdl.FIRMWARE, payload['Version']) self.device_model.updateValue(found.index, DevMdl.CORE, payload['Core']) elif found.reply == 'STATUS3': self.console_log(topic, "Received syslog information", msg) payload = loads(msg)['StatusLOG'] self.device_model.updateValue(found.index, DevMdl.TELEPERIOD, payload['TelePeriod']) elif found.reply == 'STATUS5': self.console_log(topic, "Received network status", msg) payload = loads(msg)['StatusNET'] self.device_model.updateValue(found.index, DevMdl.MAC, payload['Mac']) self.device_model.updateValue(found.index, DevMdl.IP, payload['IPAddress']) elif found.reply == 'STATUS8': self.console_log(topic, "Received telemetry", msg) payload = loads(msg)['StatusSNS'] self.parse_telemetry(found.index, payload) elif found.reply == 'STATUS11': self.console_log(topic, "Received device state", msg) payload = loads(msg)['StatusSTS'] self.parse_state(found.index, payload) elif found.reply == 'SENSOR': self.console_log(topic, "Received telemetry", msg) payload = loads(msg) self.parse_telemetry(found.index, payload) elif found.reply == 'STATE': self.console_log(topic, "Received device state", msg) payload = loads(msg) self.parse_state(found.index, payload) elif found.reply.startswith('POWER'): self.console_log(topic, "Received {} state".format(found.reply), msg) payload = {found.reply: msg} self.parse_power(found.index, payload) def parse_power(self, index, payload): old = self.device_model.power(index) power = {k: payload[k] for k in payload.keys() if k.startswith("POWER")} needs_update = False if old: for k in old.keys(): needs_update |= old[k] != power.get(k, old[k]) if needs_update: break else: needs_update = True if needs_update: self.device_model.updateValue(index, DevMdl.POWER, power) def parse_state(self, index, payload): bssid = payload['Wifi'].get('BSSId') if not bssid: bssid = payload['Wifi'].get('APMac') self.device_model.updateValue(index, DevMdl.BSSID, bssid) self.device_model.updateValue(index, DevMdl.SSID, payload['Wifi']['SSId']) self.device_model.updateValue(index, DevMdl.CHANNEL, payload['Wifi'].get('Channel')) self.device_model.updateValue(index, DevMdl.RSSI, payload['Wifi']['RSSI']) self.device_model.updateValue(index, DevMdl.UPTIME, payload['Uptime']) self.device_model.updateValue(index, DevMdl.LOADAVG, payload.get('LoadAvg')) self.parse_power(index, payload) tele_idx = self.telemetry_model.devices.get(self.device_model.topic(index)) if tele_idx: tele_device = self.telemetry_model.getNode(tele_idx) self.telemetry_model.setDeviceFriendlyName(tele_idx, self.device_model.friendly_name(index)) pr = tele_device.provides() for k in pr.keys(): self.telemetry_model.setData(pr[k], payload.get(k)) def parse_telemetry(self, index, payload): device = self.telemetry_model.devices.get(self.device_model.topic(index)) if device: node = self.telemetry_model.getNode(device) time = node.provides()['Time'] if 'Time' in payload: self.telemetry_model.setData(time, payload.pop('Time')) temp_unit = "C" pres_unit = "hPa" if 'TempUnit' in payload: temp_unit = payload.pop('TempUnit') if 'PressureUnit' in payload: pres_unit = payload.pop('PressureUnit') for sensor in sorted(payload.keys()): if sensor == 'DS18x20': for sns_name in payload[sensor].keys(): d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice(DS18x20, payload[sensor][sns_name]['Type'], device) self.telemetry_model.getNode(d).setTempUnit(temp_unit) payload[sensor][sns_name]['Id'] = payload[sensor][sns_name].pop('Address') pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor][sns_name].get(pk)) self.tview.expand(d) elif sensor.startswith('DS18B20'): d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice(DS18x20, sensor, device) self.telemetry_model.getNode(d).setTempUnit(temp_unit) pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) if sensor == 'COUNTER': d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice(CounterSns, "Counter", device) pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) else: d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice(sensor_map.get(sensor, Node), sensor, device) pr = self.telemetry_model.getNode(d).provides() if 'Temperature' in pr: self.telemetry_model.getNode(d).setTempUnit(temp_unit) if 'Pressure' in pr or 'SeaPressure' in pr: self.telemetry_model.getNode(d).setPresUnit(pres_unit) for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) self.tview.resizeColumnToContents(0) def console_log(self, topic, description, payload, known=True): device = self.device_model.findDevice(topic) fname = self.device_model.friendly_name(device.index) self.console_model.addEntry(topic, fname, description, payload, known) self.console_view.resizeColumnToContents(1) def view_payload(self, idx): idx = self.sorted_console_model.mapToSource(idx) row = idx.row() timestamp = self.console_model.data(self.console_model.index(row, CnsMdl.TIMESTAMP)) topic = self.console_model.data(self.console_model.index(row, CnsMdl.TOPIC)) payload = self.console_model.data(self.console_model.index(row, CnsMdl.PAYLOAD)) dlg = PayloadViewDialog(timestamp, topic, payload) dlg.exec_() def select_cons_entry(self, idx): self.cons_idx = idx def build_cons_ctx_menu(self): self.cons_ctx_menu = QMenu() self.cons_ctx_menu.addAction("View payload", lambda: self.view_payload(self.cons_idx)) self.cons_ctx_menu.addSeparator() self.cons_ctx_menu.addAction("Show only this device", lambda: self.cons_set_filter(self.cons_idx)) self.cons_ctx_menu.addAction("Show all devices", self.cons_set_filter) def show_cons_ctx_menu(self, at): self.select_cons_entry(self.console_view.indexAt(at)) self.cons_ctx_menu.popup(self.console_view.viewport().mapToGlobal(at)) def cons_set_filter(self, idx=None): if idx: idx = self.sorted_console_model.mapToSource(idx) topic = self.console_model.data(self.console_model.index(idx.row(), CnsMdl.FRIENDLY_NAME)) self.sorted_console_model.setFilterFixedString(topic) else: self.sorted_console_model.setFilterFixedString("") def closeEvent(self, e): self.settings.setValue("window_geometry", self.saveGeometry()) self.settings.setValue("splitter_state", self.main_splitter.saveState()) self.settings.sync() e.accept()
class TableViewSelectTypes(QtWidgets.QDialog): def setText(self, text): self._text = text if text in GridTableView.BasicTypes: self.tree.expand(self.bacisItem.index()) for i in range(self.bacisItem.rowCount()): childItem = self.bacisItem.child(i, 0) if childItem.text() == text: self.tree.setCurrentIndex(childItem.index()) else: self.tree.expand(self.otherItem.index()) names = text.split('.') parentItem = self.otherItem for v in names: isBreak = False for i in range(parentItem.rowCount()): if isBreak == True: continue if parentItem.child(i, 0).text() == v: parentItem = parentItem.child(i, 0) self.tree.expand(parentItem.index()) isBreak = True continue self.tree.setCurrentIndex(parentItem.index()) def text(self): return self._text def __init__(self, *args): super(TableViewSelectTypes, self).__init__(args[1]) self.tableView = args[1] self._text = '' gMainWindow = GlobalObjs.GetValue('MainWindow') self.setModal(True) #self.setModel(True) self.setWindowTitle('Select Type') self.vboxLayout = QtWidgets.QVBoxLayout(self) self.tree = QTreeView(self) self.model = QStandardItemModel(self) self.tree.setHeaderHidden(True) self.tree.setModel(self.model) self.bacisItem = QStandardItem() self.bacisItem.setText('BasicTypes') self.bacisItem.setFlags(QtCore.Qt.ItemIsEnabled) self.model.appendRow(self.bacisItem) self.tree.expand(self.bacisItem.index()) for v in GridTableView.BasicTypes: item = QStandardItem() item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable) item.setText(v) item.setData(True) self.bacisItem.appendRow(item) if self.tableView.Model.activeColumn != args[2]: self.otherItem = QStandardItem() self.otherItem.setText('Other') self.otherItem.setFlags(QtCore.Qt.ItemIsEnabled) self.model.appendRow(self.otherItem) self.tree.expand(self.otherItem.index()) def LoadOtherTypes(item, parent): for i in range(0, item.rowCount()): childItem = item.child(i, 0) if childItem.data(Qt.UserRole + 2) == self.tableView.Model: continue itemAdd = QStandardItem() itemAdd.setText(childItem.text()) if childItem.data() == 3: itemAdd.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable) else: itemAdd.setFlags(QtCore.Qt.ItemIsEnabled) LoadOtherTypes(childItem, itemAdd) parent.appendRow(itemAdd) LoadOtherTypes(gMainWindow.TreeRootItem, self.otherItem) self.vboxLayout.addWidget(self.tree) self.tree.doubleClicked.connect(self.OnDoubleClicked) self.tree.setFocus() self.show() def OnDoubleClicked(self, index): item = self.model.itemFromIndex(index) if item.flags( ) == QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable: if item.data() == True: self.setText(index.data()) else: names = [] parent = index while True: names.append(parent.data()) parent = parent.parent() if parent.isValid() == False or self.otherItem.index( ) == parent: break self.setText('.'.join(reversed(names))) self.done(QtWidgets.QDialog.Accepted)
class ModelGui(QDialog): """The gui to start and manipulate the analysis model""" ANALYSIS, ID, NUMPOINTS = range(3) NO_PARENT_ID = 0xFFFFFFFF def __init__(self, analysis_event_queue, select, delete, load): super(ModelGui, self).__init__() self.analysis_event_queue = analysis_event_queue self.select_callback = select self.delete_callback = delete self.load_callback = load self.name = None self.label_name = None self.meta_name = None self.hsne_name = None self.title = 'Analysis hierarchy viewer' self.left = 10 self.top = 10 self.width = 1000 self.height = 400 self.init_ui() self.id_item = {} self.root_id = None def init_ui(self): """All the ui layout in one place. Note: Part of the ui is switchable depending on the selected demo type, see the ui_matrix""" self.setWindowTitle(self.title) self.setGeometry(self.left, self.top, self.width, self.height) self.model = self.create_analysis_model(self) self.analysis_model = None self.top_scale = None self.thumb_size = (40, 40) # The analysis tree self.tree = QTreeView() self.tree.setSelectionBehavior(QAbstractItemView.SelectRows) self.tree.header().setSectionResizeMode(QHeaderView.ResizeToContents) self.tree.setIconSize(QSize(*self.thumb_size)) self.tree.setModel(self.model) self.tree.setAnimated(False) self.tree.setIndentation(20) self.tree.setSortingEnabled(True) self.tree.resize(640, 480) # Control layout controlLayout = QVBoxLayout() self.preconfigured_group = QGroupBox("Preconfigured") preconfigured_layout = QFormLayout(self) self.preconfigured_combo = QComboBox(self) self.preconfigured_combo.addItem('None', userData=None) for config in CONFIGS: self.preconfigured_combo.addItem(config.descriptor, userData=config) self.preconfigured_combo.currentIndexChanged[int].connect( self.on_preconfigured) preconfigured_layout.addRow(QLabel("Load preset demo:"), self.preconfigured_combo) self.preconfigured_group.setLayout(preconfigured_layout) controlLayout.addWidget(self.preconfigured_group) # Data type settings self.demo_type_group = QGroupBox("Data type") data_type_layout = QFormLayout(self) self.data_type_combo = QComboBox(self) self.data_type_combo.addItem("Image is a data point", DemoType.LABELLED_DEMO) self.data_type_combo.addItem("Point and metadata", DemoType.POINT_DEMO) self.data_type_combo.addItem("Hyperspectral image", DemoType.HYPERSPECTRAL_DEMO) data_type_layout.addRow(QLabel("Visualization style:"), self.data_type_combo) self.data_type_combo.currentIndexChanged[int].connect( self.on_demo_style) data_button = QPushButton("Data") data_button.clicked.connect(self.on_load) self.data_label = QLabel("<choose data .npy>") data_type_layout.addRow(data_button, self.data_label) self.xy_label = QLabel("Image size") self.label_x = QLabel("X:") self.label_y = QLabel("Y:") self.image_x = QLineEdit() self.image_x.setValidator(QIntValidator(1, 10000)) self.image_y = QLineEdit() self.image_y.setValidator(QIntValidator(1, 10000)) self.xy_container = QWidget() self.xy_layout = QHBoxLayout() self.xy_layout.setContentsMargins(0, 0, 0, 0) self.xy_layout.addWidget(self.label_x) self.xy_layout.addWidget(self.image_x) self.xy_layout.addWidget(self.label_y) self.xy_layout.addWidget(self.image_y) self.xy_container.setLayout(self.xy_layout) data_type_layout.addRow(self.xy_label, self.xy_container) self.label_button = QPushButton("Labels") self.label_button.clicked.connect(self.on_load_labels) self.label_label = QLabel("<optionally choose labels .npy>") data_type_layout.addRow(self.label_button, self.label_label) self.meta_button = QPushButton("Label/Color metadata") self.meta_button.clicked.connect(self.on_load_labelscolors) self.meta_label = QLabel("<optionally choose label/color .csv>") data_type_layout.addRow(self.meta_button, self.meta_label) self.demo_type_group.setLayout(data_type_layout) controlLayout.addWidget(self.demo_type_group) # Hsne settings self.hsne_group = QGroupBox("hSNE settings") self.hsne_form_layout = QFormLayout(self) hsne_button = QPushButton("Preload") hsne_button.clicked.connect(self.on_load_hsne) self.hsne_label = QLabel("<optionally choose existing .hsne>") self.hsne_form_layout.addRow(hsne_button, self.hsne_label) self.scale_spin = QSpinBox(self) self.scale_spin.setRange(1, 10) self.scale_spin.setValue(4) self.hsne_form_layout.addRow(QLabel("Scales:"), self.scale_spin) self.hsne_group.setLayout(self.hsne_form_layout) controlLayout.addWidget(self.hsne_group) # Embedding settings self.embedding_group = QGroupBox("Embedding settings") embed_form_layout = QFormLayout(self) self.iter_spin = QSpinBox(self) self.iter_spin.setRange(350, 1000) self.iter_spin.setSingleStep(5) self.iter_spin.setValue(500) embed_form_layout.addRow(QLabel("Iterations:"), self.iter_spin) self.embedding_group.setLayout(embed_form_layout) controlLayout.addWidget(self.embedding_group) clear_start_layout = QHBoxLayout() self.start_button = QPushButton("Start") self.start_button.clicked.connect(self.on_start) self.start_button.setDisabled(True) self.clear_button = QPushButton("Clear") self.clear_button.clicked.connect(self.on_clear) clear_start_layout.addWidget(self.start_button) clear_start_layout.addWidget(self.clear_button) controlLayout.addLayout(clear_start_layout) self.delete_button = QPushButton("Delete selected") self.delete_button.clicked.connect(self.on_delete) # Selection self.tree.clicked.connect(self.on_selected) # Layout main_layout = QGridLayout() # row, col, rowSpan, colSpan main_layout.addWidget(self.tree, 0, 0, 10, 5) main_layout.addLayout(controlLayout, 0, 5, 10, 3) main_layout.addWidget(self.delete_button, 11, 2, 1, 2) self.setLayout(main_layout) self.counter = 0 self.event_timer = QTimer(self) self.event_timer.start(500) self.event_timer.timeout.connect(self.update_tree) # Dynamic UI matrix # According to DemoType True widgets are shown False widgets hidden self.ui_matrix = { DemoType.LABELLED_DEMO: { True: [ self.label_button, self.label_label, self.xy_container, self.xy_label ], False: [self.meta_button, self.meta_label] }, DemoType.POINT_DEMO: { True: [self.meta_button, self.meta_label], False: [ self.label_button, self.label_label, self.xy_container, self.xy_label ] }, DemoType.HYPERSPECTRAL_DEMO: { True: [self.xy_container, self.xy_label], False: [ self.label_button, self.label_label, self.meta_button, self.meta_label ] } } self.show() self.on_demo_style(0) def set_analysis_model(self, analysis_model): # TODO empty event queue self.analysis_model = analysis_model self.top_scale = self.analysis_model.top_scale_id @property def iterations(self): return self.iter_spin.value() @property def scales(self): return self.scale_spin.value() @property def demo_type(self): return self.data_type_combo.currentData() @property def im_size_x(self): return int(self.image_x.text()) @property def im_size_y(self): return int(self.image_y.text()) @pyqtSlot(int) def on_preconfigured(self, index): config = self.preconfigured_combo.itemData(index) self.on_clear() if config is None: return if type(config).__name__ == "LabelledImage": self.data_type_combo.setCurrentIndex(0) self.name = config.data.data_file self.data_label.setText(config.data.data_file) self.label_name = config.data.label_file self.label_label.setText(config.data.label_file) if config.hsne.hsne_file != "": self.hsne_name = config.hsne.hsne_file self.__set_scale_from_hsne_file() self.scale_spin.setDisabled(True) elif config.hsne.scales > 0: self.scale_spin.setValue(config.hsne.scales) if config.image.dim_x > 0 and config.image.dim_y > 0: self.image_x.setText(str(config.image.dim_x)) self.image_y.setText(str(config.image.dim_y)) elif type(config).__name__ == "PointMeta": self.data_type_combo.setCurrentIndex(1) self.name = config.data.data_file self.data_label.setText(config.data.data_file) self.meta_name = config.data.meta_file self.meta_label.setText(config.data.meta_file) if config.hsne.hsne_file != "": self.hsne_name = config.hsne.hsne_file self.__set_scale_from_hsne_file() self.scale_spin.setDisabled(True) elif config.hsne.scales > 0: self.scale_spin.setValue(config.hsne.scales) elif type(config).__name__ == "HyperspectralImage": self.data_type_combo.setCurrentIndex(2) self.name = config.data.data_file self.data_label.setText(config.data.data_file) if config.hsne.hsne_file != "": self.hsne_name = config.hsne.hsne_file self.__set_scale_from_hsne_file() self.scale_spin.setDisabled(True) elif config.hsne.scales > 0: self.scale_spin.setValue(config.hsne.scales) if config.image.dim_x > 0 and config.image.dim_y > 0: self.image_x.setText(str(config.image.dim_x)) self.image_y.setText(str(config.image.dim_y)) self.start_button.setEnabled(True) @pyqtSlot(int) def on_demo_style(self, index): # set the visibility of the widgets according to the # type of demo being given for state in [False, True]: for widget in self.ui_matrix[self.demo_type][state]: widget.setVisible(state) @pyqtSlot() def on_load(self): workdir = os.path.dirname(os.path.abspath(__file__)) result = QFileDialog.getOpenFileName( self, 'Open a numpy file where each row is a data point and columns are dimensions', workdir, "Numpy files (*.npy)") if result[0]: self.hsne_label.setText("") self.hsne_name = None self.scale_spin.setEnabled(True) self.name = result[0] # print(f"Selected: {self.name}") self.start_button.setEnabled(True) if (self.demo_type == DemoType.LABELLED_DEMO or self.demo_type == DemoType.HYPERSPECTRAL_DEMO): # partial data load (in memory) to read the shape the_data = np.load(self.name, mmap_mode='r') image_flat_size = the_data.shape[1] if self.demo_type == DemoType.HYPERSPECTRAL_DEMO: image_flat_size = the_data.shape[0] xsize = int(math.sqrt(image_flat_size)) ysize = int(image_flat_size / xsize) self.image_x.setText(str(xsize)) self.image_y.setText(str(ysize)) self.data_label.setText(str(Path(self.name).name)) def __set_scale_from_hsne_file(self): scale_value = nptsne.HSne.read_num_scales(self.hsne_name) self.hsne_label.setText(str(Path(self.hsne_name).name)) self.scale_spin.setValue(scale_value) self.scale_spin.setDisabled(True) @pyqtSlot() def on_load_hsne(self): workdir = os.path.dirname(os.path.abspath(__file__)) result = QFileDialog.getOpenFileName( self, 'Open a pre-calculated hSNE analysis file .hsne', workdir, "hSNE files (*.hsne)") if result[0]: self.hsne_name = result[0] # print(f"Selected: {self.name}") self.__set_scale_from_hsne_file() @pyqtSlot() def on_load_labels(self): workdir = os.path.dirname(os.path.abspath(__file__)) result = QFileDialog.getOpenFileName( self, 'Open a numpy file where each row is an integer label', workdir, "Numpy files (*.npy)") if result[0]: self.label_name = result[0] self.label_label.setText(Path(self.label_name).name) @pyqtSlot() def on_load_labelscolors(self): workdir = os.path.dirname(os.path.abspath(__file__)) result = QFileDialog.getOpenFileName( self, 'Open a CSV file with header where the columns pairs of Label, #COLOR_', workdir, "CSV files (*.csv)") if result[0]: self.meta_name = result[0] self.meta_label.setText(Path(self.meta_name).name) @pyqtSlot() def on_start(self): self.load_callback(self.name, self.label_name, self.meta_name, self.hsne_name) @pyqtSlot() def on_selected(self): analysis_id = self._get_selected_id() if analysis_id: self.select_callback(int(analysis_id)) @pyqtSlot() def on_delete(self): analysis_id = self._get_selected_id() if analysis_id: self.delete_callback([int(analysis_id)]) @pyqtSlot() def on_clear(self): if not self.root_id is None: self.select_callback(int(self.root_id)) self.clear() self.name = None self.data_label.setText("<choose data .npy>") self.label_name = None self.label_label.setText("<optionally choose labels .npy>") self.meta_name = None self.meta_label.setText("<optionally choose label/color .csv>") self.scale_spin.setValue(4) self.scale_spin.setEnabled(True) self.start_button.setDisabled(True) self.hsne_name = None self.hsne_label.setText("<optionally choose existing .hsne>") self.image_x.setText("") self.image_y.setText("") def _get_selected_id(self): index = self.tree.currentIndex() if index is None: return None return self.model.itemData(index.siblingAtColumn(self.ID))[0] def create_analysis_model(self, parent): model = QStandardItemModel(0, 3, parent) model.setHeaderData(self.ANALYSIS, Qt.Horizontal, "Analysis") model.setHeaderData(self.ID, Qt.Horizontal, "Id") model.setHeaderData(self.NUMPOINTS, Qt.Horizontal, "#Points") return model def add_test_analysis(self): parent_id = ModelGui.NO_PARENT_ID if self.counter > 0: parent_id = self.counter - 1 self.add_analysis(self.counter, f"{self.counter} Blah blah blah", parent_id, 150) self.counter = self.counter + 1 def update_tree(self): # Update tree based on queued events while True: event = {} try: event = self.analysis_event_queue.get_nowait() except queue.Empty: break if event['event'] == AnalysisEvent.ADDED: self.add_analysis(event['id'], event['name'], event['parent_id'], event['number_of_points']) continue if event['event'] == AnalysisEvent.FINISHED: self.finish_analysis(event['id'], event['name'], event['image_buf']) continue if event['event'] == AnalysisEvent.REMOVED: self.remove_analysis(event['id']) def add_analysis(self, analysis_id, name, parent_id, numpoints): im = ImageQt.ImageQt(Image.new('RGB', self.thumb_size, (100, 0, 200))) item = QStandardItem(QIcon(QPixmap.fromImage(im)), name) # Need to persist the thumbnails otherwise the ImageQT will get garbage # collected along with the memory item.__thumb = im if parent_id == ModelGui.NO_PARENT_ID: #print("Adding root") self.clear() self.model.insertRow(0, [ item, QStandardItem(str(analysis_id)), QStandardItem(str(numpoints)) ]) self.root_id = analysis_id self.id_item[analysis_id] = item else: #print("Adding child") parent = self.find_analysis_item(parent_id) if parent is not None: parent.appendRow([ item, QStandardItem(str(analysis_id)), QStandardItem(str(numpoints)) ]) self.id_item[analysis_id] = item self.tree.expand(parent.index()) def remove_analysis(self, analysis_id): if analysis_id == self.root_id: self.clear() return item = self.find_analysis_item(analysis_id) if item: if item.parent: try: item.parent().removeRow(item.row()) except RuntimeError: # TODO fix bug that means this is being deleted twice pass del self.id_item[analysis_id] def finish_analysis(self, analysis_id, name, image_buf): print("finished ", analysis_id) img = PIL.Image.open(image_buf) thumbnail = img.resize(self.thumb_size, PIL.Image.ANTIALIAS) # thumbnail.show() im = ImageQt.ImageQt(thumbnail) item = self.find_analysis_item(analysis_id) item.setIcon(QIcon(QPixmap.fromImage(im))) item.__thumb = im def find_analysis_item(self, analysis_id): """ Get the item using the numeric analysis_id """ return self.id_item.get(analysis_id, None) def clear(self): print('Clear model content') if self.model is not None: # print('Remove rows') self.model.removeRows(0, self.model.rowCount()) # print('Reset bookkeeping') self.id_item = {} self.root_id = None
class TapeWidget(QWidget): def __init__(self, parent = None): super().__init__(parent) self._main_layout = QVBoxLayout(self) self._search_box = QLineEdit(self) self._button_layout = QHBoxLayout() self._view = QTreeView() self._view.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) self._view.setSelectionMode(QAbstractItemView.ExtendedSelection) self._view.setHeaderHidden(True) self._add_note_button = QPushButton(self) self._add_note_button.setText("New note") self._add_child_button = QPushButton(self) self._add_child_button.setText("New child") self._add_sibling_button = QPushButton(self) self._add_sibling_button.setText("New sibling") self._delete_note_button = QPushButton(self) self._delete_note_button.setText("Delete note") self._button_layout.addWidget(self._add_note_button) self._button_layout.addWidget(self._add_sibling_button) self._button_layout.addWidget(self._add_child_button) self._button_layout.addWidget(self._delete_note_button) self._button_layout.addStretch() self._main_layout.addWidget(self._search_box) self._main_layout.addLayout(self._button_layout) self._main_layout.addWidget(self._view) self._tape_filter_proxy_model = TapeFilterProxyModel() self._note_delegate = NoteDelegate() self._tape_model = QStandardItemModel() self.set_model(self._tape_model) self._view.setItemDelegate(self._note_delegate) self._view.setModel(self._tape_filter_proxy_model) self._add_note_button.clicked.connect(lambda checked: self.add_and_focus_note()) self._add_sibling_button.clicked.connect(self._new_sibling_handler) self._add_child_button.clicked.connect(self._new_child_handler) self._delete_note_button.clicked.connect(self.delete_selected_notes) self._search_box.textChanged.connect(self._tape_filter_proxy_model.setFilterFixedString) def model(self): """ Returns the model that contains all notes managed by the tape. The model should be treated as read-only. You can only modify it indirectly through the methods provided by TapeWidget. """ return self._tape_model def proxy_model(self): """ Returns the model that contains notes matching current filter. The model should be treated as read-only. You can only modify it indirectly through the methods provided by TapeWidget. """ return self._tape_filter_proxy_model def set_model(self, model): assert ( len(set([item_to_id(item) for item in all_items(model) if item_to_id(item) != None])) == len( [item_to_id(item) for item in all_items(model) if item_to_id(item) != None]) ) # NOTE: If there's an exception in setSourceModel(), we can hope that the source model # remains unchanged. That's why we assing to _tape_model only if that instruction succeeds. self._tape_filter_proxy_model.setSourceModel(model) self._tape_model = model def notes(self): return all_notes(self._tape_model) def assign_ids(self): assign_note_ids(self._tape_model) def create_empty_note(self): return Note( body = "", tags = [], created_at = datetime.utcnow() ) def add_note(self, note = None, parent_index = None): # NOTE: Remember to use indexes from _tape_model, not _tape_filter_proxy_model here assert parent_index == None or self._tape_model.itemFromIndex(parent_index) != None and parent_index.isValid() root_item = self._tape_model.invisibleRootItem() if parent_index == None: parent_item = root_item else: parent_item = self._tape_model.itemFromIndex(parent_index) if note != None: assert note not in self.notes() else: note = self.create_empty_note() item = QStandardItem() set_item_note(item, note) parent_item.appendRow(item) def add_and_focus_note(self, parent_proxy_index = None): if parent_proxy_index != None: parent_index = self._tape_filter_proxy_model.mapToSource(parent_proxy_index) else: parent_index = None self.add_note(parent_index = parent_index) if parent_proxy_index != None: self._view.expand(parent_proxy_index) parent_item = self._tape_model.itemFromIndex(parent_index) else: parent_item = self._tape_model.invisibleRootItem() # NOTE: It's likely that the new note does not match the filter and won't not be present # in the proxy model. We want to select it and focus on it so the filter must be cleared. # And it must be cleared before taking the index in the proxy because changing the filter # may change the set of notes present in the proxy and invalidate the index. self.set_filter('') new_note_index = parent_item.child(parent_item.rowCount() - 1).index() new_note_proxy_index = self._tape_filter_proxy_model.mapFromSource(new_note_index) self.clear_selection() self.set_note_selection(new_note_proxy_index, True) self._view.scrollTo(new_note_proxy_index) def remove_notes(self, indexes): remove_items(self._tape_model, indexes) def clear(self): self._tape_model.clear() def set_filter(self, text): # NOTE: This triggers textChanged() signal which applies the filter self._search_box.setText(text) def get_filter(self): return self._search_box.text() def selected_proxy_indexes(self): return self._view.selectedIndexes() def selected_indexes(self): return [self._tape_filter_proxy_model.mapToSource(proxy_index) for proxy_index in self.selected_proxy_indexes()] def set_note_selection(self, proxy_index, select): assert proxy_index != None and proxy_index.isValid() assert self._tape_model.itemFromIndex(self._tape_filter_proxy_model.mapToSource(proxy_index)) != None self._view.selectionModel().select( QItemSelection(proxy_index, proxy_index), QItemSelectionModel.Select if select else QItemSelectionModel.Deselect ) def clear_selection(self): self._view.selectionModel().clear() def delete_selected_notes(self): self.remove_notes(self.selected_indexes()) def _new_sibling_handler(self): selected_proxy_indexes = self._view.selectedIndexes() if len(selected_proxy_indexes) > 1: self.clear_selection() selected_proxy_indexes = [] if len(selected_proxy_indexes) == 0 or selected_proxy_indexes[0].parent() == QModelIndex(): self.add_and_focus_note() else: self.add_and_focus_note(selected_proxy_indexes[0].parent()) def add_child_to_selected_element(self): selected_proxy_indexes = self._view.selectedIndexes() if len(selected_proxy_indexes) != 1: return False else: self.add_and_focus_note(selected_proxy_indexes[0]) return True def _new_child_handler(self): added = self.add_child_to_selected_element() if not added: QMessageBox.warning(self, "Can't add note", "To be able to add a new child note select exactly one parent")