class QBaseServer(QObject): """Server base for QtPyNetwork.""" connected = Signal(Device, str, int) disconnected = Signal(Device) message = Signal(Device, bytes) error = Signal(Device, str) server_error = Signal(str) closed = Signal() def __init__(self, loggerName=None): super(QBaseServer, self).__init__() if loggerName: self.__logger = logging.getLogger(loggerName) else: self.__logger = logging.getLogger(self.__class__.__name__) self.__ip = None self.__port = None self.__devices = [] self.__deviceModel = Device self.__handler = None self.__handler_thread = None self.__handlerClass = None self.__server = None @Slot(str, int, bytes) def start(self, ip: str, port: int): """Start server.""" if self.__handlerClass: ip = QHostAddress(ip) self.__ip = ip self.__port = port self.__handler = self.__handlerClass() self.__handler_thread = QThread() self.__handler.moveToThread(self.__handler_thread) self.__handler.connected.connect( self.__on_handler_successful_connection) self.__handler.message.connect(self.__on_handler_device_message) self.__handler.error.connect(self.__on_handler_device_error) self.__handler.disconnected.connect( self.__on_handler_device_disconnected) self.__handler.closed.connect(self.on_closed) self.__handler_thread.started.connect(self.__handler.start) self.__handler.started.connect(self.__setup_server) self.__handler_thread.start() else: raise Exception("Handler class not set!") @Slot() def __setup_server(self): """Create QTCPServer, start listening for connections.""" self.__server = TCPServer() self.__server.connection.connect(self.__handler.on_incoming_connection) if self.__server.listen(self.__ip, self.__port): self.__logger.info("Started listening for connections") else: e = self.__server.errorString() self.__logger.error(e) self.server_error.emit(e) @Slot(int, str, int) def __on_handler_successful_connection(self, device_id, ip, port): """When client connects to server successfully.""" device = self.__deviceModel(self, device_id, ip, port) self.__devices.append(device) self.__logger.info("Added new CLIENT-{} with address {}:{}".format( device_id, ip, port)) self.on_connected(device, ip, port) @Slot(int, bytes) def __on_handler_device_message(self, device_id: int, message: bytes): """When server receives message from bot.""" self.on_message(self.get_device_by_id(device_id), message) @Slot(int) def __on_handler_device_disconnected(self, device_id): """When bot disconnects from server.""" device = self.get_device_by_id(device_id) device.set_connected(False) if device in self.__devices: self.__devices.remove(device) self.on_disconnected(device) @Slot(int, str) def __on_handler_device_error(self, device_id, error): self.on_error(self.get_device_by_id(device_id), error) @Slot(Device, str, int) def on_connected(self, device: Device, ip: str, port: int): """Called when new client connects to server. Emits connected signal. Args: device (Device): Device object. ip (str): Client ip address. port (int): Client port. """ self.connected.emit(device, ip, port) @Slot(Device, bytes) def on_message(self, device: Device, message: bytes): """Called when server receives message from client. Emits message signal. Args: device (Device): Message sender. message (bytes): Message. """ self.message.emit(device, message) @Slot(Device) def on_disconnected(self, device: Device): """Called when device disconnects from server. Emits disconnected signal. Args: device (Device): Disconnected device. """ self.disconnected.emit(device) @Slot(Device, str) def on_error(self, device: Device, error: str): """Called when a socket error occurs. Emits error signal. Args: device (Device): Device object. error (str): Error string. """ self.error.emit(device, error) @Slot() def on_closed(self): self.closed.emit() @Slot(Device, bytes) def write(self, device: Device, data: bytes): """Write data to device.""" if not self.__server or not self.__handler: raise ServerNotRunning("Server is not running") if not device.is_connected(): raise NotConnectedError("Client is not connected") self.__handler.write.emit(device.id(), data) @Slot(bytes) def write_all(self, data: bytes): """Write data to all devices.""" if not self.__server or not self.__handler: raise ServerNotRunning("Server is not running") self.__handler.write_all.emit(data) @Slot() def kick(self, device: Device): """Disconnect device from server.""" if not self.__server or not self.__handler: raise ServerNotRunning("Server is not running") if not device.is_connected(): raise NotConnectedError("Client is not connected") self.__handler.kick.emit(device.id()) @Slot() def close(self): """Disconnect clients and close server.""" self.__logger.info("Closing server...") if self.__server: self.__server.close() if self.__handler: self.__handler.close() self.__handler_thread.quit() def set_device_model(self, model): """Set model to use for device when client connects. Note: Model should be subclassing Device. """ if self.is_running(): raise Exception("Set device model before starting server!") if not issubclass(model, Device): raise ValueError("Model should be subclassing Device class.") try: model(QBaseServer(), 0, "127.0.0.1", 5000) except TypeError as e: raise TypeError( "Model is not valid class! Exception: {}".format(e)) self.__deviceModel = model def is_running(self): """Check if server is running.""" if self.__handler_thread: return self.__handler_thread.isRunning() return False def wait(self): """Wait for server thread to close.""" if self.__handler_thread: return self.__handler_thread.wait() return True @Slot(int) def get_device_by_id(self, device_id: int) -> Device: """Returns device with associated ID. Args: device_id (int): Device ID. """ for device in self.__devices: if device.id() == device_id: return device raise Exception("CLIENT-{} not found".format(device_id)) def get_devices(self): """Returns list with devices.""" return self.__devices def set_handler_class(self, handler): """Set handler to use. This should not be used outside this library.""" if self.is_running(): raise Exception("Set socket handler before starting server!") try: handler() except TypeError as e: raise TypeError( "Handler is not valid class! Exception: {}".format(e)) self.__handlerClass = handler
class GistWindow(QDialog): def __init__(self): super(GistWindow, self).__init__() self.thread = None self.worker = None self.gistToken = config.gistToken self.connected = False self.setWindowTitle("Gist") self.setMinimumWidth(380) self.layout = QVBoxLayout() self.testStatus = QLabel("") self.layout.addWidget(self.testStatus) self.layout.addWidget(QLabel("Gist Token")) self.gistTokenInput = QLineEdit() self.gistTokenInput.setText(self.gistToken) self.gistTokenInput.setMaxLength(40) self.gistTokenInput.textChanged.connect(self.enableButtons) self.layout.addWidget(self.gistTokenInput) self.testButton = QPushButton("Test Connection") self.testButton.setEnabled(False) self.testButton.clicked.connect(self.checkStatus) self.layout.addWidget(self.testButton) # self.syncHighlightsButton = QPushButton("Synch Highlights") # self.syncHighlightsButton.setEnabled(False) # self.syncHighlightsButton.clicked.connect(self.syncHighlights) # self.layout.addWidget(self.syncHighlightsButton) self.syncBibleNotesButton = QPushButton("Synch Bibles Notes") self.syncBibleNotesButton.setEnabled(False) self.syncBibleNotesButton.clicked.connect(self.syncBibleNotes) self.layout.addWidget(self.syncBibleNotesButton) buttons = QDialogButtonBox.Ok self.buttonBox = QDialogButtonBox(buttons) self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.stopSync) self.layout.addWidget(self.buttonBox) self.setLayout(self.layout) self.enableButtons() self.checkStatus() def enableButtons(self): if len(self.gistTokenInput.text()) >= 40: self.testButton.setEnabled(True) else: self.testButton.setEnabled(False) self.connected = False self.setStatus("Not connected", False) if self.connected: self.testButton.setEnabled(False) self.syncBibleNotesButton.setEnabled(True) else: self.syncBibleNotesButton.setEnabled(False) def checkStatus(self): if len(self.gistTokenInput.text()) < 40: self.setStatus("Not connected", False) self.connected = False else: self.gh = GitHubGist(self.gistTokenInput.text()) if self.gh.connected: self.setStatus("Connected to " + self.gh.user.name, True) self.connected = True config.gistToken = self.gistTokenInput.text() else: self.setStatus("Not connected", False) self.connected = False self.enableButtons() def setStatus(self, message, connected=True): self.testStatus.setText("Status: " + message) if connected: self.testStatus.setStyleSheet("color: rgb(128, 255, 7);") else: self.testStatus.setStyleSheet("color: rgb(253, 128, 8);") QApplication.processEvents() def syncBibleNotes(self): self.setStatus("Syncing ...", True) self.syncBibleNotesButton.setEnabled(False) self.thread = QThread() self.worker = SyncNotesWithGist() self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.worker.deleteLater) self.worker.finished.connect(self.syncCompleted) self.worker.progress.connect(self.setStatus) self.thread.start() def syncCompleted(self, count): self.setStatus("Done! Processed {0} notes".format(count), True) def stopSync(self): if self.thread and self.thread.isRunning(): self.thread.quit()
class PMGIpythonConsole(RichJupyterWidget): def __init__(self, *args, **kwargs): super(PMGIpythonConsole, self).__init__(*args, **kwargs) self.is_first_execution = True self.confirm_restart = False self.commands_pool = [] self.command_callback_pool: Dict[str, Callable] = {} def change_ui_theme(self, style: str): """ 改变界面主题颜色 :param style: :return: """ style = style.lower() if style == 'fusion': self.style_sheet = default_light_style_sheet self.syntax_style = default_light_syntax_style elif style == 'qdarkstyle': self.style_sheet = default_dark_style_sheet self.syntax_style = default_dark_syntax_style elif style.lower() == 'windowsvista': self.style_sheet = default_light_style_sheet self.syntax_style = default_light_syntax_style elif style.lower() == 'windows': self.style_sheet = default_light_style_sheet self.syntax_style = default_light_syntax_style def _handle_kernel_died(self, since_last_heartbit): self.is_first_execution = True self.restart_kernel(None, True) self.initialize_ipython_builtins() self.execute_command('') return True def _handle_execute_input(self, msg): super()._handle_execute_result(msg) def setup_ui(self): self.kernel_manager = None self.kernel_client = None # initialize by thread self.init_thread = QThread(self) self.console_object = ConsoleInitThread() self.console_object.moveToThread(self.init_thread) self.console_object.initialized.connect(self.slot_initialized) self.init_thread.finished.connect(self.console_object.deleteLater) self.init_thread.finished.connect(self.init_thread.deleteLater) self.init_thread.started.connect(self.console_object.run) self.init_thread.start() cursor: QTextCursor = self._prompt_cursor cursor.movePosition(QTextCursor.End) def _context_menu_make(self, pos: 'QPoint') -> QMenu: menu = super(PMGIpythonConsole, self)._context_menu_make(pos) _translate = QCoreApplication.translate trans_dic = { 'Cut': _translate("PMGIpythonConsole", 'Cut'), 'Copy': _translate("PMGIpythonConsole", 'Copy'), 'Copy (Raw Text)': _translate("PMGIpythonConsole", 'Copy(Raw Text)'), 'Paste': _translate("PMGIpythonConsole", 'Paste'), 'Select All': _translate("PMGIpythonConsole", 'Select All'), 'Save as HTML/XML': _translate("PMGIpythonConsole", 'Save as HTML/XML'), 'Print': _translate("PMGIpythonConsole", 'Print') } for action in menu.actions(): trans = trans_dic.get(action.text()) trans = trans if trans is not None else action.text() action.setText(trans) restart_action = menu.addAction( _translate("PMGIpythonConsole", 'Restart')) restart_action.triggered.connect(self.slot_restart_kernel) stop_action = menu.addAction( _translate("PMGIpythonConsole", 'Interrupt')) # stop_action.triggered.connect(self.request_interrupt_kernel) stop_action.triggered.connect(self.on_interrupt_kernel) # stop_action.setEnabled(self._executing) return menu def on_interrupt_kernel(self): """ 当点击中断执行时。 IPython会出现一个奇怪的问题——当中断执行时,可能_executing恒为False。 因此干脆不屏蔽了。 Returns: """ self.interrupt_kernel() def _custom_context_menu_requested(self, pos): super(PMGIpythonConsole, self)._custom_context_menu_requested(pos) def slot_restart_kernel(self, arg): ret = QMessageBox.warning(self, '提示', '是否要重启控制台?\n一切变量都将被重置。', QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Cancel) if ret == QMessageBox.Ok: self._restart_kernel(arg) def _restart_kernel(self, arg1): self.is_first_execution = True self.restart_kernel(None, True) self.initialize_ipython_builtins() self.execute_command('') return True def slot_initialized(self, kernel_manager, kernel_client): """ Args: kernel_manager: `qtconsole.manager.QtKernelManager` kernel_client: `qtconsole.manager.QtKernelManager.client` Returns: """ self.kernel_manager = kernel_manager self.kernel_client = kernel_client self.initialize_ipython_builtins() def initialize_ipython_builtins(self): return def _update_list(self): try: super(PMGIpythonConsole, self)._update_list() except BaseException: import traceback traceback.print_exc() def _banner_default(self): """ 自定义控制台开始的文字 Returns: """ return 'Welcome To PMGWidgets Ipython Console!\n' def closeEvent(self, event): if self.init_thread.isRunning(): self.console_object.stop() self.init_thread.quit() self.init_thread.wait(500) super(PMGIpythonConsole, self).closeEvent(event) def execute_file(self, file: str, hidden: bool = False): if not os.path.exists(file) or not file.endswith('.py'): raise FileNotFoundError(f'{file} not found or invalid') base = os.path.basename(file) cmd = os.path.splitext(base)[0] with open(file, 'r', encoding='utf-8') as f: source = f.read() self.execute_command(source, hidden=hidden, hint_text=cmd) def execute_command(self, source, hidden: bool = False, hint_text: str = '') -> str: """ :param source: :param hidden: :param hint_text: 运行代码前显示的提示 :return: str 执行命令的 msgid """ cursor: QTextCursor = self._prompt_cursor cursor.movePosition(QTextCursor.End) # 运行文件时,显示文件名,无换行符,执行选中内容时,包含换行符 # 检测换行符,在ipy console中显示执行脚本内容 hint_row_list = hint_text.split("\n") for hint in hint_row_list: if hint != "": cursor.insertText('%s\n' % hint) self._insert_continuation_prompt(cursor) else: # 删除多余的continuation_prompt self.undo() self._finalize_input_request( ) # display input string buffer in console. cursor.movePosition(QTextCursor.End) if self.kernel_client is None: self.commands_pool.append((source, hidden, hint_text)) return '' else: return self.pmexecute(source, hidden) def _handle_stream(self, msg): parent_header = msg.get('parent_header') if parent_header is not None: msg_id = parent_header.get( 'msg_id') # 'fee0bee5-074c00d093b1455be6d166b1_10''] if msg_id in self.command_callback_pool.keys(): callback = self.command_callback_pool.pop(msg_id) assert callable(callback) callback() cursor: QTextCursor = self._prompt_cursor cursor.movePosition(QTextCursor.End) super()._handle_stream(msg) def append_stream(self, text): """重写的方法。原本before_prompt属性是False。""" self._append_plain_text(text, before_prompt=False) def pmexecute(self, source: str, hidden: bool = False) -> str: """ 执行代码并且返回Msgid :param source: :param hidden: :return: """ is_legal, msg = self.is_source_code_legal(source) if not is_legal: QMessageBox.warning(self, '警告', msg) source = '' msg_id = self.kernel_client.execute(source, hidden) self._request_info['execute'][msg_id] = self._ExecutionRequest( msg_id, 'user') self._hidden = hidden if not hidden: self.executing.emit(source) return msg_id # super()._execute(source, hidden) def is_source_code_legal(self, source_code: str) -> Tuple[bool, str]: """判断注入到shell中的命令是否合法。 如果命令不合法,应当避免执行该命令。 Args: source_code: 注入到shell中的命令。 Returns: * 是否合法; * 如不合法,返回原因;如合法,返回空字符串。 """ return True, '' @staticmethod def install_translator(): global _trans app = QApplication.instance() assert app is not None _trans = QTranslator() _trans.load(QLocale.system(), 'qt_zh_CN.qm', directory=os.path.join(os.path.dirname(__file__), 'translations')) app.installTranslator(_trans)
class DownloadBibleMp3Dialog(QDialog): def __init__(self, parent): super().__init__() self.bibles = { "BBE (British accent)": ("BBE", "otseng/UniqueBible_MP3_BBE_british", "british"), "KJV (American accent)": ("KJV", "otseng/UniqueBible_MP3_KJV", "default"), "KJV (American soft music)": ("KJV", "otseng/UniqueBible_MP3_KJV_soft_music", "soft-music"), "NHEB (Indian accent)": ("NHEB", "otseng/UniqueBible_MP3_NHEB_indian", "indian"), "WEB (American accent)": ("WEB", "otseng/UniqueBible_MP3_WEB", "default"), "CUV (Chinese)": ("CUV", "otseng/UniqueBible_MP3_CUV", "default"), "HHBD (Hindi)": ("HHBD", "otseng/UniqueBible_MP3_HHBD", "default"), "RVA (Spanish)": ("RVA", "otseng/UniqueBible_MP3_RVA", "default"), "TR (Modern Greek)": ("TR", "otseng/UniqueBible_MP3_TR", "modern"), } self.parent = parent self.setWindowTitle(config.thisTranslation["gitHubBibleMp3Files"]) self.setMinimumSize(150, 450) self.selectedRendition = None self.selectedText = None self.selectedRepo = None self.selectedDirectory = None self.settingBibles = False self.thread = None self.setupUI() def setupUI(self): mainLayout = QVBoxLayout() title = QLabel(config.thisTranslation["gitHubBibleMp3Files"]) mainLayout.addWidget(title) self.versionsLayout = QVBoxLayout() self.renditionsList = QListWidget() self.renditionsList.itemClicked.connect(self.selectItem) for rendition in self.bibles.keys(): self.renditionsList.addItem(rendition) self.renditionsList.setMaximumHeight(100) self.versionsLayout.addWidget(self.renditionsList) mainLayout.addLayout(self.versionsLayout) self.downloadTable = QTableView() self.downloadTable.setEnabled(False) self.downloadTable.setFocusPolicy(Qt.StrongFocus) self.downloadTable.setEditTriggers(QAbstractItemView.NoEditTriggers) self.downloadTable.setSortingEnabled(True) self.dataViewModel = QStandardItemModel(self.downloadTable) self.downloadTable.setModel(self.dataViewModel) mainLayout.addWidget(self.downloadTable) buttonsLayout = QHBoxLayout() selectAllButton = QPushButton(config.thisTranslation["selectAll"]) selectAllButton.setFocusPolicy(Qt.StrongFocus) selectAllButton.clicked.connect(self.selectAll) buttonsLayout.addWidget(selectAllButton) selectNoneButton = QPushButton(config.thisTranslation["selectNone"]) selectNoneButton.setFocusPolicy(Qt.StrongFocus) selectNoneButton.clicked.connect(self.selectNone) buttonsLayout.addWidget(selectNoneButton) otButton = QPushButton("1-39") otButton.setFocusPolicy(Qt.StrongFocus) otButton.clicked.connect(self.selectOT) buttonsLayout.addWidget(otButton) ntButton = QPushButton("40-66") ntButton.setFocusPolicy(Qt.StrongFocus) ntButton.clicked.connect(self.selectNT) buttonsLayout.addWidget(ntButton) # buttonsLayout.addStretch() mainLayout.addLayout(buttonsLayout) self.downloadButton = QPushButton(config.thisTranslation["download"]) self.downloadButton.setFocusPolicy(Qt.StrongFocus) self.downloadButton.setAutoDefault(True) self.downloadButton.setFocus() self.downloadButton.clicked.connect(self.download) mainLayout.addWidget(self.downloadButton) self.status = QLabel("") mainLayout.addWidget(self.status) buttonLayout = QHBoxLayout() self.closeButton = QPushButton(config.thisTranslation["close"]) self.closeButton.setFocusPolicy(Qt.StrongFocus) self.closeButton.clicked.connect(self.closeDialog) buttonLayout.addWidget(self.closeButton) mainLayout.addLayout(buttonLayout) self.setLayout(mainLayout) self.renditionsList.item(0).setSelected(True) bible = self.renditionsList.item(0).text() self.selectRendition(bible) self.downloadButton.setDefault(True) QTimer.singleShot(0, self.downloadButton.setFocus) def selectItem(self, item): self.selectRendition(item.text()) def selectRendition(self, rendition): from util.GithubUtil import GithubUtil self.selectedRendition = rendition self.downloadTable.setEnabled(True) self.selectedText, self.selectedRepo, self.selectedDirectory = self.bibles[ self.selectedRendition] self.github = GithubUtil(self.selectedRepo) self.repoData = self.github.getRepoData() self.settingBibles = True self.dataViewModel.clear() rowCount = 0 for file in self.repoData.keys(): if len(str(file)) > 3: engFullBookName = file[3:] else: engFullBookName = BibleBooks().eng[str(int(file))][1] item = QStandardItem(file[:3].strip()) folder = os.path.join("audio", "bibles", self.selectedText, self.selectedDirectory, file) folderWithName = os.path.join("audio", "bibles", self.selectedText, self.selectedDirectory, file + " " + engFullBookName) if os.path.exists(folder) or os.path.exists(folderWithName): item.setCheckable(False) item.setCheckState(Qt.Unchecked) item.setEnabled(False) else: item.setCheckable(True) item.setCheckState(Qt.Checked) item.setEnabled(True) self.dataViewModel.setItem(rowCount, 0, item) item = QStandardItem(engFullBookName) self.dataViewModel.setItem(rowCount, 1, item) if os.path.exists(folder) or os.path.exists(folderWithName): item = QStandardItem("Installed") self.dataViewModel.setItem(rowCount, 2, item) else: item = QStandardItem("") self.dataViewModel.setItem(rowCount, 2, item) rowCount += 1 self.dataViewModel.setHorizontalHeaderLabels([ config.thisTranslation["menu_book"], config.thisTranslation["name"], "" ]) self.downloadTable.setColumnWidth(0, 90) self.downloadTable.setColumnWidth(1, 125) self.downloadTable.setColumnWidth(2, 125) # self.downloadTable.resizeColumnsToContents() self.settingBibles = False def selectAll(self): for index in range(self.dataViewModel.rowCount()): item = self.dataViewModel.item(index) if item.isEnabled(): item.setCheckState(Qt.Checked) def selectNone(self): for index in range(self.dataViewModel.rowCount()): item = self.dataViewModel.item(index) item.setCheckState(Qt.Unchecked) def selectOT(self): for index in range(self.dataViewModel.rowCount()): item = self.dataViewModel.item(index) bookNum = int(item.text()) if bookNum <= 39: if item.isEnabled(): item.setCheckState(Qt.Checked) else: item.setCheckState(Qt.Unchecked) else: item.setCheckState(Qt.Unchecked) def selectNT(self): for index in range(self.dataViewModel.rowCount()): item = self.dataViewModel.item(index) bookNum = int(item.text()) if bookNum >= 40: if item.isEnabled(): item.setCheckState(Qt.Checked) else: item.setCheckState(Qt.Unchecked) else: item.setCheckState(Qt.Unchecked) def download(self): self.downloadButton.setEnabled(False) self.setStatus(config.thisTranslation["message_installing"]) self.closeButton.setEnabled(False) folder = os.path.join("audio", "bibles") if not os.path.exists(folder): os.mkdir(folder) folder = os.path.join("audio", "bibles", self.selectedText) if not os.path.exists(folder): os.mkdir(folder) folder = os.path.join("audio", "bibles", self.selectedText, self.selectedDirectory) if not os.path.exists(folder): os.mkdir(folder) self.thread = QThread() self.worker = DownloadFromGitHub(self.github, self.repoData, self.dataViewModel, self.selectedText, self.selectedDirectory) self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.worker.deleteLater) self.worker.finished.connect(self.finishedDownloading) self.worker.progress.connect(self.setStatus) self.thread.start() def finishedDownloading(self, count): self.selectRendition(self.selectedRendition) self.setStatus("") self.downloadButton.setEnabled(True) self.closeButton.setEnabled(True) if count > 0: self.parent.displayMessage( config.thisTranslation["message_installed"]) def setStatus(self, message): self.status.setText(message) QApplication.processEvents() def closeDialog(self): if self.thread: if self.thread.isRunning(): self.thread.quit() self.close()
class Runner: """ init -> register -> start init -> start -> register """ def __init__(self, task_cls: Type[Task], *args, **kwargs): super().__init__() self.task: Optional[Task] = None self.thread: Optional[QThread] = None self._started = False self.task_cls = task_cls self._args = args self._kwargs = kwargs def init(self) -> None: if self._started: raise RuntimeError("Cannot init runner when task already started!") self.thread = QThread() self.task = self.task_cls(*self._args, **self._kwargs) self.task.moveToThread(self.thread) self.task.finished.connect(self.task.deleteLater) self.task.finished.connect(self.thread.quit) self.task.failed.connect(self.fail) self.thread.started.connect( self.task.execute) # lambdas don't work here hm self.thread.finished.connect(self.thread.deleteLater) def register(self, *, started: Callable = None, finished: Callable = None, progress: Callable = None, failed: Callable = None, result: Callable = None) -> None: if started is not None: self.task.started.connect(started) if finished is not None: self.task.finished.connect(finished) if progress is not None: self.task._progressThrottled.connect(progress) if failed is not None: self.task.failed.connect(failed) if result is not None: self.task.result.connect(result) def start(self) -> None: self._started = True self.thread.start() def stop(self) -> None: if self.task is not None: self.task.stop() if self.thread is not None: try: self.thread.quit() self.thread.wait() except RuntimeError: pass self._started = False def fail(self, e: Exception) -> None: raise e @property def is_running(self) -> bool: """ Without this try-except block, Qt throws an error saying that the thread has already been deleted, even if `self.thread` is not None. """ is_running = False try: if self.thread is not None: is_running = self.thread.isRunning() except RuntimeError: pass return is_running
class ProcessConsole(QTextEdit): signal_stop_qthread = Signal() signal_process_stopped = Signal() signal_process_started = Signal() insert_mode = '' def __init__(self, args: list = None): super().__init__() self._is_running = False self.auto_scroll = True self.args = args # self.setContentsMargins(20, 20, 0, 0) self.monitor_thread: 'ProcessMonitorThread' = None self.out_thread: 'QThread' = None def set_args(self, args: list = None): self.args = args def is_running(self) -> bool: """ 返回进程是否在运行 Returns: """ if self.monitor_thread is not None: process = self.get_subprocess() if process is not None: if process.poll() is None: return True return False def start_process(self): if not self.is_running(): self.out_thread = QThread(self) self.monitor_thread = ProcessMonitorThread() self.monitor_thread.args = self.args self.monitor_thread.moveToThread(self.out_thread) self.out_thread.started.connect(self.monitor_thread.run) self.out_thread.start() self.monitor_thread.on_out.connect(self.on_stdout) self.monitor_thread.on_err.connect(self.on_stderr) self.signal_stop_qthread.connect(self.monitor_thread.stop) self.out_thread.finished.connect(self.out_thread.deleteLater) self.out_thread.finished.connect(self.monitor_thread.deleteLater) self.monitor_thread.on_finished.connect(self.terminate_process) def on_stdout(self, text): if self.insert_mode == 'error': self.insertHtml('<p style="color:black;">' + '========' + '<br/></p>') self.insert_mode = 'stdout' self.insertPlainText(text) if self.auto_scroll: self.ensureCursorVisible() def on_stderr(self, text): self.insert_mode = 'error' self.insertHtml('<p style="color:red;">' + text + '<br/></p>') print(text) if self.auto_scroll: self.ensureCursorVisible() def terminate_process(self): if self.monitor_thread is not None: self.monitor_thread.process_terminated = True self.monitor_thread.process.process.terminate() if self.out_thread.isRunning(): self.signal_stop_qthread.emit() self.out_thread.quit() self.out_thread.wait(500) self.monitor_thread = None self.out_thread = None self.signal_process_stopped.emit() def keyPressEvent(self, e: 'QKeyEvent'): if e.key() == Qt.Key_Backspace or e.key() == Qt.Key_Delete: return print(e.key(), e.text()) if e.key() == Qt.Key_Return: text = '\n' else: text = e.text() if text != '' and self.monitor_thread is not None: try: print('sent:', text) self.monitor_thread.process.process.stdin.write( text.encode('utf8')) self.monitor_thread.process.process.stdin.flush() except: import traceback traceback.print_exc() super(ProcessConsole, self).keyPressEvent(e) def get_subprocess(self): """ 返回子进程 Returns: """ if hasattr(self.monitor_thread, 'process'): proc = self.monitor_thread.process if hasattr(proc, 'process'): return proc.process return None
class TaskManagerBase(QObject): """ A basic manager to handle tasks that need to be executed in a different thread than that of the main application to avoid blocking the GUI event loop. """ sig_run_tasks_finished = Signal() def __init__(self): super().__init__() self._worker = None self._task_callbacks = {} self._task_data = {} self._running_tasks = [] self._queued_tasks = [] self._pending_tasks = [] # Queued tasks are tasks whose execution has not been requested yet. # This happens when we want the Worker to execute a list of tasks # in a single run. All queued tasks are dumped in the list of pending # tasks when `run_task` is called. # # Pending tasks are tasks whose execution was postponed due to # the fact that the worker was busy. These tasks are run as soon # as the worker becomes available. # # Running tasks are tasks that are being executed by the worker. def run_tasks(self): """ Execute all the tasks that were added to the stack. """ self._run_tasks() def add_task(self, task, callback, *args, **kargs): self._add_task(task, callback, *args, **kargs) def worker(self): """Return the worker that is installed on this manager.""" return self._worker def set_worker(self, worker): """"Install the provided worker on this manager""" self._worker = worker self._thread = QThread() self._worker.moveToThread(self._thread) self._thread.started.connect(self._worker.run_tasks) # Connect the worker signals to handlers. self._worker.sig_task_completed.connect(self._exec_task_callback) # ---- Private API @Slot(object, object) def _exec_task_callback(self, task_uuid4, returned_values): """ This is the (only) slot that is called after a task is completed by the worker. """ # Run the callback associated with the specified task UUID if any. if self._task_callbacks[task_uuid4] is not None: try: self._task_callbacks[task_uuid4](*returned_values) except TypeError: self._task_callbacks[task_uuid4]() # Clean up internal variables. del self._task_callbacks[task_uuid4] del self._task_data[task_uuid4] self._running_tasks.remove(task_uuid4) if len(self._running_tasks) == 0: # This means all tasks sent to the worker were completed. if len(self._pending_tasks) > 0: self._run_pending_tasks() else: self.sig_run_tasks_finished.emit() print('All pending tasks were executed.') def _add_task(self, task, callback, *args, **kargs): task_uuid4 = uuid.uuid4() self._task_callbacks[task_uuid4] = callback self._queued_tasks.append(task_uuid4) self._task_data[task_uuid4] = (task, args, kargs) def _run_tasks(self): """ Execute all the tasks that were added to the stack. """ self._pending_tasks.extend(self._queued_tasks) self._queued_tasks = [] self._run_pending_tasks() def _run_pending_tasks(self): """Execute all pending tasks.""" if len(self._running_tasks) == 0: print('Executing {} pending tasks...'.format( len(self._pending_tasks))) # Even though the worker has executed all its tasks, # we may still need to wait a little for it to stop properly. i = 0 while self._thread.isRunning(): sleep(0.1) i += 1 if i > 100: print("Error: unable to stop {}'s working thread.".format( self.__class__.__name__)) self._running_tasks = self._pending_tasks.copy() self._pending_tasks = [] for task_uuid4 in self._running_tasks: task, args, kargs = self._task_data[task_uuid4] self._worker.add_task(task_uuid4, task, *args, **kargs) self._thread.start()
class DAQ_PID(QObject): """ """ command_pid = Signal(ThreadCommand) curr_points_signal = Signal(dict) setpoints_signal = Signal(dict) emit_curr_points_sig = Signal() models = get_models() params = [ {'title': 'Models', 'name': 'models', 'type': 'group', 'expanded': True, 'visible': True, 'children': [ {'title': 'Models class:', 'name': 'model_class', 'type': 'list', 'limits': [d['name'] for d in models]}, {'title': 'Model params:', 'name': 'model_params', 'type': 'group', 'children': []}, ]}, {'title': 'Move settings:', 'name': 'move_settings', 'expanded': True, 'type': 'group', 'visible': False, 'children': [ {'title': 'Units:', 'name': 'units', 'type': 'str', 'value': ''}]}, # here only to be compatible with DAQ_Scan, the model could update it {'title': 'Main Settings:', 'name': 'main_settings', 'expanded': True, 'type': 'group', 'children': [ {'title': 'Acquisition Timeout (ms):', 'name': 'timeout', 'type': 'int', 'value': 10000}, {'title': 'epsilon', 'name': 'epsilon', 'type': 'float', 'value': 0.01, 'tooltip': 'Precision at which move is considered as done'}, {'title': 'PID controls:', 'name': 'pid_controls', 'type': 'group', 'children': [ {'title': 'Sample time (ms):', 'name': 'sample_time', 'type': 'int', 'value': 10}, {'title': 'Refresh plot time (ms):', 'name': 'refresh_plot_time', 'type': 'int', 'value': 200}, {'title': 'Output limits:', 'name': 'output_limits', 'expanded': True, 'type': 'group', 'children': [ {'title': 'Output limit (min):', 'name': 'output_limit_min_enabled', 'type': 'bool', 'value': False}, {'title': 'Output limit (min):', 'name': 'output_limit_min', 'type': 'float', 'value': 0}, {'title': 'Output limit (max):', 'name': 'output_limit_max_enabled', 'type': 'bool', 'value': False}, {'title': 'Output limit (max:', 'name': 'output_limit_max', 'type': 'float', 'value': 100}, ]}, {'title': 'Auto mode:', 'name': 'auto_mode', 'type': 'bool', 'value': False, 'readonly': True}, {'title': 'Prop. on measurement:', 'name': 'proportional_on_measurement', 'type': 'bool', 'value': False}, {'title': 'PID constants:', 'name': 'pid_constants', 'type': 'group', 'children': [ {'title': 'Kp:', 'name': 'kp', 'type': 'float', 'value': 0.1, 'min': 0}, {'title': 'Ki:', 'name': 'ki', 'type': 'float', 'value': 0.01, 'min': 0}, {'title': 'Kd:', 'name': 'kd', 'type': 'float', 'value': 0.001, 'min': 0}, ]}, ]}, ]}, ] def __init__(self, dockarea): super().__init__() self.settings = Parameter.create(title='PID settings', name='pid_settings', type='group', children=self.params) self.title = 'PyMoDAQ PID' self.Initialized_state = False self.model_class = None self._curr_points = dict([]) self._setpoints = dict([]) self.modules_manager = None self.dock_area = dockarea self.check_moving = False self.setupUI() self.enable_controls_pid(False) self.enable_controls_pid_run(False) self.emit_curr_points_sig.connect(self.emit_curr_points) def set_module_manager(self, detector_modules, actuator_modules): self.modules_manager = ModulesManager(detector_modules, actuator_modules) def ini_PID(self): if self.ini_PID_action.isChecked(): output_limits = [None, None] if self.settings.child('main_settings', 'pid_controls', 'output_limits', 'output_limit_min_enabled').value(): output_limits[0] = self.settings.child('main_settings', 'pid_controls', 'output_limits', 'output_limit_min').value() if self.settings.child('main_settings', 'pid_controls', 'output_limits', 'output_limit_max_enabled').value(): output_limits[1] = self.settings.child('main_settings', 'pid_controls', 'output_limits', 'output_limit_max').value() self.PIDThread = QThread() pid_runner = PIDRunner(self.model_class, self.modules_manager, setpoints=self.setpoints, params=dict(Kp=self.settings.child('main_settings', 'pid_controls', 'pid_constants', 'kp').value(), Ki=self.settings.child('main_settings', 'pid_controls', 'pid_constants', 'ki').value(), Kd=self.settings.child('main_settings', 'pid_controls', 'pid_constants', 'kd').value(), sample_time=self.settings.child('main_settings', 'pid_controls', 'sample_time').value() / 1000, output_limits=output_limits, auto_mode=False), ) self.PIDThread.pid_runner = pid_runner pid_runner.pid_output_signal.connect(self.process_output) pid_runner.status_sig.connect(self.thread_status) self.command_pid.connect(pid_runner.queue_command) pid_runner.moveToThread(self.PIDThread) self.PIDThread.start() self.pid_led.set_as_true() self.enable_controls_pid_run(True) else: if hasattr(self, 'PIDThread'): if self.PIDThread.isRunning(): try: self.PIDThread.quit() except Exception: pass self.pid_led.set_as_false() self.enable_controls_pid_run(False) self.Initialized_state = True def process_output(self, datas): self.output_viewer.show_data([[dat] for dat in datas['output']]) self.input_viewer.show_data([[dat] for dat in datas['input']]) self.curr_points = datas['input'] def enable_controls_pid(self, enable=False): self.ini_PID_action.setEnabled(enable) #self.setpoint_sb.setOpts(enabled=enable) def enable_controls_pid_run(self, enable=False): self.run_action.setEnabled(enable) self.pause_action.setEnabled(enable) def setupUI(self): self.dock_pid = Dock('PID controller', self.dock_area) self.dock_area.addDock(self.dock_pid) widget = QtWidgets.QWidget() widget_toolbar = QtWidgets.QWidget() verlayout = QtWidgets.QVBoxLayout() widget.setLayout(verlayout) self.toolbar_layout = QtWidgets.QGridLayout() widget_toolbar.setLayout(self.toolbar_layout) iconquit = QtGui.QIcon() iconquit.addPixmap(QtGui.QPixmap(":/icons/Icon_Library/close2.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.quit_action = QtWidgets.QPushButton(iconquit, "Quit") self.quit_action.setToolTip('Quit the application') self.toolbar_layout.addWidget(self.quit_action, 0, 0, 1, 2) self.quit_action.clicked.connect(self.quit_fun) iconini = QtGui.QIcon() iconini.addPixmap(QtGui.QPixmap(":/icons/Icon_Library/ini.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.ini_model_action = QtWidgets.QPushButton(iconini, "Init Model") self.ini_model_action.setToolTip('Initialize the chosen model') self.toolbar_layout.addWidget(self.ini_model_action, 2, 0) self.ini_model_action.clicked.connect(self.ini_model) self.model_led = QLED() self.toolbar_layout.addWidget(self.model_led, 2, 1) self.ini_PID_action = QtWidgets.QPushButton(iconini, "Init PID") self.ini_PID_action.setToolTip('Initialize the PID loop') self.toolbar_layout.addWidget(self.ini_PID_action, 2, 2) self.ini_PID_action.setCheckable(True) self.ini_PID_action.clicked.connect(self.ini_PID) self.pid_led = QLED() self.toolbar_layout.addWidget(self.pid_led, 2, 3) self.iconrun = QtGui.QIcon() self.iconrun.addPixmap(QtGui.QPixmap(":/icons/Icon_Library/run2.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.icon_stop = QtGui.QIcon() self.icon_stop.addPixmap(QtGui.QPixmap(":/icons/Icon_Library/stop.png")) self.run_action = QtWidgets.QPushButton(self.iconrun, "", None) self.run_action.setToolTip('Start PID loop') self.run_action.setCheckable(True) self.toolbar_layout.addWidget(self.run_action, 0, 2) self.run_action.clicked.connect(self.run_PID) iconpause = QtGui.QIcon() iconpause.addPixmap(QtGui.QPixmap(":/icons/Icon_Library/pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.pause_action = QtWidgets.QPushButton(iconpause, "", None) self.pause_action.setToolTip('Pause PID') self.pause_action.setCheckable(True) self.toolbar_layout.addWidget(self.pause_action, 0, 3) self.pause_action.setChecked(True) self.pause_action.clicked.connect(self.pause_PID) lab = QtWidgets.QLabel('Target Value:') self.toolbar_layout.addWidget(lab, 3, 0, 1, 2) lab1 = QtWidgets.QLabel('Current Value:') self.toolbar_layout.addWidget(lab1, 4, 0, 1, 2) # create main parameter tree self.settings_tree = ParameterTree() self.settings_tree.setParameters(self.settings, showTop=False) verlayout.addWidget(widget_toolbar) verlayout.addWidget(self.settings_tree) self.dock_output = Dock('PID output') widget_output = QtWidgets.QWidget() self.output_viewer = Viewer0D(widget_output) self.dock_output.addWidget(widget_output) self.dock_area.addDock(self.dock_output, 'right', self.dock_pid) self.dock_input = Dock('PID input') widget_input = QtWidgets.QWidget() self.input_viewer = Viewer0D(widget_input) self.dock_input.addWidget(widget_input) self.dock_area.addDock(self.dock_input, 'bottom', self.dock_output) if len(self.models) != 0: self.get_set_model_params(self.models[0]['name']) # connecting from tree self.settings.sigTreeStateChanged.connect( self.parameter_tree_changed) # any changes on the settings will update accordingly the detector self.dock_pid.addWidget(widget) def get_set_model_params(self, model_name): self.settings.child('models', 'model_params').clearChildren() models = get_models() if len(models) > 0: model_class = find_dict_in_list_from_key_val(models, 'name', model_name)['class'] params = getattr(model_class, 'params') self.settings.child('models', 'model_params').addChildren(params) def run_PID(self): if self.run_action.isChecked(): self.run_action.setIcon(self.icon_stop) self.command_pid.emit(ThreadCommand('start_PID', [])) QtWidgets.QApplication.processEvents() QtWidgets.QApplication.processEvents() self.command_pid.emit(ThreadCommand('run_PID', [self.model_class.curr_output])) else: self.run_action.setIcon(self.iconrun) self.command_pid.emit(ThreadCommand('stop_PID')) QtWidgets.QApplication.processEvents() def pause_PID(self): for setp in self.setpoints_sb: setp.setEnabled(not self.pause_action.isChecked()) self.command_pid.emit(ThreadCommand('pause_PID', [self.pause_action.isChecked()])) def stop_moves(self, overshoot): """ Foreach module of the move module object list, stop motion. See Also -------- stop_scan, DAQ_Move_main.daq_move.stop_Motion """ self.overshoot = overshoot for mod in self.modules_manager.actuators: mod.stop_Motion() def set_model(self): model_name = self.settings.child('models', 'model_class').value() self.model_class = find_dict_in_list_from_key_val(self.models, 'name', model_name)['class'](self) self.set_setpoints_buttons() self.model_class.ini_model() self.settings.child('main_settings', 'epsilon').setValue(self.model_class.epsilon) def ini_model(self): try: if self.model_class is None: self.set_model() self.modules_manager.selected_actuators_name = self.model_class.actuators_name self.modules_manager.selected_detectors_name = self.model_class.detectors_name self.enable_controls_pid(True) self.model_led.set_as_true() self.ini_model_action.setEnabled(False) except Exception as e: logger.exception(str(e)) @property def setpoints(self): return [sp.value() for sp in self.setpoints_sb] @setpoints.setter def setpoints(self, values): for ind, sp in enumerate(self.setpoints_sb): sp.setValue(values[ind]) def setpoints_external(self, values_dict): for key in values_dict: index = self.model_class.setpoints_names.index(key) self.setpoints_sb[index].setValue(values_dict[key]) @property def curr_points(self): return [sp.value() for sp in self.currpoints_sb] @curr_points.setter def curr_points(self, values): for ind, sp in enumerate(self.currpoints_sb): sp.setValue(values[ind]) def emit_curr_points(self): if self.model_class is not None: self.curr_points_signal.emit(dict(zip(self.model_class.setpoints_names, self.curr_points))) def set_setpoints_buttons(self): self.setpoints_sb = [] self.currpoints_sb = [] for ind_set in range(self.model_class.Nsetpoints): self.setpoints_sb.append(SpinBox()) self.setpoints_sb[-1].setMinimumHeight(40) font = self.setpoints_sb[-1].font() font.setPointSizeF(20) self.setpoints_sb[-1].setFont(font) self.setpoints_sb[-1].setDecimals(6) self.toolbar_layout.addWidget(self.setpoints_sb[-1], 3, 2+ind_set, 1, 1) self.setpoints_sb[-1].valueChanged.connect(self.update_runner_setpoints) self.currpoints_sb.append(SpinBox()) self.currpoints_sb[-1].setMinimumHeight(40) self.currpoints_sb[-1].setReadOnly(True) self.currpoints_sb[-1].setDecimals(6) self.currpoints_sb[-1].setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) font = self.currpoints_sb[-1].font() font.setPointSizeF(20) self.currpoints_sb[-1].setFont(font) self.toolbar_layout.addWidget(self.currpoints_sb[-1], 4, 2+ind_set, 1, 1) self.setpoints_signal.connect(self.setpoints_external) def quit_fun(self): """ """ try: try: self.PIDThread.exit() except Exception as e: print(e) areas = self.dock_area.tempAreas[:] for area in areas: area.win.close() QtWidgets.QApplication.processEvents() QThread.msleep(1000) QtWidgets.QApplication.processEvents() self.dock_area.parent().close() except Exception as e: print(e) def parameter_tree_changed(self, param, changes): """ Foreach value changed, update : * Viewer in case of **DAQ_type** parameter name * visibility of button in case of **show_averaging** parameter name * visibility of naverage in case of **live_averaging** parameter name * scale of axis **else** (in 2D pymodaq type) Once done emit the update settings signal to link the commit. =============== =================================== ================================================================ **Parameters** **Type** **Description** *param* instance of ppyqtgraph parameter the parameter to be checked *changes* tuple list Contain the (param,changes,info) list listing the changes made =============== =================================== ================================================================ """ for param, change, data in changes: path = self.settings.childPath(param) if change == 'childAdded': pass elif change == 'value': if param.name() == 'model_class': self.get_set_model_params(param.value()) elif param.name() == 'refresh_plot_time' or param.name() == 'timeout': self.command_pid.emit(ThreadCommand('update_timer', [param.name(), param.value()])) elif param.name() == 'sample_time': self.command_pid.emit(ThreadCommand('update_options', dict(sample_time=param.value()))) elif param.name() in putils.iter_children( self.settings.child('main_settings', 'pid_controls', 'output_limits'), []): output_limits = convert_output_limits( self.settings.child('main_settings', 'pid_controls', 'output_limits', 'output_limit_min').value(), self.settings.child('main_settings', 'pid_controls', 'output_limits', 'output_limit_min_enabled').value(), self.settings.child('main_settings', 'pid_controls', 'output_limits', 'output_limit_max').value(), self.settings.child('main_settings', 'pid_controls', 'output_limits', 'output_limit_max_enabled').value()) self.command_pid.emit(ThreadCommand('update_options', dict(output_limits=output_limits))) elif param.name() in putils.iter_children( self.settings.child('main_settings', 'pid_controls', 'pid_constants'), []): Kp = self.settings.child('main_settings', 'pid_controls', 'pid_constants', 'kp').value() Ki = self.settings.child('main_settings', 'pid_controls', 'pid_constants', 'ki').value() Kd = self.settings.child('main_settings', 'pid_controls', 'pid_constants', 'kd').value() self.command_pid.emit(ThreadCommand('update_options', dict(tunings=(Kp, Ki, Kd)))) elif param.name() in putils.iter_children(self.settings.child('models', 'model_params'), []): if self.model_class is not None: self.model_class.update_settings(param) elif param.name() == 'detector_modules': self.model_class.update_detector_names() elif change == 'parent': pass def update_runner_setpoints(self): self.command_pid.emit(ThreadCommand('update_setpoints', self.setpoints)) @Slot(list) def thread_status(self, status): # general function to get datas/infos from all threads back to the main """ """ pass
class QThreadedClient(QObject): """Threaded socket client. Available signals: - finished(): When client thread stops. - closed(): After closing socket. - connected(ip: str, port: int): Successfully connected to server. - message(data: dict): Received message from server. - disconnected(): Disconnected from server. - error(error_string: str): Socket error. - failed_to_connect(ip: str, port: int): Failed to connect to server. Available slots: - start(): Start client. - write(data: bytes): Write message to server. - reconnect(): Reconnect to server. - close(): Close connection. - disconnect_from_server(): Disconnect from server. - connect_to(ip: str, port: int): (Re)connect to server. """ finished = Signal() closed = Signal() connected = Signal(str, int) message = Signal(bytes) disconnected = Signal() error = Signal(str) failed_to_connect = Signal(str, int) def __init__(self, loggerName=None): super(QThreadedClient, self).__init__(None) self.__ip = None self.__port = None self.__client = None self.__client_thread = None self.__logger_name = loggerName @Slot(str, int) def start(self, ip: str, port: int): """Start client thread and connect to server.""" self.__ip = ip self.__port = port self.__client = _SocketClient(self.__ip, self.__port, loggerName=self.__logger_name) self.__client_thread = QThread() self.__client_thread.started.connect(self.__client.start) self.__client_thread.finished.connect(self.finished.emit) self.__client.moveToThread(self.__client_thread) self.__client.connected.connect(self.on_connected) self.__client.failed_to_connect.connect(self.on_failed_to_connect) self.__client.message.connect(self.on_message) self.__client.disconnected.connect(self.on_disconnected) self.__client.error.connect(self.on_error) self.__client.closed.connect(self.on_closed) self.__client_thread.start() @Slot(str, int) def on_connected(self, ip, port): """Called when client connects to server. Emits connected signal. Args: ip (str): Client ip address. port (int): Client port. """ self.connected.emit(ip, port) @Slot(str, int) def on_failed_to_connect(self, ip, port): """Called when client fails to connect to server. Emits failed_to_connect signal. Args: ip (str): Client ip address. port (int): Client port. """ self.failed_to_connect.emit(ip, port) @Slot(bytes) def on_message(self, message: bytes): """Called when client receives message from server. Emits message signal. Args: message (bytes): Message. """ self.message.emit(message) @Slot() def on_disconnected(self): """Called when device disconnects from server. Emits disconnected signal.""" self.disconnected.emit() @Slot(str) def on_error(self, error: str): """Called when a socket error occurs. Emits error signal. Args: error (str): Error string. """ self.error.emit(error) @Slot() def on_closed(self): """Called when when the socket is closed. Emits closed signal.""" self.closed.emit() @Slot(bytes) def write(self, data: bytes): """Write data to server. Args: data (bytes): Data to write. """ self.__client.write_signal.emit(data) @Slot() def close(self): """Disconnect from server and close socket.""" if self.__client and self.__client_thread: self.__client.close_signal.emit() self.__client_thread.quit() else: self.error.emit("Client not running") @Slot() def disconnect_from_server(self): """Disconnect from server.""" self.__client.disconnect_signal.emit() @Slot(str, int) def connect_to(self, ip: str, port: int): """(Re)connect to server. Args: ip (str): IP address. port (int): Port. """ self.__client.connect_signal.emit(ip, port) @Slot() def reconnect(self): self.__client.reconnect_signal.emit() @Slot() def is_running(self): """Check if server is running""" if self.__client_thread: return self.__client_thread.isRunning() return False @Slot() def wait(self): """Wait for server thread to finish.""" if self.__client_thread: return self.__client_thread.wait() return True
class ProcessConsole(QTextEdit): signal_stop_qthread = Signal() signal_process_stopped = Signal() signal_process_started = Signal() signal_hyperlink_clicked = Signal(str) signal_goto_file = Signal(str, int) insert_mode = '' def __init__(self, args: list = None): super().__init__() self.anchor = None self._is_running = False self.auto_scroll = True self.args = args # self.setContentsMargins(20, 20, 0, 0) self.monitor_thread: 'ProcessMonitorThread' = None self.out_thread: 'QThread' = None self.signal_hyperlink_clicked.connect(self.on_hyperlink_clicked) def set_args(self, args: list = None): self.args = args def is_running(self): if self.monitor_thread is not None: if self.monitor_thread.process.process.poll() is None: return True return False def start_process(self): if not self.is_running(): self.out_thread = QThread(self) self.monitor_thread = ProcessMonitorThread() self.monitor_thread.args = self.args self.monitor_thread.moveToThread(self.out_thread) self.out_thread.started.connect(self.monitor_thread.run) self.out_thread.start() self.monitor_thread.on_out.connect(self.on_stdout) self.monitor_thread.on_err.connect(self.on_stderr) self.signal_stop_qthread.connect(self.monitor_thread.stop) self.out_thread.finished.connect(self.out_thread.deleteLater) self.out_thread.finished.connect(self.monitor_thread.deleteLater) self.monitor_thread.on_finished.connect(self.terminate_process) self.clear() self.insertHtml('<p style="color:#0063c5;">' + ' '.join(self.args) + '<br/></p>') self.insertHtml('<p style="color:#aaaaaa;">' + '' + '</p>') def on_stdout(self, text): if self.insert_mode == 'error': self.insert_mode = 'stdout' self.insertHtml( self.insertHtml('<p style="color:#aaaaaa;">' + text + '<br/></p>')) if self.auto_scroll: self.ensureCursorVisible() def on_stderr(self, text): self.insert_mode = 'error' # result = re.search(r'(/|([a-zA-Z]:((\\)|/))).*:[0-9].*', text) result = re.search(r'(/|([a-zA-Z]:((\\)|/))).* line [0-9].*,', text) print(result) if result is None: self.insertHtml('<p style="color:red;">' + text + '<br/></p>') else: span = result.span() self.insertHtml('<p style="color:red;">' + text[:span[0]] + '</p>') print(result.group()) self.insert_hyperlink(text[span[0]:span[1]]) # self.insertHtml('<p style="color:red;">' + text[result[0]:result[1]] + '</p>') self.insertHtml('<p style="color:red;">' + text[span[1]:] + '<br/></p>') if self.auto_scroll: self.ensureCursorVisible() def terminate_process(self): if self.monitor_thread is not None: self.monitor_thread.process_terminated = True self.monitor_thread.process.process.terminate() if self.out_thread.isRunning(): self.signal_stop_qthread.emit() self.out_thread.quit() self.out_thread.wait(500) self.monitor_thread = None self.out_thread = None self.signal_process_stopped.emit() def keyPressEvent(self, e: 'QKeyEvent'): if e.key() == Qt.Key_Backspace or e.key() == Qt.Key_Delete: return print(e.key(), e.text()) if e.key() == Qt.Key_Return: text = '\n' else: text = e.text() if text != '' and self.monitor_thread is not None: try: print('sent:', text) self.monitor_thread.process.process.stdin.write( text.encode('utf8')) self.monitor_thread.process.process.stdin.flush() except: import traceback traceback.print_exc() super(ProcessConsole, self).keyPressEvent(e) def mousePressEvent(self, e): super(ProcessConsole, self).mousePressEvent(e) self.anchor = self.anchorAt(e.pos()) # if self.anchor: # QApplication.setOverrideCursor(Qt.PointingHandCursor) def mouseMoveEvent(self, e): super(ProcessConsole, self).mouseMoveEvent(e) self.anchor = self.anchorAt(e.pos()) if not self.anchor: QApplication.setOverrideCursor(Qt.ArrowCursor) else: QApplication.setOverrideCursor(Qt.PointingHandCursor) def mouseReleaseEvent(self, e): super(ProcessConsole, self).mouseReleaseEvent(e) if self.anchor: # QDesktopServices.openUrl(QUrl(self.anchor)) # QApplication.setOverrideCursor(Qt.ArrowCursor) self.signal_hyperlink_clicked.emit(self.anchor) self.anchor = None def insert_hyperlink(self, link: str, text=''): cursor = self.textCursor() fmt = cursor.charFormat() fmt.setForeground(QColor('#0063c5')) # address = 'http://example.com' fmt.setAnchor(True) fmt.setAnchorHref(link) fmt.setToolTip(link) if text == '': cursor.insertText(link, fmt) else: cursor.insertText(text, fmt) def on_hyperlink_clicked(self, hyperlink_text: str): if re.search(r'(/|([a-zA-Z]:((\\)|/))).* line [0-9].*,', hyperlink_text) is not None: try: l = hyperlink_text.split('line') assert len(l) == 2, l for i in [0, 1]: l[i] = l[i].strip(', \"\'') file_path, row_str = l row = int(row_str) if not os.path.exists(file_path): QMessageBox.warning(self, self.tr('Warning'), self.tr('文件%s不存在!' % file_path)) else: self.signal_goto_file.emit(file_path, row) logger.info('goto file %s, line %d' % (file_path, row)) except: import traceback traceback.print_exc()