class EditorMainWindow(object): def __init__(self): self.seq = 0 self.widget = loadUi(MAIN_UI_PATH) self.init_mdi() self.init_actions() self.init_instance() self.widget.closeEvent = self.close_handler self.widget.showMaximized() def get_seq(self): self.seq += 1 return self.seq def init_mdi(self): self.mdi = QMdiArea(self.widget) self.mdi.setViewMode(QMdiArea.TabbedView) self.mdi.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.mdi.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.mdi.setTabsMovable(True) self.mdi.setTabsClosable(True) self.mdi.setTabShape(QTabWidget.Rounded) self.widget.setCentralWidget(self.mdi) def init_actions(self): self.widget.actionNew.triggered.connect(self.action_new_handler) self.widget.actionOpen.triggered.connect(self.action_open_handler) self.widget.actionSave.triggered.connect(self.action_save_handler) self.widget.actionSave_As.triggered.connect(self.action_save_as_handler) self.widget.actionClose.triggered.connect(self.action_close_handler) self.widget.actionClose_All.triggered.connect(self.action_close_all_handler) self.widget.actionExport.triggered.connect(self.action_export_handler) self.widget.actionStates.triggered.connect(self.action_states_handler) self.widget.actionEvents.triggered.connect(self.action_events_handler) def close_handler(self, ev): l = self.mdi.subWindowList() if len(l) != 0: self.mdi.closeAllSubWindows() ev.ignore() def init_instance(self): self.instances = [] def remove_instance(self, ins): self.instances.remove(ins) def find_non_existing_name(self): while True: tmp_path = os.path.join( config.src_path, "NewFsm" + str(self.get_seq()) + FSM_FILE_EXT) if not os.path.exists(tmp_path): return tmp_path def action_new_handler(self): tmp_path = self.find_non_existing_name() m = FsmModel() m.default_init() vm = InstanceVM(m, self, tmp_path) vm.set_modified(True) def file_already_open(self, pth): for i in self.instances: pth = os.path.abspath(pth) if pth == i.file_path: return i return None def action_open_handler(self): p = QFileDialog() p.setViewMode(QFileDialog.List) p.setFileMode(QFileDialog.ExistingFiles) p.setDirectory(config.src_path) p.exec() paths = p.selectedFiles() for pth in paths: i = self.file_already_open(pth) if i: self.mdi.setActiveSubWindow(i.sub_window) else: m = FsmModel.load_file(pth) vm = InstanceVM(m, self, pth) def action_save_handler(self): w = self.mdi.activeSubWindow() if w is None: return model = w.instance.model model.dump_file(w.instance.file_path) w.instance.set_modified(False) def action_save_as_handler(self): w = self.mdi.activeSubWindow() if w is None: return p = QFileDialog() p.setViewMode(QFileDialog.List) p.setDirectory(config.src_path) p.exec() paths = p.selectedFiles() if len(paths) == 0: return w.instance.file_path = os.path.abspath(paths[0]) w.instance.update_title() model = w.instance.model model.dump_file(w.instance.file_path) w.instance.set_modified(False) def action_close_handler(self): w = self.mdi.activeSubWindow() if w is None: return w.close() def action_close_all_handler(self): self.mdi.closeAllSubWindows() def action_export_handler(self): fsm_files = os.listdir(config.src_path) for fn in fsm_files: full_name = os.path.join(config.src_path, fn) b, e = os.path.splitext(full_name) if e == FSM_FILE_EXT: f = open(full_name, "rb") basename, e = os.path.splitext(fn) model_object = pickle.load(f) f.close() exporter = FsmModelPythonExporter(model_object) exporter.export(os.path.join(config.export_path, basename + "_fsm.py")) def action_states_handler(self): cur = self.get_current_instance() if cur is None: return model = cur.model w = loadUi(STATE_LIST_DIALOG_PATH) list_vm = MultiColumnListModel( model.state, LIST_DIALOG_COLUMNS, STATE_DIALOG_HEADERS) add_item = functools.partial(model.add_item, StateItem, "state") remove_item = functools.partial(model.remove_item, "state") dialog = StateListPanelVM( list_vm, model.state, add_item, remove_item, w) dialog.run() cur.table_vm.refresh() def action_events_handler(self): cur = self.get_current_instance() if cur is None: return model = cur.model w = loadUi(EVENT_DIALOG_PATH) list_vm = MultiColumnListModel( model.event, LIST_DIALOG_COLUMNS, EVENT_DIALOG_HEADERS) add_item = functools.partial(model.add_item, EventItem, "event") remove_item = functools.partial(model.remove_item, "event") dialog = ListEditPanelVM( list_vm, model.event, add_item, remove_item, w) dialog.run() cur.table_vm.refresh() def get_current_instance(self): current = self.mdi.activeSubWindow() if not current: return None for i in self.instances: if i.sub_window == current: return i assert(False) # there is an active sub window but there's no matching instance
class MainWindow(QMainWindow): """This create the main window of the application""" def __init__(self): super(MainWindow, self).__init__() # remove close & maximize window buttons #self.setWindowFlags(Qt.CustomizeWindowHint|Qt.WindowMinimizeButtonHint) self.setMinimumSize(500, 666) #self.setMaximumSize(1000,666) self.mdiArea = QMdiArea() self.mdiArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.mdiArea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setCentralWidget(self.mdiArea) self.mdiArea.subWindowActivated.connect(self.updateMenus) self.mdiArea.setViewMode(QMdiArea.TabbedView) self.windowMapper = QSignalMapper(self) self.windowMapper.mapped[QWidget].connect(self.setActiveSubWindow) self.child = None self.createActions() self.createMenus() self.createStatusBar() self.updateMenus() self.readSettings() self.setWindowTitle("LEKTURE") mytoolbar = QToolBar() #self.toolbar = self.addToolBar() mytoolbar.addAction(self.newAct) mytoolbar.addAction(self.openAct) mytoolbar.addAction(self.saveAct) mytoolbar.addAction(self.saveAsAct) mytoolbar.addSeparator() mytoolbar.addAction(self.outputsAct) mytoolbar.addAction(self.scenarioAct) self.scenarioAct.setVisible(False) mytoolbar.setMovable(False) mytoolbar.setFixedWidth(60) self.addToolBar(Qt.LeftToolBarArea, mytoolbar) def closeEvent(self, scenario): """method called when the main window wants to be closed""" self.mdiArea.closeAllSubWindows() if self.mdiArea.currentSubWindow(): scenario.ignore() else: self.writeSettings() scenario.accept() def newFile(self): """creates a new project""" child = self.createProjekt() child.newFile() child.show() self.child = child def open(self): """open a project""" fileName, _ = QFileDialog.getOpenFileName(self) if fileName: existing = self.findProjekt(fileName) if existing: self.mdiArea.setActiveSubWindow(existing) return child = self.createProjekt() if child.loadFile(fileName): self.statusBar().showMessage("File loaded", 2000) child.show() else: child.close() def save(self): """called when user save a project""" if self.activeProjekt() and self.activeProjekt().save(): self.statusBar().showMessage("File saved", 2000) else: self.statusBar().showMessage("Error when trying to save the file") def saveAs(self): """called when user save AS a project""" if self.activeProjekt() and self.activeProjekt().saveAs(): self.statusBar().showMessage("File saved", 2000) else: self.statusBar().showMessage("Error when trying to save the file") def openFolder(self): """called when user calls 'reveal in finder' function""" if self.activeProjekt() and self.activeProjekt().openFolder(): self.statusBar().showMessage("File revealed in Finder", 2000) def about(self): """called when user wants to know a bit more on the app""" import sys python_version = str(sys.version_info[0]) python_version_temp = sys.version_info[1:5] for item in python_version_temp: python_version = python_version + "." + str(item) QMessageBox.about(self, "About Lekture", "pylekture build " + str(pylekture.__version__ + "\n" + \ "python version " + str(python_version))) def updateMenus(self): """update menus""" hasProjekt = (self.activeProjekt() is not None) self.saveAct.setEnabled(hasProjekt) self.saveAsAct.setEnabled(hasProjekt) self.outputsAct.setEnabled(hasProjekt) self.scenarioAct.setEnabled(hasProjekt) self.openFolderAct.setEnabled(hasProjekt) self.closeAct.setEnabled(hasProjekt) self.closeAllAct.setEnabled(hasProjekt) self.nextAct.setEnabled(hasProjekt) self.previousAct.setEnabled(hasProjekt) self.separatorAct.setVisible(hasProjekt) def updateWindowMenu(self): """unpates menus on the window toolbar""" self.windowMenu.clear() self.windowMenu.addAction(self.closeAct) self.windowMenu.addAction(self.closeAllAct) self.windowMenu.addSeparator() self.windowMenu.addAction(self.nextAct) self.windowMenu.addAction(self.previousAct) self.windowMenu.addAction(self.separatorAct) windows = self.mdiArea.subWindowList() self.separatorAct.setVisible(len(windows) != 0) for i, window in enumerate(windows): child = window.widget() text = "%d %s" % (i + 1, child.userFriendlyCurrentFile()) if i < 9: text = '&' + text action = self.windowMenu.addAction(text) action.setCheckable(True) action.setChecked(child is self.activeProjekt()) action.triggered.connect(self.windowMapper.map) self.windowMapper.setMapping(action, window) def createProjekt(self): """create a new project""" child = Projekt() self.mdiArea.addSubWindow(child) self.child = child return child def createActions(self): """create all actions""" self.newAct = QAction("&New", self, shortcut=QKeySequence.New, statusTip="Create a new file", triggered=self.newFile) self.openAct = QAction("&Open...", self, shortcut=QKeySequence.Open, statusTip="Open an existing file", triggered=self.open) self.saveAct = QAction("&Save", self, shortcut=QKeySequence.Save, statusTip="Save the document to disk", triggered=self.save) self.saveAsAct = QAction("Save &As...", self, shortcut=QKeySequence.SaveAs, statusTip="Save the document under a new name", triggered=self.saveAs) self.openFolderAct = QAction("Open Project Folder", self, statusTip="Reveal Project in Finder", triggered=self.openFolder) self.exitAct = QAction("E&xit", self, shortcut=QKeySequence.Quit, statusTip="Exit the application", triggered=QApplication.instance().closeAllWindows) self.closeAct = QAction("Cl&ose", self, statusTip="Close the active window", triggered=self.mdiArea.closeActiveSubWindow) self.outputsAct = QAction("Outputs", self, statusTip="Open the outputs panel", triggered=self.openOutputsPanel) self.scenarioAct = QAction("Scenario", self, statusTip="Open the scenario panel", triggered=self.openScenarioPanel) self.closeAllAct = QAction("Close &All", self, statusTip="Close all the windows", triggered=self.mdiArea.closeAllSubWindows) self.nextAct = QAction("Ne&xt", self, shortcut=QKeySequence.NextChild, statusTip="Move the focus to the next window", triggered=self.mdiArea.activateNextSubWindow) self.previousAct = QAction("Pre&vious", self, shortcut=QKeySequence.PreviousChild, statusTip="Move the focus to the previous window", triggered=self.mdiArea.activatePreviousSubWindow) self.separatorAct = QAction(self) self.separatorAct.setSeparator(True) self.aboutAct = QAction("&About", self, statusTip="Show the application's About box", triggered=self.about) def createMenus(self): """create all menus""" self.fileMenu = self.menuBar().addMenu("&File") self.fileMenu.addAction(self.newAct) self.fileMenu.addAction(self.openAct) self.fileMenu.addAction(self.saveAct) self.fileMenu.addAction(self.saveAsAct) self.fileMenu.addSeparator() self.fileMenu.addAction(self.openFolderAct) self.fileMenu.addAction(self.exitAct) self.viewMenu = self.menuBar().addMenu("&View") self.viewMenu.addAction(self.outputsAct) self.viewMenu.addAction(self.scenarioAct) self.windowMenu = self.menuBar().addMenu("&Window") self.updateWindowMenu() self.windowMenu.aboutToShow.connect(self.updateWindowMenu) self.menuBar().addSeparator() self.helpMenu = self.menuBar().addMenu("&Help") self.helpMenu.addAction(self.aboutAct) def createStatusBar(self): """create the status bar""" self.statusBar().showMessage("Ready") def readSettings(self): """read the settings""" settings = QSettings('Pixel Stereo', 'lekture') pos = settings.value('pos', QPoint(200, 200)) size = settings.value('size', QSize(1000, 650)) self.move(pos) self.resize(size) def writeSettings(self): """write settings""" settings = QSettings('Pixel Stereo', 'lekture') settings.setValue('pos', self.pos()) settings.setValue('size', self.size()) def activeProjekt(self): """return the active project object""" activeSubWindow = self.mdiArea.activeSubWindow() if activeSubWindow: return activeSubWindow.widget() else: return None def findProjekt(self, fileName): """return the project""" canonicalFilePath = QFileInfo(fileName).canonicalFilePath() for window in self.mdiArea.subWindowList(): if window.widget().currentFile() == canonicalFilePath: return window return None def setActiveSubWindow(self, window): """set the active sub window""" if window: self.mdiArea.setActiveSubWindow(window) def openOutputsPanel(self): """switch to the outputs editor""" if self.child: project = self.activeProjekt() project.scenario_events_group.setVisible(False) project.outputs_group.setVisible(True) self.scenarioAct.setVisible(True) self.outputsAct.setVisible(False) def openScenarioPanel(self): """switch to the scenario editors""" if self.child: project = self.activeProjekt() project.outputs_group.setVisible(False) project.scenario_events_group.setVisible(True) self.scenarioAct.setVisible(False) self.outputsAct.setVisible(True)
class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self._version = __version__ self.setWindowIcon(QIcon(":/logo.png")) self.setWindowTitle("Tasmota Device Manager {}".format(self._version)) self.unknown = [] self.env = TasmotaEnvironment() self.device = None self.topics = [] self.mqtt_queue = [] self.fulltopic_queue = [] # ensure TDM directory exists in the user directory if not os.path.isdir("{}/TDM".format(QDir.homePath())): os.mkdir("{}/TDM".format(QDir.homePath())) self.settings = QSettings("{}/TDM/tdm.cfg".format(QDir.homePath()), QSettings.IniFormat) self.devices = QSettings("{}/TDM/devices.cfg".format(QDir.homePath()), QSettings.IniFormat) self.setMinimumSize(QSize(1000, 600)) # configure logging logging.basicConfig(filename="{}/TDM/tdm.log".format(QDir.homePath()), level=self.settings.value("loglevel", "INFO"), datefmt="%Y-%m-%d %H:%M:%S", format='%(asctime)s [%(levelname)s] %(message)s') logging.info("### TDM START ###") # load devices from the devices file, create TasmotaDevices and add the to the envvironment for mac in self.devices.childGroups(): self.devices.beginGroup(mac) device = TasmotaDevice(self.devices.value("topic"), self.devices.value("full_topic"), self.devices.value("device_name")) device.debug = self.devices.value("debug", False, bool) device.p['Mac'] = mac.replace("-", ":") device.env = self.env self.env.devices.append(device) # load device command history self.devices.beginGroup("history") for k in self.devices.childKeys(): device.history.append(self.devices.value(k)) self.devices.endGroup() self.devices.endGroup() self.device_model = TasmotaDevicesModel(self.env) self.setup_mqtt() self.setup_main_layout() self.add_devices_tab() self.build_mainmenu() # self.build_toolbars() self.setStatusBar(QStatusBar()) pbSubs = QPushButton("Show subscriptions") pbSubs.setFlat(True) pbSubs.clicked.connect(self.showSubs) self.statusBar().addPermanentWidget(pbSubs) self.queue_timer = QTimer() self.queue_timer.timeout.connect(self.mqtt_publish_queue) self.queue_timer.start(250) self.auto_timer = QTimer() self.auto_timer.timeout.connect(self.auto_telemetry) self.load_window_state() if self.settings.value("connect_on_startup", False, bool): self.actToggleConnect.trigger() self.tele_docks = {} self.consoles = [] def setup_main_layout(self): self.mdi = QMdiArea() self.mdi.setActivationOrder(QMdiArea.ActivationHistoryOrder) self.mdi.setTabsClosable(True) self.setCentralWidget(self.mdi) 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): self.devices_list = ListWidget(self) sub = self.mdi.addSubWindow(self.devices_list) sub.setWindowState(Qt.WindowMaximized) self.devices_list.deviceSelected.connect(self.selectDevice) self.devices_list.openConsole.connect(self.openConsole) self.devices_list.openRulesEditor.connect(self.openRulesEditor) self.devices_list.openTelemetry.connect(self.openTelemetry) self.devices_list.openWebUI.connect(self.openWebUI) def load_window_state(self): wndGeometry = self.settings.value('window_geometry') if wndGeometry: self.restoreGeometry(wndGeometry) def build_mainmenu(self): mMQTT = self.menuBar().addMenu("MQTT") self.actToggleConnect = QAction(QIcon(":/disconnect.png"), "Connect") self.actToggleConnect.setCheckable(True) self.actToggleConnect.toggled.connect(self.toggle_connect) mMQTT.addAction(self.actToggleConnect) mMQTT.addAction(QIcon(), "Broker", self.setup_broker) mMQTT.addAction(QIcon(), "Autodiscovery patterns", self.patterns) mMQTT.addSeparator() mMQTT.addAction(QIcon(), "Clear obsolete retained LWTs", self.clear_LWT) mMQTT.addSeparator() mMQTT.addAction(QIcon(), "Auto telemetry period", self.auto_telemetry_period) self.actToggleAutoUpdate = QAction(QIcon(":/auto_telemetry.png"), "Auto telemetry") self.actToggleAutoUpdate.setCheckable(True) self.actToggleAutoUpdate.toggled.connect(self.toggle_autoupdate) mMQTT.addAction(self.actToggleAutoUpdate) mSettings = self.menuBar().addMenu("Settings") mSettings.addAction(QIcon(), "BSSId aliases", self.bssid) mSettings.addSeparator() mSettings.addAction(QIcon(), "Preferences", self.prefs) # mExport = self.menuBar().addMenu("Export") # mExport.addAction(QIcon(), "OpenHAB", self.openhab) def build_toolbars(self): main_toolbar = Toolbar(orientation=Qt.Horizontal, iconsize=24, label_position=Qt.ToolButtonTextBesideIcon) main_toolbar.setObjectName("main_toolbar") def initial_query(self, device, queued=False): for c in initial_commands(): cmd, payload = c cmd = device.cmnd_topic(cmd) if queued: self.mqtt_queue.append([cmd, payload]) else: self.mqtt.publish(cmd, payload, 1) 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 == True: if self.mqtt.state == self.mqtt.Connected: for d in self.env.devices: self.mqtt.publish(d.cmnd_topic('STATUS'), payload=8) self.auto_timer.setInterval( self.settings.value("autotelemetry", 5000, int)) self.auto_timer.start() else: self.auto_timer.stop() 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 auto_telemetry(self): if self.mqtt.state == self.mqtt.Connected: for d in self.env.devices: self.mqtt.publish(d.cmnd_topic('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(":/connect.png")) self.actToggleConnect.setText("Disconnect") 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() def mqtt_subscribe(self): # clear old topics self.topics.clear() custom_patterns.clear() # load custom autodiscovery patterns self.settings.beginGroup("Patterns") for k in self.settings.childKeys(): custom_patterns.append(self.settings.value(k)) self.settings.endGroup() # expand fulltopic patterns to subscribable topics for pat in default_patterns: # tasmota default and SO19 self.topics += expand_fulltopic(pat) # check if custom patterns can be matched by default patterns for pat in custom_patterns: if pat.startswith("%prefix%") or pat.split('/')[1] == "%prefix%": continue # do nothing, default subcriptions will match this topic else: self.topics += expand_fulltopic(pat) for d in self.env.devices: # if device has a non-standard pattern, check if the pattern is found in the custom patterns if not d.is_default() and d.p['FullTopic'] not in custom_patterns: # if pattern is not found then add the device topics to subscription list. # if the pattern is found, it will be matched without implicit subscription self.topics += expand_fulltopic(d.p['FullTopic']) # passing a list of tuples as recommended by paho self.mqtt.subscribe([(topic, 0) for topic in self.topics]) @pyqtSlot(str, str) def mqtt_publish(self, t, p): self.mqtt.publish(t, p) 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(":/disconnect.png")) self.actToggleConnect.setText("Connect") 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): # try to find a device by matching known FullTopics against the MQTT topic of the message device = self.env.find_device(topic) if device: if topic.endswith("LWT"): if not msg: msg = "Offline" device.update_property("LWT", msg) if msg == 'Online': # known device came online, query initial state self.initial_query(device, True) else: # forward the message for processing device.parse_message(topic, msg) if device.debug: logging.debug("MQTT: %s %s", topic, msg) else: # unknown device, start autodiscovery process if topic.endswith("LWT"): self.env.lwts.append(topic) logging.info("DISCOVERY: LWT from an unknown device %s", topic) # STAGE 1 # load default and user-provided FullTopic patterns and for all the patterns, # try matching the LWT topic (it follows the device's FullTopic syntax for p in default_patterns + custom_patterns: match = re.fullmatch( p.replace("%topic%", "(?P<topic>.*?)").replace( "%prefix%", "(?P<prefix>.*?)") + ".*$", topic) if match: # assume that the matched topic is the one configured in device settings possible_topic = match.groupdict().get('topic') if possible_topic not in ('tele', 'stat'): # if the assumed topic is different from tele or stat, there is a chance that it's a valid topic # query the assumed device for its FullTopic. False positives won't reply. possible_topic_cmnd = p.replace( "%prefix%", "cmnd").replace( "%topic%", possible_topic) + "FullTopic" logging.debug( "DISCOVERY: Asking an unknown device for FullTopic at %s", possible_topic_cmnd) self.mqtt_queue.append([possible_topic_cmnd, ""]) elif topic.endswith("RESULT") or topic.endswith( "FULLTOPIC"): # reply from an unknown device # STAGE 2 full_topic = loads(msg).get('FullTopic') if full_topic: # the device replies with its FullTopic # here the Topic is extracted using the returned FullTopic, identifying the device parsed = parse_topic(full_topic, topic) if parsed: # got a match, we query the device's MAC address in case it's a known device that had its topic changed logging.debug( "DISCOVERY: topic %s is matched by fulltopic %s", topic, full_topic) d = self.env.find_device(topic=parsed['topic']) if d: d.update_property("FullTopic", full_topic) else: logging.info( "DISCOVERY: Discovered topic=%s with fulltopic=%s", parsed['topic'], full_topic) d = TasmotaDevice(parsed['topic'], full_topic) self.env.devices.append(d) self.device_model.addDevice(d) logging.debug( "DISCOVERY: Sending initial query to topic %s", parsed['topic']) self.initial_query(d, True) tele_topic = d.tele_topic("LWT") if tele_topic in self.env.lwts: self.env.lwts.remove(tele_topic) d.update_property("LWT", "Online") 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_() def patterns(self): PatternsDialog().exec_() # def openhab(self): # OpenHABDialog(self.env).exec_() def showSubs(self): QMessageBox.information(self, "Subscriptions", "\n".join(sorted(self.topics))) def clear_LWT(self): dlg = ClearLWTDialog(self.env) if dlg.exec_() == ClearLWTDialog.Accepted: for row in range(dlg.lw.count()): itm = dlg.lw.item(row) if itm.checkState() == Qt.Checked: topic = itm.text() self.mqtt.publish(topic, retain=True) self.env.lwts.remove(topic) logging.info("MQTT: Cleared %s", topic) def prefs(self): dlg = PrefsDialog() if dlg.exec_() == QDialog.Accepted: update_devices = False devices_short_version = self.settings.value( "devices_short_version", True, bool) if devices_short_version != dlg.cbDevShortVersion.isChecked(): update_devices = True self.settings.setValue("devices_short_version", dlg.cbDevShortVersion.isChecked()) update_consoles = False console_font_size = self.settings.value("console_font_size", 9) if console_font_size != dlg.sbConsFontSize.value(): update_consoles = True self.settings.setValue("console_font_size", dlg.sbConsFontSize.value()) console_word_wrap = self.settings.value("console_word_wrap", True, bool) if console_word_wrap != dlg.cbConsWW.isChecked(): update_consoles = True self.settings.setValue("console_word_wrap", dlg.cbConsWW.isChecked()) if update_consoles: for c in self.consoles: c.console.setWordWrapMode(dlg.cbConsWW.isChecked()) new_font = QFont(c.console.font()) new_font.setPointSize(dlg.sbConsFontSize.value()) c.console.setFont(new_font) self.settings.sync() def auto_telemetry_period(self): curr_val = self.settings.value("autotelemetry", 5000, int) period, ok = QInputDialog.getInt( self, "Set AutoTelemetry period", "Values under 5000ms may cause increased ESP LoadAvg", curr_val, 1000) if ok: self.settings.setValue("autotelemetry", period) self.settings.sync() @pyqtSlot(TasmotaDevice) def selectDevice(self, d): self.device = d @pyqtSlot() def openTelemetry(self): if self.device: tele_widget = TelemetryWidget(self.device) self.addDockWidget(Qt.RightDockWidgetArea, tele_widget) self.mqtt_publish(self.device.cmnd_topic('STATUS'), "8") @pyqtSlot() def openConsole(self): if self.device: console_widget = ConsoleWidget(self.device) self.mqtt.messageSignal.connect(console_widget.consoleAppend) console_widget.sendCommand.connect(self.mqtt.publish) self.addDockWidget(Qt.BottomDockWidgetArea, console_widget) console_widget.command.setFocus() self.consoles.append(console_widget) @pyqtSlot() def openRulesEditor(self): if self.device: rules = RulesWidget(self.device) self.mqtt.messageSignal.connect(rules.parseMessage) rules.sendCommand.connect(self.mqtt_publish) self.mdi.setViewMode(QMdiArea.TabbedView) self.mdi.addSubWindow(rules) rules.setWindowState(Qt.WindowMaximized) rules.destroyed.connect(self.updateMDI) self.mqtt_queue.append((self.device.cmnd_topic("ruletimer"), "")) self.mqtt_queue.append((self.device.cmnd_topic("rule1"), "")) self.mqtt_queue.append((self.device.cmnd_topic("Var"), "")) self.mqtt_queue.append((self.device.cmnd_topic("Mem"), "")) @pyqtSlot() def openWebUI(self): if self.device and self.device.p.get('IPAddress'): url = QUrl("http://{}".format(self.device.p['IPAddress'])) try: webui = QWebEngineView() webui.load(url) frm_webui = QFrame() frm_webui.setWindowTitle("WebUI [{}]".format(self.device.name)) frm_webui.setFrameShape(QFrame.StyledPanel) frm_webui.setLayout(VLayout(0)) frm_webui.layout().addWidget(webui) frm_webui.destroyed.connect(self.updateMDI) self.mdi.addSubWindow(frm_webui) self.mdi.setViewMode(QMdiArea.TabbedView) frm_webui.setWindowState(Qt.WindowMaximized) except NameError: QDesktopServices.openUrl( QUrl("http://{}".format(self.device.p['IPAddress']))) def updateMDI(self): if len(self.mdi.subWindowList()) == 1: self.mdi.setViewMode(QMdiArea.SubWindowView) self.devices_list.setWindowState(Qt.WindowMaximized) def closeEvent(self, e): self.settings.setValue("version", self._version) self.settings.setValue("window_geometry", self.saveGeometry()) self.settings.setValue("views_order", ";".join(self.devices_list.views.keys())) self.settings.beginGroup("Views") for view, items in self.devices_list.views.items(): self.settings.setValue(view, ";".join(items[1:])) self.settings.endGroup() self.settings.sync() for d in self.env.devices: mac = d.p.get('Mac') topic = d.p['Topic'] full_topic = d.p['FullTopic'] device_name = d.name if mac: self.devices.beginGroup(mac.replace(":", "-")) self.devices.setValue("topic", topic) self.devices.setValue("full_topic", full_topic) self.devices.setValue("device_name", device_name) for i, h in enumerate(d.history): self.devices.setValue("history/{}".format(i), h) self.devices.endGroup() self.devices.sync() e.accept()
class DemoMdi(QMainWindow): def __init__(self, parent=None): super(DemoMdi, self).__init__(parent) # 设置窗口标题 self.setWindowTitle( 'MDI with a dockWidget tree and a tab-view mdiArea') # 设置窗口大小 self.resize(800, 640) self.initUi() self.mytimer = QTimer(self) self.mytimer.start(1000) self.mytimer.timeout.connect(self.timerCallback) def initUi(self): self.initMenuBar() self.initToolBar() self.initDockTree() self.initStatusBar() self.mdiArea = QMdiArea(self) self.setCentralWidget(self.mdiArea) # set as tabbedView by default self.mdiArea.setViewMode(QMdiArea.TabbedView) self.mdiArea.setTabShape(QTabWidget.Triangular) self.mdiArea.setTabsClosable(True) self.mdiArea.setTabsMovable(True) # index of document self.newDocIndex = 1 def initDockTree(self): self.dockWind = QDockWidget(self) self.dockWind.setWindowTitle('QProfile Explorer') self.initTree() self.dockWind.setWidget(self.tree) self.dockWind.setFloating(False) # set floating = false self.addDockWidget(Qt.LeftDockWidgetArea, self.dockWind) # set the position at left side # remove all features of DockWidget like Closable, Moveable, Floatable, VerticalTitle etc. self.dockWind.setFeatures(QDockWidget.NoDockWidgetFeatures) def initTree(self): self.tree = QTreeWidget() self.tree.setColumnCount(1) #设置列数 #self.tree.setHeaderLabels(['QProfiler items']) #设置树形控件头部的标题 self.tree.setIndentation(20) # 项目的缩进 self.tree.setHeaderHidden(True) #设置根节点 Perfmon = myQTreeWidgetItem(sin(pi * perfmon_x)) Perfmon.setText(0, 'Perfmon') perfmon_00 = myQTreeWidgetItem(sin(2 * pi * perfmon_x)) perfmon_00.setText(0, 'perfmon_00') Perfmon.addChild(perfmon_00) perfmon_01 = QTreeWidgetItem() perfmon_01.setText(0, 'perfmon_01') Perfmon.addChild(perfmon_01) perfmon_02 = QTreeWidgetItem() perfmon_02.setText(0, 'perfmon_02') Perfmon.addChild(perfmon_02) perfmon_03 = QTreeWidgetItem() perfmon_03.setText(0, 'perfmon_03') Perfmon.addChild(perfmon_03) self.tree.addTopLevelItem(Perfmon) # CPU cpuLoad = QTreeWidgetItem() cpuLoad.setText(0, 'CPU') cpuLoad_1 = QTreeWidgetItem() cpuLoad_1.setText(0, 'core 1') cpuLoad.addChild(cpuLoad_1) cpuLoad_2 = QTreeWidgetItem() cpuLoad_2.setText(0, 'core 2') cpuLoad.addChild(cpuLoad_2) self.tree.addTopLevelItem(cpuLoad) # treeItem signal self.tree.itemClicked[QTreeWidgetItem, int].connect(self.treeItemClicked) def treeItemWindow_open(self, item): title = item.text(0) subWind = QMdiSubWindow(self) subWind.setAttribute(Qt.WA_DeleteOnClose) subWind.setWindowTitle(title) self.newDocIndex += 1 mainWid = QWidget() l = QtWidgets.QVBoxLayout(mainWid) txtWind = QPlainTextEdit(mainWid) txtWind.setPlainText(f"perfmon.x = {item.x}, \n y = {item.y}") figWind = MyCanvas(mainWid, width=5, height=4, dpi=100, treeWidgetItem=item) l.addWidget(figWind) l.addWidget(txtWind) l.setStretch(0, 3) # 设置第一列的伸展比例为 3 l.setStretch(1, 1) # 设置第二列的伸展比例为 1, 这样2列的伸展比为3:1 subWind.setWidget(mainWid) self.mdiArea.addSubWindow(subWind) subWind.show() def treeItemClicked(self, item, column): tab = self.get_treeItem_tab(item.text(column)) if tab is not None: tab.setFocus() else: if item.text(column) == 'Perfmon': self.treeItemWindow_open(item) else: newDoc = QMdiSubWindow(self) newDoc.setAttribute(Qt.WA_DeleteOnClose) newDoc.setWindowTitle(item.text(column)) self.newDocIndex += 1 newDoc.setWidget(QPlainTextEdit( item.text(column) * 10, newDoc)) self.mdiArea.addSubWindow(newDoc) newDoc.show() def get_treeItem_tab(self, title): for wind in self.mdiArea.subWindowList(): if title == wind.windowTitle(): return wind return None def initStatusBar(self): self.statusBar = self.statusBar() self.statusBar.showMessage('Ready to start ...', 0) def initMenuBar(self): menuBar = self.menuBar() style = QApplication.style() #==== 文件 ====# fileMenu = menuBar.addMenu('文件') #新建一个文档 aFileNew = QAction('新建文档', self) aFileNew.setIcon(style.standardIcon(QStyle.SP_FileIcon)) aFileNew.triggered.connect(self.onFileNew) fileMenu.addAction(aFileNew) #打开一个文档 aFileOpen = QAction('打开文档', self) aFileOpen.setIcon(style.standardIcon(QStyle.SP_DialogOpenButton)) aFileOpen.triggered.connect(self.onFileOpen) fileMenu.addAction(aFileOpen) #关闭一个文档 aFileCloseAll = QAction('关闭全部', self) aFileCloseAll.setIcon(style.standardIcon(QStyle.SP_DialogCloseButton)) aFileOpen.triggered.connect(self.onFileCloseAll) fileMenu.addAction(aFileCloseAll) #添加分割线 fileMenu.addSeparator() #退出 aFileExit = QAction('退出', self) aFileExit.triggered.connect(self.close) fileMenu.addAction(aFileExit) #==== 编辑 ====# editMenu = menuBar.addMenu('编辑') #剪切 aEditCut = QAction('剪切', self) aEditCut.setIcon(QIcon(':/ico/cut.png')) aEditCut.triggered.connect(self.onEditCut) editMenu.addAction(aEditCut) #复制 aEditCopy = QAction('复制', self) aEditCopy.setIcon(QIcon(':/ico/copy.png')) aEditCopy.triggered.connect(self.onEditCopy) editMenu.addAction(aEditCopy) #粘贴 aEditPaste = QAction('粘贴', self) aEditPaste.setIcon(QIcon(':/ico/paste.png')) aEditPaste.triggered.connect(self.onEditPaste) editMenu.addAction(aEditPaste) #==== 窗口排列方式 ====# windowMenu = menuBar.addMenu('窗口') #子窗口模式 aWndSubView = QAction('子窗口模式', self) aWndSubView.triggered.connect(lambda: self.onWinowdMode(0)) windowMenu.addAction(aWndSubView) #标签页模式 aWndTab = QAction('标签页模式', self) aWndTab.triggered.connect(lambda: self.onWinowdMode(1)) windowMenu.addAction(aWndTab) windowMenu.addSeparator() #平铺模式 aWndTile = QAction('平铺模式', self) aWndTile.triggered.connect(lambda: self.onWinowdMode(2)) windowMenu.addAction(aWndTile) #窗口级联模式 aWndCascade = QAction('窗口级联模式', self) aWndCascade.triggered.connect(lambda: self.onWinowdMode(3)) windowMenu.addAction(aWndCascade) def initToolBar(self): toolBar = self.addToolBar('ToolBar') style = QApplication.style() min_width = 64 btnFileNew = QToolButton(self) btnFileNew.setText('新建文档') btnFileNew.setMinimumWidth(min_width) btnFileNew.setIcon(style.standardIcon(QStyle.SP_FileIcon)) btnFileNew.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) btnFileNew.clicked.connect(self.onFileNew) toolBar.addWidget(btnFileNew) btnFileOpen = QToolButton(self) btnFileOpen.setText('打开文档') btnFileOpen.setMinimumWidth(min_width) btnFileOpen.setIcon(style.standardIcon(QStyle.SP_DialogOpenButton)) btnFileOpen.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) btnFileOpen.clicked.connect(self.onFileOpen) toolBar.addWidget(btnFileOpen) btnFileCloseAll = QToolButton(self) btnFileCloseAll.setText('关闭全部') btnFileCloseAll.setMinimumWidth(min_width) btnFileCloseAll.setIcon(style.standardIcon( QStyle.SP_DialogCloseButton)) btnFileCloseAll.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) btnFileCloseAll.clicked.connect(self.onFileCloseAll) toolBar.addWidget(btnFileCloseAll) toolBar.addSeparator() btnEditCut = QToolButton(self) btnEditCut.setText('剪切') btnEditCut.setMinimumWidth(64) btnEditCut.setIcon(QIcon(':/ico/cut.png')) btnEditCut.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) btnEditCut.clicked.connect(self.onEditCut) toolBar.addWidget(btnEditCut) btnEditCopy = QToolButton(self) btnEditCopy.setText('复制') btnEditCopy.setMinimumWidth(64) btnEditCopy.setIcon(QIcon(':/ico/copy.png')) btnEditCopy.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) btnEditCopy.clicked.connect(self.onEditCopy) toolBar.addWidget(btnEditCopy) btnEditPaste = QToolButton(self) btnEditPaste.setText('粘贴') btnEditPaste.setMinimumWidth(64) btnEditPaste.setIcon(QIcon(':/ico/paste.png')) btnEditPaste.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) btnEditPaste.clicked.connect(self.onEditPaste) toolBar.addWidget(btnEditPaste) def msgCritical(self, strInfo): dlg = QMessageBox(self) dlg.setIcon(QMessageBox.Critical) dlg.setText(strInfo) dlg.show() def onFileNew(self): newDoc = QMdiSubWindow(self) newDoc.setAttribute(Qt.WA_DeleteOnClose) newDoc.setWindowTitle('新文档 ' + str(self.newDocIndex)) self.newDocIndex += 1 newDoc.setWidget(QPlainTextEdit(newDoc)) self.mdiArea.addSubWindow(newDoc) newDoc.show() def onFileOpen(self): path, _ = QFileDialog.getOpenFileName(self, '打开文件', '', '文本文件 (*.txt, *.prf)') if path: try: with open(path, 'rU') as f: text = f.read() except Exception as e: self.msgCritical(str(e)) else: openDoc = QMdiSubWindow(self) openDoc.setWindowTitle(path) txtEdit = QPlainTextEdit(openDoc) txtEdit.setPlainText(text) openDoc.setWidget(txtEdit) self.mdiArea.addSubWindow(openDoc) openDoc.show() def onFileCloseAll(self): self.mdiArea.closeAllSubWindows() def onEditCut(self): txtEdit = self.mdiArea.activeSubWindow().widget() txtEdit.cut() def onEditCopy(self): txtEdit = self.mdiArea.activeSubWindow().widget() txtEdit.copy() def onEditPaste(self): txtEdit = self.mdiArea.activeSubWindow().widget() txtEdit.paste() def onWinowdMode(self, index): if index == 3: self.mdiArea.cascadeSubWindows() elif index == 2: self.mdiArea.tileSubWindows() elif index == 1: self.mdiArea.setViewMode(QMdiArea.TabbedView) else: self.mdiArea.setViewMode(QMdiArea.SubWindowView) def timerCallback(self): self.statusBar.showMessage( f'Document Index = {self.newDocIndex}, subWind num ={len(self.mdiArea.subWindowList())}', 0)
class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self._version = "0.1.11" self.setWindowIcon(QIcon("GUI/icons/logo.png")) self.setWindowTitle("Tasmota Device Manager {}".format(self._version)) self.main_splitter = QSplitter() self.devices_splitter = QSplitter(Qt.Vertical) self.fulltopic_queue = [] self.settings = QSettings() self.setMinimumSize(QSize(1280,800)) self.device_model = TasmotaDevicesModel() self.telemetry_model = TasmotaDevicesTree() self.console_model = ConsoleModel() self.sorted_console_model = QSortFilterProxyModel() self.sorted_console_model.setSourceModel(self.console_model) self.sorted_console_model.setFilterKeyColumn(CnsMdl.FRIENDLY_NAME) self.setup_mqtt() self.setup_telemetry_view() self.setup_main_layout() self.add_devices_tab() self.build_toolbars() self.setStatusBar(QStatusBar()) self.queue_timer = QTimer() self.queue_timer.setSingleShot(True) self.queue_timer.timeout.connect(self.mqtt_ask_for_fulltopic) self.build_cons_ctx_menu() self.load_window_state() def setup_main_layout(self): self.mdi = QMdiArea() self.mdi.setActivationOrder(QMdiArea.ActivationHistoryOrder) self.mdi.setViewMode(QMdiArea.TabbedView) self.mdi.setDocumentMode(True) mdi_widget = QWidget() mdi_widget.setLayout(VLayout()) mdi_widget.layout().addWidget(self.mdi) self.devices_splitter.addWidget(mdi_widget) vl_console = VLayout() self.console_view = TableView() self.console_view.setModel(self.sorted_console_model) self.console_view.setupColumns(columns_console) self.console_view.setAlternatingRowColors(True) self.console_view.setSortingEnabled(True) self.console_view.sortByColumn(CnsMdl.TIMESTAMP, Qt.DescendingOrder) self.console_view.verticalHeader().setDefaultSectionSize(20) self.console_view.setMinimumHeight(200) self.console_view.setContextMenuPolicy(Qt.CustomContextMenu) vl_console.addWidget(self.console_view) console_widget = QWidget() console_widget.setLayout(vl_console) self.devices_splitter.addWidget(console_widget) self.main_splitter.insertWidget(0, self.devices_splitter) self.setCentralWidget(self.main_splitter) self.console_view.clicked.connect(self.select_cons_entry) self.console_view.doubleClicked.connect(self.view_payload) self.console_view.customContextMenuRequested.connect(self.show_cons_ctx_menu) def setup_telemetry_view(self): tele_widget = QWidget() vl_tele = VLayout() self.tview = QTreeView() self.tview.setMinimumWidth(300) self.tview.setModel(self.telemetry_model) self.tview.setAlternatingRowColors(True) self.tview.setUniformRowHeights(True) self.tview.setIndentation(15) self.tview.setSizePolicy(QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)) self.tview.expandAll() self.tview.resizeColumnToContents(0) vl_tele.addWidget(self.tview) tele_widget.setLayout(vl_tele) self.main_splitter.addWidget(tele_widget) def setup_mqtt(self): self.mqtt = MqttClient() self.mqtt.connecting.connect(self.mqtt_connecting) self.mqtt.connected.connect(self.mqtt_connected) self.mqtt.disconnected.connect(self.mqtt_disconnected) self.mqtt.connectError.connect(self.mqtt_connectError) self.mqtt.messageSignal.connect(self.mqtt_message) def add_devices_tab(self): tabDevicesList = DevicesListWidget(self) self.mdi.addSubWindow(tabDevicesList) tabDevicesList.setWindowState(Qt.WindowMaximized) def load_window_state(self): wndGeometry = self.settings.value('window_geometry') if wndGeometry: self.restoreGeometry(wndGeometry) spltState = self.settings.value('splitter_state') if spltState: self.main_splitter.restoreState(spltState) def build_toolbars(self): main_toolbar = Toolbar(orientation=Qt.Horizontal, iconsize=32, label_position=Qt.ToolButtonIconOnly) main_toolbar.setObjectName("main_toolbar") self.addToolBar(main_toolbar) main_toolbar.addAction(QIcon("./GUI/icons/connections.png"), "Configure MQTT broker", self.setup_broker) agBroker = QActionGroup(self) agBroker.setExclusive(True) self.actConnect = CheckableAction(QIcon("./GUI/icons/connect.png"), "Connect to the broker", agBroker) self.actDisconnect = CheckableAction(QIcon("./GUI/icons/disconnect.png"), "Disconnect from broker", agBroker) self.actDisconnect.setChecked(True) self.actConnect.triggered.connect(self.mqtt_connect) self.actDisconnect.triggered.connect(self.mqtt_disconnect) main_toolbar.addActions(agBroker.actions()) main_toolbar.addSeparator() def initial_query(self, idx): for q in initial_queries: topic = "{}status".format(self.device_model.commandTopic(idx)) self.mqtt.publish(topic, q) q = q if q else '' self.console_log(topic, "Asked for STATUS {}".format(q), q) def setup_broker(self): brokers_dlg = BrokerDialog() if brokers_dlg.exec_() == QDialog.Accepted and self.mqtt.state == self.mqtt.Connected: self.mqtt.disconnect() def mqtt_connect(self): self.broker_hostname = self.settings.value('hostname', 'localhost') self.broker_port = self.settings.value('port', 1883, int) self.broker_username = self.settings.value('username') self.broker_password = self.settings.value('password') self.mqtt.hostname = self.broker_hostname self.mqtt.port = self.broker_port if self.broker_username: self.mqtt.setAuth(self.broker_username, self.broker_password) if self.mqtt.state == self.mqtt.Disconnected: self.mqtt.connectToHost() def mqtt_disconnect(self): self.mqtt.disconnectFromHost() def mqtt_connecting(self): self.statusBar().showMessage("Connecting to broker") def mqtt_connected(self): self.statusBar().showMessage("Connected to {}:{} as {}".format(self.broker_hostname, self.broker_port, self.broker_username if self.broker_username else '[anonymous]')) self.mqtt_subscribe() for d in range(self.device_model.rowCount()): idx = self.device_model.index(d, 0) self.initial_query(idx) def mqtt_subscribe(self): main_topics = ["+/stat/+", "+/tele/+", "stat/#", "tele/#"] for d in range(self.device_model.rowCount()): idx = self.device_model.index(d, 0) if not self.device_model.isDefaultTemplate(idx): main_topics.append(self.device_model.commandTopic(idx)) main_topics.append(self.device_model.statTopic(idx)) for t in main_topics: self.mqtt.subscribe(t) def mqtt_ask_for_fulltopic(self): for i in range(len(self.fulltopic_queue)): self.mqtt.publish(self.fulltopic_queue.pop(0)) def mqtt_disconnected(self): self.statusBar().showMessage("Disconnected") def mqtt_connectError(self, rc): reason = { 1: "Incorrect protocol version", 2: "Invalid client identifier", 3: "Server unavailable", 4: "Bad username or password", 5: "Not authorized", } self.statusBar().showMessage("Connection error: {}".format(reason[rc])) self.actDisconnect.setChecked(True) def mqtt_message(self, topic, msg): found = self.device_model.findDevice(topic) if found.reply == 'LWT': if not msg: msg = "offline" if found.index.isValid(): self.console_log(topic, "LWT update: {}".format(msg), msg) self.device_model.updateValue(found.index, DevMdl.LWT, msg) elif msg == "Online": self.console_log(topic, "LWT for unknown device '{}'. Asking for FullTopic.".format(found.topic), msg, False) self.fulltopic_queue.append("cmnd/{}/fulltopic".format(found.topic)) self.fulltopic_queue.append("{}/cmnd/fulltopic".format(found.topic)) self.queue_timer.start(1500) elif found.reply == 'RESULT': full_topic = loads(msg).get('FullTopic') new_topic = loads(msg).get('Topic') template_name = loads(msg).get('NAME') if full_topic: # TODO: update FullTopic for existing device AFTER the FullTopic changes externally (the message will arrive from new FullTopic) if not found.index.isValid(): self.console_log(topic, "FullTopic for {}".format(found.topic), msg, False) new_idx = self.device_model.addDevice(found.topic, full_topic, lwt='online') tele_idx = self.telemetry_model.addDevice(TasmotaDevice, found.topic) self.telemetry_model.devices[found.topic] = tele_idx #TODO: add QSortFilterProxyModel to telemetry treeview and sort devices after adding self.initial_query(new_idx) self.console_log(topic, "Added {} with fulltopic {}, querying for STATE".format(found.topic, full_topic), msg) self.tview.expand(tele_idx) self.tview.resizeColumnToContents(0) if new_topic: if found.index.isValid() and found.topic != new_topic: self.console_log(topic, "New topic for {}".format(found.topic), msg) self.device_model.updateValue(found.index, DevMdl.TOPIC, new_topic) tele_idx = self.telemetry_model.devices.get(found.topic) if tele_idx: self.telemetry_model.setDeviceName(tele_idx, new_topic) self.telemetry_model.devices[new_topic] = self.telemetry_model.devices.pop(found.topic) if template_name: self.device_model.updateValue(found.index, DevMdl.MODULE, template_name) elif found.index.isValid(): if found.reply == 'STATUS': self.console_log(topic, "Received device status", msg) payload = loads(msg)['Status'] self.device_model.updateValue(found.index, DevMdl.FRIENDLY_NAME, payload['FriendlyName'][0]) self.telemetry_model.setDeviceFriendlyName(self.telemetry_model.devices[found.topic], payload['FriendlyName'][0]) self.tview.resizeColumnToContents(0) module = payload['Module'] if module == '0': self.mqtt.publish(self.device_model.commandTopic(found.index)+"template") else: self.device_model.updateValue(found.index, DevMdl.MODULE, module) elif found.reply == 'STATUS1': self.console_log(topic, "Received program information", msg) payload = loads(msg)['StatusPRM'] self.device_model.updateValue(found.index, DevMdl.RESTART_REASON, payload['RestartReason']) elif found.reply == 'STATUS2': self.console_log(topic, "Received firmware information", msg) payload = loads(msg)['StatusFWR'] self.device_model.updateValue(found.index, DevMdl.FIRMWARE, payload['Version']) self.device_model.updateValue(found.index, DevMdl.CORE, payload['Core']) elif found.reply == 'STATUS3': self.console_log(topic, "Received syslog information", msg) payload = loads(msg)['StatusLOG'] self.device_model.updateValue(found.index, DevMdl.TELEPERIOD, payload['TelePeriod']) elif found.reply == 'STATUS5': self.console_log(topic, "Received network status", msg) payload = loads(msg)['StatusNET'] self.device_model.updateValue(found.index, DevMdl.MAC, payload['Mac']) self.device_model.updateValue(found.index, DevMdl.IP, payload['IPAddress']) elif found.reply == 'STATUS8': self.console_log(topic, "Received telemetry", msg) payload = loads(msg)['StatusSNS'] self.parse_telemetry(found.index, payload) elif found.reply == 'STATUS11': self.console_log(topic, "Received device state", msg) payload = loads(msg)['StatusSTS'] self.parse_state(found.index, payload) elif found.reply == 'SENSOR': self.console_log(topic, "Received telemetry", msg) payload = loads(msg) self.parse_telemetry(found.index, payload) elif found.reply == 'STATE': self.console_log(topic, "Received device state", msg) payload = loads(msg) self.parse_state(found.index, payload) elif found.reply.startswith('POWER'): self.console_log(topic, "Received {} state".format(found.reply), msg) payload = {found.reply: msg} self.parse_power(found.index, payload) def parse_power(self, index, payload): old = self.device_model.power(index) power = {k: payload[k] for k in payload.keys() if k.startswith("POWER")} needs_update = False if old: for k in old.keys(): needs_update |= old[k] != power.get(k, old[k]) if needs_update: break else: needs_update = True if needs_update: self.device_model.updateValue(index, DevMdl.POWER, power) def parse_state(self, index, payload): bssid = payload['Wifi'].get('BSSId') if not bssid: bssid = payload['Wifi'].get('APMac') self.device_model.updateValue(index, DevMdl.BSSID, bssid) self.device_model.updateValue(index, DevMdl.SSID, payload['Wifi']['SSId']) self.device_model.updateValue(index, DevMdl.CHANNEL, payload['Wifi'].get('Channel')) self.device_model.updateValue(index, DevMdl.RSSI, payload['Wifi']['RSSI']) self.device_model.updateValue(index, DevMdl.UPTIME, payload['Uptime']) self.device_model.updateValue(index, DevMdl.LOADAVG, payload.get('LoadAvg')) self.parse_power(index, payload) tele_idx = self.telemetry_model.devices.get(self.device_model.topic(index)) if tele_idx: tele_device = self.telemetry_model.getNode(tele_idx) self.telemetry_model.setDeviceFriendlyName(tele_idx, self.device_model.friendly_name(index)) pr = tele_device.provides() for k in pr.keys(): self.telemetry_model.setData(pr[k], payload.get(k)) def parse_telemetry(self, index, payload): device = self.telemetry_model.devices.get(self.device_model.topic(index)) if device: node = self.telemetry_model.getNode(device) time = node.provides()['Time'] if 'Time' in payload: self.telemetry_model.setData(time, payload.pop('Time')) temp_unit = "C" pres_unit = "hPa" if 'TempUnit' in payload: temp_unit = payload.pop('TempUnit') if 'PressureUnit' in payload: pres_unit = payload.pop('PressureUnit') for sensor in sorted(payload.keys()): if sensor == 'DS18x20': for sns_name in payload[sensor].keys(): d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice(DS18x20, payload[sensor][sns_name]['Type'], device) self.telemetry_model.getNode(d).setTempUnit(temp_unit) payload[sensor][sns_name]['Id'] = payload[sensor][sns_name].pop('Address') pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor][sns_name].get(pk)) self.tview.expand(d) elif sensor.startswith('DS18B20'): d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice(DS18x20, sensor, device) self.telemetry_model.getNode(d).setTempUnit(temp_unit) pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) if sensor == 'COUNTER': d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice(CounterSns, "Counter", device) pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) else: d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice(sensor_map.get(sensor, Node), sensor, device) pr = self.telemetry_model.getNode(d).provides() if 'Temperature' in pr: self.telemetry_model.getNode(d).setTempUnit(temp_unit) if 'Pressure' in pr or 'SeaPressure' in pr: self.telemetry_model.getNode(d).setPresUnit(pres_unit) for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) self.tview.resizeColumnToContents(0) def console_log(self, topic, description, payload, known=True): device = self.device_model.findDevice(topic) fname = self.device_model.friendly_name(device.index) self.console_model.addEntry(topic, fname, description, payload, known) self.console_view.resizeColumnToContents(1) def view_payload(self, idx): idx = self.sorted_console_model.mapToSource(idx) row = idx.row() timestamp = self.console_model.data(self.console_model.index(row, CnsMdl.TIMESTAMP)) topic = self.console_model.data(self.console_model.index(row, CnsMdl.TOPIC)) payload = self.console_model.data(self.console_model.index(row, CnsMdl.PAYLOAD)) dlg = PayloadViewDialog(timestamp, topic, payload) dlg.exec_() def select_cons_entry(self, idx): self.cons_idx = idx def build_cons_ctx_menu(self): self.cons_ctx_menu = QMenu() self.cons_ctx_menu.addAction("View payload", lambda: self.view_payload(self.cons_idx)) self.cons_ctx_menu.addSeparator() self.cons_ctx_menu.addAction("Show only this device", lambda: self.cons_set_filter(self.cons_idx)) self.cons_ctx_menu.addAction("Show all devices", self.cons_set_filter) def show_cons_ctx_menu(self, at): self.select_cons_entry(self.console_view.indexAt(at)) self.cons_ctx_menu.popup(self.console_view.viewport().mapToGlobal(at)) def cons_set_filter(self, idx=None): if idx: idx = self.sorted_console_model.mapToSource(idx) topic = self.console_model.data(self.console_model.index(idx.row(), CnsMdl.FRIENDLY_NAME)) self.sorted_console_model.setFilterFixedString(topic) else: self.sorted_console_model.setFilterFixedString("") def closeEvent(self, e): self.settings.setValue("window_geometry", self.saveGeometry()) self.settings.setValue("splitter_state", self.main_splitter.saveState()) self.settings.sync() e.accept()
class DemoMdi(QMainWindow): def __init__(self, parent=None): super(DemoMdi, self).__init__(parent) # 设置窗口标题 self.setWindowTitle('实战PyQt5: MDI多文档接口程序 演示') # 设置窗口大小 self.resize(480, 360) self.initUi() def initUi(self): self.initMenuBar() self.initToolBar() self.mdiArea = QMdiArea(self) self.setCentralWidget(self.mdiArea) self.newDocIndex = 1 def initMenuBar(self): menuBar = self.menuBar() style = QApplication.style() #==== 文件 ====# fileMenu = menuBar.addMenu('文件') #新建一个文档 aFileNew = QAction('新建文档', self) aFileNew.setIcon(style.standardIcon(QStyle.SP_FileIcon)) aFileNew.triggered.connect(self.onFileNew) fileMenu.addAction(aFileNew) #打开一个文档 aFileOpen = QAction('打开文档', self) aFileOpen.setIcon(style.standardIcon(QStyle.SP_DialogOpenButton)) aFileOpen.triggered.connect(self.onFileOpen) fileMenu.addAction(aFileOpen) #关闭一个文档 aFileCloseAll = QAction('关闭全部', self) aFileCloseAll.setIcon(style.standardIcon(QStyle.SP_DialogCloseButton)) aFileOpen.triggered.connect(self.onFileCloseAll) fileMenu.addAction(aFileCloseAll) #添加分割线 fileMenu.addSeparator() #退出 aFileExit = QAction('退出', self) aFileExit.triggered.connect(self.close) fileMenu.addAction(aFileExit) #==== 编辑 ====# editMenu = menuBar.addMenu('编辑') #剪切 aEditCut = QAction('剪切', self) aEditCut.setIcon(QIcon(':/ico/cut.png')) aEditCut.triggered.connect(self.onEditCut) editMenu.addAction(aEditCut) #复制 aEditCopy = QAction('复制', self) aEditCopy.setIcon(QIcon(':/ico/copy.png')) aEditCopy.triggered.connect(self.onEditCopy) editMenu.addAction(aEditCopy) #粘贴 aEditPaste = QAction('粘贴', self) aEditPaste.setIcon(QIcon(':/ico/paste.png')) aEditPaste.triggered.connect(self.onEditPaste) editMenu.addAction(aEditPaste) #==== 窗口排列方式 ====# windowMenu = menuBar.addMenu('窗口') #子窗口模式 aWndSubView = QAction('子窗口模式', self) aWndSubView.triggered.connect(lambda: self.onWinowdMode(0)) windowMenu.addAction(aWndSubView) #标签页模式 aWndTab = QAction('标签页模式', self) aWndTab.triggered.connect(lambda: self.onWinowdMode(1)) windowMenu.addAction(aWndTab) windowMenu.addSeparator() #平铺模式 aWndTile = QAction('平铺模式', self) aWndTile.triggered.connect(lambda: self.onWinowdMode(2)) windowMenu.addAction(aWndTile) #窗口级联模式 aWndCascade = QAction('窗口级联模式', self) aWndCascade.triggered.connect(lambda: self.onWinowdMode(3)) windowMenu.addAction(aWndCascade) def initToolBar(self): toolBar = self.addToolBar('') style = QApplication.style() min_width = 64 btnFileNew = QToolButton(self) btnFileNew.setText('新建文档') btnFileNew.setMinimumWidth(min_width) btnFileNew.setIcon(style.standardIcon(QStyle.SP_FileIcon)) btnFileNew.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) btnFileNew.clicked.connect(self.onFileNew) toolBar.addWidget(btnFileNew) btnFileOpen = QToolButton(self) btnFileOpen.setText('打开文档') btnFileOpen.setMinimumWidth(min_width) btnFileOpen.setIcon(style.standardIcon(QStyle.SP_DialogOpenButton)) btnFileOpen.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) btnFileOpen.clicked.connect(self.onFileOpen) toolBar.addWidget(btnFileOpen) btnFileCloseAll = QToolButton(self) btnFileCloseAll.setText('关闭全部') btnFileCloseAll.setMinimumWidth(min_width) btnFileCloseAll.setIcon(style.standardIcon( QStyle.SP_DialogCloseButton)) btnFileCloseAll.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) btnFileCloseAll.clicked.connect(self.onFileCloseAll) toolBar.addWidget(btnFileCloseAll) toolBar.addSeparator() btnEditCut = QToolButton(self) btnEditCut.setText('剪切') btnEditCut.setMinimumWidth(64) btnEditCut.setIcon(QIcon(':/ico/cut.png')) btnEditCut.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) btnEditCut.clicked.connect(self.onEditCut) toolBar.addWidget(btnEditCut) btnEditCopy = QToolButton(self) btnEditCopy.setText('复制') btnEditCopy.setMinimumWidth(64) btnEditCopy.setIcon(QIcon(':/ico/copy.png')) btnEditCopy.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) btnEditCopy.clicked.connect(self.onEditCopy) toolBar.addWidget(btnEditCopy) btnEditPaste = QToolButton(self) btnEditPaste.setText('粘贴') btnEditPaste.setMinimumWidth(64) btnEditPaste.setIcon(QIcon(':/ico/paste.png')) btnEditPaste.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) btnEditPaste.clicked.connect(self.onEditPaste) toolBar.addWidget(btnEditPaste) def msgCritical(self, strInfo): dlg = QMessageBox(self) dlg.setIcon(QMessageBox.Critical) dlg.setText(strInfo) dlg.show() def onFileNew(self): newDoc = QMdiSubWindow(self) newDoc.setWindowTitle('新文档 ' + str(self.newDocIndex)) self.newDocIndex += 1 newDoc.setWidget(QPlainTextEdit(newDoc)) self.mdiArea.addSubWindow(newDoc) newDoc.show() def onFileOpen(self): path, _ = QFileDialog.getOpenFileName(self, '打开文件', '', '文本文件 (*.txt)') if path: try: with open(path, 'rU') as f: text = f.read() except Exception as e: self.msgCritical(str(e)) else: openDoc = QMdiSubWindow(self) openDoc.setWindowTitle(path) txtEdit = QPlainTextEdit(openDoc) txtEdit.setPlainText(text) openDoc.setWidget(txtEdit) self.mdiArea.addSubWindow(openDoc) openDoc.show() def onFileCloseAll(self): self.mdiArea.closeAllSubWindows() def onEditCut(self): txtEdit = self.mdiArea.activeSubWindow().widget() txtEdit.cut() def onEditCopy(self): txtEdit = self.mdiArea.activeSubWindow().widget() txtEdit.copy() def onEditPaste(self): txtEdit = self.mdiArea.activeSubWindow().widget() txtEdit.paste() def onWinowdMode(self, index): if index == 3: self.mdiArea.cascadeSubWindows() elif index == 2: self.mdiArea.tileSubWindows() elif index == 1: self.mdiArea.setViewMode(QMdiArea.TabbedView) else: self.mdiArea.setViewMode(QMdiArea.SubWindowView)
class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.mdiArea = QMdiArea() self.mdiArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.mdiArea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.mdiArea.setViewMode(QMdiArea.TabbedView) self.mdiArea.setDocumentMode(True) self.mdiArea.setTabsClosable(True) self.mdiArea.setTabsMovable(True) self.setCentralWidget(self.mdiArea) self.mdiArea.subWindowActivated.connect(self.updateMenus) self.windowMapper = QSignalMapper(self) self.windowMapper.mapped[QWidget].connect(self.setActiveSubWindow) self.createActions() self.createMenus() self.createToolBars() self.createStatusBar() self.updateMenus() self.readSettings() self.setWindowTitle("MDI") def closeEvent(self, event): self.mdiArea.closeAllSubWindows() if self.mdiArea.currentSubWindow(): event.ignore() else: self.writeSettings() event.accept() def newFile(self): child = self.createMdiChild() child.newFile() child.show() def open(self): fileName, _ = QFileDialog.getOpenFileName(self) if fileName: existing = self.findMdiChild(fileName) if existing: self.mdiArea.setActiveSubWindow(existing) return child = self.createMdiChild() if child.loadFile(fileName): self.statusBar().showMessage("File loaded", 2000) child.show() else: child.close() def save(self): if self.activeMdiChild() and self.activeMdiChild().save(): self.statusBar().showMessage("File saved", 2000) def saveAs(self): if self.activeMdiChild() and self.activeMdiChild().saveAs(): self.statusBar().showMessage("File saved", 2000) def cut(self): if self.activeMdiChild(): self.activeMdiChild().cut() def copy(self): if self.activeMdiChild(): self.activeMdiChild().copy() def paste(self): if self.activeMdiChild(): self.activeMdiChild().paste() def about(self): QMessageBox.about( self, "About MDI", "The <b>MDI</b> example demonstrates how to write multiple " "document interface applications using Qt.") def updateMenus(self): hasMdiChild = (self.activeMdiChild() is not None) self.saveAct.setEnabled(hasMdiChild) self.saveAsAct.setEnabled(hasMdiChild) self.pasteAct.setEnabled(hasMdiChild) self.closeAct.setEnabled(hasMdiChild) self.closeAllAct.setEnabled(hasMdiChild) self.tileAct.setEnabled(hasMdiChild) self.cascadeAct.setEnabled(hasMdiChild) self.nextAct.setEnabled(hasMdiChild) self.previousAct.setEnabled(hasMdiChild) self.separatorAct.setVisible(hasMdiChild) hasSelection = (self.activeMdiChild() is not None and self.activeMdiChild().textCursor().hasSelection()) self.cutAct.setEnabled(hasSelection) self.copyAct.setEnabled(hasSelection) def updateWindowMenu(self): self.windowMenu.clear() self.windowMenu.addAction(self.closeAct) self.windowMenu.addAction(self.closeAllAct) self.windowMenu.addSeparator() self.windowMenu.addAction(self.tileAct) self.windowMenu.addAction(self.cascadeAct) self.windowMenu.addSeparator() self.windowMenu.addAction(self.nextAct) self.windowMenu.addAction(self.previousAct) self.windowMenu.addAction(self.separatorAct) windows = self.mdiArea.subWindowList() self.separatorAct.setVisible(len(windows) != 0) for i, window in enumerate(windows): child = window.widget() text = "%d %s" % (i + 1, child.userFriendlyCurrentFile()) if i < 9: text = '&' + text action = self.windowMenu.addAction(text) action.setCheckable(True) action.setChecked(child is self.activeMdiChild()) action.triggered.connect(self.windowMapper.map) self.windowMapper.setMapping(action, window) def createMdiChild(self): child = MdiChild() self.mdiArea.addSubWindow(child) child.copyAvailable.connect(self.cutAct.setEnabled) child.copyAvailable.connect(self.copyAct.setEnabled) return child def createActions(self): self.newAct = QAction(QIcon(':/images/new.png'), "&New", self, shortcut=QKeySequence.New, statusTip="Create a new file", triggered=self.newFile) self.openAct = QAction(QIcon(':/images/open.png'), "&Open...", self, shortcut=QKeySequence.Open, statusTip="Open an existing file", triggered=self.open) self.saveAct = QAction(QIcon(':/images/save.png'), "&Save", self, shortcut=QKeySequence.Save, statusTip="Save the document to disk", triggered=self.save) self.saveAsAct = QAction( "Save &As...", self, shortcut=QKeySequence.SaveAs, statusTip="Save the document under a new name", triggered=self.saveAs) self.exitAct = QAction( "E&xit", self, shortcut=QKeySequence.Quit, statusTip="Exit the application", triggered=QApplication.instance().closeAllWindows) self.cutAct = QAction( QIcon(':/images/cut.png'), "Cu&t", self, shortcut=QKeySequence.Cut, statusTip="Cut the current selection's contents to the clipboard", triggered=self.cut) self.copyAct = QAction( QIcon(':/images/copy.png'), "&Copy", self, shortcut=QKeySequence.Copy, statusTip="Copy the current selection's contents to the clipboard", triggered=self.copy) self.pasteAct = QAction( QIcon(':/images/paste.png'), "&Paste", self, shortcut=QKeySequence.Paste, statusTip= "Paste the clipboard's contents into the current selection", triggered=self.paste) self.closeAct = QAction("Cl&ose", self, statusTip="Close the active window", triggered=self.mdiArea.closeActiveSubWindow) self.closeAllAct = QAction("Close &All", self, statusTip="Close all the windows", triggered=self.mdiArea.closeAllSubWindows) self.tileAct = QAction("&Tile", self, statusTip="Tile the windows", triggered=self.mdiArea.tileSubWindows) self.cascadeAct = QAction("&Cascade", self, statusTip="Cascade the windows", triggered=self.mdiArea.cascadeSubWindows) self.nextAct = QAction("Ne&xt", self, shortcut=QKeySequence.NextChild, statusTip="Move the focus to the next window", triggered=self.mdiArea.activateNextSubWindow) self.previousAct = QAction( "Pre&vious", self, shortcut=QKeySequence.PreviousChild, statusTip="Move the focus to the previous window", triggered=self.mdiArea.activatePreviousSubWindow) self.separatorAct = QAction(self) self.separatorAct.setSeparator(True) self.aboutAct = QAction("&About", self, statusTip="Show the application's About box", triggered=self.about) self.aboutQtAct = QAction("About &Qt", self, statusTip="Show the Qt library's About box", triggered=QApplication.instance().aboutQt) def createMenus(self): self.fileMenu = self.menuBar().addMenu("&File") self.fileMenu.addAction(self.newAct) self.fileMenu.addAction(self.openAct) self.fileMenu.addAction(self.saveAct) self.fileMenu.addAction(self.saveAsAct) self.fileMenu.addSeparator() action = self.fileMenu.addAction("Switch layout direction") action.triggered.connect(self.switchLayoutDirection) self.fileMenu.addAction(self.exitAct) self.editMenu = self.menuBar().addMenu("&Edit") self.editMenu.addAction(self.cutAct) self.editMenu.addAction(self.copyAct) self.editMenu.addAction(self.pasteAct) self.windowMenu = self.menuBar().addMenu("&Window") self.updateWindowMenu() self.windowMenu.aboutToShow.connect(self.updateWindowMenu) self.menuBar().addSeparator() self.helpMenu = self.menuBar().addMenu("&Help") self.helpMenu.addAction(self.aboutAct) self.helpMenu.addAction(self.aboutQtAct) def createToolBars(self): self.fileToolBar = self.addToolBar("File") self.fileToolBar.addAction(self.newAct) self.fileToolBar.addAction(self.openAct) self.fileToolBar.addAction(self.saveAct) self.editToolBar = self.addToolBar("Edit") self.editToolBar.addAction(self.cutAct) self.editToolBar.addAction(self.copyAct) self.editToolBar.addAction(self.pasteAct) def createStatusBar(self): self.statusBar().showMessage("Ready") def readSettings(self): settings = QSettings('Trolltech', 'MDI Example') pos = settings.value('pos', QPoint(200, 200)) size = settings.value('size', QSize(400, 400)) self.move(pos) self.resize(size) def writeSettings(self): settings = QSettings('Trolltech', 'MDI Example') settings.setValue('pos', self.pos()) settings.setValue('size', self.size()) def activeMdiChild(self): activeSubWindow = self.mdiArea.activeSubWindow() if activeSubWindow: return activeSubWindow.widget() return None def findMdiChild(self, fileName): canonicalFilePath = QFileInfo(fileName).canonicalFilePath() for window in self.mdiArea.subWindowList(): if window.widget().currentFile() == canonicalFilePath: return window return None def switchLayoutDirection(self): if self.layoutDirection() == Qt.LeftToRight: QApplication.setLayoutDirection(Qt.RightToLeft) else: QApplication.setLayoutDirection(Qt.LeftToRight) def setActiveSubWindow(self, window): if window: self.mdiArea.setActiveSubWindow(window)