class PMQThreadManager(QObject): signal_server_message_received = Signal(dict) signal_data_changed = Signal(str) def __init__(self, parent=None, work_fcn: Callable = None, loop_fcn: Callable = None, exit_fcn: Callable = None): super().__init__(parent) self.thread_recv = QThread() self.worker_recv = PMGThreadWorker() if work_fcn is not None and loop_fcn is not None: raise ValueError( 'work_fcn and loop_fcn cannot be both not None at the same time!' ) if work_fcn is not None: self.worker_recv.work = work_fcn else: self.worker_recv.work_loop_fcn = loop_fcn self.worker_recv.moveToThread(self.thread_recv) self.thread_recv.started.connect( self.worker_recv.work) # self.worker_recv.work) self.worker_recv.on_exit_fcn = exit_fcn self.thread_recv.start() def shut_down(self): logger.info('client quit') self.worker_recv.on_exit() # self.worker_recv.close_socket() self.thread_recv.quit() self.thread_recv.wait(500)
class PMClient(QObject, BaseClient): signal_server_message_received = Signal(dict) signal_data_changed = Signal(str) def __init__(self, parent=None, name='Anonymous QtClient'): super().__init__(parent) self.name = name self.client = self.init_socket(12306) self.thread_recv = QThread() self.worker_recv = RecvWork(self.client, self.name) self.signal_received = self.worker_recv.signal_received self.worker_recv.signal_received.connect(self.on_server_message_received) self.worker_recv.moveToThread(self.thread_recv) self.thread_recv.started.connect(self.worker_recv.work) self.thread_recv.start() def shut_down(self): logger.info('client quit') self.worker_recv.close_socket() self.thread_recv.quit() self.thread_recv.wait(500) def on_server_message_received(self, packet: bytes): try: dic = json.loads(packet) logger.info(dic) msg = dic.get('message') if msg == 'data_changed': data_name = dic.get('data_name') if data_name is not None: self.signal_data_changed.emit(data_name) self.signal_server_message_received.emit(dic) except: import traceback traceback.print_exc() pass def send_bytes(self, packet: bytes): self.client.sendall(packet) @timeit def compress(self, byte_str): return zlib.compress(byte_str)
def _start(self, worker=None): """Start threads and check for inactive workers.""" if worker: self._queue_workers.append(worker) if self._queue_workers and self._running_threads < self._max_threads: #print('Queue: {0} Running: {1} Workers: {2} ' # 'Threads: {3}'.format(len(self._queue_workers), # self._running_threads, # len(self._workers), # len(self._threads))) self._running_threads += 1 worker = self._queue_workers.popleft() thread = QThread(None) if isinstance(worker, PythonWorker): worker.moveToThread(thread) worker.sig_finished.connect(thread.quit) thread.started.connect(worker._start) thread.start() elif isinstance(worker, ProcessWorker): thread.quit() thread.wait() worker._start() self._threads.append(thread) else: self._timer.start() if self._workers: for w in self._workers: if w.is_finished(): self._bag_collector.append(w) self._workers.remove(w) if self._threads: for t in self._threads: if t.isFinished(): self._threads.remove(t) self._running_threads -= 1 if len(self._threads) == 0 and len(self._workers) == 0: self._timer.stop() self._timer_worker_delete.start()
class PMQThreadObject(QObject): signal_server_message_received = Signal(dict) signal_data_changed = Signal(str) def __init__(self, parent=None, worker: QObject = None): super().__init__(parent) self.thread = QThread() self.worker = worker self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.work) # self.worker_recv.work) self.thread.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) self.thread.start() def terminate(self): logger.info('client quit') self.worker.on_exit() self.thread.quit() self.thread.wait(500)
class PMGQThreadManager(QObject): signal_server_message_received = Signal(dict) signal_data_changed = Signal(str) signal_finished = Signal() def __init__(self, parent=None, worker: QObject = None): super().__init__(parent) self.thread_recv = QThread() self.worker_recv = worker self.worker_recv.moveToThread(self.thread_recv) self.thread_recv.started.connect(self.worker_recv.work) self.thread_recv.start() self.thread_recv.finished.connect(self.signal_finished.emit) def shut_down(self): """ 关闭线程,并且退出。 :return: """ self.worker_recv.on_exit() self.thread_recv.quit() self.thread_recv.wait(500)
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 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 RefaceDXLibApp(QApplication): """The main application object with the central event handling.""" name = "Reface DX Lib" def __init__(self, args=None): if args is None: args = sys.argv super().__init__(args) self.setOrganizationName('chrisarndt.de') self.setOrganizationDomain('chrisarndt.de') self.setApplicationName(self.name) QSettings.setDefaultFormat(QSettings.IniFormat) self.config = QSettings() self.config.setIniCodec('UTF-8') QIcon.setThemeName(self.config.value('gui/icon_theme', "tango")) self.debug = True if '-v' in args[1:] else self.config.value( 'application/debug', False) logging.basicConfig( level=logging.DEBUG if self.debug else logging.INFO, format='%(levelname)s - %(message)s') self.mainwin = RefaceDXLibMainWin(self.tr(self.name)) self.load_database( self.config.value('database/last_opened', 'refacedx.db')) self.midiin_conn = None self.midiout_conn = None self.setup_midi_thread() # signal connections self.aboutToQuit.connect(self.quit) self.mainwin.action_open.triggered.connect(self.open_database) self.mainwin.action_quit.triggered.connect(self.quit) self.mainwin.action_import.triggered.connect(self.import_patches) self.mainwin.action_export.triggered.connect(self.export_patches) self.mainwin.action_send.triggered.connect(self.send_patches) self.mainwin.action_request.triggered.connect(self.request_patch) self.mainwin.action_delete.triggered.connect(self.delete_patches) # dialogs (initialized on-demand) self.add_patch_dialog = None self.style = DarkAppStyle(self) self.mainwin.show() def load_database(self, filename): db_uri = 'sqlite:///{}'.format(filename) self.session = initdb(db_uri, debug=self.config.value('database/debug', False)) self.patches = PatchlistTableModel(self.session) self.mainwin.set_patchtable_model(self.patches) def setup_midi_thread(self): self.midithread = QThread() self.midiworker = MidiWorker(self.config) self.midiworker.moveToThread(self.midithread) self.midithread.started.connect(self.midiworker.initialize) self.midiworker.send_patch_start.connect( partial(self.mainwin.set_send_action_enabled, False)) self.midiworker.send_patch_complete.connect( partial(self.mainwin.set_send_action_enabled, True)) self.midiworker.recv_patch_start.connect( partial(self.mainwin.set_request_action_enabled, False)) self.midiworker.recv_patch_complete.connect(self.receive_patch) self.midiworker.recv_patch_failed.connect( partial(self.mainwin.set_request_action_enabled, True)) self.midiworker.input_ports_changed.connect( self.build_midi_input_selector) self.midiworker.output_ports_changed.connect( self.build_midi_output_selector) # Start thread self.midithread.start() self.timer = QTimer() self.timer.timeout.connect(self.midiworker.scan_ports.emit) self.timer.start(3000) @Slot(object) def build_midi_input_selector(self, ports): log.debug("Building MIDI input selector...") cb = self.mainwin.midiin_cb if self.midiin_conn: cb.currentIndexChanged.disconnect(self.set_midiin_port) cb.setEnabled(False) cb.clear() selected = -1 for i, (port, is_open) in enumerate(ports): cb.addItem(port, port) if is_open: selected = i cb.setCurrentIndex(selected) self.midiin_conn = cb.currentIndexChanged.connect(self.set_midiin_port) cb.setEnabled(True) log.debug("MIDI input selector (re-)built.") @Slot(object) def build_midi_output_selector(self, ports): log.debug("Building MIDI output selector...") cb = self.mainwin.midiout_cb if self.midiout_conn: cb.currentIndexChanged.disconnect(self.set_midiout_port) cb.setEnabled(False) cb.clear() selected = -1 for i, (port, is_open) in enumerate(ports): cb.addItem(port, port) if is_open: selected = i cb.setCurrentIndex(selected) self.midiout_conn = cb.currentIndexChanged.connect( self.set_midiout_port) cb.setEnabled(True) log.debug("MIDI output selector (re-)built.") def set_midiin_port(self, index): log.debug("MIDI input selector index changed: %r", index) if index != -1: port = self.mainwin.midiin_cb.itemData(index) self.midiworker.set_input_port.emit(port) def set_midiout_port(self, index): log.debug("MIDI output selector index changed: %r", index) if index != -1: port = self.mainwin.midiout_cb.itemData(index) self.midiworker.set_output_port.emit(port) # action handlers def quit(self): self.midiworker.close.emit() self.midithread.quit() self.midithread.wait() self.mainwin.close() def open_database(self): options = QFileDialog.Options() if not self.config.value('native_dialogs', False): options |= QFileDialog.DontUseNativeDialog filename, _ = QFileDialog.getOpenFileName( self.mainwin, self.tr("Open patch database"), self.config.value('paths/last_database_path', ''), "SQLite Database (*.sqlite *.db);;All Files (*)", options=options) if filename and exists(filename): self.config.setValue('paths/last_database_path', dirname(filename)) log.info(f"Opening database file '{filename}'...") try: self.load_database(filename) except Exception as exc: log.exception(f"Error opening database file '{filename}'.") dlg = self.create_error_dlg( self.tr("Could not load patch database <i>{}</i>.").format( basename(filename)), detail=str(exc), ignore_buttons=False) dlg.exec_() else: self.config.setValue('database/last_opened', filename) def save_patch(self, data, **meta): name = meta.get('name', '').strip() if not name: name = get_patch_name(data) with self.session.begin(): author = meta.get('author', '').strip() if author: author, created = get_or_create(self.session, Author, create_kwargs={'name': author}, displayname=author) else: author = None manufacturer = meta.get('manufacturer', '').strip() if manufacturer: manufacturer, created = get_or_create( self.session, Manufacturer, create_kwargs={'name': manufacturer}, displayname=manufacturer) else: manufacturer = None device = meta.get('device', '').strip() if device: device, created = get_or_create(self.session, Device, create_kwargs={'name': device}, displayname=device) else: device = None patch = Patch(name=name, displayname=meta.get('displayname', '').strip() or name, description=meta.get('description', '').strip() or None, rating=meta.get('rating', 0), author=author, manufacturer=manufacturer, device=device, created=meta.get('created', datetime.now()), data=set_patch_name(data, name)) self.session.add(patch) tags = (tag.strip() for tag in meta.get('tags', '').split(',')) patch.update_tags(self.session, (tag for tag in tags if tag)) def request_patch(self): self.midiworker.request_patch.emit(None) def receive_patch(self, data): log.debug("Patch received: %s", get_patch_name(data)) if self.add_patch_dialog is None: self.add_patch_dialog = AddPatchDialog(self) metadata = self.add_patch_dialog.new_from_data( data, self.tr("Add new patch")) if metadata: log.debug("Patch meta data: %r", metadata) self.save_patch(data, **metadata) self.patches._update() self.patches.layoutChanged.emit() self.mainwin.set_request_action_enabled(True) def delete_patches(self): if self.mainwin.selection.hasSelection(): rows = sorted( [r.row() for r in self.mainwin.selection.selectedRows()]) patches = tuple(self.patches.get_row(r).displayname for r in rows) msg_box = QMessageBox() if len(rows) == 1: msg_box.setText( self.tr("Delete patch '{}'?").format(patches[0])) else: msg_box.setText( self.tr("Delete {} patches?").format(len(rows))) msg_box.setDetailedText('\n'.join(patches)) msg_box.setInformativeText( self.tr("Patches can only be restored by re-importing them.")) msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel) msg_box.setDefaultButton(QMessageBox.Cancel) msg_box.setIcon(QMessageBox.Warning) if msg_box.exec_() == QMessageBox.Yes: with self.session.begin(): for n, row in enumerate(rows): self.patches.removeRows(row - n) def import_patches(self): options = QFileDialog.Options() if not self.config.value('native_dialogs', False): options |= QFileDialog.DontUseNativeDialog files, _ = QFileDialog.getOpenFileNames( self.mainwin, self.tr("Import SysEx patches"), self.config.value('paths/last_import_path', ''), "SysEx Files (*.syx);;All Files (*)", options=options) if files: self.config.setValue('paths/last_import_path', dirname(files[0])) self.patches.layoutAboutToBeChanged.emit() with self.session.begin(): for file in files: with open(file, 'rb') as syx: data = syx.read() #assert len(data) == 241 if is_reface_dx_voice(data): name = get_patch_name(data) displayname = splitext(basename(file))[0].replace( '_', ' ').strip() patch = Patch(name=name, displayname=displayname, data=data) self.session.add(patch) # TODO: check if any patches were actually added self.patches._update() self.patches.layoutChanged.emit() def export_patches(self): if self.mainwin.selection.hasSelection(): options = QFileDialog.Options() if not self.config.value('native_dialogs', False): options |= (QFileDialog.DontUseNativeDialog | QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks) dir_ = QFileDialog.getExistingDirectory( self.mainwin, self.tr("Choose directory to export SysEx patches"), self.config.value('paths/last_export_path', ''), options=options) if dir_: self.config.setValue('paths/last_export_path', dir_) ignore_all = False exported = 0 for row in self.mainwin.selection.selectedRows(): patch = self.patches.get_row(row) try: filename = patch.displayname.replace(' ', '_') + '.syx' with open(join(dir_, filename), 'wb') as syx: syx.write(patch.data) except OSError as exc: log.error("Could not write SysEx file at '%s': %s", filename, exc) if ignore_all: continue dlg = self.create_error_dlg( self.tr("Could not write SysEx file " "<i>{}</i>.").format(basename(filename)), info=self. tr("Abort export, ignore error or ignore all errors?" ), detail=str(exc)) ret = dlg.exec_() if dlg.clickedButton() == dlg.btn_ignoreall: ignore_all = True elif ret == QMessageBox.Abort: log.warning("Patch export aborted.") break elif ret == QMessageBox.Ignore: pass else: log.error( "Unknown error dialog response. This shouldn't happen." ) else: exported += 1 self.set_status_text( self.tr("{} patches exported.").format(exported)) else: self.set_status_text(self.tr("Patch export cancelled.")) def send_patches(self): if self.mainwin.selection.hasSelection(): for row in self.mainwin.selection.selectedRows(): patch = self.patches.get_row(row) self.midiworker.send_patch.emit(patch.data) log.debug("Sent patch: %s (%s)", patch.displayname, patch.name) def create_error_dlg(self, message, info=None, detail=None, ignore_buttons=True): dlg = QMessageBox() dlg.setText(message) if info: dlg.setInformativeText(info) if detail: dlg.setDetailedText(detail) dlg.setWindowTitle(self.name + self.tr(" - Error")) dlg.setIcon(QMessageBox.Critical) if ignore_buttons: dlg.setStandardButtons(QMessageBox.Abort | QMessageBox.Ignore) dlg.btn_ignoreall = dlg.addButton(self.tr("Ignore A&ll"), QMessageBox.ActionRole) dlg.setDefaultButton(QMessageBox.Ignore) else: dlg.setDefaultButton(QMessageBox.NoButton) return dlg def set_status_text(self, message, timeout=5000): self.mainwin.statusbar.showMessage(message, timeout)
class DAQ_Move(Ui_Form, QObject): """ | DAQ_Move object is a module used to control one motor from a specified list. | | Preset is an optional list of dicts used to managers programatically settings such as the name of the controller from the list of possible controllers, COM address... | | Init is a boolean to tell the programm to initialize the controller at the start of the programm given the managers options ========================= ================================================= **Attributes** **Type** *command_stage* instance of Signal *move_done_signal* instance of Signal *update_settings_signal* instance of Signal *status_signal* instance of Signal *bounds_signal* instance of Signal *params* dictionnary list *ui* instance of UI_Form *parent* QObject *title* string *wait_time* int *initialized_state* boolean *Move_done* boolean *controller* instance of the specific controller object *stage* instance of the stage (axis or wathever) object *current_position* float *target_position* float *wait_position_flag* boolean *stage_types* string list ========================= ================================================= See Also -------- set_enabled_move_buttons, set_setting_tree, stage_changed, quit_fun, ini_stage_fun, move_Abs, move_Rel, move_Home, get_position, stop_Motion, show_settings, show_fine_tuning References ---------- QLocale, QObject, Signal, QStatusBar, ParameterTree """ init_signal = Signal(bool) command_stage = Signal(ThreadCommand) command_tcpip = Signal(ThreadCommand) move_done_signal = Signal( str, float ) # to be used in external program to make sure the move has been done, export the current position. str refer to the unique title given to the module update_settings_signal = Signal(edict) status_signal = Signal(str) bounds_signal = Signal(bool) params = daq_move_params def __init__(self, parent, title="pymodaq Move", init=False): """DAQ_Move object is a module used to control one motor from a specified list. managers is an optional list of dicts used to managers programatically settings such as the name of the controller from the list of possible controllers, COM address... init is a boolean to tell the programm to initialize the controller at the start of the programm given the managers options To differenciate various instance of this class """ self.logger = utils.set_logger(f'{logger.name}.{title}') self.logger.info(f'Initializing DAQ_Move: {title}') super().__init__() here = Path(__file__).parent splash = QtGui.QPixmap(str(here.parent.joinpath('splash.png'))) self.splash_sc = QtWidgets.QSplashScreen(splash, Qt.WindowStaysOnTopHint) self.ui = Ui_Form() self.ui.setupUi(parent) self.ui.Moveto_pb_bis_2.setVisible(False) self.parent = parent self.ui.title_label.setText(title) self.title = title self.ui.statusbar = QtWidgets.QStatusBar(parent) self.ui.StatusBarLayout.addWidget(self.ui.statusbar) self.ui.statusbar.setMaximumHeight(20) self.send_to_tcpip = False self.tcpclient_thread = None self.wait_time = 1000 self.ui.Ini_state_LED self.ui.Ini_state_LED.clickable = False self.ui.Ini_state_LED.set_as_false() self.ui.Move_Done_LED.clickable = False self.ui.Move_Done_LED.set_as_false() self.initialized_state = False self.ui.Current_position_sb.setReadOnly(False) self.move_done_bool = True # ###########IMPORTANT############################ self.controller = None # the hardware controller/set after initialization and to be used by other modules # ################################################ self.current_position = 0 self.target_position = 0 self.wait_position_flag = True self.ui.Current_position_sb.setValue(self.current_position) self.set_enabled_move_buttons(enable=False) self.ui.groupBox.hide() self.parent.resize(150, 200) # #Setting stages types self.stage_types = [mov['name'] for mov in DAQ_Move_Stage_type] self.ui.Stage_type_combo.clear() self.ui.Stage_type_combo.addItems(self.stage_types) # create main parameter tree self.ui.settings_tree = ParameterTree() self.ui.verticalLayout_2.addWidget(self.ui.settings_tree) self.ui.settings_tree.setMinimumWidth(300) self.settings = Parameter.create(name='Settings', type='group', children=self.params) self.ui.settings_tree.setParameters(self.settings, showTop=False) # connecting from tree self.settings.sigTreeStateChanged.connect( self.parameter_tree_changed ) # any changes on the settings will update accordingly the detector self.ui.settings_tree.setVisible(False) self.set_setting_tree() QtWidgets.QApplication.processEvents() # #Connecting buttons: self.ui.Stage_type_combo.currentIndexChanged.connect( self.set_setting_tree) self.ui.Stage_type_combo.currentIndexChanged.connect( self.stage_changed) self.ui.Quit_pb.clicked.connect(self.quit_fun) self.ui.IniStage_pb.clicked.connect(self.ini_stage_fun) self.update_status("Ready", wait_time=self.wait_time) self.ui.Move_Abs_pb.clicked.connect( lambda: self.move_Abs(self.ui.Abs_position_sb.value())) self.ui.Move_Rel_plus_pb.clicked.connect( lambda: self.move_Rel(self.ui.Rel_position_sb.value())) self.ui.Move_Rel_minus_pb.clicked.connect( lambda: self.move_Rel(-self.ui.Rel_position_sb.value())) self.ui.Find_Home_pb.clicked.connect(self.move_Home) self.ui.Get_position_pb.clicked.connect(self.get_position) self.ui.Stop_pb.clicked.connect(self.stop_Motion) self.ui.parameters_pb.clicked.connect(self.show_settings) self.ui.fine_tuning_pb.clicked.connect(self.show_fine_tuning) self.ui.Abs_position_sb.valueChanged.connect( self.ui.Abs_position_sb_bis.setValue) self.ui.Abs_position_sb_bis.valueChanged.connect( self.ui.Abs_position_sb.setValue) self.ui.Moveto_pb_bis.clicked.connect( lambda: self.move_Abs(self.ui.Abs_position_sb_bis.value())) # initialize the controller if init=True if init: self.ui.IniStage_pb.click() @property def actuator(self): return self.ui.Stage_type_combo.currentText() @actuator.setter def actuator(self, actuator): self.ui.Stage_type_combo.setCurrentText(actuator) if self.actuator != actuator: raise ActuatorError( f'{actuator} is not a valid installed actuator: {self.stage_types}' ) def init(self): self.ui.IniStage_pb.click() def ini_stage_fun(self): """ Init : * a DAQ_move_stage instance if not exists * a linked thread connected by signal to the DAQ_move_main instance See Also -------- set_enabled_move_buttons, DAQ_utils.ThreadCommand, DAQ_Move_stage, DAQ_Move_stage.queue_command, thread_status, DAQ_Move_stage.update_settings, update_status """ try: if not self.ui.IniStage_pb.isChecked(): try: self.set_enabled_move_buttons(enable=False) self.ui.Stage_type_combo.setEnabled(True) self.ui.Ini_state_LED.set_as_false() self.command_stage.emit(ThreadCommand(command="close")) except Exception as e: self.logger.exception(str(e)) else: self.stage_name = self.ui.Stage_type_combo.currentText() stage = DAQ_Move_stage(self.stage_name, self.current_position, self.title) self.stage_thread = QThread() stage.moveToThread(self.stage_thread) self.command_stage[ThreadCommand].connect(stage.queue_command) stage.status_sig[ThreadCommand].connect(self.thread_status) self.update_settings_signal[edict].connect( stage.update_settings) self.stage_thread.stage = stage self.stage_thread.start() self.ui.Stage_type_combo.setEnabled(False) self.command_stage.emit( ThreadCommand(command="ini_stage", attributes=[ self.settings.child( ('move_settings')).saveState(), self.controller ])) except Exception as e: self.logger.exception(str(e)) self.set_enabled_move_buttons(enable=False) def get_position(self): """ Get the current position from the launched thread via the "check_position" Thread Command. See Also -------- update_status, DAQ_utils.ThreadCommand """ try: self.command_stage.emit(ThreadCommand(command="check_position")) except Exception as e: self.logger.exception(str(e)) def move(self, move_command: MoveCommand): """Public method to trigger the correct action on the actuator. Should be used by external applications""" if move_command.move_type == 'abs': self.move_Abs(move_command.value) elif move_command.move_type == 'rel': self.move_Rel(move_command.value) elif move_command.move_type == 'home': self.move_Home(move_command.value) def move_Abs(self, position, send_to_tcpip=False): """ | Make the move from an absolute position. | | The move is made if target is in bounds, sending the thread command "Reset_Stop_Motion" and "move_Abs". =============== ========== =========================================== **Parameters** **Type** **Description** *position* float The absolute target position of the move =============== ========== =========================================== See Also -------- update_status, check_out_bounds, DAQ_utils.ThreadCommand """ try: self.send_to_tcpip = send_to_tcpip if not (position == self.current_position and self.stage_name == "Thorlabs_Flipper"): self.ui.Move_Done_LED.set_as_false() self.move_done_bool = False self.target_position = position self.update_status("Moving", wait_time=self.wait_time) # self.check_out_bounds(position) self.command_stage.emit( ThreadCommand(command="Reset_Stop_Motion")) self.command_stage.emit( ThreadCommand(command="move_Abs", attributes=[position])) except Exception as e: self.logger.exception(str(e)) def move_Home(self, send_to_tcpip=False): """ Send the thread commands "Reset_Stop_Motion" and "move_Home" and update the status. See Also -------- update_status, DAQ_utils.ThreadCommand """ self.send_to_tcpip = send_to_tcpip try: self.ui.Move_Done_LED.set_as_false() self.move_done_bool = False self.update_status("Moving", wait_time=self.wait_time) self.command_stage.emit(ThreadCommand(command="Reset_Stop_Motion")) self.command_stage.emit(ThreadCommand(command="move_Home")) except Exception as e: self.logger.exception(str(e)) def move_Rel_p(self): self.ui.Move_Rel_plus_pb.click() def move_Rel_m(self, send_to_tcpip=False): self.ui.Move_Rel_minus_pb.click() def move_Rel(self, rel_position, send_to_tcpip=False): """ | Make a move from the given relative psition and the current one. | | The move is done if (current position + relative position) is in bounds sending Threads Commands "Reset_Stop_Motion" and "move_done" =============== ========== =================================================== **Parameters** **Type** **Description** *position* float The relative target position from the current one =============== ========== =================================================== See Also -------- update_status, check_out_bounds, DAQ_utils.ThreadCommand """ try: self.send_to_tcpip = send_to_tcpip self.ui.Move_Done_LED.set_as_false() self.move_done_bool = False self.target_position = self.current_position + rel_position self.update_status("Moving", wait_time=self.wait_time) # self.check_out_bounds(self.target_position) self.command_stage.emit(ThreadCommand(command="Reset_Stop_Motion")) self.command_stage.emit( ThreadCommand(command="move_Rel", attributes=[rel_position])) except Exception as e: self.logger.exception(str(e)) def parameter_tree_changed(self, param, changes): """ | Check eventual changes in the changes list parameter. | | In case of changed values, emit the signal containing the current path and parameter via update_settings_signal to the connected hardware. =============== ==================================== ================================================== **Parameters** **Type** **Description** *param* instance of pyqtgraph parameter The parameter to be checked *changes* (parameter,change,infos) tuple list The (parameter,change,infos) list to be treated =============== ==================================== ================================================== """ for param, change, data in changes: path = self.settings.childPath(param) if path is not None: childName = '.'.join(path) else: childName = param.name() if change == 'childAdded': if 'main_settings' not in path: self.update_settings_signal.emit( edict(path=path, param=data[0].saveState(), change=change)) elif change == 'value': if param.name() == 'connect_server': if param.value(): self.connect_tcp_ip() else: self.command_tcpip.emit(ThreadCommand('quit')) elif param.name() == 'ip_address' or param.name == 'port': self.command_tcpip.emit( ThreadCommand( 'update_connection', dict(ipaddress=self.settings.child( 'main_settings', 'tcpip', 'ip_address').value(), port=self.settings.child( 'main_settings', 'tcpip', 'port').value()))) if path is not None: if 'main_settings' not in path: self.update_settings_signal.emit( edict(path=path, param=param, change=change)) if self.settings.child('main_settings', 'tcpip', 'tcp_connected').value(): self.command_tcpip.emit( ThreadCommand('send_info', dict(path=path, param=param))) elif change == 'parent': if param.name() not in putils.iter_children( self.settings.child('main_settings'), []): self.update_settings_signal.emit( edict(path=['move_settings'], param=param, change=change)) def connect_tcp_ip(self): if self.settings.child('main_settings', 'tcpip', 'connect_server').value(): self.tcpclient_thread = QThread() tcpclient = TCPClient(self.settings.child('main_settings', 'tcpip', 'ip_address').value(), self.settings.child('main_settings', 'tcpip', 'port').value(), self.settings.child(('move_settings')), client_type="ACTUATOR") tcpclient.moveToThread(self.tcpclient_thread) self.tcpclient_thread.tcpclient = tcpclient tcpclient.cmd_signal.connect(self.process_tcpip_cmds) self.command_tcpip[ThreadCommand].connect(tcpclient.queue_command) self.tcpclient_thread.start() tcpclient.init_connection() @Slot(ThreadCommand) def process_tcpip_cmds(self, status): if 'move_abs' in status.command: self.move_Abs(status.attributes[0], send_to_tcpip=True) elif 'move_rel' in status.command: self.move_Rel(status.attributes[0], send_to_tcpip=True) elif 'move_home' in status.command: self.move_Home(send_to_tcpip=True) elif 'check_position' in status.command: self.send_to_tcpip = True self.command_stage.emit(ThreadCommand('check_position')) elif status.command == 'connected': self.settings.child('main_settings', 'tcpip', 'tcp_connected').setValue(True) elif status.command == 'disconnected': self.settings.child('main_settings', 'tcpip', 'tcp_connected').setValue(False) elif status.command == 'Update_Status': self.thread_status(status) elif status.command == 'set_info': param_dict = ioxml.XML_string_to_parameter(status.attributes[1])[0] param_tmp = Parameter.create(**param_dict) param = self.settings.child('move_settings', *status.attributes[0][1:]) param.restoreState(param_tmp.saveState()) def quit_fun(self): """ Leave the current instance of DAQ_Move_Main closing the parent widget. """ # insert anything that needs to be closed before leaving try: if self.initialized_state: self.ui.IniStage_pb.click() self.parent.close() # close the parent widget try: self.parent.parent().parent().close( ) # the dock parent (if any) except Exception as e: self.logger.info('No dock parent to close') except Exception as e: icon = QtGui.QIcon() icon.addPixmap( QtGui.QPixmap(":/Labview_icons/Icon_Library/close2.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) msgBox = QtWidgets.QMessageBox(parent=None) msgBox.addButton(QtWidgets.QMessageBox.Yes) msgBox.addButton(QtWidgets.QMessageBox.No) msgBox.setWindowTitle("Error") msgBox.setText( str(e) + " error happened when uninitializing the stage.\nDo you still want to quit?" ) msgBox.setDefaultButton(QtWidgets.QMessageBox.Yes) ret = msgBox.exec() if ret == QtWidgets.QMessageBox.Yes: self.parent.close() @Slot() def raise_timeout(self): """ Update status with "Timeout occured" statement. See Also -------- update_status """ self.update_status("Timeout occured", wait_time=self.wait_time) self.wait_position_flag = False def set_enabled_move_buttons(self, enable=False): """ Set the move buttons enabled (or not) in User Interface from the gridLayout_buttons course. =============== ========== ================================================ **Parameters** **Type** **Description** *enable* boolean The parameter making enable or not the buttons =============== ========== ================================================ """ Nchildren = self.ui.gridLayout_buttons.count() for ind in range(Nchildren): widget = self.ui.gridLayout_buttons.itemAt(ind).widget() if widget is not None: widget.setEnabled(enable) self.ui.Moveto_pb_bis.setEnabled(enable) self.ui.Abs_position_sb_bis.setEnabled(enable) self.ui.Current_position_sb.setEnabled(enable) @Slot(int) def set_setting_tree(self, index=0): """ Set the move settings parameters tree, clearing the current tree and setting the 'move_settings' node. See Also -------- update_status """ self.stage_name = self.ui.Stage_type_combo.currentText() self.settings.child('main_settings', 'move_type').setValue(self.stage_name) try: for child in self.settings.child(('move_settings')).children(): child.remove() parent_module = utils.find_dict_in_list_from_key_val( DAQ_Move_Stage_type, 'name', self.stage_name) class_ = getattr( getattr(parent_module['module'], 'daq_move_' + self.stage_name), 'DAQ_Move_' + self.stage_name) params = getattr(class_, 'params') move_params = Parameter.create(name='move_settings', type='group', children=params) self.settings.child( ('move_settings')).addChildren(move_params.children()) except Exception as e: self.logger.exception(str(e)) def show_fine_tuning(self): """ Make GroupBox visible if User Interface corresponding attribute is checked to show fine tuning in. """ if self.ui.fine_tuning_pb.isChecked(): self.ui.groupBox.show() else: self.ui.groupBox.hide() def show_settings(self): """ Make settings tree visible if User Interface corresponding attribute is checked to show the settings tree in. """ if self.ui.parameters_pb.isChecked(): self.ui.settings_tree.setVisible(True) else: self.ui.settings_tree.setVisible(False) @Slot(int) def stage_changed(self, index=0): """ See Also -------- move_Abs """ pass def stop_Motion(self): """ stop any motion via the launched thread with the "stop_Motion" Thread Command. See Also -------- update_status, DAQ_utils.ThreadCommand """ try: self.command_stage.emit(ThreadCommand(command="stop_Motion")) except Exception as e: self.logger.exception(str(e)) @Slot(ThreadCommand) def thread_status( self, status ): # general function to get datas/infos from all threads back to the main """ | General function to get datas/infos from all threads back to the main0 | Interpret a command from the command given by the ThreadCommand status : * In case of **'Update_status'** command, call the update_status method with status attributes as parameters * In case of **'ini_stage'** command, initialise a Stage from status attributes * In case of **'close'** command, close the launched stage thread * In case of **'check_position'** command, set the Current_position value from status attributes * In case of **'move_done'** command, set the Current_position value, make profile of move_done and send the move done signal with status attributes * In case of **'Move_Not_Done'** command, set the current position value from the status attributes, make profile of Not_Move_Done and send the Thread Command "Move_abs" * In case of **'update_settings'** command, create child "Move Settings" from status attributes (if possible) ================ ================= ====================================================== **Parameters** **Type** **Description** *status* ThreadCommand() instance of ThreadCommand containing two attributes : * *command* str * *attributes* list ================ ================= ====================================================== See Also -------- update_status, set_enabled_move_buttons, get_position, DAQ_utils.ThreadCommand, parameter_tree_changed, raise_timeout """ if status.command == "Update_Status": if len(status.attributes) > 2: self.update_status(status.attributes[0], wait_time=self.wait_time, log_type=status.attributes[1]) else: self.update_status(status.attributes[0], wait_time=self.wait_time) elif status.command == "ini_stage": # status.attributes[0]=edict(initialized=bool,info="", controller=) self.update_status("Stage initialized: {:} info: {:}".format( status.attributes[0]['initialized'], status.attributes[0]['info']), wait_time=self.wait_time) if status.attributes[0]['initialized']: self.controller = status.attributes[0]['controller'] self.set_enabled_move_buttons(enable=True) self.ui.Ini_state_LED.set_as_true() self.initialized_state = True else: self.initialized_state = False if self.initialized_state: self.get_position() self.init_signal.emit(self.initialized_state) elif status.command == "close": try: self.update_status(status.attributes[0], wait_time=self.wait_time) self.stage_thread.exit() self.stage_thread.wait() finished = self.stage_thread.isFinished() if finished: pass delattr(self, 'stage_thread') else: self.update_status('thread is locked?!', self.wait_time, 'log') except Exception as e: self.logger.exception(str(e)) self.initialized_state = False self.init_signal.emit(self.initialized_state) elif status.command == "check_position": self.ui.Current_position_sb.setValue(status.attributes[0]) self.current_position = status.attributes[0] if self.settings.child( 'main_settings', 'tcpip', 'tcp_connected').value() and self.send_to_tcpip: self.command_tcpip.emit( ThreadCommand('position_is', status.attributes)) elif status.command == "move_done": self.ui.Current_position_sb.setValue(status.attributes[0]) self.current_position = status.attributes[0] self.move_done_bool = True self.ui.Move_Done_LED.set_as_true() self.move_done_signal.emit(self.title, status.attributes[0]) if self.settings.child( 'main_settings', 'tcpip', 'tcp_connected').value() and self.send_to_tcpip: self.command_tcpip.emit( ThreadCommand('move_done', status.attributes)) elif status.command == "Move_Not_Done": self.ui.Current_position_sb.setValue(status.attributes[0]) self.current_position = status.attributes[0] self.move_done_bool = False self.ui.Move_Done_LED.set_as_false() self.command_stage.emit( ThreadCommand(command="move_Abs", attributes=[self.target_position])) elif status.command == 'update_main_settings': # this is a way for the plugins to update main settings of the ui (solely values, limits and options) try: if status.attributes[2] == 'value': self.settings.child('main_settings', *status.attributes[0]).setValue( status.attributes[1]) elif status.attributes[2] == 'limits': self.settings.child('main_settings', *status.attributes[0]).setLimits( status.attributes[1]) elif status.attributes[2] == 'options': self.settings.child( 'main_settings', *status.attributes[0]).setOpts(**status.attributes[1]) except Exception as e: self.logger.exception(str(e)) elif status.command == 'update_settings': # ThreadCommand(command='update_settings',attributes=[path,data,change])) try: self.settings.sigTreeStateChanged.disconnect( self.parameter_tree_changed ) # any changes on the settings will update accordingly the detector except Exception: pass try: if status.attributes[2] == 'value': self.settings.child('move_settings', *status.attributes[0]).setValue( status.attributes[1]) elif status.attributes[2] == 'limits': self.settings.child('move_settings', *status.attributes[0]).setLimits( status.attributes[1]) elif status.attributes[2] == 'options': self.settings.child( 'move_settings', *status.attributes[0]).setOpts(**status.attributes[1]) elif status.attributes[2] == 'childAdded': child = Parameter.create(name='tmp') child.restoreState(status.attributes[1][0]) self.settings.child('move_settings', *status.attributes[0]).addChild( status.attributes[1][0]) except Exception as e: self.logger.exception(str(e)) self.settings.sigTreeStateChanged.connect( self.parameter_tree_changed ) # any changes on the settings will update accordingly the detector elif status.command == 'raise_timeout': self.raise_timeout() elif status.command == 'outofbounds': self.bounds_signal.emit(True) elif status.command == 'show_splash': self.ui.settings_tree.setEnabled(False) self.splash_sc.show() self.splash_sc.raise_() self.splash_sc.showMessage(status.attributes[0], color=Qt.white) elif status.command == 'close_splash': self.splash_sc.close() self.ui.settings_tree.setEnabled(True) elif status.command == 'set_allowed_values': if 'decimals' in status.attributes: self.ui.Current_position_sb.setDecimals( status.attributes['decimals']) self.ui.Abs_position_sb.setDecimals( status.attributes['decimals']) self.ui.Abs_position_sb_bis.setDecimals( status.attributes['decimals']) if 'minimum' in status.attributes: self.ui.Current_position_sb.setMinimum( status.attributes['minimum']) self.ui.Abs_position_sb.setMinimum( status.attributes['minimum']) self.ui.Abs_position_sb_bis.setMinimum( status.attributes['minimum']) if 'maximum' in status.attributes: self.ui.Current_position_sb.setMaximum( status.attributes['maximum']) self.ui.Abs_position_sb.setMaximum( status.attributes['maximum']) self.ui.Abs_position_sb_bis.setMaximum( status.attributes['maximum']) if 'step' in status.attributes: self.ui.Current_position_sb.setSingleStep( status.attributes['step']) self.ui.Abs_position_sb.setSingleStep( status.attributes['step']) self.ui.Abs_position_sb_bis.setSingleStep( status.attributes['step']) def update_status(self, txt, wait_time=0): """ Show the given txt message in the status bar with a delay of wait_time ms if specified (0 by default). ================ ========== ================================= **Parameters** **Type** **Description** *txt* string The message to show *wait_time* int The delay time of showing ================ ========== ================================= """ self.ui.statusbar.showMessage(txt, wait_time) self.status_signal.emit(txt) self.logger.info(txt)
class MainApp(QWidget): LANDSCAPE = 0 PORTRAIT = 1 stop_signal = Signal() quality_changed = Signal(str) style_changed = Signal(object) last_frame_changed = Signal(object) def __init__(self): QWidget.__init__(self) self.styles = load_styles() self.image = StyledCapture(QImage(256, 256, QImage.Format_RGB888), '') # placeholder self.freeze = None if isinstance(settings.CAPTURE_HANDLER, string_types): self.capture_handler = locate(settings.CAPTURE_HANDLER) else: self.capture_handler = settings.CAPTURE_HANDLER self.setup_ui() self.frame_grabber = FrameGrabber(settings.SIZES[0]) self.frame_thread = QThread() # self.frame_grabber.image_signal.connect(self.display_frame) self.frame_grabber.last_frame_signal.connect(self.last_frame) self.frame_grabber.moveToThread(self.frame_thread) self.frame_thread.started.connect(self.frame_grabber.grab) self.stop_signal.connect(self.frame_grabber.stop_work) self.quality_changed.connect(self.frame_grabber.change_size) self.frame_thread.start() self.image_processor = ImageProcessor(self.styles[0]) self.image_thread = QThread() self.image_processor.image_signal.connect(self.display_frame) self.image_processor.moveToThread(self.image_thread) self.image_thread.started.connect(self.image_processor.monitor_images) self.stop_signal.connect(self.image_processor.stop_work) self.style_changed.connect(self.image_processor.change_style) self.last_frame_changed.connect(self.image_processor.change_last_frame) self.image_thread.start() def closeEvent(self, event): self.stop_signal.emit() self.frame_thread.quit() self.image_thread.quit() self.frame_thread.wait() self.image_thread.wait() def setup_ui(self): """Initialize widgets.""" def switch_style(i): view = self.landscape_view if self.view_mode == MainApp.LANDSCAPE else self.portrait_view self.style_changed.emit(self.styles[i]) view.selected_style = self.styles[i] for i in range(min(len(self.styles), len(settings.STYLE_SHORTCUTS))): QShortcut(QKeySequence(settings.STYLE_SHORTCUTS[i]), self, lambda x=i: switch_style(x)) self.landscape_view = LandscapeView(self.styles) self.landscape_view.style_changed.connect(self.style_button_clicked) self.landscape_view.toggle_fullscreen_signal.connect( self.toggle_fullscreen) self.landscape_view.quality_changed.connect(self.quality_choice) self.portrait_view = PortraitView(self.styles) self.portrait_view.style_changed.connect(self.style_button_clicked) self.portrait_view.toggle_fullscreen_signal.connect( self.toggle_fullscreen) self.portrait_view.quality_changed.connect(self.quality_choice) self.main_layout = QStackedLayout() self.main_layout.addWidget(self.landscape_view) self.main_layout.addWidget(self.portrait_view) self.setLayout(self.main_layout) self.view_mode = MainApp.LANDSCAPE self.setStyleSheet('background-color:black;' 'font-family: Arial;' 'font-style: normal;' 'font-size: 12pt;' 'font-weight: bold;' 'color:white;') self.setWindowTitle('Stylize') def keyPressEvent(self, event): if event.key() == Qt.Key_Escape and not settings.KIOSK: if self.windowState() & Qt.WindowFullScreen: self.showNormal() elif event.key() == Qt.Key_Enter or event.key() == Qt.Key_Return: self.image_capture() def last_frame(self, frame): self.last_frame_changed.emit(frame) def display_frame(self, image): self.image = image if not self.freeze: if self.view_mode == MainApp.LANDSCAPE: self.landscape_view.set_image(self.image.image) else: self.portrait_view.set_image(self.image.image) def resizeEvent(self, event): super(MainApp, self).resizeEvent(event) new_view_mode = MainApp.LANDSCAPE if self.width() >= self.height( ) else MainApp.PORTRAIT if self.view_mode != new_view_mode: old_view = self.portrait_view if new_view_mode == MainApp.LANDSCAPE else self.landscape_view new_view = self.landscape_view if new_view_mode == MainApp.LANDSCAPE else self.portrait_view new_view.quality = old_view.quality new_view.selected_style = old_view.selected_style self.view_mode = new_view_mode self.main_layout.setCurrentIndex(self.view_mode) def style_button_clicked(self, style): self.style_changed.emit(style) def toggle_fullscreen(self): if self.windowState() & Qt.WindowFullScreen: self.showNormal() else: self.showFullScreen() def quality_choice(self, quality): self.quality_changed.emit(quality) def image_capture(self): self.freeze = self.image.copy() # prevent background update try: self.capture_handler(self, self.freeze) except Exception: msg = QMessageBox(self) msg.setIcon(QMessageBox.Critical) msg.setText('Error during capture.') msg.setWindowFlags(Qt.FramelessWindowHint | Qt.Popup) msg.exec_() self.freeze = None
class SnippetsActor(QObject): #: Signal emitted when the Thread is ready sig_snippets_ready = Signal() sig_snippets_response = Signal(int, dict) sig_update_snippets = Signal(dict) sig_mailbox = Signal(dict) def __init__(self, parent): QObject.__init__(self) self.stopped = False self.daemon = True self.mutex = QMutex() self.language_snippets = {} self.thread = QThread(None) self.moveToThread(self.thread) self.thread.started.connect(self.started) self.sig_mailbox.connect(self.handle_msg) self.sig_update_snippets.connect(self.update_snippets) def stop(self): """Stop actor.""" with QMutexLocker(self.mutex): logger.debug("Snippets plugin stopping...") self.thread.quit() self.thread.wait() def start(self): """Start thread.""" self.thread.start() def started(self): """Thread started.""" logger.debug('Snippets plugin starting...') self.sig_snippets_ready.emit() @Slot(dict) def update_snippets(self, snippets): """Update available snippets.""" logger.debug('Updating snippets...') for language in snippets: lang_snippets = snippets[language] lang_trie = Trie() for trigger in lang_snippets: trigger_descriptions = lang_snippets[trigger] lang_trie[trigger] = (trigger, trigger_descriptions) self.language_snippets[language] = lang_trie @Slot(dict) def handle_msg(self, message): """Handle one message""" msg_type, _id, file, msg = [ message[k] for k in ('type', 'id', 'file', 'msg')] logger.debug(u'Perform request {0} with id {1}'.format(msg_type, _id)) if msg_type == CompletionRequestTypes.DOCUMENT_COMPLETION: language = msg['language'] current_word = msg['current_word'] snippets = [] if current_word is None: snippets = {'params': snippets} self.sig_snippets_response.emit(_id, snippets) return if language in self.language_snippets: language_snippets = self.language_snippets[language] if language_snippets[current_word]: for node in language_snippets[current_word]: trigger, info = node.value for description in info: description_snippet = info[description] text = description_snippet['text'] remove_trigger = description_snippet[ 'remove_trigger'] snippets.append({ 'kind': CompletionItemKind.SNIPPET, 'insertText': text, 'label': f'{trigger} ({description})', 'sortText': f'zzz{trigger}', 'filterText': trigger, 'documentation': '', 'provider': SNIPPETS_COMPLETION, 'remove_trigger': remove_trigger }) snippets = {'params': snippets} self.sig_snippets_response.emit(_id, snippets)
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 ApplicationContainer(PluginMainContainer): sig_report_issue_requested = Signal() """ Signal to request reporting an issue to Github. """ sig_load_log_file = Signal(str) """ Signal to load a log file """ def __init__(self, name, plugin, parent=None): super().__init__(name, plugin, parent) # Keep track of dpi message self.current_dpi = None self.dpi_messagebox = None # ---- PluginMainContainer API # ------------------------------------------------------------------------- def setup(self): # Compute dependencies in a thread to not block the interface. self.dependencies_thread = QThread(None) # Attributes self.dialog_manager = DialogManager() self.give_updates_feedback = False self.thread_updates = None self.worker_updates = None self.updates_timer = None # Actions # Documentation actions self.documentation_action = self.create_action( ApplicationActions.SpyderDocumentationAction, text=_("Spyder documentation"), icon=self.create_icon("DialogHelpButton"), triggered=lambda: start_file(__docs_url__), context=Qt.ApplicationShortcut, register_shortcut=True, shortcut_context="_") spyder_video_url = ("https://www.youtube.com/playlist" "?list=PLPonohdiDqg9epClEcXoAPUiK0pN5eRoc") self.video_action = self.create_action( ApplicationActions.SpyderDocumentationVideoAction, text=_("Tutorial videos"), icon=self.create_icon("VideoIcon"), triggered=lambda: start_file(spyder_video_url)) # Support actions self.trouble_action = self.create_action( ApplicationActions.SpyderTroubleshootingAction, _("Troubleshooting..."), triggered=lambda: start_file(__trouble_url__)) self.report_action = self.create_action( ConsoleActions.SpyderReportAction, _("Report issue..."), icon=self.create_icon('bug'), triggered=self.sig_report_issue_requested) self.dependencies_action = self.create_action( ApplicationActions.SpyderDependenciesAction, _("Dependencies..."), triggered=self.show_dependencies, icon=self.create_icon('advanced')) self.check_updates_action = self.create_action( ApplicationActions.SpyderCheckUpdatesAction, _("Check for updates..."), triggered=self.check_updates) self.support_group_action = self.create_action( ApplicationActions.SpyderSupportAction, _("Spyder support..."), triggered=lambda: start_file(__forum_url__)) # About action self.about_action = self.create_action( ApplicationActions.SpyderAbout, _("About %s...") % "Spyder", icon=self.create_icon('MessageBoxInformation'), triggered=self.show_about, menurole=QAction.AboutRole) # Tools actions if WinUserEnvDialog is not None: self.winenv_action = self.create_action( ApplicationActions.SpyderWindowsEnvVariables, _("Current user environment variables..."), icon=self.create_icon('win_env'), tip=_("Show and edit current user environment " "variables in Windows registry " "(i.e. for all sessions)"), triggered=self.show_windows_env_variables) else: self.winenv_action = None # Application base actions self.restart_action = self.create_action( ApplicationActions.SpyderRestart, _("&Restart"), icon=self.create_icon('restart'), tip=_("Restart"), triggered=self.restart_normal, context=Qt.ApplicationShortcut, shortcut_context="_", register_shortcut=True) self.restart_debug_action = self.create_action( ApplicationActions.SpyderRestartDebug, _("&Restart in debug mode"), tip=_("Restart in debug mode"), triggered=self.restart_debug, context=Qt.ApplicationShortcut, shortcut_context="_", register_shortcut=True) # Debug logs if get_debug_level() >= 2: self.menu_debug_logs = self.create_menu( ApplicationPluginMenus.DebugLogsMenu, _("Debug logs")) # The menu can't be built at startup because Completions can # start after Application. self.menu_debug_logs.aboutToShow.connect( self.create_debug_log_actions) def update_actions(self): pass # ---- Other functionality # ------------------------------------------------------------------------- def on_close(self): """To call from Spyder when the plugin is closed.""" self.dialog_manager.close_all() if self.updates_timer is not None: self.updates_timer.stop() if self.thread_updates is not None: self.thread_updates.quit() self.thread_updates.wait() if self.dependencies_thread is not None: self.dependencies_thread.quit() self.dependencies_thread.wait() @Slot() def show_about(self): """Show Spyder About dialog.""" abt = AboutDialog(self) abt.show() @Slot() def show_windows_env_variables(self): """Show Windows current user environment variables.""" self.dialog_manager.show(WinUserEnvDialog(self)) # ---- Updates # ------------------------------------------------------------------------- def _check_updates_ready(self): """Show results of the Spyder update checking process.""" # `feedback` = False is used on startup, so only positive feedback is # given. `feedback` = True is used when after startup (when using the # menu action, and gives feeback if updates are, or are not found. feedback = self.give_updates_feedback # Get results from worker update_available = self.worker_updates.update_available latest_release = self.worker_updates.latest_release error_msg = self.worker_updates.error # Release url if sys.platform == 'darwin': url_r = ('https://github.com/spyder-ide/spyder/releases/latest/' 'download/Spyder.dmg') else: url_r = ('https://github.com/spyder-ide/spyder/releases/latest/' 'download/Spyder_64bit_full.exe') url_i = 'https://docs.spyder-ide.org/installation.html' # Define the custom QMessageBox box = MessageCheckBox(icon=QMessageBox.Information, parent=self) box.setWindowTitle(_("New Spyder version")) box.setAttribute(Qt.WA_ShowWithoutActivating) box.set_checkbox_text(_("Check for updates at startup")) box.setStandardButtons(QMessageBox.Ok) box.setDefaultButton(QMessageBox.Ok) # Adjust the checkbox depending on the stored configuration option = 'check_updates_on_startup' check_updates = self.get_conf(option) box.set_checked(check_updates) if error_msg is not None: msg = error_msg box.setText(msg) box.set_check_visible(False) box.exec_() check_updates = box.is_checked() else: if update_available: header = _("<b>Spyder {} is available!</b><br><br>").format( latest_release) footer = _( "For more information visit our " "<a href=\"{}\">installation guide</a>.").format(url_i) if is_anaconda(): content = _( "<b>Important note:</b> Since you installed " "Spyder with Anaconda, please <b>don't</b> use " "<code>pip</code> to update it as that will break " "your installation.<br><br>" "Instead, run the following commands in a " "terminal:<br>" "<code>conda update anaconda</code><br>" "<code>conda install spyder={}</code><br><br>").format( latest_release) else: content = _("Click <a href=\"{}\">this link</a> to " "download it.<br><br>").format(url_r) msg = header + content + footer box.setText(msg) box.set_check_visible(True) box.show() check_updates = box.is_checked() elif feedback: msg = _("Spyder is up to date.") box.setText(msg) box.set_check_visible(False) box.exec_() check_updates = box.is_checked() # Update checkbox based on user interaction self.set_conf(option, check_updates) # Enable check_updates_action after the thread has finished self.check_updates_action.setDisabled(False) # Provide feeback when clicking menu if check on startup is on self.give_updates_feedback = True @Slot() def check_updates(self, startup=False): """Check for spyder updates on github releases using a QThread.""" # Disable check_updates_action while the thread is working self.check_updates_action.setDisabled(True) if self.thread_updates is not None: self.thread_updates.quit() self.thread_updates.wait() self.thread_updates = QThread(None) self.worker_updates = WorkerUpdates(self, startup=startup) self.worker_updates.sig_ready.connect(self._check_updates_ready) self.worker_updates.sig_ready.connect(self.thread_updates.quit) self.worker_updates.moveToThread(self.thread_updates) self.thread_updates.started.connect(self.worker_updates.start) # Delay starting this check to avoid blocking the main window # while loading. # Fixes spyder-ide/spyder#15839 self.updates_timer = QTimer(self) self.updates_timer.setInterval(3000) self.updates_timer.setSingleShot(True) self.updates_timer.timeout.connect(self.thread_updates.start) self.updates_timer.start() # ---- Dependencies # ------------------------------------------------------------------------- @Slot() def show_dependencies(self): """Show Spyder Dependencies dialog.""" # This is here in case the user tries to display the dialog before # dependencies_thread has finished. if not dependencies.DEPENDENCIES: dependencies.declare_dependencies() dlg = DependenciesDialog(self) dlg.set_data(dependencies.DEPENDENCIES) dlg.show() def _compute_dependencies(self): """Compute dependencies without errors.""" # Skip error when trying to register dependencies several times. # This can happen if the user tries to display the dependencies # dialog before dependencies_thread has finished. try: dependencies.declare_dependencies() except ValueError: pass def compute_dependencies(self): """Compute dependencies.""" self.dependencies_thread.run = self._compute_dependencies self.dependencies_thread.finished.connect( self.report_missing_dependencies) # This avoids computing missing deps before the window is fully up dependencies_timer = QTimer(self) dependencies_timer.setInterval(10000) dependencies_timer.setSingleShot(True) dependencies_timer.timeout.connect(self.dependencies_thread.start) dependencies_timer.start() @Slot() def report_missing_dependencies(self): """Show a QMessageBox with a list of missing hard dependencies.""" missing_deps = dependencies.missing_dependencies() if missing_deps: InstallerMissingDependencies(missing_deps) # We change '<br>' by '\n', in order to replace the '<' # that appear in our deps by '<' (to not break html # formatting) and finally we restore '<br>' again. missing_deps = (missing_deps.replace('<br>', '\n').replace( '<', '<').replace('\n', '<br>')) message = ( _("<b>You have missing dependencies!</b>" "<br><br><tt>%s</tt><br>" "<b>Please install them to avoid this message.</b>" "<br><br>" "<i>Note</i>: Spyder could work without some of these " "dependencies, however to have a smooth experience when " "using Spyder we <i>strongly</i> recommend you to install " "all the listed missing dependencies.<br><br>" "Failing to install these dependencies might result in bugs." " Please be sure that any found bugs are not the direct " "result of missing dependencies, prior to reporting a new " "issue.") % missing_deps) message_box = QMessageBox(self) message_box.setIcon(QMessageBox.Critical) message_box.setAttribute(Qt.WA_DeleteOnClose) message_box.setAttribute(Qt.WA_ShowWithoutActivating) message_box.setStandardButtons(QMessageBox.Ok) message_box.setWindowModality(Qt.NonModal) message_box.setWindowTitle(_('Error')) message_box.setText(message) message_box.show() # ---- Restart # ------------------------------------------------------------------------- @Slot() def restart_normal(self): """Restart in standard mode.""" os.environ['SPYDER_DEBUG'] = '' self.sig_restart_requested.emit() @Slot() def restart_debug(self): """Restart in debug mode.""" box = QMessageBox(self) box.setWindowTitle(_("Question")) box.setIcon(QMessageBox.Question) box.setText(_("Which debug mode do you want Spyder to restart in?")) button_verbose = QPushButton(_('Verbose')) button_minimal = QPushButton(_('Minimal')) box.addButton(button_verbose, QMessageBox.AcceptRole) box.addButton(button_minimal, QMessageBox.AcceptRole) box.setStandardButtons(QMessageBox.Cancel) box.exec_() if box.clickedButton() == button_minimal: os.environ['SPYDER_DEBUG'] = '2' elif box.clickedButton() == button_verbose: os.environ['SPYDER_DEBUG'] = '3' else: return self.sig_restart_requested.emit() # ---- Log files # ------------------------------------------------------------------------- def create_debug_log_actions(self): """Create an action for each lsp and debug log file.""" self.menu_debug_logs.clear_actions() files = [os.environ['SPYDER_DEBUG_FILE']] files += glob.glob(os.path.join(get_conf_path('lsp_logs'), '*.log')) debug_logs_actions = [] for file in files: action = self.create_action( file, os.path.basename(file), tip=file, triggered=lambda _, file=file: self.load_log_file(file), overwrite=True, register_action=False) debug_logs_actions.append(action) # Add Spyder log on its own section self.add_item_to_menu(debug_logs_actions[0], self.menu_debug_logs, section=LogsMenuSections.SpyderLogSection) # Add LSP logs for action in debug_logs_actions[1:]: self.add_item_to_menu(action, self.menu_debug_logs, section=LogsMenuSections.LSPLogsSection) # Render menu self.menu_debug_logs._render() def load_log_file(self, file): """Load log file in editor""" self.sig_load_log_file.emit(file) # ---- DPI changes # ------------------------------------------------------------------------- def set_window(self, window): """Set window property of main window.""" self._window = window def handle_new_screen(self, new_screen): """Connect DPI signals for new screen.""" if new_screen is not None: new_screen_dpi = new_screen.logicalDotsPerInch() if self.current_dpi != new_screen_dpi: self.show_dpi_change_message(new_screen_dpi) else: new_screen.logicalDotsPerInchChanged.connect( self.show_dpi_change_message) def handle_dpi_change_response(self, result, dpi): """Handle dpi change message dialog result.""" if self.dpi_messagebox.is_checked(): self.set_conf('show_dpi_message', False) self.dpi_messagebox = None if result == 0: # Restart button was clicked # Activate HDPI auto-scaling option since is needed for a # proper display when using OS scaling self.set_conf('normal_screen_resolution', False) self.set_conf('high_dpi_scaling', True) self.set_conf('high_dpi_custom_scale_factor', False) self.sig_restart_requested.emit() else: # Update current dpi for future checks self.current_dpi = dpi def show_dpi_change_message(self, dpi): """Show message to restart Spyder since the DPI scale changed.""" if not self.get_conf('show_dpi_message'): return if self.current_dpi != dpi: # Check the window state to not show the message if the window # is in fullscreen mode. window = self._window.windowHandle() if (window.windowState() == Qt.WindowFullScreen and sys.platform == 'darwin'): return if self.get_conf('high_dpi_scaling'): return if self.dpi_messagebox is not None: self.dpi_messagebox.activateWindow() self.dpi_messagebox.raise_() return self.dpi_messagebox = MessageCheckBox(icon=QMessageBox.Warning, parent=self) self.dpi_messagebox.set_checkbox_text(_("Don't show again.")) self.dpi_messagebox.set_checked(False) self.dpi_messagebox.set_check_visible(True) self.dpi_messagebox.setText( _("A monitor scale change was detected. <br><br>" "We recommend restarting Spyder to ensure that it's properly " "displayed. If you don't want to do that, please be sure to " "activate the option<br><br><tt>Enable auto high DPI scaling" "</tt><br><br>in <tt>Preferences > Application > " "Interface</tt>, in case Spyder is not displayed " "correctly.<br><br>" "Do you want to restart Spyder?")) self.dpi_messagebox.addButton(_('Restart now'), QMessageBox.NoRole) dismiss_button = self.dpi_messagebox.addButton( _('Dismiss'), QMessageBox.NoRole) self.dpi_messagebox.setDefaultButton(dismiss_button) self.dpi_messagebox.finished.connect( lambda result: self.handle_dpi_change_response(result, dpi)) self.dpi_messagebox.open() # Show dialog always in the primary screen to prevent not being # able to see it if a screen gets disconnected while # in suspended state. See spyder-ide/spyder#16390 dpi_messagebox_width = self.dpi_messagebox.rect().width() dpi_messagebox_height = self.dpi_messagebox.rect().height() screen_geometry = QGuiApplication.primaryScreen().geometry() x = (screen_geometry.width() - dpi_messagebox_width) / 2 y = (screen_geometry.height() - dpi_messagebox_height) / 2 # Convert coordinates to int to avoid a TypeError in Python 3.10 # Fixes spyder-ide/spyder#17677 self.dpi_messagebox.move(int(x), int(y)) self.dpi_messagebox.adjustSize()
class MainWindow(QMainWindow): def __init__(self): QMainWindow.__init__(self) #self.verticalLayout_2 = QVBoxLayout() self.tableHeaders = ['VT', 'I:E', 'RR', 'FIO2'] self.widget = uic.loadUi(_UI, self) window_title = "Rhythm" self.json = JsonSettings("settings.json") self.settings_dict = self.json.dict pprint.pprint(self.settings_dict) self.table = QTableWidget(self) self.table.setRowCount(2) self.table.setColumnCount(5) self.table.setHorizontalHeaderLabels(self.tableHeaders) self.table.setItem(0,0, QTableWidgetItem(self.settings_dict[r"vt"])) self.table.setItem(0,1, QTableWidgetItem(self.settings_dict[r"ie"])) self.table.setItem(0,2, QTableWidgetItem(self.settings_dict[r"rr"])) self.table.setItem(0,3, QTableWidgetItem(self.settings_dict[r"fio2"])) #self.table.itemChanged.connect(self.SaveSettings) self.vt = int(self.settings_dict[r"vt"]) self.rr = int(self.settings_dict[r"rr"]) self.ie = int(self.settings_dict[r"ie"]) self.fio2 = int(self.settings_dict[r"fio2"]) self.verticalLayout_2.addWidget(self.table) self.generator = GcodeGenerator(self.vt, self.rr, self.ie, self.fio2) self.motion_table = QTableWidget(self) self.motion_table_headers = ['variables', 'values'] self.motion_table.setColumnCount(2) self.motion_table.setRowCount(10) self.motion_table.setHorizontalHeaderLabels(self.motion_table_headers) self.motion_table.hide() self.dat = deque() self.datpeak = deque() self.lungpressure_line_pen = pg.mkPen(200, 100, 0) self.plotter = PlotWidget() self.plotter.showGrid(x=True, y=True, alpha=None) self.curve1 = self.plotter.plot(0,0,"lungpressure", 'b') self.curve2 = self.plotter.plot(0,0,"peakpressure", pen = self.lungpressure_line_pen) #self.motion_table.setSizeAdjustPolicy(QtWidget.QAbstractScrollArea.AdjustToContents) self.CalculateSettings() self.verticalLayout_2.addWidget(self.motion_table) self.verticalLayout_2.addWidget(self.plotter) self.motion_table.hide() self.gcodetable = QTableWidget(self) self.gcodetable.setRowCount(1) self.gcodetable.setColumnCount(1) #self.verticalLayout_2.addWidget(self.gcodetable) self.hbox = QHBoxLayout() #self.verticalLayout_2.addChildLayout(self.hbox) self.verticalLayout_2.addLayout(self.hbox) self.hbox.addWidget(self.gcodetable) self.txrxtable = QTableWidget() self.txrxtable.setRowCount(1) self.txrxtable.setColumnCount(1) self.hbox.addWidget(self.txrxtable) #self.hbox.addLayout self.peepdial.valueChanged.connect(self.peepDialChanged) self.peeplcd.display(self.peepdial.value()) self.peakdial.valueChanged.connect(self.peakDialChanged) self.peaklcd.display(self.peakdial.value()) self.ipapdial.valueChanged.connect(self.ipapDialChanged) self.ipaplcd.display(self.ipapdial.value()) self.vtdial.valueChanged.connect(self.vtDialChanged) self.vtlcd.display(self.vtdial.value()) self.iedial.valueChanged.connect(self.ieDialChanged) self.ielcd.display(self.iedial.value()) self.rrdial.valueChanged.connect(self.rrDialChanged) self.rrlcd.display(self.rrdial.value()) self.fiodial.valueChanged.connect(self.fioDialChanged) self.fiolcd.display(self.fiodial.value()) self.s = "" self.s2 = "" self.ports = list(port_list.comports()) self.primaryThreadCreated = False self.workerThreadCreated = False self.sensorThreadCreated = False self.serialPortOpen = False self.serialSensorOpen = False def update_param_table(self): self.table.setItem(0,0, QTableWidgetItem(self.settings_dict[r"vt"])) self.table.setItem(0,1, QTableWidgetItem(self.settings_dict[r"ie"])) self.table.setItem(0,2, QTableWidgetItem(self.settings_dict[r"rr"])) self.table.setItem(0,3, QTableWidgetItem(self.settings_dict[r"fio2"])) def peepDialChanged(self): self.peeplcd.display(self.peepdial.value()) def peakDialChanged(self): self.peaklcd.display(self.peakdial.value()) def ipapDialChanged(self): self.ipaplcd.display(self.ipapdial.value()) def vtDialChanged(self): self.vtlcd.display(self.vtdial.value()) self.vt = self.vtdial.value() self.settings_dict[r"vt"] = str(self.vt) self.update_param_table() self.SaveSettings() def ieDialChanged(self): self.table.setItem(0,1, QTableWidgetItem(self.settings_dict[r"ie"])) self.ielcd.display(self.iedial.value()) self.ie = self.iedial.value() self.settings_dict[r"ie"] = str(self.ie) self.update_param_table() self.SaveSettings() def rrDialChanged(self): self.rrlcd.display(self.rrdial.value()) self.rr = self.rrdial.value() self.settings_dict[r"rr"] = str(self.rr) self.update_param_table() self.SaveSettings() def fioDialChanged(self): self.fiolcd.display(self.fiodial.value()) self.fio2 = self.fiodial.value() self.settings_dict[r"fio2"] = str(self.fio2) self.update_param_table() self.SaveSettings() def ShowGcodeTable(self): codelist = self.generator.gcodestr.splitlines() rowcount = len(codelist) self.gcodetable.setRowCount(rowcount) self.gcodetable.setColumnCount(1) for i in range(rowcount): self.gcodetable.setItem(i, 0, QTableWidgetItem(codelist[i])) def CalculateSettings(self): del self.generator ###self.json = JsonSettings("settings.json") ###self.settings_dict = self.json.dict #self.vt = int(self.settings_dict[r"vt"]) #self.rr = int(self.settings_dict[r"rr"]) #self.ie = int(self.settings_dict[r"ie"]) #self.fio2 = int(self.settings_dict[r"fio2"]) self.generator = GcodeGenerator(self.vt, self.rr, self.ie, self.fio2) self.motion_table.setItem(0,0, QTableWidgetItem('Dp')) self.motion_table.setItem(0,1, QTableWidgetItem(str(self.generator.Dp))) self.motion_table.setItem(1,0, QTableWidgetItem('Dr')) self.motion_table.setItem(1,1, QTableWidgetItem(str(self.generator.Dr))) self.motion_table.setItem(2,0, QTableWidgetItem('Dt')) self.motion_table.setItem(2,1, QTableWidgetItem(str(self.generator.Dt))) self.motion_table.setItem(3,0, QTableWidgetItem('Ti')) self.motion_table.setItem(3,1, QTableWidgetItem(str(self.generator.Ti))) self.motion_table.setItem(4,0, QTableWidgetItem('Th')) self.motion_table.setItem(4,1, QTableWidgetItem(str(self.generator.Th))) self.motion_table.setItem(5,0, QTableWidgetItem('Vi')) self.motion_table.setItem(5,1, QTableWidgetItem(str(self.generator.Vi))) self.motion_table.setItem(6,0, QTableWidgetItem('Vh')) self.motion_table.setItem(6,1, QTableWidgetItem(str(self.generator.Vh))) def sensorData(self, data_stream): #print(data_stream.split(',')) lst = data_stream.split(",") self.maxLen = 400 # max number of data points to show on graph if(len(lst) > 1): if len(self.dat) > self.maxLen: self.dat.popleft() # remove oldest if len(self.datpeak) > self.maxLen: self.datpeak.popleft() self.datpeak.append(float(self.peakdial.value())) self.dat.append(float(lst[1])) self.curve1.setData(self.dat) self.curve2.setData(self.datpeak) def write_info(self, data_stream): rcount = self.txrxtable.rowCount() self.txrxtable.insertRow(rcount) self.txrxtable.setItem(rcount,0, QTableWidgetItem(data_stream)) self.txrxtable.scrollToBottom() self.txrxtable.resizeColumnsToContents() self.txrxtable.resizeRowsToContents() if data_stream == "Stopped": if self.primaryThreadCreated: self.primaryThread.exit() self.primaryThread.wait() self.primaryThreadCreated = False del self.primaryThread #elif data_stream == "Loop": # self.primaryThread.exit() ### # self.worker = WorkerThread(self.s, self.generator) # self.workerThread = QThread() # self.workerThread.started.connect(self.worker.run) # self.worker.signal.connect(self.write_info) # self.worker.moveToThread(self.workerThread) # self.workerThread.start() # self.workerThreadCreated = True # print("Starting Worker Thread") #if data_stream == "Stopped": # self.worker = WorkerThread(self.s) # self.workerThread = QThread() # self.workerThread.started.connect(self.worker.run) # self.worker.signal.connect(self.write_info) # self.worker.moveToThread(self.workerThread) # self.workerThread.start() # print("Starting Thread") @Slot() def on_gengcode_clicked(self): self.CalculateSettings() self.generator.Generate() self.ShowGcodeTable() @Slot() def on_scanPorts_clicked(self): self.ports = list(port_list.comports()) self.widget.portsList.clear() self.widget.monitoringPort.clear() print(len(self.ports)) for p in self.ports: self.widget.portsList.addItem(p[0]) self.widget.monitoringPort.addItem(p[0]) @Slot() def on_btninit_clicked(self): if not self.workerThreadCreated: if not self.primaryThreadCreated: self.primary = PrimaryThread(self.s, self.generator) self.primaryThread = QThread() self.primaryThread.started.connect(self.primary.run) self.primary.signal.connect(self.write_info) self.primary.moveToThread(self.primaryThread) self.primaryThread.start() self.primaryThreadCreated = True print("Starting Primary Thread") if self.serialSensorOpen: if not self.sensorThreadCreated: self.sensor = SensorThread(self.s2) self.sensorThread = QThread() self.sensorThread.started.connect(self.sensor.run) self.sensor.signal.connect(self.sensorData) self.sensor.moveToThread(self.sensorThread) self.sensorThread.start() self.sensorThreadCreated = True print("Starting Sensor Thread ...") @Slot() def on_runloop_clicked(self): if not self.primaryThreadCreated: if not self.workerThreadCreated: self.worker = WorkerThread(self.s, self.generator) self.workerThread = QThread() self.workerThread.started.connect(self.worker.run) self.worker.signal.connect(self.write_info) self.worker.moveToThread(self.workerThread) self.workerThread.start() self.workerThreadCreated = True print("Starting Worker Thread") @Slot() def on_disconnect_clicked(self): if self.serialPortOpen: if self.workerThreadCreated: self.worker.Stop() self.workerThread.exit() self.workerThread.wait() self.workerThreadCreated = False del self.workerThread if self.primaryThreadCreated: self.primaryThread.exit() self.primaryThread.wait() self.primaryThreadCreated = False del self.primaryThread if self.sensorThreadCreated: self.sensor.Stop() self.sensorThread.exit() self.sensorThread.wait() self.sensorThreadCreated = False self.s.close() if self.serialSensorOpen: self.s2.close() self.serialPortOpen = False self.serialSensorOpen = False @Slot() def on_connect_clicked(self): try: if not self.serialPortOpen: print("Serial Port Name : " + self.portsList.currentText()) self.s = serial.Serial(self.portsList.currentText(), baudrate=115200, timeout=1) #self.s.open() time.sleep(1) self.serialPortOpen = True #self.s.write("\r\n\r\n") # Hit enter a few times to wake the Printrbot #time.sleep(2) # Wait for Printrbot to initialize while self.s.in_waiting: self.s.readline() #print(self.s.readline().decode("ascii")) #self.s.flushInput() # Flush startup text in serial input #monitoringPort if not self.serialSensorOpen: self.s2 = serial.Serial(self.monitoringPort.currentText(), baudrate=115200, timeout=1) self.serialSensorOpen = True except serial.SerialException as ex: self.serialPortOpen = False print(ex.strerror) print("Error Opening Serial Port..........................................") @Slot() def on_btnCPAP_clicked(self): if self.btnCPAP.isChecked(): #self.btnCPAP.toggle() self.btnCPAP.setStyleSheet('QPushButton {background-color: #7F7F7F}') else: self.btnCPAP.setStyleSheet('QPushButton {background-color: #404040}') def SaveSettings(self): ###self.json = JsonSettings("settings.json") ###self.settings_dict = self.json.dict self.json.dict[r'vt'] = str(self.vt) self.json.dict[r'ie'] = str(self.ie) self.json.dict[r'rr'] = str(self.rr) self.json.dict[r'fio2'] = str(self.fio2) self.generator = GcodeGenerator(self.vt, self.rr, self.ie, self.fio2) self.generator.Generate() if self.workerThreadCreated: self.worker.updateGcode(self.generator) pprint.pprint(self.generator.gcodestr) #self.json.dumptojson() ###self.vt = int(self.settings_dict[r"vt"]) ###self.rr = int(self.settings_dict[r"rr"]) ###self.ie = int(self.settings_dict[r"ie"]) ###self.fio2 = int(self.settings_dict[r"fio2"]) self.CalculateSettings()
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 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()