class LmrManager(QWidget): def __init__(self): super(LmrManager, self).__init__() self.h_layout = QHBoxLayout() self.left_frame = LeftFrame() self.middle_frame = MiddleFrame() self.right_frame = RightFrame() self.mr_splitter = QSplitter() # 缩小三个框直接的缝隙 self.mr_splitter.setHandleWidth(1) self.mr_splitter.insertWidget(0, self.middle_frame) self.mr_splitter.insertWidget(1, self.right_frame) self.mr_splitter.setStretchFactor( 0, 1) # 全屏后保持1:4的比例,但是之前设置的最小宽度此时可能就比较小了 self.mr_splitter.setStretchFactor(1, 4) # 设置为不可拖动至隐藏 self.mr_splitter.setCollapsible(0, False) self.mr_splitter.setCollapsible(1, False) self.h_layout.addWidget(self.left_frame) self.h_layout.addWidget(self.mr_splitter) self.setLayout(self.h_layout)
def split_frame(self, frame, orientation): """ Split the given frame either horizontally or vertically. :param frame: frame to split :param orientation: orientation along which the frame will be split """ opposite_orientation = Qt.Vertical if orientation == Qt.Horizontal else Qt.Horizontal # get opposite orientation parent_splitter = frame.splitter # get reference to the splitter wherein the frame is embedded (maybe none) splitter = None # variable to hold the splitter that will be used to embed this frame after splitting # if the frame is not embedded in a splitter or the orientation of the embedding splitter does not match a new splitter has to be created if parent_splitter is None or parent_splitter.orientation() == opposite_orientation: splitter = QSplitter(orientation) # create new splitter if parent_splitter is None: # if the frame was not embedded into a splitter the new splitter is the base splitter self.addWidget(splitter) self._base_splitter = splitter splitter.addWidget(frame) else: # if the frame was embedded into a differently oriented splitter, the new splitter will take its place splitter_index = frame.splitter_index # get the correct position of the new splitter within the parent splitter # get the sizes of all the widgets within the parent splitter if orientation == Qt.Horizontal: sizes = [parent_splitter.widget(i).geometry().height() for i in range(parent_splitter.count())] else: sizes = [parent_splitter.widget(i).geometry().width() for i in range(parent_splitter.count())] splitter.addWidget(frame) # add the frame to the new splitter parent_splitter.insertWidget(splitter_index, splitter) # add the splitter to the parent splitter parent_splitter.setSizes(sizes) # restore the dimensions of the widgets within the parent splitter frame.splitter = splitter # store the reference to the newly created splitter within the frame elif parent_splitter.orientation() == orientation: splitter = parent_splitter # if the orientation of the existing splitter matches just use it to proceed # store the sizes of all the existing widgets in the splitter # half the size of the frame to be split and add an additional size which is also half of the frames current dimension geometry = frame.geometry() if orientation == Qt.Horizontal: sizes = [splitter.widget(i).geometry().height() for i in range(splitter.count())] sizes[frame.splitter_index] = geometry.width() / 2 sizes.insert(frame.splitter_index + 1, geometry.width() / 2) else: sizes = [splitter.widget(i).geometry().width() for i in range(splitter.count())] sizes[frame.splitter_index] = geometry.height() / 2 sizes.insert(frame.splitter_index + 1, geometry.height() / 2) insert_index = frame.splitter_index + 1 # get the index where the new frame should be inserted into the splitter new_frame = self._add_modular_frame_() # create the new frame new_frame.splitter = splitter # set the splitter of the new frame splitter.insertWidget(insert_index, new_frame) # insert the new frame into the splitter splitter.setSizes(sizes) # setup the proper dimensions of all the widgets in the splitter
class MainWindow(QMainWindow): on_run = pyqtSignal(dict) disconnected = pyqtSignal() def __init__(self): super(MainWindow, self).__init__() self.host, ok_pressed = QInputDialog.getText(self, '连接', '机器人地址', QLineEdit.Normal, '127.0.0.1') if not ok_pressed or len(self.host) < 7: exit(0) try: self.ros_client = Ros(self.host, 9090) self.ros_client.run(3) self.ros_client.on('close', self.on_lost_connect) except Exception as e: QMessageBox.critical(self, "错误", e.args[0]) exit(0) self.setWindowTitle('调试器') self.statusBar().setSizeGripEnabled(False) self.statusBar().setStyleSheet('border: 1px solid black;') self._toolMenu = self.menuBar().addMenu('工具') self._srv_action = QAction('Service表', self._toolMenu) self._srv_action.setCheckable(True) self._srv_action.toggled.connect(self.on_srv_table_widget) self._topics_action = QAction('Topics图', self._toolMenu) self._topics_action.setCheckable(True) self._topics_action.toggled.connect(self.on_topics_widget) self._remote_action = QAction('遥控', self._toolMenu) self._remote_action.setCheckable(True) self._remote_action.toggled.connect(self.on_remote_widget) self._logs_action = QAction('日志', self._toolMenu) self._logs_action.setCheckable(True) self._logs_action.toggled.connect(self.on_log_widget) self._image_action = QAction('图像', self._toolMenu) self._image_action.setCheckable(True) self._image_action.toggled.connect(self.on_image_widget) self._params_action = QAction('参数', self._toolMenu) self._params_action.setCheckable(True) self._params_action.toggled.connect(self.on_params_widget) self._toolMenu.addAction(self._srv_action) self._toolMenu.addAction(self._topics_action) self._toolMenu.addAction(self._remote_action) self._toolMenu.addAction(self._logs_action) self._toolMenu.addAction(self._image_action) self._toolMenu.addAction(self._params_action) self.menuBar().addSeparator() self._act_debug_action = self.menuBar().addAction('动作调试器') self._act_debug_action.triggered.connect(self.on_act_debug) self.menuBar().addSeparator() self._state_monitor_action = self.menuBar().addAction('状态监测器') self._state_monitor_action.triggered.connect(self.on_state_monitor) self.hostLabel = QLabel(self.host) self.hostLabel.setText(self.host) self.statusBar().addWidget(self.hostLabel) self.mainWidget = QWidget() self.main_layout = QVBoxLayout() self.main_splitter = QSplitter(Qt.Horizontal, self) self.left_splitter = QSplitter(Qt.Vertical, self.main_splitter) self.middle_splitter = QSplitter(Qt.Vertical, self.main_splitter) self.right_splitter = QSplitter(Qt.Vertical, self.main_splitter) self.main_splitter.addWidget(self.left_splitter) self.main_splitter.addWidget(self.middle_splitter) self.main_splitter.addWidget(self.right_splitter) self.boxqss = '''QGroupBox { border: 2px solid black; border-radius: 5px; margin-top:1ex; } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top center; padding: 0 3px; }''' self.main_layout.addWidget(self.main_splitter) self.mainWidget.setLayout(self.main_layout) self.setCentralWidget(self.mainWidget) self.action_debug_client = None self.on_run.connect(self.run_add_angles) self.disconnected.connect(self.on_disconnected) self.srvWidget = None self.graphWidget = None self.remoteWidget = None self.logWidget = None self.imageWidget = None self.paramsWidget = None self.on_srv_table_widget() self.on_topics_widget() self.on_remote_widget() self.on_log_widget() self.on_image_widget() self.on_params_widget() def on_recv_action(self, req, res): self.on_run.emit(req) return True def run_add_angles(self, req): try: run_service = Service(self.ros_client, '/add_angles', 'common/AddAngles') run_service.call(ServiceRequest(req)) except Exception as e: QMessageBox.critical(self, "错误", e.args[0]) def on_act_debug(self): if not self.ros_client.is_connected: QMessageBox.critical(self, "错误", '尚未连接到ROS') return if self.host == '127.0.0.1' or self.host == 'localhost': self.action_debug_client = self.ros_client cmd = 'rosrun action action_debuger' else: cmd = 'roslaunch start start_action_debug_robot.launch' try: if self.action_debug_client is None: print("run action_debug_client at 127.0.0.1:9090") self.action_debug_client = Ros('127.0.0.1', 9090) self.action_debug_client.run() elif not self.action_debug_client.is_connected: print("connecting action debug client") self.action_debug_client.connect() except Exception as e: QMessageBox.critical(self, "错误", '无法连接到本地调试器 %s' % str(e)) return act_service = Service(self.action_debug_client, '/debug/action/run', 'common/AddAngles') act_service.advertise(self.on_recv_action) os.popen(cmd) def on_state_monitor(self): cmd = 'rosrun team_monitor team_monitor ' os.popen(cmd) def on_lost_connect(self, proto): self.disconnected.emit() def on_disconnected(self): QMessageBox.critical(self, "错误", '连接已断开') self.close() def on_srv_table_widget(self, show=None): if show is None: groupbox = QGroupBox('Service表') groupbox.setStyleSheet(self.boxqss) layout = QVBoxLayout() self.srvWidget = ServiceTableWidget(self.ros_client) layout.addWidget(self.srvWidget) groupbox.setLayout(layout) groupbox.hide() self.left_splitter.insertWidget(0, groupbox) elif show: self.left_splitter.widget(0).show() else: self.left_splitter.widget(0).hide() self.adjustSize() def on_topics_widget(self, show=None): if show is None: groupbox = QGroupBox('Topic拓扑图') groupbox.setStyleSheet(self.boxqss) layout = QVBoxLayout() self.graphWidget = TopicGraphWidget(self.ros_client) layout.addWidget(self.graphWidget) groupbox.setLayout(layout) groupbox.hide() self.left_splitter.insertWidget(1, groupbox) elif show: self.left_splitter.widget(1).show() else: self.left_splitter.widget(1).hide() self.adjustSize() def on_remote_widget(self, show=None): if show is None: groupbox = QGroupBox('遥控') groupbox.setStyleSheet(self.boxqss) layout = QVBoxLayout() self.remoteWidget = RemoteWidget(self.ros_client) layout.addWidget(self.remoteWidget) groupbox.setLayout(layout) groupbox.hide() self.middle_splitter.insertWidget(0, groupbox) elif show: self.middle_splitter.widget(0).show() else: self.middle_splitter.widget(0).hide() self.adjustSize() def on_log_widget(self, show=None): if show is None: groupbox = QGroupBox('运行日志') groupbox.setStyleSheet(self.boxqss) layout = QVBoxLayout() self.logWidget = LogWidget(self.ros_client) layout.addWidget(self.logWidget) groupbox.setLayout(layout) groupbox.hide() self.middle_splitter.insertWidget(1, groupbox) elif show: self.middle_splitter.widget(1).show() else: self.middle_splitter.widget(1).hide() self.adjustSize() def on_image_widget(self, show=None): if show is None: groupbox = QGroupBox('在线图像') groupbox.setStyleSheet(self.boxqss) layout = QVBoxLayout() self.imageWidget = ImageWidget(self.ros_client) layout.addWidget(self.imageWidget) groupbox.setLayout(layout) groupbox.hide() self.right_splitter.insertWidget(0, groupbox) elif show: self.right_splitter.widget(0).show() else: self.right_splitter.widget(0).hide() self.adjustSize() def on_params_widget(self, show=None): if show is None: groupbox = QGroupBox('参数') groupbox.setStyleSheet(self.boxqss) layout = QVBoxLayout() self.paramsWidget = ParamsWidget(self.ros_client) layout.addWidget(self.paramsWidget) groupbox.setLayout(layout) groupbox.hide() self.right_splitter.insertWidget(1, groupbox) elif show: self.right_splitter.widget(1).show() else: self.right_splitter.widget(1).hide() self.adjustSize() def closeEvent(self, a0: QtGui.QCloseEvent) -> None: if self.ros_client is not None: self.ros_client.terminate()
class MSplitter(QFrame, MHierarchicalElement): VERTICAL = 'vertical' HORIZONTAL = 'horizontal' def __init__(self, parent_window): super().__init__() self.setObjectName("main_frame") # Construct top-level window elements self.main_layout = QVBoxLayout() self.setLayout(self.main_layout) self.parent_container = None #self.set_parent_he(parent_window) self.main_splitter = QSplitter() self.main_splitter.setObjectName("main_splitter") self.main_splitter.show() self.main_layout.setContentsMargins(0, 0, 0, 0) #self.header_frame = MHeaderBar(self) #self.main_layout.addWidget(self.header_frame) self.content = self.main_splitter self.main_layout.addWidget(self.main_splitter) self.show() self.setStyleSheet( self.styleSheet() + "QFrame#main_frame{background-color:rgb(200,200,200)}\r\n" "QSplitter::handle#main_splitter" "{" " border: 2px solid rgb(50,50,50);" " background-color:rgb(100,100,100)" "}" "QSplitter::handle:pressed#main_splitter" "{" " border: 2px solid rgb(100,100,100);" " background-color:rgb(200,100,20)" "}") self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) self.orientation = None def set_sizes(self, one, two): self.main_splitter.setSizes([one, two]) def get_sizes(self): return self.main_splitter.sizes() def add_content(self, container, location=None): # if not (type(container) is MContainer): # raise TypeError("Expected type %s, got %s" % (str(MWindow), type(container))) if location is None: self.main_splitter.addWidget(container) elif location is "top": self.main_splitter.setOrientation(Qt.Vertical) self.main_splitter.insertWidget(0, container) self.orientation = self.VERTICAL elif location is "left": self.main_splitter.setOrientation(Qt.Horizontal) self.main_splitter.insertWidget(0, container) self.orientation = self.HORIZONTAL elif location is "right": self.main_splitter.setOrientation(Qt.Horizontal) self.main_splitter.insertWidget(1, container) self.orientation = self.HORIZONTAL elif location is "bottom": self.main_splitter.setOrientation(Qt.Vertical) self.main_splitter.insertWidget(1, container) self.orientation = self.VERTICAL container.set_parent_he(self.get_parent_he()) self.updateGeometry() def get_orientation(self): return self.orientation def get_position(self): return self.main_splitter.sizes() def get_num_widgets(self): return self.main_splitter.count() def get_item_at(self, index): return self.main_splitter.widget(index) # # def get_parent_container(self): # return self.parent_container # # def set_parent_container(self, win): # # if type(win) is MWindow or win is None: # # # Remove self from old parent # if self.parent_container is not None: # self.parent_container._remove_child_container(self) # # # Add self to new parent # if (win is not None): # win._add_child_container(self) # # # Set local reference to parent # self.parent_container = win # # else: # raise TypeError("Parent window must be type %s, not %s" % (str(type(MWindow)), str(type(win)))) def show_drop_regions(self): pass def hide_drop_regions(self): pass
class Editor(QWidget): """ This class is the central widget of the MainWindow. It contains the items library, diagram graphics scene and graphics view, and the inspector widget Function of Connections: Logically: A Connection is composed of a fromPort and a toPort, which gives the direction of the pipe. Ports are attached to Blocks. Visually: A diagram editor has a QGraphicsLineItem (connLineItem) which is set Visible only when a connection is being created Function of BlockItems: Items can be added to the library by adding them to the model of the library broswer view. Then they can be dragged and dropped into the diagram view. Function of trnsysExport: When exporting the trnsys file, exportData() is called. Function of save and load: A diagram can be saved to a json file by calling encodeDiagram and can then be loaded by calling decodeDiagram wiht appropiate filenames. Attributes ---------- projectFolder : str Path to the folder of the project diagramName : str Name used for saving the diagram saveAsPath : :obj:`Path` Default saving location is trnsysGUI/diagrams, path only set if "save as" used idGen : :obj:`IdGenerator` Is used to distribute ids (id, trnsysId(for trnsysExport), etc) alignMode : bool Enables mode in which a dragged block is aligned to y or x value of another one Toggled in the MainWindow class in toggleAlignMode() editorMode : int Mode 0: Pipes are PolySun-like Mode 1: Pipes have only 90deg angles, visio-like snapGrid : bool Enable/Disable align grid snapSize : int Size of align grid horizontalLayout : :obj:`QHBoxLayout` Contains the diagram editor and the layout containing the library browser view and the listview vertL : :obj:`QVBoxLayout` Cointains the library browser view and the listWidget moveDirectPorts: bool Enables/Disables moving direct ports of storagetank (doesn't work with HxPorts yet) diagramScene : :obj:`QGraphicsScene` Contains the "logical" part of the diagram diagramView : :obj:`QGraphicsView` Contains the visualization of the diagramScene _currentlyDraggedConnectionFromPort : :obj:`PortItem` connectionList : :obj:`List` of :obj:`Connection` trnsysObj : :obj:`List` of :obj:`BlockItem` and :obj:`Connection` graphicalObj : :obj:`List` of :obj:`GraphicalItem` connLine : :obj:`QLineF` connLineItem = :obj:`QGraphicsLineItem` """ def __init__(self, parent, projectFolder, jsonPath, loadValue, logger): super().__init__(parent) self.logger = logger self.logger.info("Initializing the diagram editor") self.projectFolder = projectFolder self.diagramName = os.path.split(self.projectFolder)[-1] + ".json" self.saveAsPath = _pl.Path() self.idGen = IdGenerator() self.testEnabled = False self.existReference = True self.controlExists = 0 self.controlDirectory = "" self.alignMode = False self.moveDirectPorts = False self.editorMode = 1 # Related to the grid blocks can snap to self.snapGrid = False self.snapSize = 20 self.trnsysPath = _pl.Path(r"C:\Trnsys17\Exe\TRNExe.exe") self.horizontalLayout = QHBoxLayout(self) self.libraryBrowserView = QListView(self) self.libraryModel = LibraryModel(self) self.libraryBrowserView.setGridSize(QSize(65, 65)) self.libraryBrowserView.setResizeMode(QListView.Adjust) self.libraryModel.setColumnCount(0) componentNamesWithIcon = [ ("Connector", _img.CONNECTOR_SVG.icon()), ("TeePiece", _img.TEE_PIECE_SVG.icon()), ("DPTee", _img.DP_TEE_PIECE_SVG.icon()), ("SPCnr", _img.SINGLE_DOUBLE_PIPE_CONNECTOR_SVG.icon()), ("DPCnr", _img.DOUBLE_DOUBLE_PIPE_CONNECTOR_SVG.icon()), ("TVentil", _img.T_VENTIL_SVG.icon()), ("WTap_main", _img.W_TAP_MAIN_SVG.icon()), ("WTap", _img.W_TAP_SVG.icon()), ("Pump", _img.PUMP_SVG.icon()), ("Collector", _img.COLLECTOR_SVG.icon()), ("GroundSourceHx", _img.GROUND_SOURCE_HX_SVG.icon()), ("PV", _img.PV_SVG.icon()), ("HP", _img.HP_SVG.icon()), ("HPTwoHx", _img.HP_TWO_HX_SVG.icon()), ("HPDoubleDual", _img.HP_DOUBLE_DUAL_SVG.icon()), ("HPDual", _img.HP_DUAL_SVG.icon()), ("AirSourceHP", _img.AIR_SOURCE_HP_SVG.icon()), ("StorageTank", _img.STORAGE_TANK_SVG.icon()), ("IceStorage", _img.ICE_STORAGE_SVG.icon()), ("PitStorage", _img.PIT_STORAGE_SVG.icon()), ("IceStorageTwoHx", _img.ICE_STORAGE_TWO_HX_SVG.icon()), ("ExternalHx", _img.EXTERNAL_HX_SVG.icon()), ("Radiator", _img.RADIATOR_SVG.icon()), ("Boiler", _img.BOILER_SVG.icon()), ("Sink", _img.SINK_SVG.icon()), ("Source", _img.SOURCE_SVG.icon()), ("SourceSink", _img.SOURCE_SINK_SVG.icon()), ("Geotherm", _img.GEOTHERM_SVG.icon()), ("Water", _img.WATER_SVG.icon()), ("Crystalizer", _img.CRYSTALIZER_SVG.icon()), ("GenericBlock", _img.GENERIC_BLOCK_PNG.icon()), ("GraphicalItem", _img.GENERIC_ITEM_PNG.icon()), ] libItems = [ QtGui.QStandardItem(icon, name) for name, icon in componentNamesWithIcon ] for i in libItems: self.libraryModel.appendRow(i) self.libraryBrowserView.setModel(self.libraryModel) self.libraryBrowserView.setViewMode(self.libraryBrowserView.IconMode) self.libraryBrowserView.setDragDropMode( self.libraryBrowserView.DragOnly) self.diagramScene = Scene(self) self.diagramView = View(self.diagramScene, self) # For list view self.vertL = QVBoxLayout() self.vertL.addWidget(self.libraryBrowserView) self.vertL.setStretchFactor(self.libraryBrowserView, 2) self.listV = QListWidget() self.vertL.addWidget(self.listV) self.vertL.setStretchFactor(self.listV, 1) # for file browser self.projectPath = "" self.fileList = [] if loadValue == "new" or loadValue == "json": self.createProjectFolder() self.fileBrowserLayout = QVBoxLayout() self.pathLayout = QHBoxLayout() self.projectPathLabel = QLabel("Project Path:") self.PPL = QLineEdit(self.projectFolder) self.PPL.setDisabled(True) self.pathLayout.addWidget(self.projectPathLabel) self.pathLayout.addWidget(self.PPL) self.scroll = QScrollArea() self.scroll.setWidgetResizable(True) self.splitter = QSplitter(Qt.Vertical, ) self.splitter.setChildrenCollapsible(False) self.scroll.setWidget(self.splitter) self.scroll.setFixedWidth(350) self.fileBrowserLayout.addLayout(self.pathLayout) self.fileBrowserLayout.addWidget(self.scroll) self.createDdckTree(self.projectFolder) if loadValue == "new" or loadValue == "json": self.createConfigBrowser(self.projectFolder) self.copyGenericFolder(self.projectFolder) self.createHydraulicDir(self.projectFolder) self.createWeatherAndControlDirs(self.projectFolder) self.horizontalLayout.addLayout(self.vertL) self.horizontalLayout.addWidget(self.diagramView) self.horizontalLayout.addLayout(self.fileBrowserLayout) self.horizontalLayout.setStretchFactor(self.diagramView, 5) self.horizontalLayout.setStretchFactor(self.libraryBrowserView, 1) self._currentlyDraggedConnectionFromPort = None self.connectionList = [] self.trnsysObj = [] self.graphicalObj = [] self.fluids = _hlm.Fluids([]) self.hydraulicLoops = _hlm.HydraulicLoops([]) self.copyGroupList = QGraphicsItemGroup() self.selectionGroupList = QGraphicsItemGroup() self.printerUnitnr = 0 # Different colors for connLineColor colorsc = "red" linePx = 4 if colorsc == "red": connLinecolor = QColor(Qt.red) elif colorsc == "blueish": connLinecolor = QColor(3, 124, 193) # Blue elif colorsc == "darkgray": connLinecolor = QColor(140, 140, 140) # Gray else: connLinecolor = QColor(196, 196, 196) # Gray # Only for displaying on-going creation of connection self.connLine = QLineF() self.connLineItem = QGraphicsLineItem(self.connLine) self.connLineItem.setPen(QtGui.QPen(connLinecolor, linePx)) self.connLineItem.setVisible(False) self.diagramScene.addItem(self.connLineItem) # For line that shows quickly up when using the align mode self.alignYLine = QLineF() self.alignYLineItem = QGraphicsLineItem(self.alignYLine) self.alignYLineItem.setPen(QtGui.QPen(QColor(196, 249, 252), 2)) self.alignYLineItem.setVisible(False) self.diagramScene.addItem(self.alignYLineItem) # For line that shows quickly up when using align mode self.alignXLine = QLineF() self.alignXLineItem = QGraphicsLineItem(self.alignXLine) self.alignXLineItem.setPen(QtGui.QPen(QColor(196, 249, 252), 2)) self.alignXLineItem.setVisible(False) self.diagramScene.addItem(self.alignXLineItem) if loadValue == "load" or loadValue == "copy": self._decodeDiagram(os.path.join(self.projectFolder, self.diagramName), loadValue=loadValue) elif loadValue == "json": self._decodeDiagram(jsonPath, loadValue=loadValue) # Debug function def dumpInformation(self): self.logger.debug("Diagram information:") self.logger.debug("Mode is " + str(self.editorMode)) self.logger.debug("Next ID is " + str(self.idGen.getID())) self.logger.debug("Next cID is " + str(self.idGen.getConnID())) self.logger.debug("TrnsysObjects are:") for t in self.trnsysObj: self.logger.debug(str(t)) self.logger.debug("") self.logger.debug("Scene items are:") sItems = self.diagramScene.items() for it in sItems: self.logger.info(str(it)) self.logger.debug("") for c in self.connectionList: c.printConn() self.logger.debug("") # Connections related methods def startConnection(self, port): self._currentlyDraggedConnectionFromPort = port def _createConnection(self, startPort, endPort) -> None: if startPort is not endPort: if (isinstance(startPort.parent, StorageTank) and isinstance(endPort.parent, StorageTank) and startPort.parent != endPort.parent): msgSTank = QMessageBox(self) msgSTank.setText( "Storage Tank to Storage Tank connection is not working atm!" ) msgSTank.exec_() isValidSinglePipeConnection = isinstance( startPort, SinglePipePortItem) and isinstance( endPort, SinglePipePortItem) if isValidSinglePipeConnection: command = CreateSinglePipeConnectionCommand( startPort, endPort, self) elif isinstance(startPort, DoublePipePortItem) and isinstance( endPort, DoublePipePortItem): command = CreateDoublePipeConnectionCommand( startPort, endPort, self) else: raise AssertionError( "Can only connect port items. Also, they have to be of the same type." ) self.parent().undoStack.push(command) def sceneMouseMoveEvent(self, event): """ This function is for dragging and connecting one port to another. When dragging, the fromPort will remain enlarged and black in color and when the toPort is hovered over, it will be enlarged and turn red. A port's details will also be displayed at the widget when they are hovered over. """ fromPort = self._currentlyDraggedConnectionFromPort if not fromPort: return fromX = fromPort.scenePos().x() fromY = fromPort.scenePos().y() toX = event.scenePos().x() toY = event.scenePos().y() self.connLine.setLine(fromX, fromY, toX, toY) self.connLineItem.setLine(self.connLine) self.connLineItem.setVisible(True) hitPortItem = self._getHitPortItemOrNone(event) if not hitPortItem: return mousePosition = event.scenePos() portItemX = hitPortItem.scenePos().x() portItemY = hitPortItem.scenePos().y() distance = _math.sqrt((mousePosition.x() - portItemX)**2 + (mousePosition.y() - portItemY)**2) if distance <= 3.5: hitPortItem.enlargePortSize() hitPortItem.innerCircle.setBrush(hitPortItem.ashColorR) self.listV.clear() hitPortItem.debugprint() else: hitPortItem.resetPortSize() hitPortItem.innerCircle.setBrush(hitPortItem.visibleColor) self.listV.clear() fromPort.debugprint() fromPort.enlargePortSize() fromPort.innerCircle.setBrush(hitPortItem.visibleColor) def _getHitPortItemOrNone(self, event: QEvent) -> _tp.Optional[PortItemBase]: fromPort = self._currentlyDraggedConnectionFromPort mousePosition = event.scenePos() relevantPortItems = self._getRelevantHitPortItems( mousePosition, fromPort) if not relevantPortItems: return None numberOfHitPortsItems = len(relevantPortItems) if numberOfHitPortsItems > 1: raise NotImplementedError( "Can't deal with overlapping port items.") hitPortItem = relevantPortItems[0] return hitPortItem def sceneMouseReleaseEvent(self, event): if not self._currentlyDraggedConnectionFromPort: return fromPort = self._currentlyDraggedConnectionFromPort self._currentlyDraggedConnectionFromPort = None self.connLineItem.setVisible(False) mousePosition = event.scenePos() relevantPortItems = self._getRelevantHitPortItems( mousePosition, fromPort) numberOfHitPortsItems = len(relevantPortItems) if numberOfHitPortsItems > 1: raise NotImplementedError( "Can't deal with overlapping port items.") if numberOfHitPortsItems == 1: toPort = relevantPortItems[0] if toPort != fromPort: self._createConnection(fromPort, toPort) def _getRelevantHitPortItems( self, mousePosition: QPointF, fromPort: PortItemBase) -> _tp.Sequence[PortItemBase]: hitItems = self.diagramScene.items(mousePosition) relevantPortItems = [ i for i in hitItems if isinstance(i, PortItemBase) and type(i) == type(fromPort) and not i.connectionList ] return relevantPortItems def cleanUpConnections(self): for c in self.connectionList: c.niceConn() def exportHydraulics(self, exportTo=_tp.Literal["ddck", "mfs"]): assert exportTo in ["ddck", "mfs"] if not self._isHydraulicConnected(): messageBox = QMessageBox() messageBox.setWindowTitle("Hydraulic not connected") messageBox.setText( "You need to connect all port items before you can export the hydraulics." ) messageBox.setStandardButtons(QMessageBox.Ok) messageBox.exec() return self.logger.info( "------------------------> START OF EXPORT <------------------------" ) self.sortTrnsysObj() fullExportText = "" ddckFolder = os.path.join(self.projectFolder, "ddck") if exportTo == "mfs": mfsFileName = self.diagramName.split(".")[0] + "_mfs.dck" exportPath = os.path.join(self.projectFolder, mfsFileName) elif exportTo == "ddck": exportPath = os.path.join(ddckFolder, "hydraulic\\hydraulic.ddck") if self._doesFileExistAndDontOverwrite(exportPath): return None self.logger.info("Printing the TRNSYS file...") if exportTo == "mfs": header = open(os.path.join(ddckFolder, "generic\\head.ddck"), "r") headerLines = header.readlines() for line in headerLines: if line[:4] == "STOP": fullExportText += "STOP = 1 \n" else: fullExportText += line header.close() elif exportTo == "ddck": fullExportText += "*************************************\n" fullExportText += "** BEGIN hydraulic.ddck\n" fullExportText += "*************************************\n\n" fullExportText += "*************************************\n" fullExportText += "** Outputs to energy balance in kWh\n" fullExportText += ( "** Following this naming standard : qSysIn_name, qSysOut_name, elSysIn_name, elSysOut_name\n" ) fullExportText += "*************************************\n" fullExportText += "EQUATIONS 1\n" fullExportText += "qSysOut_PipeLoss = PipeLossTot\n" simulationUnit = 450 simulationType = 935 descConnLength = 20 exporter = self._createExporter() blackBoxProblem, blackBoxText = exporter.exportBlackBox( exportTo=exportTo) if blackBoxProblem: return None fullExportText += blackBoxText if exportTo == "mfs": fullExportText += exporter.exportMassFlows() fullExportText += exporter.exportPumpOutlets() fullExportText += exporter.exportDivSetting(simulationUnit - 10) fullExportText += exporter.exportDoublePipeParameters( exportTo=exportTo) fullExportText += exporter.exportParametersFlowSolver( simulationUnit, simulationType, descConnLength) fullExportText += exporter.exportInputsFlowSolver() fullExportText += exporter.exportOutputsFlowSolver(simulationUnit) fullExportText += exporter.exportFluids() + "\n" fullExportText += exporter.exportHydraulicLoops() + "\n" fullExportText += exporter.exportPipeAndTeeTypesForTemp( simulationUnit + 1) # DC-ERROR fullExportText += exporter.exportPrintPipeLosses() fullExportText += exporter.exportMassFlowPrinter( self.printerUnitnr, 15) fullExportText += exporter.exportTempPrinter(self.printerUnitnr + 1, 15) if exportTo == "mfs": fullExportText += "CONSTANTS 1\nTRoomStore=1\n" fullExportText += "ENDS" self.logger.info( "------------------------> END OF EXPORT <------------------------" ) if exportTo == "mfs": f = open(exportPath, "w") f.truncate(0) f.write(fullExportText) f.close() elif exportTo == "ddck": if fullExportText[:1] == "\n": fullExportText = fullExportText[1:] hydraulicFolder = os.path.split(exportPath)[0] if not (os.path.isdir(hydraulicFolder)): os.makedirs(hydraulicFolder) f = open(exportPath, "w") f.truncate(0) f.write(fullExportText) f.close() try: lines = _du.loadDeck(exportPath, eraseBeginComment=True, eliminateComments=True) _du.checkEquationsAndConstants(lines, exportPath) except Exception as error: errorMessage = f"An error occurred while exporting the system hydraulics: {error}" _errs.showErrorMessageBox(errorMessage) return None return exportPath def _createExporter(self) -> Export: massFlowContributors = self._getMassFlowContributors() exporter = Export(massFlowContributors, self) return exporter def _getMassFlowContributors( self) -> _tp.Sequence[_mfs.MassFlowNetworkContributorMixin]: massFlowContributors = [ o for o in self.trnsysObj if isinstance(o, _mfs.MassFlowNetworkContributorMixin) ] return massFlowContributors def _isHydraulicConnected(self) -> bool: for obj in self.trnsysObj: if not isinstance(obj, _mfs.MassFlowNetworkContributorMixin): continue internalPiping = obj.getInternalPiping() for portItem in internalPiping.modelPortItemsToGraphicalPortItem.values( ): if not portItem.connectionList: return False return True def _doesFileExistAndDontOverwrite(self, folderPath): if not _pl.Path(folderPath).exists(): return False qmb = QMessageBox(self) qmb.setText( f"Warning: {folderPath} already exists. Do you want to overwrite it or cancel?" ) qmb.setStandardButtons(QMessageBox.Save | QMessageBox.Cancel) qmb.setDefaultButton(QMessageBox.Cancel) ret = qmb.exec() if ret == QMessageBox.Cancel: self.canceled = True self.logger.info("Canceling") return True self.canceled = False self.logger.info("Overwriting") return False def exportHydraulicControl(self): self.logger.info( "------------------------> START OF EXPORT <------------------------" ) self.sortTrnsysObj() fullExportText = "" ddckFolder = os.path.join(self.projectFolder, "ddck") hydCtrlPath = os.path.join(ddckFolder, "control\\hydraulic_control.ddck") if _pl.Path(hydCtrlPath).exists(): qmb = QMessageBox(self) qmb.setText( "Warning: " + "The file hydraulic_control.ddck already exists in the control folder. Do you want to overwrite it or cancel?" ) qmb.setStandardButtons(QMessageBox.Save | QMessageBox.Cancel) qmb.setDefaultButton(QMessageBox.Cancel) ret = qmb.exec() if ret == QMessageBox.Save: self.canceled = False self.logger.info("Overwriting") else: self.canceled = True self.logger.info("Canceling") return fullExportText += "*************************************\n" fullExportText += "**BEGIN hydraulic_control.ddck\n" fullExportText += "*************************************\n" simulationUnit = 450 exporter = self._createExporter() fullExportText += exporter.exportPumpOutlets() fullExportText += exporter.exportMassFlows() fullExportText += exporter.exportDivSetting(simulationUnit - 10) self.logger.info( "------------------------> END OF EXPORT <------------------------" ) if fullExportText[:1] == "\n": fullExportText = fullExportText[1:] controlFolder = os.path.split(hydCtrlPath)[0] if not (os.path.isdir(controlFolder)): os.makedirs(controlFolder) f = open(str(hydCtrlPath), "w") f.truncate(0) f.write(fullExportText) f.close() return hydCtrlPath def sortTrnsysObj(self): res = self.trnsysObj.sort(key=self.sortId) for s in self.trnsysObj: self.logger.debug("s has tr id " + str(s.trnsysId) + " has dname " + s.displayName) def sortId(self, l1): """ Sort function returning a sortable key Parameters ---------- l1 : Block/Connection Returns ------- """ return l1.trnsysId def setName(self, newName): self.diagramName = newName def delBlocks(self): """ Deletes the whole diagram Returns ------- """ self.hydraulicLoops.clear() while len(self.trnsysObj) > 0: self.logger.info("In deleting...") self.trnsysObj[0].deleteBlock() while len(self.graphicalObj) > 0: self.graphicalObj[0].deleteBlock() # Encoding / decoding def encodeDiagram(self, filename): """ Encodes the diagram to a json file. Parameters ---------- filename : str Returns ------- """ self.logger.info("filename is at encoder " + str(filename)) # if filename != "": with open(filename, "w") as jsonfile: json.dump(self, jsonfile, indent=4, sort_keys=True, cls=Encoder) def _decodeDiagram(self, filename, loadValue="load"): self.logger.info("Decoding " + filename) with open(filename, "r") as jsonfile: blocklist = json.load(jsonfile, cls=Decoder, editor=self) blockFolderNames = [] for j in blocklist["Blocks"]: for k in j: if isinstance(k, BlockItem): k.setParent(self.diagramView) k.changeSize() self.diagramScene.addItem(k) blockFolderNames.append(k.displayName) if isinstance(k, StorageTank): k.updateImage() if isinstance(k, GraphicalItem): k.setParent(self.diagramView) self.diagramScene.addItem(k) if isinstance(k, dict): if "__idDct__" in k: # here we don't set the ids because the copyGroup would need access to idGen self.logger.debug( "Found the id dict while loading, not setting the ids" ) self.idGen.setID(k["GlobalId"]) self.idGen.setTrnsysID(k["trnsysID"]) self.idGen.setConnID(k["globalConnID"]) if "__nameDct__" in k: self.logger.debug("Found the name dict while loading") if loadValue == "load": self.diagramName = k["DiagramName"] blockFolderNames.append("generic") blockFolderNames.append("hydraulic") blockFolderNames.append("weather") blockFolderNames.append("control") ddckFolder = os.path.join(self.projectFolder, "ddck") ddckFolders = os.listdir(ddckFolder) additionalFolders = [] for folder in ddckFolders: if folder not in blockFolderNames and "StorageTank" not in folder: additionalFolders.append(folder) if len(additionalFolders) > 0: warnBox = QMessageBox() warnBox.setWindowTitle("Additional ddck-folders") if len(additionalFolders) == 1: text = "The following ddck-folder does not have a corresponding component in the diagram:" else: text = "The following ddck-folders do not have a corresponding component in the diagram:" for folder in additionalFolders: text += "\n\t" + folder warnBox.setText(text) warnBox.setStandardButtons(QMessageBox.Ok) warnBox.setDefaultButton(QMessageBox.Ok) warnBox.exec() for t in self.trnsysObj: t.assignIDsToUninitializedValuesAfterJsonFormatMigration( self.idGen) self.logger.debug("Tr obj is" + str(t) + " " + str(t.trnsysId)) if hasattr(t, "isTempering"): self.logger.debug("tv has " + str(t.isTempering)) self._decodeHydraulicLoops(blocklist) def _decodeHydraulicLoops(self, blocklist): singlePipeConnections = [ c for c in self.connectionList if isinstance(c, SinglePipeConnection) ] if "hydraulicLoops" not in blocklist: hydraulicLoops = _hlmig.createLoops(singlePipeConnections, self.fluids.WATER) else: serializedHydraulicLoops = blocklist["hydraulicLoops"] hydraulicLoops = _hlm.HydraulicLoops.createFromJson( serializedHydraulicLoops, singlePipeConnections, self.fluids) self.hydraulicLoops = hydraulicLoops def exportSvg(self): """ For exporting a svg file (text is still too large) Returns ------- """ generator = QSvgGenerator() generator.setResolution(300) generator.setSize( QSize(self.diagramScene.width(), self.diagramScene.height())) # generator.setViewBox(QRect(0, 0, 800, 800)) generator.setViewBox(self.diagramScene.sceneRect()) generator.setFileName("VectorGraphicsExport.svg") painter = QPainter() painter.begin(generator) painter.setRenderHint(QPainter.Antialiasing) self.diagramScene.render(painter) painter.end() # Saving related def save(self, showWarning=True): """ If saveas has not been used, diagram will be saved in "/diagrams" If saveas has been used, diagram will be saved in self.saveAsPath Returns ------- """ self.diagramName = os.path.split(self.projectFolder)[-1] + ".json" diagramPath = os.path.join(self.projectFolder, self.diagramName) if os.path.isfile(diagramPath) and showWarning: qmb = QMessageBox(self) qmb.setText( "Warning: " + "This diagram name exists already. Do you want to overwrite or cancel?" ) qmb.setStandardButtons(QMessageBox.Save | QMessageBox.Cancel) qmb.setDefaultButton(QMessageBox.Cancel) ret = qmb.exec() if ret != QMessageBox.Save: self.logger.info("Canceling") return self.logger.info("Overwriting") self.encodeDiagram(diagramPath) self.encodeDiagram(diagramPath) msgb = QMessageBox(self) msgb.setText("Saved diagram at " + diagramPath) msgb.exec() def saveToProject(self): projectPath = self.projectPath def renameDiagram(self, newName): """ Parameters ---------- newName Returns ------- """ if self.saveAsPath.name != "": # print("Path name is " + self.saveAsPath.name) if newName + ".json" in self.saveAsPath.glob("*"): QMessageBox( self, "Warning", "This diagram name exists already in the directory." " Please rename this diagram") else: self.saveAsPath = _pl.Path( self.saveAsPath.stem[0:self.saveAsPath.name. index(self.diagramName)] + newName) self.diagramName = newName self.parent().currentFile = newName # fromPath = self.projectFolder # destPath = os.path.dirname(__file__) # destPath = os.path.join(destPath, 'default') # destPath = os.path.join(destPath, newName) # os.rename(fromPath, destPath) # print("Path is now: " + str(self.saveAsPath)) # print("Diagram name is: " + self.diagramName) def saveAtClose(self): self.logger.info("saveaspath is " + str(self.saveAsPath)) # closeDialog = closeDlg() # if closeDialog.closeBool: filepath = _pl.Path( _pl.Path(__file__).resolve().parent.joinpath("recent")) self.encodeDiagram(str(filepath.joinpath(self.diagramName + ".json"))) # Mode related def setAlignMode(self, b): self.alignMode = True def setEditorMode(self, b): self.editorMode = b def setMoveDirectPorts(self, b): """ Sets the bool moveDirectPorts. When mouse released in diagramScene, moveDirectPorts is set to False again Parameters ---------- b : bool Returns ------- """ self.moveDirectPorts = b def setSnapGrid(self, b): self.snapGrid = b def setSnapSize(self, s): self.snapSize = s def setConnLabelVis(self, isVisible: bool) -> None: for c in self.trnsysObj: if isinstance(c, ConnectionBase): c.setLabelVisible(isVisible) if isinstance(c, BlockItem): c.label.setVisible(isVisible) if isinstance(c, TVentil): c.posLabel.setVisible(isVisible) def updateConnGrads(self): for t in self.trnsysObj: if isinstance(t, ConnectionBase): t.updateSegGrads() # Dialog calls def showBlockDlg(self, bl): c = BlockDlg(bl, self) def showDoublePipeBlockDlg(self, bl): c = DoublePipeBlockDlg(bl, self) def showPumpDlg(self, bl): c = PumpDlg(bl, self) def showDiagramDlg(self): c = diagramDlg(self) def showGenericPortPairDlg(self, bl): c = GenericPortPairDlg(bl, self) def showHxDlg(self, hx): c = hxDlg(hx, self) def showSegmentDlg(self, seg): c = segmentDlg(seg, self) def showTVentilDlg(self, bl): c = TVentilDlg(bl, self) def showConfigStorageDlg(self, bl): c = ConfigureStorageDialog(bl, self) def getConnection(self, n): return self.connectionList[int(n)] # Unused def create_icon(self, map_icon): map_icon.fill() painter = QPainter(map_icon) painter.fillRect(10, 10, 40, 40, QColor(88, 233, 252)) # painter.setBrush(Qt.red) painter.setBrush(QColor(252, 136, 98)) painter.drawEllipse(36, 2, 15, 15) painter.setBrush(Qt.yellow) painter.drawEllipse(20, 20, 20, 20) painter.end() def setTrnsysIdBack(self): self.idGen.trnsysID = max(t.trnsysId for t in self.trnsysObj) def findStorageCorrespPorts1(self, portList): """ This function gets the ports on the other side of pipes connected to a port of the StorageTank. Unused Parameters ---------- portList : :obj:`List` of :obj:`PortItems` Returns ------- """ res = [] # print("Finding c ports") for p in portList: if len(p.connectionList) > 0: # check if not >1 needed # connectionList[0] is the hidden connection created when the portPair is i = 0 # while type(p.connectionList[i].fromPort.parent) is StorageTank and type(p.connectionList[i].toPort.parent) is StorageTank: while (p.connectionList[i].fromPort.parent) == ( p.connectionList[i].toPort.parent): i += 1 if len(p.connectionList) >= i + 1: if p.connectionList[i].fromPort is p: res.append(p.connectionList[i].toPort) elif p.connectionList[i].toPort is p: res.append(p.connectionList[i].fromPort) else: self.logger.debug("Port is not fromPort nor toPort") # [print(p.parent.displayName) for p in res] return res def printPDF(self): """ --------------------------------------------- Export diagram as pdf onto specified folder fn = user input directory --------------------------------------------- """ fn, _ = QFileDialog.getSaveFileName(self, "Export PDF", None, "PDF files (.pdf);;All Files()") if fn != "": if QFileInfo(fn).suffix() == "": fn += ".pdf" printer = QPrinter(QPrinter.HighResolution) printer.setOrientation(QPrinter.Landscape) printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFileName(fn) painter = QPainter(printer) self.diagramScene.render(painter) painter.end() self.logger.info("File exported to %s" % fn) def openProject(self): self.projectPath = str( QFileDialog.getExistingDirectory(self, "Select Project Path")) if self.projectPath != "": test = self.parent() self.parent().newDia() self.PPL.setText(self.projectPath) loadPath = os.path.join(self.projectPath, "ddck") self.createConfigBrowser(self.projectPath) self.copyGenericFolder(self.projectPath) self.createHydraulicDir(self.projectPath) self.createWeatherAndControlDirs(self.projectPath) self.createDdckTree(loadPath) # todo : open diagram # todo : add files into list def createDdckTree(self, loadPath): treeToRemove = self.findChild(QTreeView, "ddck") try: # treeToRemove.hide() treeToRemove.deleteLater() except AttributeError: self.logger.debug("Widget doesnt exist!") else: self.logger.debug("Deleted widget") if self.projectPath == "": loadPath = os.path.join(loadPath, "ddck") if not os.path.exists(loadPath): os.makedirs(loadPath) self.model = MyQFileSystemModel() self.model.setRootPath(loadPath) self.model.setName("ddck") self.tree = MyQTreeView(self.model, self) self.tree.setModel(self.model) self.tree.setRootIndex(self.model.index(loadPath)) self.tree.setObjectName("ddck") self.tree.setMinimumHeight(600) self.tree.setSortingEnabled(True) self.splitter.insertWidget(0, self.tree) def createConfigBrowser(self, loadPath): self.layoutToRemove = self.findChild(QHBoxLayout, "Config_Layout") try: # treeToRemove.hide() self.layoutToRemove.deleteLater() except AttributeError: self.logger.debug("Widget doesnt exist!") else: self.logger.debug("Deleted widget") runConfigData = self._getPackageResourceData("templates/run.config") runConfigPath = _pl.Path(loadPath) / "run.config" runConfigPath.write_bytes(runConfigData) self.HBox = QHBoxLayout() self.refreshButton = QPushButton(self) self.refreshButton.setIcon(_img.ROTATE_TO_RIGHT_PNG.icon()) self.refreshButton.clicked.connect(self.refreshConfig) self.model = MyQFileSystemModel() self.model.setRootPath(loadPath) self.model.setName("Config File") self.model.setFilter(QDir.Files) self.tree = MyQTreeView(self.model, self) self.tree.setModel(self.model) self.tree.setRootIndex(self.model.index(loadPath)) self.tree.setObjectName("config") self.tree.setFixedHeight(60) self.tree.setSortingEnabled(False) self.HBox.addWidget(self.refreshButton) self.HBox.addWidget(self.tree) self.HBox.setObjectName("Config_Layout") self.fileBrowserLayout.addLayout(self.HBox) def createProjectFolder(self): if not os.path.exists(self.projectFolder): os.makedirs(self.projectFolder) def refreshConfig(self): # configPath = os.path.dirname(__file__) # configPath = os.path.join(configPath, 'project') # configPath = os.path.join(configPath, self.date_time) # emptyConfig = os.path.join(configPath, 'run.config') if self.projectPath == "": localPath = self.projectFolder else: localPath = self.projectPath self.configToEdit = os.path.join(localPath, "run.config") os.remove(self.configToEdit) shutil.copy(self.emptyConfig, localPath) self.configToEdit = os.path.join(localPath, "run.config") localDdckPath = os.path.join(localPath, "ddck") with open(self.configToEdit, "r") as file: lines = file.readlines() localPathStr = "string LOCAL$ %s" % str(localDdckPath) # localPathStr.replace('/', '\\') lines[21] = localPathStr + "\n" with open(self.configToEdit, "w") as file: file.writelines(lines) # print(localPathStr) self.userInputList() def userInputList(self): self.logger.debug(self.fileList) dia = FileOrderingDialog(self.fileList, self) def copyGenericFolder(self, loadPath): genericFolderPath = _pl.Path(loadPath) / "ddck" / "generic" if not genericFolderPath.exists(): self.logger.info("Creating %s", genericFolderPath) genericFolderPath.mkdir() headData = self._getPackageResourceData("templates/generic/head.ddck") self.logger.info("Copying head.ddck") (genericFolderPath / "head.ddck").write_bytes(headData) endData = self._getPackageResourceData("templates/generic/end.ddck") self.logger.info("Copying end.ddck") (genericFolderPath / "end.ddck").write_bytes(endData) @staticmethod def _getPackageResourceData(resourcePath): data = _pu.get_data(_tgui.__name__, resourcePath) assert data, f"{resourcePath} package resource not found" return data def createHydraulicDir(self, projectPath): self.hydraulicFolder = os.path.join(projectPath, "ddck") self.hydraulicFolder = os.path.join(self.hydraulicFolder, "hydraulic") if not os.path.exists(self.hydraulicFolder): self.logger.info("Creating " + self.hydraulicFolder) os.makedirs(self.hydraulicFolder) def createWeatherAndControlDirs(self, projectPath): ddckFolder = os.path.join(projectPath, "ddck") weatherFolder = os.path.join(ddckFolder, "weather") controlFolder = os.path.join(ddckFolder, "control") if not os.path.exists(weatherFolder): self.logger.info("Creating " + weatherFolder) os.makedirs(weatherFolder) if not os.path.exists(controlFolder): self.logger.info("Creating " + controlFolder) os.makedirs(controlFolder) def editHydraulicLoop(self, singlePipeConnection: SinglePipeConnection): assert isinstance(singlePipeConnection.fromPort, SinglePipePortItem) hydraulicLoop = self.hydraulicLoops.getLoopForExistingConnection( singlePipeConnection) _hledit.edit(hydraulicLoop, self.hydraulicLoops, self.fluids)
class SignalTabController(QWidget): frame_closed = pyqtSignal(SignalFrameController) not_show_again_changed = pyqtSignal() signal_frame_updated = pyqtSignal(SignalFrameController) signal_created = pyqtSignal(Signal) files_dropped = pyqtSignal(list) frame_was_dropped = pyqtSignal(int, int) @property def num_signals(self): return self.splitter.count() - 1 @property def signal_frames(self): """ :rtype: list of SignalFrameController """ return [self.splitter.widget(i) for i in range(self.num_signals)] @property def signal_views(self): """ :rtype: list of EpicGraphicView """ return [sWidget.ui.gvSignal for sWidget in self.signal_frames] @property def signal_numbers(self): """ :rtype: list of int """ return [sw.signal.signal_frame_number for sw in self.signal_frames] @property def signal_undo_stack(self): return self.undo_stack def __init__(self, project_manager, parent=None): super().__init__(parent) self.ui = Ui_Interpretation() self.ui.setupUi(self) self.splitter = QSplitter() self.splitter.setOrientation(Qt.Vertical) self.splitter.setChildrenCollapsible(True) self.placeholder_widget = QWidget() # self.placeholder_widget.setMaximumHeight(1) self.placeholder_widget.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) self.undo_stack = QUndoStack() self.project_manager = project_manager self.splitter.addWidget(self.placeholder_widget) self.signal_vlay = QVBoxLayout() self.signal_vlay.addWidget(self.splitter) self.ui.scrlAreaSignals.setLayout(self.signal_vlay) self.drag_pos = None @pyqtSlot(QPoint) def frame_dragged(self, pos: QPoint): self.drag_pos = pos @pyqtSlot(QPoint) def frame_dropped(self, pos: QPoint): start = self.drag_pos if start is None: return end = pos start_index = -1 end_index = -1 if self.num_signals > 1: for i, w in enumerate(self.signal_frames): if w.geometry().contains(start): start_index = i if w.geometry().contains(end): end_index = i self.swap_frames(start_index, end_index) self.frame_was_dropped.emit(start_index, end_index) @pyqtSlot(int, int) def swap_frames(self, from_index: int, to_index: int): if from_index != to_index: start_sig_widget = self.splitter.widget(from_index) self.splitter.insertWidget(to_index, start_sig_widget) def on_files_dropped(self, files): self.files_dropped.emit(files) def close_frame(self, frame: SignalFrameController): self.frame_closed.emit(frame) @pyqtSlot(bool) def set_shift_statuslabel(self, shift_pressed): if shift_pressed and constants.SETTINGS.value( 'hold_shift_to_drag', False, type=bool): self.ui.lShiftStatus.setText("[SHIFT] Use Mouse to scroll signal.") self.ui.lCtrlStatus.clear() elif shift_pressed and not constants.SETTINGS.value( 'hold_shift_to_drag', False, type=bool): self.ui.lShiftStatus.setText( "[SHIFT] Use mouse to create a selection.") self.ui.lCtrlStatus.clear() else: self.ui.lShiftStatus.clear() @pyqtSlot(bool) def set_ctrl_statuslabel(self, ctrl_pressed): if ctrl_pressed and len(self.ui.lShiftStatus.text()) == 0: self.ui.lCtrlStatus.setText( "[CTRL] Zoom signal with mousclicks or arrow up/down.") else: self.ui.lCtrlStatus.clear() def reset_all_signalx_zoom(self): for gvs in self.signal_views: gvs.showing_full_signal = True def add_signal_frame(self, proto_analyzer): # self.set_tab_busy(True) sig_frame = SignalFrameController(proto_analyzer, self.undo_stack, self.project_manager, parent=self) sframes = self.signal_frames prev_signal_frame = sframes[-1] if len(sframes) > 0 else None if len(proto_analyzer.signal.filename) == 0: # Neues Signal aus "Create Signal from Selection" sig_frame.ui.btnSaveSignal.show() self.__create_connects_for_signal_frame(signal_frame=sig_frame) sig_frame.signal_created.connect(self.signal_created.emit) sig_frame.not_show_again_changed.connect( self.not_show_again_changed.emit) sig_frame.ui.gvSignal.shift_state_changed.connect( self.set_shift_statuslabel) sig_frame.ui.gvSignal.ctrl_state_changed.connect( self.set_ctrl_statuslabel) sig_frame.ui.lineEditSignalName.setToolTip( self.tr("Sourcefile: ") + proto_analyzer.signal.filename) sig_frame.apply_to_all_clicked.connect( self.handle_apply_to_all_clicked) if prev_signal_frame is not None: sig_frame.ui.cbProtoView.setCurrentIndex( prev_signal_frame.ui.cbProtoView.currentIndex()) sig_frame.blockSignals(True) if proto_analyzer.signal.qad_demod_file_loaded: sig_frame.ui.cbSignalView.setCurrentIndex(1) sig_frame.ui.cbSignalView.setDisabled(True) self.splitter.insertWidget(self.num_signals, sig_frame) self.reset_all_signalx_zoom() sig_frame.blockSignals(False) default_view = constants.SETTINGS.value('default_view', 0, int) sig_frame.ui.cbProtoView.setCurrentIndex(default_view) return sig_frame def __create_connects_for_signal_frame( self, signal_frame: SignalFrameController): signal_frame.hold_shift = constants.SETTINGS.value( 'hold_shift_to_drag', False, type=bool) signal_frame.drag_started.connect(self.frame_dragged) signal_frame.frame_dropped.connect(self.frame_dropped) signal_frame.files_dropped.connect(self.on_files_dropped) signal_frame.closed.connect(self.close_frame) def add_empty_frame(self, filename: str, proto): sig_frame = SignalFrameController( proto_analyzer=proto, undo_stack=self.undo_stack, project_manager=self.project_manager, proto_bits=proto.decoded_proto_bits_str, parent=self) sig_frame.ui.lineEditSignalName.setText(filename) sig_frame.setMinimumHeight(sig_frame.height()) sig_frame.set_empty_frame_visibilities() self.__create_connects_for_signal_frame(signal_frame=sig_frame) self.splitter.insertWidget(self.num_signals, sig_frame) QCoreApplication.processEvents() return sig_frame def set_frame_numbers(self): for i, f in enumerate(self.signal_frames): f.ui.lSignalNr.setText("{0:d}:".format(i + 1)) def minimize_all(self): for f in self.signal_frames: f.is_minimized = False f.minimize_maximize() def maximize_all(self): for f in self.signal_frames: f.is_minimized = True f.minimize_maximize() @pyqtSlot() def save_all(self): if self.num_signals == 0: return settings = constants.SETTINGS try: not_show = settings.value('not_show_save_dialog', type=bool) except TypeError: not_show = False if not not_show: ok, notshowagain = SaveAllDialog.dialog(self) settings.setValue("not_show_save_dialog", notshowagain) self.not_show_again_changed.emit() if not ok: return for f in self.signal_frames: if f.signal is None or f.signal.filename == "": continue f.signal.save() @pyqtSlot() def close_all(self): for f in self.signal_frames: f.my_close() @pyqtSlot(Signal) def handle_apply_to_all_clicked(self, signal: Signal): for frame in self.signal_frames: if frame.signal is not None: frame.signal.noise_min_plot = signal.noise_min_plot frame.signal.noise_max_plot = signal.noise_max_plot frame.signal.block_protocol_update = True proto_needs_update = False if frame.signal.modulation_type != signal.modulation_type: frame.signal.modulation_type = signal.modulation_type proto_needs_update = True if frame.signal.qad_center != signal.qad_center: frame.signal.qad_center = signal.qad_center proto_needs_update = True if frame.signal.tolerance != signal.tolerance: frame.signal.tolerance = signal.tolerance proto_needs_update = True if frame.signal.noise_threshold != signal.noise_threshold: frame.signal.noise_threshold = signal.noise_threshold proto_needs_update = True if frame.signal.bit_len != signal.bit_len: frame.signal.bit_len = signal.bit_len proto_needs_update = True frame.signal.block_protocol_update = False if proto_needs_update: frame.signal.protocol_needs_update.emit() def __get_sorted_positions(self): frame_names = [ sf.ui.lineEditSignalName.text() for sf in self.signal_frames ] sorted_frame_names = frame_names[:] sorted_frame_names.sort() sorted_positions = [] for name in frame_names: pos = sorted_frame_names.index(name) if pos in sorted_positions: pos += 1 sorted_positions.append(pos) return sorted_positions def refresh_participant_information(self): for sframe in self.signal_frames: sframe.on_participant_changed()
class AppWindow(Window): def __init__(self, parent=None): self.currEmail = '' self.loggedIn = False self.fileList = List() self.file_icon = QIcon(QApplication.style().standardIcon(QStyle.SP_FileIcon)) ### This is how new entries are added: self.fileList._addEntry("Additional list entry", self.file_icon) self.mainWidget = QWidget() self.splitter = QSplitter() self.splitter.addWidget(self.fileList) self.treeView = Tree() self.treeView.hide() self.splitter.insertWidget(0, self.treeView) self.treeInserted = False self.splitter.setStretchFactor(0, 10) self.splitter.setStretchFactor(1, 25) super().__init__(self.splitter) self.setWindowTitle('Main App Window') self.setGeometry(100, 100, 800, 500) self.selectedItems = [] self.tools.addAction('Download', self.downloadAction) self.tools.addAction('Upload', self.uploadAction) self.tools.addAction('Login', self.loginAction) self.tools.addAction('Tree View', self.treeviewAction) self.fileList.itemSelectionChanged.connect(self.selectionChanged) self.fileList.itemDropped.connect(self.dropHandle) def showEvent(self, event): QTimer.singleShot(50, self.functionAfterShown) def functionAfterShown(self): self.notify("Attempting automatic login...") #time.sleep(5) res, email = utils.try_easy_login() if res: self.notify("Successfully logged in") self.currEmail = email self.loggedIn = True self.updateFileList() else: self.notify("Automatic login unsuccessful") def selectionChanged(self): self.selectedItems = self.fileList.selectedItems() def treeviewAction(self): if not self.treeInserted: self.treeView.show() self.treeInserted = True else: self.treeView.hide() self.treeInserted = False def loginAction(self): if self.loggedIn: self.notify("Already logged in") return formdiag = FormDialog() if formdiag.val == 'success': self.notify("Attempting to log in...") res = utils.login(formdiag.email.text(), formdiag.password.text()) if res: self.notify("Login successful") self.currEmail = formdiag.email.text() else: self.notify("Login unsuccessful") def downloadAction(self): if self.selectedItems: for selectedItem in self.selectedItems: selectedFile = selectedItem.text() self.notify("Downloading: " + selectedFile) if not utils.send_file(selectedFile, self.currEmail): self.notify("Failed to download file: " + selectedFile) continue self.notify("Download completed", 10000) def uploadAction(self): mydiag = FileDialog() if mydiag: filesToUpload = mydiag.openFileNamesDialog() self.uploadFileList(filesToUpload) def dropHandle(self): if self.fileList.droppedUrls: self.uploadFileList(self.fileList.droppedUrls) self.fileList.droppedUrls = [] def uploadFileList(self, fileList): for fileToUpload in fileList: self.notify("Uploading: " + fileToUpload) if not utils.send_file(fileToUpload, self.currEmail): self.notify("Failed to upload file: " + fileToUpload) continue self.notify("Upload completed", 10000) self.updateFileList() def updateFileList(self): self.notify("Updating file list...") fileList = utils.list_available() for singleFile in fileList: if not self.fileList.findItems(singleFile, Qt.MatchExactly): self.fileList._addEntry(singleFile, self.file_icon) self.notify("File list updated", 10000)
class Interface(QWidget): def __init__(self, beatmap_info, replays, events, library, speeds, \ start_speed, paint_info, statistic_functions, snaps_args): super().__init__() self.speeds = speeds self.replays = replays self.library = library self.snaps_args = snaps_args self.current_replay_info = None # maps `circleguard.Replay` to `circlevis.ReplayInfo`, as its creation # is relatively expensive and users might open and close the same info # panel multiple times self.replay_info_cache = {} # we calculate some statistics in the background so users aren't hit # with multi-second wait times when accessing replay info. Initialize # with `None` so if the replay info *is* accessed before we calculate # everything, no harm - `ReplayInfo` will calculate it instead. self.replay_statistics_precalculated = {} for replay in replays: self.replay_statistics_precalculated[replay] = (None, None, None, None) # only precalculate statistics if we're visualizing 5 or fewer replays. # Otherwise, the thread is too overworked and lags the main draw thread # significantly until all statistics are precalculated. # TODO This may be resolved properly by using a QThread with a low # priority instead, so as not to starve the draw thread. We should be # using QThreads instead of python threads regardless. if len(replays) <= 5: # and here's the thread which will actually start those calculations cg_statistics_worked = Thread(target=self.calculate_cg_statistics) # allow users to quit before we're done calculating cg_statistics_worked.daemon = True cg_statistics_worked.start() # create our own library in a temp dir if one wasn't passed if not self.library: # keep a reference so it doesn't get deleted self.temp_dir = TemporaryDirectory() self.library = Library(self.temp_dir.name) self.beatmap = None if beatmap_info.path: self.beatmap = Beatmap.from_path(beatmap_info.path) elif beatmap_info.map_id: # TODO move temporary directory creation to slider probably, since # this logic is now duplicated here and in circlecore self.beatmap = self.library.lookup_by_id(beatmap_info.map_id, download=True, save=True) dt_enabled = any(Mod.DT in replay.mods for replay in replays) ht_enabled = any(Mod.HT in replay.mods for replay in replays) if dt_enabled: start_speed = 1.5 if ht_enabled: start_speed = 0.75 self.renderer = Renderer(self.beatmap, replays, events, start_speed, paint_info, statistic_functions) self.renderer.update_time_signal.connect(self.update_slider) # if the renderer wants to pause itself (eg when the playback hits the # end of the replay), we kick it back to us (the `Interface`) so we can # also update the pause button's state. self.renderer.pause_signal.connect(self.toggle_pause) # we want to give `VisualizerControls` the union of all the replay's # mods mods = Mod.NM for replay in replays: mods += replay.mods self.controls = VisualizerControls(start_speed, mods, replays) self.controls.pause_button.clicked.connect(self.toggle_pause) self.controls.play_reverse_button.clicked.connect(self.play_reverse) self.controls.play_normal_button.clicked.connect(self.play_normal) self.controls.next_frame_button.clicked.connect( lambda: self.change_frame(reverse=False)) self.controls.previous_frame_button.clicked.connect( lambda: self.change_frame(reverse=True)) self.controls.speed_up_button.clicked.connect(self.increase_speed) self.controls.speed_down_button.clicked.connect(self.lower_speed) self.controls.copy_to_clipboard_button.clicked.connect( self.copy_to_clipboard) self.controls.time_slider.sliderMoved.connect(self.renderer.seek_to) self.controls.time_slider.setRange(self.renderer.playback_start, self.renderer.playback_end) self.controls.raw_view_changed.connect(self.renderer.raw_view_changed) self.controls.only_color_keydowns_changed.connect( self.renderer.only_color_keydowns_changed) self.controls.hitobjects_changed.connect( self.renderer.hitobjects_changed) self.controls.approach_circles_changed.connect( self.renderer.approach_circles_changed) self.controls.num_frames_changed.connect( self.renderer.num_frames_changed) self.controls.draw_hit_error_bar_changed.connect( self.renderer.draw_hit_error_bar_changed) self.controls.circle_size_mod_changed.connect( self.renderer.circle_size_mod_changed) self.controls.show_info_for_replay.connect(self.show_info_panel) self.splitter = QSplitter() # splitter lays widgets horizontally by default, so combine renderer and # controls into one single widget vertically self.splitter.addWidget( Combined([self.renderer, self.controls], Qt.Vertical)) layout = QGridLayout() layout.addWidget(self.splitter, 1, 0, 1, 1) layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) def play_normal(self): self.unpause() self.renderer.play_direction = 1 self.update_speed(abs(self.renderer.clock.current_speed)) def update_slider(self, value): self.controls.time_slider.setValue(value) def change_by(self, delta): self.pause() self.renderer.seek_to(self.renderer.clock.time_counter + delta) def play_reverse(self): self.unpause() self.renderer.play_direction = -1 self.update_speed(abs(self.renderer.clock.current_speed)) def update_speed(self, speed): self.renderer.clock.change_speed(speed * self.renderer.play_direction) def change_frame(self, reverse): self.pause() self.renderer.search_nearest_frame(reverse=reverse) def toggle_pause(self): if self.renderer.paused: self.unpause() else: self.pause() def pause(self): self.controls.set_paused_state(True) self.renderer.pause() def unpause(self): self.controls.set_paused_state(False) self.renderer.resume() def lower_speed(self): index = self.speeds.index(abs(self.renderer.clock.current_speed)) if index == 0: return speed = self.speeds[index - 1] self.controls.speed_label.setText(str(speed) + "x") self.update_speed(speed) def increase_speed(self): index = self.speeds.index(abs(self.renderer.clock.current_speed)) if index == len(self.speeds) - 1: return speed = self.speeds[index + 1] self.controls.speed_label.setText(str(speed) + "x") self.update_speed(speed) def copy_to_clipboard(self): timestamp = int(self.renderer.clock.get_time()) clipboard = QApplication.clipboard() # TODO accomodate arbitrary numbers of replays (including 0 replays) r1 = self.replays[0] if len(self.replays) == 2: r2 = self.replays[1] user_str = f"u={r1.user_id}&m1={r1.mods.short_name()}&u2={r2.user_id}&m2={r2.mods.short_name()}" else: user_str = f"u={r1.user_id}&m1={r1.mods.short_name()}" clipboard.setText( f"circleguard://m={r1.map_id}&{user_str}&t={timestamp}") def show_info_panel(self, replay): """ Shows an info panel containing stats about the replay to the left of the renderer. The visualizer window will expand to accomodate for this extra space. """ if replay in self.replay_info_cache: replay_info = self.replay_info_cache[replay] replay_info.show() else: ur, frametime, snaps, judgments = self.replay_statistics_precalculated[ replay] replay_info = ReplayInfo(replay, self.library.path, ur, frametime, snaps, judgments, self.snaps_args) replay_info.seek_to.connect(self.seek_to) # don't show two of the same info panels at once if self.current_replay_info is not None: # if they're the same, don't change anything if replay_info == self.current_replay_info: return # Otherwise, close the current one and show the new one. # simulate a "close" button press self.current_replay_info.close_button_clicked.emit() def remove_replay_info(): replay_info.hide() self.current_replay_info = None replay_info.close_button_clicked.connect(remove_replay_info) self.splitter.insertWidget(0, replay_info) self.current_replay_info = replay_info self.replay_info_cache[replay] = replay_info def seek_to(self, time): self.pause() self.renderer.seek_to(time) def calculate_cg_statistics(self): cg = KeylessCircleguard() for replay in self.replays: ur = None judgments = None if cg.map_available(replay): ur = cg.ur(replay) judgments = cg.judgments(replay) frametime = cg.frametime(replay) snaps = cg.snaps(replay, **self.snaps_args) self.replay_statistics_precalculated[replay] = (ur, frametime, snaps, judgments)
class SignalTabController(QWidget): frame_closed = pyqtSignal(SignalFrameController) not_show_again_changed = pyqtSignal() signal_created = pyqtSignal(Signal) files_dropped = pyqtSignal(list) frame_was_dropped = pyqtSignal(int, int) @property def num_frames(self): return self.splitter.count() - 1 @property def signal_frames(self): """ :rtype: list of SignalFrameController """ return [self.splitter.widget(i) for i in range(self.num_frames)] @property def signal_undo_stack(self): return self.undo_stack def __init__(self, project_manager, parent=None): super().__init__(parent) self.ui = Ui_Interpretation() self.ui.setupUi(self) self.splitter = QSplitter() self.splitter.setStyleSheet("QSplitter::handle:vertical {\nmargin: 4px 0px; background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, \nstop:0 rgba(255, 255, 255, 0), \nstop:0.5 rgba(100, 100, 100, 100), \nstop:1 rgba(255, 255, 255, 0));\n image: url(:/icons/data/icons/splitter_handle_horizontal.svg);\n}") self.splitter.setOrientation(Qt.Vertical) self.splitter.setChildrenCollapsible(True) self.splitter.setHandleWidth(6) placeholder_widget = QWidget() placeholder_widget.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) self.undo_stack = QUndoStack() self.project_manager = project_manager self.splitter.addWidget(placeholder_widget) self.signal_vlay = QVBoxLayout() self.signal_vlay.setContentsMargins(0,0,0,0) self.signal_vlay.addWidget(self.splitter) self.ui.scrlAreaSignals.setLayout(self.signal_vlay) self.drag_pos = None def on_files_dropped(self, files): self.files_dropped.emit(files) def close_frame(self, frame:SignalFrameController): self.frame_closed.emit(frame) def add_signal_frame(self, proto_analyzer): sig_frame = SignalFrameController(proto_analyzer, self.undo_stack, self.project_manager, parent=self) sframes = self.signal_frames if len(proto_analyzer.signal.filename) == 0: # new signal from "create signal from selection" sig_frame.ui.btnSaveSignal.show() self.__create_connects_for_signal_frame(signal_frame=sig_frame) sig_frame.signal_created.connect(self.signal_created.emit) sig_frame.not_show_again_changed.connect(self.not_show_again_changed.emit) sig_frame.ui.lineEditSignalName.setToolTip(self.tr("Sourcefile: ") + proto_analyzer.signal.filename) sig_frame.apply_to_all_clicked.connect(self.on_apply_to_all_clicked) prev_signal_frame = sframes[-1] if len(sframes) > 0 else None if prev_signal_frame is not None and hasattr(prev_signal_frame, "ui"): sig_frame.ui.cbProtoView.setCurrentIndex(prev_signal_frame.ui.cbProtoView.currentIndex()) sig_frame.blockSignals(True) if proto_analyzer.signal.qad_demod_file_loaded: sig_frame.ui.cbSignalView.setCurrentIndex(1) sig_frame.ui.cbSignalView.setDisabled(True) self.splitter.insertWidget(self.num_frames, sig_frame) sig_frame.blockSignals(False) default_view = constants.SETTINGS.value('default_view', 0, int) sig_frame.ui.cbProtoView.setCurrentIndex(default_view) return sig_frame def __create_connects_for_signal_frame(self, signal_frame: SignalFrameController): signal_frame.hold_shift = constants.SETTINGS.value('hold_shift_to_drag', False, type=bool) signal_frame.drag_started.connect(self.frame_dragged) signal_frame.frame_dropped.connect(self.frame_dropped) signal_frame.files_dropped.connect(self.on_files_dropped) signal_frame.closed.connect(self.close_frame) def add_empty_frame(self, filename: str, proto): sig_frame = SignalFrameController(proto_analyzer=proto, undo_stack=self.undo_stack, project_manager=self.project_manager, proto_bits=proto.decoded_proto_bits_str, parent=self) sig_frame.ui.lineEditSignalName.setText(filename) sig_frame.setMinimumHeight(sig_frame.height()) sig_frame.set_empty_frame_visibilities() self.__create_connects_for_signal_frame(signal_frame=sig_frame) self.splitter.insertWidget(self.num_frames, sig_frame) return sig_frame def set_frame_numbers(self): for i, f in enumerate(self.signal_frames): f.ui.lSignalNr.setText("{0:d}:".format(i + 1)) @pyqtSlot() def save_all(self): if self.num_frames == 0: return settings = constants.SETTINGS try: not_show = settings.value('not_show_save_dialog', type=bool, defaultValue=False) except TypeError: not_show = False if not not_show: cb = QCheckBox("Don't ask me again.") msg_box = QMessageBox(QMessageBox.Question, self.tr("Confirm saving all signals"), self.tr("All changed signal files will be overwritten. OK?")) msg_box.addButton(QMessageBox.Yes) msg_box.addButton(QMessageBox.No) msg_box.setCheckBox(cb) reply = msg_box.exec() not_show_again = cb.isChecked() settings.setValue("not_show_save_dialog", not_show_again) self.not_show_again_changed.emit() if reply != QMessageBox.Yes: return for f in self.signal_frames: if f.signal is None or f.signal.filename == "": continue f.signal.save() @pyqtSlot() def close_all(self): for f in self.signal_frames: f.my_close() @pyqtSlot(Signal) def on_apply_to_all_clicked(self, signal: Signal): for frame in self.signal_frames: if frame.signal is not None: frame.signal.noise_min_plot = signal.noise_min_plot frame.signal.noise_max_plot = signal.noise_max_plot frame.signal.block_protocol_update = True proto_needs_update = False if frame.signal.modulation_type != signal.modulation_type: frame.signal.modulation_type = signal.modulation_type proto_needs_update = True if frame.signal.qad_center != signal.qad_center: frame.signal.qad_center = signal.qad_center proto_needs_update = True if frame.signal.tolerance != signal.tolerance: frame.signal.tolerance = signal.tolerance proto_needs_update = True if frame.signal.noise_threshold != signal.noise_threshold: frame.signal.noise_threshold = signal.noise_threshold proto_needs_update = True if frame.signal.bit_len != signal.bit_len: frame.signal.bit_len = signal.bit_len proto_needs_update = True frame.signal.block_protocol_update = False if proto_needs_update: frame.signal.protocol_needs_update.emit() @pyqtSlot(QPoint) def frame_dragged(self, pos: QPoint): self.drag_pos = pos @pyqtSlot(QPoint) def frame_dropped(self, pos: QPoint): start = self.drag_pos if start is None: return end = pos start_index = -1 end_index = -1 if self.num_frames > 1: for i, w in enumerate(self.signal_frames): if w.geometry().contains(start): start_index = i if w.geometry().contains(end): end_index = i self.swap_frames(start_index, end_index) self.frame_was_dropped.emit(start_index, end_index) @pyqtSlot(int, int) def swap_frames(self, from_index: int, to_index: int): if from_index != to_index: start_sig_widget = self.splitter.widget(from_index) self.splitter.insertWidget(to_index, start_sig_widget) @pyqtSlot() def on_participant_changed(self): for sframe in self.signal_frames: sframe.on_participant_changed()
class SignalTabController(QWidget): frame_closed = pyqtSignal(SignalFrameController) not_show_again_changed = pyqtSignal() signal_created = pyqtSignal(Signal) files_dropped = pyqtSignal(list) frame_was_dropped = pyqtSignal(int, int) @property def num_frames(self): return self.splitter.count() - 1 @property def signal_frames(self): """ :rtype: list of SignalFrameController """ return [self.splitter.widget(i) for i in range(self.num_frames)] @property def signal_undo_stack(self): return self.undo_stack def __init__(self, project_manager, parent=None): super().__init__(parent) self.ui = Ui_Interpretation() self.ui.setupUi(self) self.splitter = QSplitter() self.splitter.setStyleSheet( "QSplitter::handle:vertical {\nmargin: 4px 0px; background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, \nstop:0 rgba(255, 255, 255, 0), \nstop:0.5 rgba(100, 100, 100, 100), \nstop:1 rgba(255, 255, 255, 0));\n image: url(:/icons/data/icons/splitter_handle.png);\n}" ) self.splitter.setOrientation(Qt.Vertical) self.splitter.setChildrenCollapsible(True) self.splitter.setHandleWidth(6) placeholder_widget = QWidget() placeholder_widget.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) self.undo_stack = QUndoStack() self.project_manager = project_manager self.splitter.addWidget(placeholder_widget) self.signal_vlay = QVBoxLayout() self.signal_vlay.setContentsMargins(0, 0, 0, 0) self.signal_vlay.addWidget(self.splitter) self.ui.scrlAreaSignals.setLayout(self.signal_vlay) self.drag_pos = None def on_files_dropped(self, files): self.files_dropped.emit(files) def close_frame(self, frame: SignalFrameController): self.frame_closed.emit(frame) def add_signal_frame(self, proto_analyzer): sig_frame = SignalFrameController(proto_analyzer, self.undo_stack, self.project_manager, parent=self) sframes = self.signal_frames if len(proto_analyzer.signal.filename) == 0: # new signal from "create signal from selection" sig_frame.ui.btnSaveSignal.show() self.__create_connects_for_signal_frame(signal_frame=sig_frame) sig_frame.signal_created.connect(self.signal_created.emit) sig_frame.not_show_again_changed.connect( self.not_show_again_changed.emit) sig_frame.ui.gvSignal.shift_state_changed.connect( self.set_shift_statuslabel) sig_frame.ui.lineEditSignalName.setToolTip( self.tr("Sourcefile: ") + proto_analyzer.signal.filename) sig_frame.apply_to_all_clicked.connect(self.on_apply_to_all_clicked) prev_signal_frame = sframes[-1] if len(sframes) > 0 else None if prev_signal_frame is not None and hasattr(prev_signal_frame, "ui"): sig_frame.ui.cbProtoView.setCurrentIndex( prev_signal_frame.ui.cbProtoView.currentIndex()) sig_frame.blockSignals(True) if proto_analyzer.signal.qad_demod_file_loaded: sig_frame.ui.cbSignalView.setCurrentIndex(1) sig_frame.ui.cbSignalView.setDisabled(True) self.splitter.insertWidget(self.num_frames, sig_frame) sig_frame.blockSignals(False) default_view = constants.SETTINGS.value('default_view', 0, int) sig_frame.ui.cbProtoView.setCurrentIndex(default_view) return sig_frame def __create_connects_for_signal_frame( self, signal_frame: SignalFrameController): signal_frame.hold_shift = constants.SETTINGS.value( 'hold_shift_to_drag', False, type=bool) signal_frame.drag_started.connect(self.frame_dragged) signal_frame.frame_dropped.connect(self.frame_dropped) signal_frame.files_dropped.connect(self.on_files_dropped) signal_frame.closed.connect(self.close_frame) def add_empty_frame(self, filename: str, proto): sig_frame = SignalFrameController( proto_analyzer=proto, undo_stack=self.undo_stack, project_manager=self.project_manager, proto_bits=proto.decoded_proto_bits_str, parent=self) sig_frame.ui.lineEditSignalName.setText(filename) sig_frame.setMinimumHeight(sig_frame.height()) sig_frame.set_empty_frame_visibilities() self.__create_connects_for_signal_frame(signal_frame=sig_frame) self.splitter.insertWidget(self.num_frames, sig_frame) return sig_frame def set_frame_numbers(self): for i, f in enumerate(self.signal_frames): f.ui.lSignalNr.setText("{0:d}:".format(i + 1)) @pyqtSlot() def save_all(self): if self.num_frames == 0: return settings = constants.SETTINGS try: not_show = settings.value('not_show_save_dialog', type=bool, defaultValue=False) except TypeError: not_show = False if not not_show: cb = QCheckBox("Don't ask me again.") msg_box = QMessageBox( QMessageBox.Question, self.tr("Confirm saving all signals"), self.tr("All changed signal files will be overwritten. OK?")) msg_box.addButton(QMessageBox.Yes) msg_box.addButton(QMessageBox.No) msg_box.setCheckBox(cb) reply = msg_box.exec() not_show_again = cb.isChecked() settings.setValue("not_show_save_dialog", not_show_again) self.not_show_again_changed.emit() if reply != QMessageBox.Yes: return for f in self.signal_frames: if f.signal is None or f.signal.filename == "": continue f.signal.save() @pyqtSlot() def close_all(self): for f in self.signal_frames: f.my_close() @pyqtSlot(Signal) def on_apply_to_all_clicked(self, signal: Signal): for frame in self.signal_frames: if frame.signal is not None: frame.signal.noise_min_plot = signal.noise_min_plot frame.signal.noise_max_plot = signal.noise_max_plot frame.signal.block_protocol_update = True proto_needs_update = False if frame.signal.modulation_type != signal.modulation_type: frame.signal.modulation_type = signal.modulation_type proto_needs_update = True if frame.signal.qad_center != signal.qad_center: frame.signal.qad_center = signal.qad_center proto_needs_update = True if frame.signal.tolerance != signal.tolerance: frame.signal.tolerance = signal.tolerance proto_needs_update = True if frame.signal.noise_threshold != signal.noise_threshold: frame.signal.noise_threshold = signal.noise_threshold proto_needs_update = True if frame.signal.bit_len != signal.bit_len: frame.signal.bit_len = signal.bit_len proto_needs_update = True frame.signal.block_protocol_update = False if proto_needs_update: frame.signal.protocol_needs_update.emit() @pyqtSlot(QPoint) def frame_dragged(self, pos: QPoint): self.drag_pos = pos @pyqtSlot(QPoint) def frame_dropped(self, pos: QPoint): start = self.drag_pos if start is None: return end = pos start_index = -1 end_index = -1 if self.num_frames > 1: for i, w in enumerate(self.signal_frames): if w.geometry().contains(start): start_index = i if w.geometry().contains(end): end_index = i self.swap_frames(start_index, end_index) self.frame_was_dropped.emit(start_index, end_index) @pyqtSlot(int, int) def swap_frames(self, from_index: int, to_index: int): if from_index != to_index: start_sig_widget = self.splitter.widget(from_index) self.splitter.insertWidget(to_index, start_sig_widget) @pyqtSlot(bool) def set_shift_statuslabel(self, shift_pressed): if shift_pressed and constants.SETTINGS.value( 'hold_shift_to_drag', False, type=bool): self.ui.lShiftStatus.setText("[SHIFT] Use Mouse to scroll signal.") elif shift_pressed and not constants.SETTINGS.value( 'hold_shift_to_drag', False, type=bool): self.ui.lShiftStatus.setText( "[SHIFT] Use mouse to create a selection.") else: self.ui.lShiftStatus.clear() @pyqtSlot() def on_participant_changed(self): for sframe in self.signal_frames: sframe.on_participant_changed()
class CenterUI(QWidget): def __init__(self): super().__init__() self.__initVariable() self.__initUI() def __initUI(self): self.tabwidget = QTabWidget() self.fileViewer = FileListView() #图片列表 self.imageViewer = ImageView() #显示图像 self.tabwidget.addTab(self.fileViewer, '图片列表') self.tabwidget.setTabPosition(QTabWidget.South) self.splitter = QSplitter(Qt.Horizontal) self.splitter.addWidget(self.tabwidget) self.splitter.addWidget(self.imageViewer) #设置初始每个Widget的大小 width = self.width() ratio = [0.2, 0.8] self.splitterSize = [width * ratio[0], width * ratio[1]] self.__setSplitterSize() hbox = QHBoxLayout() hbox.addWidget(self.splitter) self.setLayout(hbox) self.fileViewer.selectImage.connect(self.__showImage) self.splitter.splitterMoved.connect(self.__getSplitterSize) def __initVariable(self): # self.currentImageFile=None #0普通模式 #1控制点模式 #2圆模式 self.Mode = 0 self.pLister = None self.cLister = None def __setSplitterSize(self): self.splitter.setSizes(self.splitterSize) def __getSplitterSize(self, pos, index): self.splitterSize = self.splitter.sizes() #设置模式 def setMode(self, mode): self.Mode = mode self.imageViewer.clearImage() def addPointListView(self): if self.Mode != 1: return num = self.tabwidget.count() if num == 2: self.tabwidget.removeTab(1) self.pLister = PointsListView() self.tabwidget.addTab(self.pLister, '控制点列表') def deletePointListView(self): if self.Mode == 1: raise RuntimeError('can not delete PointsListView when it working') num = self.tabwidget.count() if num == 2: self.tabwidget.removeTab(1) self.pLister = None def addCircleListView(self): if self.Mode != 2: return num = self.tabwidget.count() if num == 2: self.tabwidget.removeTab(1) self.cLister = CirclesListView() self.tabwidget.addTab(self.cLister, '基桩列表') def deleteCircleListView(self): if self.Mode == 2: raise RuntimeError( 'can not delete CirclesListView when it working') num = self.tabwidget.count() if num == 2: self.tabwidget.removeTab(1) self.cLister = None #显示图像 def __showImage(self, idx, file): if self.Mode == 0: self.__showNormalImage(file) elif self.Mode == 1: self.__showImageCPoints(idx, file) elif self.Mode == 2: self.__showImageCircles(idx, file) def __showNormalImage(self, file): if not os.path.exists(file): QMessageBox.information(self, '失败', '图像不存在,请检查路径') return # self.currentImageFile=file frame = Image.open(file) img = ImageQt.ImageQt(frame).convertToFormat(QImage.Format_RGB888) if type(self.imageViewer) is not ImageView: #删除掉旧控件 viewer = self.splitter.widget(1) viewer.deleteLater() #设置当前显示图像的控件 del self.imageViewer self.imageViewer = ImageView() self.splitter.insertWidget(1, self.imageViewer) self.__setSplitterSize() self.imageViewer.clearImage() self.imageViewer.setImage(img) def showImportCircle(self, names, loaction, radius): if len(names) == 0: return if type(self.imageViewer) is not ImageShowCircle: #删除掉旧控件 viewer = self.splitter.widget(1) # viewer.setParent(self) viewer.deleteLater() #设置当前显示图像的控件 del self.imageViewer self.imageViewer = ImageShowCircle() self.splitter.insertWidget(1, self.imageViewer) self.__setSplitterSize() self.imageViewer.setCircles(names, loaction, radius) def __showImageCPoints(self, idx, file): if not os.path.exists(file): return #读取图像 frame = Image.open(file) img = ImageQt.ImageQt(frame).convertToFormat(QImage.Format_RGB888) #删除掉旧控件 viewer = self.splitter.widget(1) viewer.deleteLater() #设置当前显示图像的控件 del self.imageViewer self.imageViewer = ImagePoints() self.splitter.insertWidget(1, self.imageViewer) self.__setSplitterSize() self.imageViewer.setImage(idx, img) def __showImageCircles(self, idx, file): if not os.path.exists(file): return #读取图像 frame = Image.open(file) img = ImageQt.ImageQt(frame).convertToFormat(QImage.Format_RGB888) #删除掉旧控件 viewer = self.splitter.widget(1) viewer.deleteLater() #设置当前显示图像的控件 del self.imageViewer self.imageViewer = ImageCircles() self.splitter.insertWidget(1, self.imageViewer) self.__setSplitterSize() self.imageViewer.setImageWithIdx(idx, img) def showImportComposite(self): #删除掉旧控件 viewer = self.splitter.widget(1) # viewer.setParent(self) viewer.deleteLater() #设置当前显示图像的控件 del self.imageViewer self.imageViewer = ImageComposite() self.splitter.insertWidget(1, self.imageViewer) self.__setSplitterSize()
class HelpDialog(QObject, LogMixin): """Class implementing qthelp viewer dialog""" def __init__(self, qthelp_file, parent = None): """ Constructor of HelpDialog :param qthelp_file: full path to qthelp helpfile """ super(HelpDialog,self).__init__(parent) # instantiate help engine helpEngine = QHelpEngine(qthelp_file) helpEngine.setupData() self._helpEngine = helpEngine # base dialog widget self.ui = QDialog(None, QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowMinMaxButtonsHint | QtCore.Qt.WindowCloseButtonHint ) self.ui.setWindowTitle("HelpViewer") self.ui.setWindowIcon(QIcon(":/images/prog_icons/help/help.ico")) # Create webview for help information # and assign a custom URL scheme handler for scheme "qthelp) self._wv = QWebEngineView(self.ui) self._urlschemehandler = HelpSchemeHandler(self._helpEngine, self._wv.page().profile()) self._wv.page().profile().installUrlSchemeHandler(b'qthelp', self._urlschemehandler) # get help content overview widget self._helpContent = self._helpEngine.contentWidget() self._helpIndex = self._helpEngine.indexWidget() self._helpSearchQuery = self._helpEngine.searchEngine().queryWidget() self._helpSearchResult = self._helpEngine.searchEngine().resultWidget() self._se = self._helpEngine.searchEngine() self._se.reindexDocumentation() self._helpSearchQuery.search.connect(self.search) # create QSplitter self._splitterMain = QSplitter(QtCore.Qt.Vertical) self._splitterMain.setOpaqueResize(False) self._splitterSearch = QSplitter(QtCore.Qt.Horizontal) self._splitterSearch.setOpaqueResize(False) self._splitterUpper = QSplitter(QtCore.Qt.Horizontal) self._splitterUpper.setOpaqueResize(False) self._splitterLower = QSplitter(QtCore.Qt.Horizontal) self._splitterLower.setOpaqueResize(False) # create horzLayout self._horzLayoutSearch = QHBoxLayout() self._horzLayoutUpper = QHBoxLayout() self._horzLayoutLower = QHBoxLayout() # create vertLayout self._vertLayout = QVBoxLayout() # main widgets self._upperWidget = QWidget() self._lowerWidget = QWidget() self._btnReset = QPushButton() self._btnReset.setMaximumHeight(23) self._btnReset.setMaximumWidth(100) # build search structure self._splitterSearch.insertWidget(0, self._helpSearchQuery) self._splitterSearch.insertWidget(1, self._btnReset) # build upper inner structure self._splitterUpper.insertWidget(0, self._helpContent) self._splitterUpper.insertWidget(1, self._wv) self._horzLayoutUpper.addWidget(self._splitterUpper) self._upperWidget.setLayout(self._horzLayoutUpper) # build lower inner structure self._splitterLower.insertWidget(0, self._helpIndex) self._splitterLower.insertWidget(1, self._helpSearchResult) self._horzLayoutLower.addWidget(self._splitterLower) self._lowerWidget.setLayout(self._horzLayoutLower) # build outer structure self._splitterMain.insertWidget(0, self._splitterSearch) self._splitterMain.insertWidget(1, self._upperWidget) self._splitterMain.insertWidget(2, self._lowerWidget) self._helpSearchResult.hide() self._btnReset.hide() self._vertLayout.addWidget(self._splitterMain) self.ui.setLayout(self._vertLayout) # set splitter width w = self._splitterUpper.geometry().width() self._splitterUpper.setSizes([w*(1/4), w*(3/4)]) w = self._splitterLower.geometry().width() self._splitterLower.setSizes([w*(1/5), w*(4/5)]) h = self._splitterMain.geometry().height() self._splitterMain.setSizes([h*(1/9), h*(7/9), h*(1/9)]) self._helpContent.linkActivated.connect(self._wv.setUrl) self._helpIndex.linkActivated.connect(self._wv.setUrl) self._helpSearchResult.requestShowLink.connect(self._wv.setUrl) self._se.searchingFinished.connect(self.showResults) self._btnReset.clicked.connect(self.resetResult) self.retranslateMsg() def retranslateMsg(self): self.logger.debug("Retranslating further messages...") self._btnReset.setText(translate("HelpViewer", "Reset")) self._btnReset.setText(translate("HelpViewer", "Search")) def search(self): """Initiate qthelp search""" self._se.search(self._helpSearchQuery.query()) def showResults(self): """Show search results, if any""" if self._se.hitCount() > 0: self._helpIndex.hide() h = self._splitterMain.geometry().height() self._splitterMain.setSizes([h*(1/3), h*(1/3), h*(1/3)]) self._helpSearchResult.show() self._btnReset.show() def resetResult(self): """Reset search result widget""" self._helpSearchResult.hide() self._btnReset.hide() self._helpIndex.show() h = self._splitterMain.geometry().height() self._splitterMain.setSizes([h*(1/9), h*(7/9), h*(1/9)])
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 MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self._version = "0.1.20" self.setWindowIcon(QIcon("GUI/icons/logo.png")) self.setWindowTitle("Tasmota Device Manager {}".format(self._version)) self.main_splitter = QSplitter() self.devices_splitter = QSplitter(Qt.Vertical) self.mqtt_queue = [] self.devices = {} self.fulltopic_queue = [] old_settings = QSettings() self.settings = QSettings("{}/TDM/tdm.cfg".format(QDir.homePath()), QSettings.IniFormat) self.setMinimumSize(QSize(1280, 800)) for k in old_settings.allKeys(): self.settings.setValue(k, old_settings.value(k)) old_settings.remove(k) self.device_model = TasmotaDevicesModel() self.telemetry_model = TasmotaDevicesTree() self.console_model = ConsoleModel() self.sorted_console_model = QSortFilterProxyModel() self.sorted_console_model.setSourceModel(self.console_model) self.sorted_console_model.setFilterKeyColumn(CnsMdl.FRIENDLY_NAME) self.setup_mqtt() self.setup_telemetry_view() self.setup_main_layout() self.add_devices_tab() self.build_toolbars() self.setStatusBar(QStatusBar()) self.queue_timer = QTimer() self.queue_timer.timeout.connect(self.mqtt_publish_queue) self.queue_timer.start(500) self.auto_timer = QTimer() self.auto_timer.timeout.connect(self.autoupdate) self.load_window_state() if self.settings.value("connect_on_startup", False, bool): self.actToggleConnect.trigger() def setup_main_layout(self): self.mdi = QMdiArea() self.mdi.setActivationOrder(QMdiArea.ActivationHistoryOrder) self.mdi.setViewMode(QMdiArea.TabbedView) self.mdi.setDocumentMode(True) mdi_widget = QWidget() mdi_widget.setLayout(VLayout()) mdi_widget.layout().addWidget(self.mdi) self.devices_splitter.addWidget(mdi_widget) vl_console = VLayout() hl_filter = HLayout() self.cbFilter = QCheckBox("Console filtering") self.cbxFilterDevice = QComboBox() self.cbxFilterDevice.setEnabled(False) self.cbxFilterDevice.setFixedWidth(200) self.cbxFilterDevice.setModel(self.device_model) self.cbxFilterDevice.setModelColumn(DevMdl.FRIENDLY_NAME) hl_filter.addWidgets([self.cbFilter, self.cbxFilterDevice]) hl_filter.addStretch(0) vl_console.addLayout(hl_filter) self.console_view = TableView() self.console_view.setModel(self.console_model) self.console_view.setupColumns(columns_console) self.console_view.setAlternatingRowColors(True) self.console_view.verticalHeader().setDefaultSectionSize(20) self.console_view.setMinimumHeight(200) vl_console.addWidget(self.console_view) console_widget = QWidget() console_widget.setLayout(vl_console) self.devices_splitter.addWidget(console_widget) self.main_splitter.insertWidget(0, self.devices_splitter) self.setCentralWidget(self.main_splitter) self.console_view.clicked.connect(self.select_cons_entry) self.console_view.doubleClicked.connect(self.view_payload) self.cbFilter.toggled.connect(self.toggle_console_filter) self.cbxFilterDevice.currentTextChanged.connect( self.select_console_filter) def setup_telemetry_view(self): tele_widget = QWidget() vl_tele = VLayout() self.tview = QTreeView() self.tview.setMinimumWidth(300) self.tview.setModel(self.telemetry_model) self.tview.setAlternatingRowColors(True) self.tview.setUniformRowHeights(True) self.tview.setIndentation(15) self.tview.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)) self.tview.expandAll() self.tview.resizeColumnToContents(0) vl_tele.addWidget(self.tview) tele_widget.setLayout(vl_tele) self.main_splitter.addWidget(tele_widget) def setup_mqtt(self): self.mqtt = MqttClient() self.mqtt.connecting.connect(self.mqtt_connecting) self.mqtt.connected.connect(self.mqtt_connected) self.mqtt.disconnected.connect(self.mqtt_disconnected) self.mqtt.connectError.connect(self.mqtt_connectError) self.mqtt.messageSignal.connect(self.mqtt_message) def add_devices_tab(self): tabDevicesList = DevicesListWidget(self) self.mdi.addSubWindow(tabDevicesList) tabDevicesList.setWindowState(Qt.WindowMaximized) def load_window_state(self): wndGeometry = self.settings.value('window_geometry') if wndGeometry: self.restoreGeometry(wndGeometry) spltState = self.settings.value('splitter_state') if spltState: self.main_splitter.restoreState(spltState) def build_toolbars(self): main_toolbar = Toolbar(orientation=Qt.Horizontal, iconsize=16, label_position=Qt.ToolButtonTextBesideIcon) main_toolbar.setObjectName("main_toolbar") self.addToolBar(main_toolbar) main_toolbar.addAction(QIcon("./GUI/icons/connections.png"), "Broker", self.setup_broker) self.actToggleConnect = QAction(QIcon("./GUI/icons/disconnect.png"), "MQTT") self.actToggleConnect.setCheckable(True) self.actToggleConnect.toggled.connect(self.toggle_connect) main_toolbar.addAction(self.actToggleConnect) self.actToggleAutoUpdate = QAction(QIcon("./GUI/icons/automatic.png"), "Auto telemetry") self.actToggleAutoUpdate.setCheckable(True) self.actToggleAutoUpdate.toggled.connect(self.toggle_autoupdate) main_toolbar.addAction(self.actToggleAutoUpdate) main_toolbar.addSeparator() main_toolbar.addAction(QIcon("./GUI/icons/bssid.png"), "BSSId", self.bssid) main_toolbar.addAction(QIcon("./GUI/icons/export.png"), "Export list", self.export) def initial_query(self, idx, queued=False): for q in initial_queries: topic = "{}status".format(self.device_model.commandTopic(idx)) if queued: self.mqtt_queue.append([topic, q]) else: self.mqtt.publish(topic, q, 1) self.console_log(topic, "Asked for STATUS {}".format(q), q) def setup_broker(self): brokers_dlg = BrokerDialog() if brokers_dlg.exec_( ) == QDialog.Accepted and self.mqtt.state == self.mqtt.Connected: self.mqtt.disconnect() def toggle_autoupdate(self, state): if state: self.auto_timer.setInterval(5000) self.auto_timer.start() def toggle_connect(self, state): if state and self.mqtt.state == self.mqtt.Disconnected: self.broker_hostname = self.settings.value('hostname', 'localhost') self.broker_port = self.settings.value('port', 1883, int) self.broker_username = self.settings.value('username') self.broker_password = self.settings.value('password') self.mqtt.hostname = self.broker_hostname self.mqtt.port = self.broker_port if self.broker_username: self.mqtt.setAuth(self.broker_username, self.broker_password) self.mqtt.connectToHost() elif not state and self.mqtt.state == self.mqtt.Connected: self.mqtt_disconnect() def autoupdate(self): if self.mqtt.state == self.mqtt.Connected: for d in range(self.device_model.rowCount()): idx = self.device_model.index(d, 0) cmnd = self.device_model.commandTopic(idx) self.mqtt.publish(cmnd + "STATUS", payload=8) def mqtt_connect(self): self.broker_hostname = self.settings.value('hostname', 'localhost') self.broker_port = self.settings.value('port', 1883, int) self.broker_username = self.settings.value('username') self.broker_password = self.settings.value('password') self.mqtt.hostname = self.broker_hostname self.mqtt.port = self.broker_port if self.broker_username: self.mqtt.setAuth(self.broker_username, self.broker_password) if self.mqtt.state == self.mqtt.Disconnected: self.mqtt.connectToHost() def mqtt_disconnect(self): self.mqtt.disconnectFromHost() def mqtt_connecting(self): self.statusBar().showMessage("Connecting to broker") def mqtt_connected(self): self.actToggleConnect.setIcon(QIcon("./GUI/icons/connect.png")) self.statusBar().showMessage("Connected to {}:{} as {}".format( self.broker_hostname, self.broker_port, self.broker_username if self.broker_username else '[anonymous]')) self.mqtt_subscribe() for d in range(self.device_model.rowCount()): idx = self.device_model.index(d, 0) self.initial_query(idx) def mqtt_subscribe(self): main_topics = ["+/stat/+", "+/tele/+", "stat/#", "tele/#"] for d in range(self.device_model.rowCount()): idx = self.device_model.index(d, 0) if not self.device_model.isDefaultTemplate(idx): main_topics.append(self.device_model.commandTopic(idx)) main_topics.append(self.device_model.statTopic(idx)) for t in main_topics: self.mqtt.subscribe(t) def mqtt_publish_queue(self): for q in self.mqtt_queue: t, p = q self.mqtt.publish(t, p) self.mqtt_queue.pop(self.mqtt_queue.index(q)) def mqtt_disconnected(self): self.actToggleConnect.setIcon(QIcon("./GUI/icons/disconnect.png")) self.statusBar().showMessage("Disconnected") def mqtt_connectError(self, rc): reason = { 1: "Incorrect protocol version", 2: "Invalid client identifier", 3: "Server unavailable", 4: "Bad username or password", 5: "Not authorized", } self.statusBar().showMessage("Connection error: {}".format(reason[rc])) self.actToggleConnect.setChecked(False) def mqtt_message(self, topic, msg): found = self.device_model.findDevice(topic) if found.reply == 'LWT': if not msg: msg = "offline" if found.index.isValid(): self.console_log(topic, "LWT update: {}".format(msg), msg) self.device_model.updateValue(found.index, DevMdl.LWT, msg) self.initial_query(found.index, queued=True) elif msg == "Online": self.console_log( topic, "LWT for unknown device '{}'. Asking for FullTopic.". format(found.topic), msg, False) self.mqtt_queue.append( ["cmnd/{}/fulltopic".format(found.topic), ""]) self.mqtt_queue.append( ["{}/cmnd/fulltopic".format(found.topic), ""]) elif found.reply == 'RESULT': try: full_topic = loads(msg).get('FullTopic') new_topic = loads(msg).get('Topic') template_name = loads(msg).get('NAME') ota_url = loads(msg).get('OtaUrl') teleperiod = loads(msg).get('TelePeriod') if full_topic: # TODO: update FullTopic for existing device AFTER the FullTopic changes externally (the message will arrive from new FullTopic) if not found.index.isValid(): self.console_log( topic, "FullTopic for {}".format(found.topic), msg, False) new_idx = self.device_model.addDevice(found.topic, full_topic, lwt='online') tele_idx = self.telemetry_model.addDevice( TasmotaDevice, found.topic) self.telemetry_model.devices[found.topic] = tele_idx #TODO: add QSortFilterProxyModel to telemetry treeview and sort devices after adding self.initial_query(new_idx) self.console_log( topic, "Added {} with fulltopic {}, querying for STATE". format(found.topic, full_topic), msg) self.tview.expand(tele_idx) self.tview.resizeColumnToContents(0) elif new_topic: if found.index.isValid() and found.topic != new_topic: self.console_log( topic, "New topic for {}".format(found.topic), msg) self.device_model.updateValue(found.index, DevMdl.TOPIC, new_topic) tele_idx = self.telemetry_model.devices.get( found.topic) if tele_idx: self.telemetry_model.setDeviceName( tele_idx, new_topic) self.telemetry_model.devices[ new_topic] = self.telemetry_model.devices.pop( found.topic) elif template_name: self.device_model.updateValue( found.index, DevMdl.MODULE, "{} (0)".format(template_name)) elif ota_url: self.device_model.updateValue(found.index, DevMdl.OTA_URL, ota_url) elif teleperiod: self.device_model.updateValue(found.index, DevMdl.TELEPERIOD, teleperiod) except JSONDecodeError as e: self.console_log( topic, "JSON payload decode error. Check error.log for additional info." ) with open("{}/TDM/error.log".format(QDir.homePath()), "a+") as l: l.write("{}\t{}\t{}\t{}\n".format( QDateTime.currentDateTime().toString( "yyyy-MM-dd hh:mm:ss"), topic, msg, e.msg)) elif found.index.isValid(): ok = False try: if msg.startswith("{"): payload = loads(msg) else: payload = msg ok = True except JSONDecodeError as e: self.console_log( topic, "JSON payload decode error. Check error.log for additional info." ) with open("{}/TDM/error.log".format(QDir.homePath()), "a+") as l: l.write("{}\t{}\t{}\t{}\n".format( QDateTime.currentDateTime().toString( "yyyy-MM-dd hh:mm:ss"), topic, msg, e.msg)) if ok: try: if found.reply == 'STATUS': self.console_log(topic, "Received device status", msg) payload = payload['Status'] self.device_model.updateValue( found.index, DevMdl.FRIENDLY_NAME, payload['FriendlyName'][0]) self.telemetry_model.setDeviceFriendlyName( self.telemetry_model.devices[found.topic], payload['FriendlyName'][0]) module = payload['Module'] if module == 0: self.mqtt.publish( self.device_model.commandTopic(found.index) + "template") else: self.device_model.updateValue( found.index, DevMdl.MODULE, modules.get(module, 'Unknown')) self.device_model.updateValue(found.index, DevMdl.MODULE_ID, module) elif found.reply == 'STATUS1': self.console_log(topic, "Received program information", msg) payload = payload['StatusPRM'] self.device_model.updateValue( found.index, DevMdl.RESTART_REASON, payload.get('RestartReason')) self.device_model.updateValue(found.index, DevMdl.OTA_URL, payload.get('OtaUrl')) elif found.reply == 'STATUS2': self.console_log(topic, "Received firmware information", msg) payload = payload['StatusFWR'] self.device_model.updateValue(found.index, DevMdl.FIRMWARE, payload['Version']) self.device_model.updateValue(found.index, DevMdl.CORE, payload['Core']) elif found.reply == 'STATUS3': self.console_log(topic, "Received syslog information", msg) payload = payload['StatusLOG'] self.device_model.updateValue(found.index, DevMdl.TELEPERIOD, payload['TelePeriod']) elif found.reply == 'STATUS5': self.console_log(topic, "Received network status", msg) payload = payload['StatusNET'] self.device_model.updateValue(found.index, DevMdl.MAC, payload['Mac']) self.device_model.updateValue(found.index, DevMdl.IP, payload['IPAddress']) elif found.reply in ('STATE', 'STATUS11'): self.console_log(topic, "Received device state", msg) if found.reply == 'STATUS11': payload = payload['StatusSTS'] self.parse_state(found.index, payload) elif found.reply in ('SENSOR', 'STATUS8'): self.console_log(topic, "Received telemetry", msg) if found.reply == 'STATUS8': payload = payload['StatusSNS'] self.parse_telemetry(found.index, payload) elif found.reply.startswith('POWER'): self.console_log( topic, "Received {} state".format(found.reply), msg) payload = {found.reply: msg} self.parse_power(found.index, payload) except KeyError as k: self.console_log( topic, "JSON key error. Check error.log for additional info.") with open("{}/TDM/error.log".format(QDir.homePath()), "a+") as l: l.write("{}\t{}\t{}\tKeyError: {}\n".format( QDateTime.currentDateTime().toString( "yyyy-MM-dd hh:mm:ss"), topic, payload, k.args[0])) def parse_power(self, index, payload, from_state=False): old = self.device_model.power(index) power = { k: payload[k] for k in payload.keys() if k.startswith("POWER") } # TODO: fix so that number of relays get updated properly after module/no. of relays change needs_update = False if old: # if from_state and len(old) != len(power): # needs_update = True # # else: for k in old.keys(): needs_update |= old[k] != power.get(k, old[k]) if needs_update: break else: needs_update = True if needs_update: self.device_model.updateValue(index, DevMdl.POWER, power) def parse_state(self, index, payload): bssid = payload['Wifi'].get('BSSId') if not bssid: bssid = payload['Wifi'].get('APMac') self.device_model.updateValue(index, DevMdl.BSSID, bssid) self.device_model.updateValue(index, DevMdl.SSID, payload['Wifi']['SSId']) self.device_model.updateValue(index, DevMdl.CHANNEL, payload['Wifi'].get('Channel', "n/a")) self.device_model.updateValue(index, DevMdl.RSSI, payload['Wifi']['RSSI']) self.device_model.updateValue(index, DevMdl.UPTIME, payload['Uptime']) self.device_model.updateValue(index, DevMdl.LOADAVG, payload.get('LoadAvg')) self.device_model.updateValue(index, DevMdl.LINKCOUNT, payload['Wifi'].get('LinkCount', "n/a")) self.device_model.updateValue(index, DevMdl.DOWNTIME, payload['Wifi'].get('Downtime', "n/a")) self.parse_power(index, payload, True) tele_idx = self.telemetry_model.devices.get( self.device_model.topic(index)) if tele_idx: tele_device = self.telemetry_model.getNode(tele_idx) self.telemetry_model.setDeviceFriendlyName( tele_idx, self.device_model.friendly_name(index)) pr = tele_device.provides() for k in pr.keys(): self.telemetry_model.setData(pr[k], payload.get(k)) def parse_telemetry(self, index, payload): device = self.telemetry_model.devices.get( self.device_model.topic(index)) if device: node = self.telemetry_model.getNode(device) time = node.provides()['Time'] if 'Time' in payload: self.telemetry_model.setData(time, payload.pop('Time')) temp_unit = "C" pres_unit = "hPa" if 'TempUnit' in payload: temp_unit = payload.pop('TempUnit') if 'PressureUnit' in payload: pres_unit = payload.pop('PressureUnit') for sensor in sorted(payload.keys()): if sensor == 'DS18x20': for sns_name in payload[sensor].keys(): d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice( DS18x20, payload[sensor][sns_name]['Type'], device) self.telemetry_model.getNode(d).setTempUnit(temp_unit) payload[sensor][sns_name]['Id'] = payload[sensor][ sns_name].pop('Address') pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData( pr[pk], payload[sensor][sns_name].get(pk)) self.tview.expand(d) elif sensor.startswith('DS18B20'): d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice( DS18x20, sensor, device) self.telemetry_model.getNode(d).setTempUnit(temp_unit) pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) if sensor == 'COUNTER': d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice( CounterSns, "Counter", device) pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) else: d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice( sensor_map.get(sensor, Node), sensor, device) pr = self.telemetry_model.getNode(d).provides() if 'Temperature' in pr: self.telemetry_model.getNode(d).setTempUnit(temp_unit) if 'Pressure' in pr or 'SeaPressure' in pr: self.telemetry_model.getNode(d).setPresUnit(pres_unit) for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) # self.tview.resizeColumnToContents(0) def console_log(self, topic, description, payload="", known=True): longest_tp = 0 longest_fn = 0 short_topic = "/".join(topic.split("/")[0:-1]) fname = self.devices.get(short_topic, "") if not fname: device = self.device_model.findDevice(topic) fname = self.device_model.friendly_name(device.index) self.devices.update({short_topic: fname}) self.console_model.addEntry(topic, fname, description, payload, known) if len(topic) > longest_tp: longest_tp = len(topic) self.console_view.resizeColumnToContents(1) if len(fname) > longest_fn: longest_fn = len(fname) self.console_view.resizeColumnToContents(1) def view_payload(self, idx): if self.cbFilter.isChecked(): idx = self.sorted_console_model.mapToSource(idx) row = idx.row() timestamp = self.console_model.data( self.console_model.index(row, CnsMdl.TIMESTAMP)) topic = self.console_model.data( self.console_model.index(row, CnsMdl.TOPIC)) payload = self.console_model.data( self.console_model.index(row, CnsMdl.PAYLOAD)) dlg = PayloadViewDialog(timestamp, topic, payload) dlg.exec_() def select_cons_entry(self, idx): self.cons_idx = idx def export(self): fname, _ = QFileDialog.getSaveFileName(self, "Export device list as...", directory=QDir.homePath(), filter="CSV files (*.csv)") if fname: if not fname.endswith(".csv"): fname += ".csv" with open(fname, "w", encoding='utf8') as f: column_titles = [ 'mac', 'topic', 'friendly_name', 'full_topic', 'cmnd_topic', 'stat_topic', 'tele_topic', 'module', 'module_id', 'firmware', 'core' ] c = csv.writer(f) c.writerow(column_titles) for r in range(self.device_model.rowCount()): d = self.device_model.index(r, 0) c.writerow([ self.device_model.mac(d), self.device_model.topic(d), self.device_model.friendly_name(d), self.device_model.fullTopic(d), self.device_model.commandTopic(d), self.device_model.statTopic(d), self.device_model.teleTopic(d), modules.get(self.device_model.module(d)), self.device_model.module(d), self.device_model.firmware(d), self.device_model.core(d) ]) def bssid(self): BSSIdDialog().exec_() # if dlg.exec_() == QDialog.Accepted: def toggle_console_filter(self, state): self.cbxFilterDevice.setEnabled(state) if state: self.console_view.setModel(self.sorted_console_model) else: self.console_view.setModel(self.console_model) def select_console_filter(self, fname): self.sorted_console_model.setFilterFixedString(fname) def closeEvent(self, e): self.settings.setValue("window_geometry", self.saveGeometry()) self.settings.setValue("splitter_state", self.main_splitter.saveState()) self.settings.sync() e.accept()
class QuantumGui(QMainWindow): def __init__(self): super().__init__() # The current loaded set self.currentSet = 1 #self.background = cv2.imread("test2.png", cv2.IMREAD_GRAYSCALE) # FOR TESTING self.title = 'Imaging' self.left = 5 self.top = 5 self.width = 800 self.height = 500 self.images = list() # [0, 1, 2, 3] // [0] = Big image - [1] to [3] = Smaller images self.imageLabels = list() # [0, 1, 2, 3] // [0] = Big image - [1] to [3] = Smaller images ---- These are PyQt5 Labels that gets updated. # Initializing the UI, loading images and rendering the objects self.initUI() # QTimer used for updating the images with a given time interval self.timer = QTimer() self.timer.timeout.connect(self.update) # Means that this timer will be calling the 'self.update()' method self.timer.start(settings.guiUpdateInterval) # Starts the timer and updates it at a given interval def initUI(self): # Assigning the title of the window self.setWindowTitle(self.title) # Assigning dimensions of this object self.setGeometry(self.left, self.top, self.width, self.height) # Basically initializing all image loading as well as rendering it self.createGridLayout(self.currentSet) self.show() """ This function loads the set of images specified, and renders them to the screen (It's only to be used when initiating the class) """ def createGridLayout(self, set): # Load the given set self.loadSet(set) # The grid self.hbox = QHBoxLayout() # Object that contains all the stuff to be displayed self.central_widget = QWidget() self.central_widget.setLayout(self.hbox) self.setCentralWidget(self.central_widget) # Left side = Big Image self.left = QSplitter(Qt.QtCore.Qt.Vertical) # Right side = 3 Small images self.right = QSplitter(Qt.QtCore.Qt.Vertical) # Puts the images onto the proper splitters for i in range(len(self.imageLabels)): label = self.imageLabels[i] if i == 3: # Big image is on index 3 in the images list print("Adding left side: ",label) self.left.addWidget(label) self.addToolBar(Qt.QtCore.Qt.BottomToolBarArea,NavigationToolbar(label, self)) else: # The smaller images are on index 0 through 2 in the images list print("Adding right side: ",label) self.right.addWidget(label) # Splitter is horizontal, first adding left side, then the right side self.splitter = QSplitter(Qt.QtCore.Qt.Horizontal) self.splitter.addWidget(self.left) self.splitter.addWidget(self.right) self.container = QSplitter(Qt.QtCore.Qt.Vertical) self.container.addWidget(self.splitter) # Adding the splitter, containg left and right sides, to the main container self.hbox.addWidget(self.container) """ This function loads the set of images specified and updates the image and image labels """ def loadSet(self, set): # Clear the old data self.images.clear() self.imageLabels.clear() # Dimensions of the window width = self.geometry().width() height = self.geometry().height() for i in range(settings.snapsPerSet): # Path to the image path = "img/snap_{},{}.tiff".format(self.currentSet, i+1) # Inserting the image object into the image list self.images.append(Image(path, int(width/3), int(height / 3 - 105))) print(path,":",self.images[i-1].cvimg) # Inserting the image (With text) into the list for image labels figure = showImage(self.images[i].cvimg, False) label = FigureCanvas(figure) self.imageLabels.append(label) label = FigureCanvas(showImage(opticalImageDepth(self.images[0].cvimg, self.images[1].cvimg, self.images[2].cvimg), True)) self.imageLabels.append(label) def update(self): if not settings.updateGui: return width = self.geometry().width() height = self.geometry().height() print(width,height) print("Updated") self.loadSet(self.currentSet) # Hide the old images (Because that's just how PyQt5 works) for c in self.left.children(): c.hide() for c in self.right.children(): c.hide() # Loop through newly loaded images, and insert them above the hidden old images for i in range(len(self.imageLabels)): label = self.imageLabels[i] if i == 3: #label.setPixmap(self.images[i].getPixmap().scaledToHeight(height - 105)) # Should be relative to the window size self.left.insertWidget(i, label) else: #label.setPixmap(self.images[i].getPixmap().scaledToHeight(height / 3 - 105)) # Should be relative to the window size self.right.insertWidget(i-1, label) # Update the containers self.left.update() self.right.update() self.splitter.update() self.container.update() self.hbox.update() settings.updateGui = False self.currentSet += 1
class _s_CentralWidget(QWidget): ############################################################################### # CentralWidget SIGNALS ############################################################################### """ splitterCentralRotated() """ splitterCentralRotated = pyqtSignal() ############################################################################### def __init__(self, parent=None): super(_s_CentralWidget, self).__init__(parent) self.parent = parent #This variables are used to save the splitter sizes before hide self._splitterMainSizes = None self._splitterAreaSizes = None self.lateralPanel = None hbox = QHBoxLayout(self) hbox.setContentsMargins(0, 0, 0, 0) hbox.setSpacing(0) #Create Splitters to divide the UI in: MainPanel, Explorer, Misc self._splitterArea = QSplitter(Qt.Horizontal) self._splitterMain = QSplitter(Qt.Vertical) #Create scrollbar for follow mode self.scrollBar = QScrollBar(Qt.Vertical, self) self.scrollBar.setFixedWidth(20) self.scrollBar.setToolTip('Follow Mode: Scroll the Editors together') self.scrollBar.hide() self.scrollBar.valueChanged[int].connect(self.move_follow_scrolls) #Add to Main Layout hbox.addWidget(self.scrollBar) hbox.addWidget(self._splitterArea) def insert_central_container(self, container): self.mainContainer = container self._splitterMain.insertWidget(0, container) def insert_lateral_container(self, container): self.lateralPanel = LateralPanel(container) self._splitterArea.insertWidget(0, self.lateralPanel) def insert_bottom_container(self, container): self.misc = container self._splitterMain.insertWidget(1, container) def showEvent(self, event): #Show Event QWidget.showEvent(self, event) #Avoid recalculate the panel sizes if they are already loaded if self._splitterArea.count() == 2: return #Rearrange widgets on Window self._splitterArea.insertWidget(0, self._splitterMain) if not event.spontaneous(): self.change_misc_visibility() if bin(settings.UI_LAYOUT)[-1] == '1': self.splitter_central_rotate() if bin(settings.UI_LAYOUT >> 1)[-1] == '1': self.splitter_misc_rotate() if bin(settings.UI_LAYOUT >> 2)[-1] == '1': self.splitter_central_orientation() qsettings = QSettings(resources.SETTINGS_PATH, QSettings.IniFormat) #Lists of sizes as list of QVariant- heightList = [QVariant, QVariant] heightList = list(qsettings.value("window/central/mainSize", [(self.height() / 3) * 2, self.height() / 3])) widthList = list(qsettings.value("window/central/areaSize", [(self.width() / 6) * 5, self.width() / 6])) self._splitterMainSizes = [int(heightList[0]), int(heightList[1])] self._splitterAreaSizes = [int(widthList[0]), int(widthList[1])] #Set the sizes to splitters #self._splitterMain.setSizes(self._splitterMainSizes) self._splitterMain.setSizes(self._splitterMainSizes) self._splitterArea.setSizes(self._splitterAreaSizes) self.misc.setVisible( qsettings.value("window/show_misc", False, type=bool)) def change_misc_visibility(self, on_start=False): if self.misc.isVisible(): self._splitterMainSizes = self._splitterMain.sizes() self.misc.hide() widget = self.mainContainer.get_actual_widget() if widget: widget.setFocus() else: self.misc.show() self.misc.gain_focus() def change_main_visibility(self): if self.mainContainer.isVisible(): self.mainContainer.hide() else: self.mainContainer.show() def change_explorer_visibility(self, force_hide=False): if self.lateralPanel.isVisible() or force_hide: self._splitterAreaSizes = self._splitterArea.sizes() self.lateralPanel.hide() else: self.lateralPanel.show() def splitter_central_rotate(self): w1, w2 = self._splitterArea.widget(0), self._splitterArea.widget(1) self._splitterArea.insertWidget(0, w2) self._splitterArea.insertWidget(1, w1) self.splitterCentralRotated.emit() def splitter_central_orientation(self): if self._splitterArea.orientation() == Qt.Horizontal: self._splitterArea.setOrientation(Qt.Vertical) else: self._splitterArea.setOrientation(Qt.Horizontal) def splitter_misc_rotate(self): w1, w2 = self._splitterMain.widget(0), self._splitterMain.widget(1) self._splitterMain.insertWidget(0, w2) self._splitterMain.insertWidget(1, w1) def splitter_misc_orientation(self): if self._splitterMain.orientation() == Qt.Horizontal: self._splitterMain.setOrientation(Qt.Vertical) else: self._splitterMain.setOrientation(Qt.Horizontal) def get_area_sizes(self): if self.lateralPanel.isVisible(): self._splitterAreaSizes = self._splitterArea.sizes() return self._splitterAreaSizes def get_main_sizes(self): if self.misc.isVisible(): self._splitterMainSizes = self._splitterMain.sizes() return self._splitterMainSizes def enable_follow_mode_scrollbar(self, val): if val: editorWidget = self.mainContainer.get_actual_editor() maxScroll = editorWidget.verticalScrollBar().maximum() position = editorWidget.verticalScrollBar().value() self.scrollBar.setMaximum(maxScroll) self.scrollBar.setValue(position) self.scrollBar.setVisible(val) def move_follow_scrolls(self, val): widget = self.mainContainer._tabMain.currentWidget() diff = widget._sidebarWidget.highest_line - val s1 = self.mainContainer._tabMain.currentWidget().verticalScrollBar() s2 = self.mainContainer._tabSecondary.\ currentWidget().verticalScrollBar() s1.setValue(val) s2.setValue(val + diff)
class PanelContainer(QWidget): def __init__(self, panelWin): super(QWidget, self).__init__() self.panelWindow = panelWin self.panelCount = 0 self.mainLayout = QGridLayout(self) self.mainLayout.setContentsMargins(0, 0, 0, 0) self.splitter = QSplitter(self) self.containerParent = 0 self.mainLayout.addWidget(self.splitter, 0, 0) def splitter(self): return self.splitter def addPanel(self, panel=None): if panel: panel.setParent(self) panel.setContainer(self) self.splitter.addWidget(panel) self.panelCount += 1 self.updatePanelSignals(panel) else: panel = self.createPanel() self.splitter.addWidget(panel) return panel def addPanelSplit(self, panel, direction): panel = PanelWidget(self) if 0 else panel # Store original size origSize = panel.size() # reparent the panel panel.setParent(self) panel.setContainer(self) # set orientation and add the panel self.splitter.setOrientation(direction) self.splitter.addWidget(panel) # add another panel for split panel = self.createPanel() self.splitter.addWidget(panel) sizes = list() origSize *= 0.5 if direction == Qt.Horizontal: sizes.append(origSize.width()) sizes.append(origSize.width()) else: sizes.append(origSize.height()) sizes.append(origSize.height()) self.splitter.setSizes(sizes) self.panelCount += 1 def addContainer(self, child): child = PanelContainer(self) if 0 else child self.splitter.addWidget(child) child.setParentContainer(self) def insertContainer(self, child, index): child = PanelContainer(self) if 0 else child self.splitter.insertWidget(index, child) child.setParentContainer(self) def setParentContainer(self, parent): self.containerParent = parent def parentContainer(self): return self.containerParent def childContainers(self): # childContainers = list() # for index in range(0, self.splitter.count()): # container = self.splitter.widget(index) # if container: # childContainers.append(container) return self.sortedChildren() def panels(self): # panels = list() # for index in range(0, self.splitter.count()): # panel = self.splitter.widget(index) # if panel: # panels.append(panel) return self.sortedChildren() def sortedChildren(self): return (self.splitter.widget(index) for index in range(self.splitter.count())) def numberOfPanels(self): return self.panelCount def createPanel(self): panel = PanelWidget(self) panel.createMenu(WorkbenchWidget.panelNames()) self.connectPanelSignals(panel) self.panelCount += 1 return panel def connectPanelSignals(self, panel): panel = PanelWidget(self) if 0 else panel panel.panelSplit.connect(lambda: self.panelWindow.splitPanel()) panel.panelFloat.connect(lambda : self.panelWindow.floatPanel()) panel.panelClosed.connect(lambda : self.panelWindow.closePanel()) panel.panelMenuTriggered.connect(lambda : self.panelWindow.closePanel()) panel.tabClosed.connect(lambda : self.panelWindow.closePanel()) def updatePanelSignals(self, panel): panel = PanelWidget(self) if 0 else panel panel.panelSplit.disconnect() panel.panelFloat.disconnect() panel.panelClosed.disconnect() panel.panelMenuTriggered.disconnect() panel.tabClosed.disconnect()
class HelpDialog(QObject, LogMixin): """Class implementing qthelp viewer dialog""" def __init__(self, qthelp_file, parent = None): """ Constructor of HelpDialog :param qthelp_file: full path to qthelp helpfile """ super(HelpDialog,self).__init__(parent) # instantiate help engine helpEngine = QHelpEngine(qthelp_file) helpEngine.setupData() self._helpEngine = helpEngine # base dialog widget self.ui = QDialog(None, QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowMinMaxButtonsHint | QtCore.Qt.WindowCloseButtonHint ) self.ui.setWindowTitle("HelpViewer") self.ui.setWindowIcon(QIcon(":/images/prog_icons/help/help.ico")) # Create webview for help information # and assign a custom URL scheme handler for scheme "qthelp) self._wv = QWebEngineView(self.ui) self._urlschemehandler = HelpSchemeHandler(self._helpEngine, self._wv.page().profile()) self._wv.page().profile().installUrlSchemeHandler(b'qthelp', self._urlschemehandler) # get help content overview widget self._helpContent = self._helpEngine.contentWidget() self._helpIndex = self._helpEngine.indexWidget() self._helpSearchQuery = self._helpEngine.searchEngine().queryWidget() self._helpSearchResult = self._helpEngine.searchEngine().resultWidget() self._se = self._helpEngine.searchEngine() self._se.reindexDocumentation() self._helpSearchQuery.search.connect(self.search) # create QSplitter self._splitterMain = QSplitter(QtCore.Qt.Vertical) self._splitterMain.setOpaqueResize(False) self._splitterSearch = QSplitter(QtCore.Qt.Horizontal) self._splitterSearch.setOpaqueResize(False) self._splitterUpper = QSplitter(QtCore.Qt.Horizontal) self._splitterUpper.setOpaqueResize(False) self._splitterLower = QSplitter(QtCore.Qt.Horizontal) self._splitterLower.setOpaqueResize(False) # create horzLayout self._horzLayoutSearch = QHBoxLayout() self._horzLayoutUpper = QHBoxLayout() self._horzLayoutLower = QHBoxLayout() # create vertLayout self._vertLayout = QVBoxLayout() # main widgets self._upperWidget = QWidget() self._lowerWidget = QWidget() self._btnReset = QPushButton() self._btnReset.setMaximumHeight(23) self._btnReset.setMaximumWidth(100) # build search structure self._splitterSearch.insertWidget(0, self._helpSearchQuery) self._splitterSearch.insertWidget(1, self._btnReset) # build upper inner structure self._splitterUpper.insertWidget(0, self._helpContent) self._splitterUpper.insertWidget(1, self._wv) self._horzLayoutUpper.addWidget(self._splitterUpper) self._upperWidget.setLayout(self._horzLayoutUpper) # build lower inner structure self._splitterLower.insertWidget(0, self._helpIndex) self._splitterLower.insertWidget(1, self._helpSearchResult) self._horzLayoutLower.addWidget(self._splitterLower) self._lowerWidget.setLayout(self._horzLayoutLower) # build outer structure self._splitterMain.insertWidget(0, self._splitterSearch) self._splitterMain.insertWidget(1, self._upperWidget) self._splitterMain.insertWidget(2, self._lowerWidget) self._helpSearchResult.hide() self._btnReset.hide() self._vertLayout.addWidget(self._splitterMain) self.ui.setLayout(self._vertLayout) # set splitter width w = self._splitterUpper.geometry().width() self._splitterUpper.setSizes([w*(1/4), w*(3/4)]) w = self._splitterLower.geometry().width() self._splitterLower.setSizes([w*(1/5), w*(4/5)]) h = self._splitterMain.geometry().height() self._splitterMain.setSizes([h*(1/9), h*(7/9), h*(1/9)]) self._helpContent.linkActivated.connect(self._wv.setUrl) self._helpIndex.linkActivated.connect(self._wv.setUrl) self._helpSearchResult.requestShowLink.connect(self._wv.setUrl) self._se.searchingFinished.connect(self.showResults) self._btnReset.clicked.connect(self.resetResult) self.retranslateMsg() def retranslateMsg(self): self.logger.debug("Retranslating further messages...") self._btnReset.setText(translate("HelpViewer", "Reset")) self._btnReset.setText(translate("HelpViewer", "Search")) def search(self): """Initiate qthelp search""" self._se.search(self._helpSearchQuery.query()) def showResults(self): """Show search results, if any""" if self._se.hitCount() > 0: self._helpIndex.hide() h = self._splitterMain.geometry().height() self._splitterMain.setSizes([h*(1/3), h*(1/3), h*(1/3)]) self._helpSearchResult.show() self._btnReset.show() def resetResult(self): """Reset search result widget""" self._helpSearchResult.hide() self._btnReset.hide() self._helpIndex.show() h = self._splitterMain.geometry().height() self._splitterMain.setSizes([h*(1/9), h*(7/9), h*(1/9)])
class CenterWidget(QWidget): def __init__(self): super().__init__() self.__initUI() self.__initVariable() def __initUI(self): tabwidget = QTabWidget() self.fileViewer = FileListView() #图片列表 self.pointsViewer = PointsListView() #控制点列表 self.fullViewer = FileListView() #全图列表 self.imageViewer = ImageView() #显示图像 tabwidget.addTab(self.fileViewer, '待拼接图列表') tabwidget.addTab(self.pointsViewer, '控制点列表') tabwidget.addTab(self.fullViewer, '全图列表') tabwidget.setTabPosition(QTabWidget.South) self.splitter = QSplitter(Qt.Horizontal) self.splitter.addWidget(tabwidget) self.splitter.addWidget(self.imageViewer) #设置初始每个Widget的大小 width = self.width() ratio = [0.2, 0.8] self.splitterSize = [width * ratio[0], width * ratio[1]] self.__setSplitterSize() hbox = QHBoxLayout() hbox.addWidget(self.splitter) self.setLayout(hbox) self.fileViewer.itemDoubleClicked.connect(self.showCurrentImage) self.fullViewer.itemDoubleClicked.connect(self.showFullImage) self.splitter.splitterMoved.connect(self.__getSplitterSize) def __initVariable(self): self.currentImageFile = None def __setSplitterSize(self): self.splitter.setSizes(self.splitterSize) def __getSplitterSize(self, pos, index): self.splitterSize = self.splitter.sizes() def showCurrentImage(self, file): if not os.path.exists(file): return self.currentImageFile = file frame = Image.open(file) img = ImageQt.ImageQt(frame).convertToFormat(QImage.Format_RGB888) if type(self.imageViewer) is not ImageView: #删除掉旧控件 viewer = self.splitter.widget(1) # viewer.setParent(self) viewer.deleteLater() #设置当前显示图像的控件 self.imageViewer = ImageView() self.splitter.insertWidget(1, self.imageViewer) self.__setSplitterSize() self.imageViewer.clearImage() self.imageViewer.setImage(img) def showFullImage(self, file): if not os.path.exists(file): return self.currentImageFile = file frame = Image.open(file) img = ImageQt.ImageQt(frame).convertToFormat(QImage.Format_RGB888) if type(self.imageViewer) is not ImageViewPoint: #删除掉旧控件 viewer = self.splitter.widget(1) # viewer.setParent(self) viewer.deleteLater() #设置当前显示图像的控件 self.imageViewer = ImageViewPoint() self.splitter.insertWidget(1, self.imageViewer) self.__setSplitterSize() self.imageViewer.clearImage() self.imageViewer.setImage(img) def showCircleByGround(self, names, loaction, radius): if type(self.imageViewer) is not ImageViewCircle: #删除掉旧控件 viewer = self.splitter.widget(1) # viewer.setParent(self) viewer.deleteLater() #设置当前显示图像的控件 self.imageViewer = ImageViewCircle() self.splitter.insertWidget(1, self.imageViewer) self.__setSplitterSize() self.imageViewer.setCircles(names, loaction, radius) def showSegmentImage(self, file): if not os.path.exists(file): return self.currentImageFile = file frame = Image.open(file) img = ImageQt.ImageQt(frame).convertToFormat(QImage.Format_RGB888) if type(self.imageViewer) is not ImageViewSegment: #删除掉旧控件 viewer = self.splitter.widget(1) # viewer.setParent(self) viewer.deleteLater() #设置当前显示图像的控件 self.imageViewer = ImageViewSegment() self.splitter.insertWidget(1, self.imageViewer) self.__setSplitterSize() self.imageViewer.clearImage() self.imageViewer.setImage(img)