Пример #1
0
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
Пример #2
0
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)
Пример #3
0
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()
Пример #4
0
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)
Пример #5
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()
Пример #6
0
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)
Пример #7
0
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)