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)
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() 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() 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 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 GistWindow(QDialog): def __init__(self): super(GistWindow, self).__init__() self.thread = None self.worker = None self.gistToken = config.gistToken self.connected = False self.setWindowTitle("Gist") self.setMinimumWidth(380) self.layout = QVBoxLayout() self.testStatus = QLabel("") self.layout.addWidget(self.testStatus) self.layout.addWidget(QLabel("Gist Token")) self.gistTokenInput = QLineEdit() self.gistTokenInput.setText(self.gistToken) self.gistTokenInput.setMaxLength(40) self.gistTokenInput.textChanged.connect(self.enableButtons) self.layout.addWidget(self.gistTokenInput) self.testButton = QPushButton("Test Connection") self.testButton.setEnabled(False) self.testButton.clicked.connect(self.checkStatus) self.layout.addWidget(self.testButton) # self.syncHighlightsButton = QPushButton("Synch Highlights") # self.syncHighlightsButton.setEnabled(False) # self.syncHighlightsButton.clicked.connect(self.syncHighlights) # self.layout.addWidget(self.syncHighlightsButton) self.syncBibleNotesButton = QPushButton("Synch Bibles Notes") self.syncBibleNotesButton.setEnabled(False) self.syncBibleNotesButton.clicked.connect(self.syncBibleNotes) self.layout.addWidget(self.syncBibleNotesButton) buttons = QDialogButtonBox.Ok self.buttonBox = QDialogButtonBox(buttons) self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.stopSync) self.layout.addWidget(self.buttonBox) self.setLayout(self.layout) self.enableButtons() self.checkStatus() def enableButtons(self): if len(self.gistTokenInput.text()) >= 40: self.testButton.setEnabled(True) else: self.testButton.setEnabled(False) self.connected = False self.setStatus("Not connected", False) if self.connected: self.testButton.setEnabled(False) self.syncBibleNotesButton.setEnabled(True) else: self.syncBibleNotesButton.setEnabled(False) def checkStatus(self): if len(self.gistTokenInput.text()) < 40: self.setStatus("Not connected", False) self.connected = False else: self.gh = GitHubGist(self.gistTokenInput.text()) if self.gh.connected: self.setStatus("Connected to " + self.gh.user.name, True) self.connected = True config.gistToken = self.gistTokenInput.text() else: self.setStatus("Not connected", False) self.connected = False self.enableButtons() def setStatus(self, message, connected=True): self.testStatus.setText("Status: " + message) if connected: self.testStatus.setStyleSheet("color: rgb(128, 255, 7);") else: self.testStatus.setStyleSheet("color: rgb(253, 128, 8);") QApplication.processEvents() def syncBibleNotes(self): self.setStatus("Syncing ...", True) self.syncBibleNotesButton.setEnabled(False) self.thread = QThread() self.worker = SyncNotesWithGist() self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.worker.deleteLater) self.worker.finished.connect(self.syncCompleted) self.worker.progress.connect(self.setStatus) self.thread.start() def syncCompleted(self, count): self.setStatus("Done! Processed {0} notes".format(count), True) def stopSync(self): if self.thread and self.thread.isRunning(): self.thread.quit()
class DownloadBibleMp3Dialog(QDialog): def __init__(self, parent): super().__init__() self.bibles = { "BBE (British accent)": ("BBE", "otseng/UniqueBible_MP3_BBE_british", "british"), "KJV (American accent)": ("KJV", "otseng/UniqueBible_MP3_KJV", "default"), "KJV (American soft music)": ("KJV", "otseng/UniqueBible_MP3_KJV_soft_music", "soft-music"), "NHEB (Indian accent)": ("NHEB", "otseng/UniqueBible_MP3_NHEB_indian", "indian"), "WEB (American accent)": ("WEB", "otseng/UniqueBible_MP3_WEB", "default"), "CUV (Chinese)": ("CUV", "otseng/UniqueBible_MP3_CUV", "default"), "HHBD (Hindi)": ("HHBD", "otseng/UniqueBible_MP3_HHBD", "default"), "RVA (Spanish)": ("RVA", "otseng/UniqueBible_MP3_RVA", "default"), "TR (Modern Greek)": ("TR", "otseng/UniqueBible_MP3_TR", "modern"), } self.parent = parent self.setWindowTitle(config.thisTranslation["gitHubBibleMp3Files"]) self.setMinimumSize(150, 450) self.selectedRendition = None self.selectedText = None self.selectedRepo = None self.selectedDirectory = None self.settingBibles = False self.thread = None self.setupUI() def setupUI(self): mainLayout = QVBoxLayout() title = QLabel(config.thisTranslation["gitHubBibleMp3Files"]) mainLayout.addWidget(title) self.versionsLayout = QVBoxLayout() self.renditionsList = QListWidget() self.renditionsList.itemClicked.connect(self.selectItem) for rendition in self.bibles.keys(): self.renditionsList.addItem(rendition) self.renditionsList.setMaximumHeight(100) self.versionsLayout.addWidget(self.renditionsList) mainLayout.addLayout(self.versionsLayout) self.downloadTable = QTableView() self.downloadTable.setEnabled(False) self.downloadTable.setFocusPolicy(Qt.StrongFocus) self.downloadTable.setEditTriggers(QAbstractItemView.NoEditTriggers) self.downloadTable.setSortingEnabled(True) self.dataViewModel = QStandardItemModel(self.downloadTable) self.downloadTable.setModel(self.dataViewModel) mainLayout.addWidget(self.downloadTable) buttonsLayout = QHBoxLayout() selectAllButton = QPushButton(config.thisTranslation["selectAll"]) selectAllButton.setFocusPolicy(Qt.StrongFocus) selectAllButton.clicked.connect(self.selectAll) buttonsLayout.addWidget(selectAllButton) selectNoneButton = QPushButton(config.thisTranslation["selectNone"]) selectNoneButton.setFocusPolicy(Qt.StrongFocus) selectNoneButton.clicked.connect(self.selectNone) buttonsLayout.addWidget(selectNoneButton) otButton = QPushButton("1-39") otButton.setFocusPolicy(Qt.StrongFocus) otButton.clicked.connect(self.selectOT) buttonsLayout.addWidget(otButton) ntButton = QPushButton("40-66") ntButton.setFocusPolicy(Qt.StrongFocus) ntButton.clicked.connect(self.selectNT) buttonsLayout.addWidget(ntButton) # buttonsLayout.addStretch() mainLayout.addLayout(buttonsLayout) self.downloadButton = QPushButton(config.thisTranslation["download"]) self.downloadButton.setFocusPolicy(Qt.StrongFocus) self.downloadButton.setAutoDefault(True) self.downloadButton.setFocus() self.downloadButton.clicked.connect(self.download) mainLayout.addWidget(self.downloadButton) self.status = QLabel("") mainLayout.addWidget(self.status) buttonLayout = QHBoxLayout() self.closeButton = QPushButton(config.thisTranslation["close"]) self.closeButton.setFocusPolicy(Qt.StrongFocus) self.closeButton.clicked.connect(self.closeDialog) buttonLayout.addWidget(self.closeButton) mainLayout.addLayout(buttonLayout) self.setLayout(mainLayout) self.renditionsList.item(0).setSelected(True) bible = self.renditionsList.item(0).text() self.selectRendition(bible) self.downloadButton.setDefault(True) QTimer.singleShot(0, self.downloadButton.setFocus) def selectItem(self, item): self.selectRendition(item.text()) def selectRendition(self, rendition): from util.GithubUtil import GithubUtil self.selectedRendition = rendition self.downloadTable.setEnabled(True) self.selectedText, self.selectedRepo, self.selectedDirectory = self.bibles[ self.selectedRendition] self.github = GithubUtil(self.selectedRepo) self.repoData = self.github.getRepoData() self.settingBibles = True self.dataViewModel.clear() rowCount = 0 for file in self.repoData.keys(): if len(str(file)) > 3: engFullBookName = file[3:] else: engFullBookName = BibleBooks().eng[str(int(file))][1] item = QStandardItem(file[:3].strip()) folder = os.path.join("audio", "bibles", self.selectedText, self.selectedDirectory, file) folderWithName = os.path.join("audio", "bibles", self.selectedText, self.selectedDirectory, file + " " + engFullBookName) if os.path.exists(folder) or os.path.exists(folderWithName): item.setCheckable(False) item.setCheckState(Qt.Unchecked) item.setEnabled(False) else: item.setCheckable(True) item.setCheckState(Qt.Checked) item.setEnabled(True) self.dataViewModel.setItem(rowCount, 0, item) item = QStandardItem(engFullBookName) self.dataViewModel.setItem(rowCount, 1, item) if os.path.exists(folder) or os.path.exists(folderWithName): item = QStandardItem("Installed") self.dataViewModel.setItem(rowCount, 2, item) else: item = QStandardItem("") self.dataViewModel.setItem(rowCount, 2, item) rowCount += 1 self.dataViewModel.setHorizontalHeaderLabels([ config.thisTranslation["menu_book"], config.thisTranslation["name"], "" ]) self.downloadTable.setColumnWidth(0, 90) self.downloadTable.setColumnWidth(1, 125) self.downloadTable.setColumnWidth(2, 125) # self.downloadTable.resizeColumnsToContents() self.settingBibles = False def selectAll(self): for index in range(self.dataViewModel.rowCount()): item = self.dataViewModel.item(index) if item.isEnabled(): item.setCheckState(Qt.Checked) def selectNone(self): for index in range(self.dataViewModel.rowCount()): item = self.dataViewModel.item(index) item.setCheckState(Qt.Unchecked) def selectOT(self): for index in range(self.dataViewModel.rowCount()): item = self.dataViewModel.item(index) bookNum = int(item.text()) if bookNum <= 39: if item.isEnabled(): item.setCheckState(Qt.Checked) else: item.setCheckState(Qt.Unchecked) else: item.setCheckState(Qt.Unchecked) def selectNT(self): for index in range(self.dataViewModel.rowCount()): item = self.dataViewModel.item(index) bookNum = int(item.text()) if bookNum >= 40: if item.isEnabled(): item.setCheckState(Qt.Checked) else: item.setCheckState(Qt.Unchecked) else: item.setCheckState(Qt.Unchecked) def download(self): self.downloadButton.setEnabled(False) self.setStatus(config.thisTranslation["message_installing"]) self.closeButton.setEnabled(False) folder = os.path.join("audio", "bibles") if not os.path.exists(folder): os.mkdir(folder) folder = os.path.join("audio", "bibles", self.selectedText) if not os.path.exists(folder): os.mkdir(folder) folder = os.path.join("audio", "bibles", self.selectedText, self.selectedDirectory) if not os.path.exists(folder): os.mkdir(folder) self.thread = QThread() self.worker = DownloadFromGitHub(self.github, self.repoData, self.dataViewModel, self.selectedText, self.selectedDirectory) self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.worker.deleteLater) self.worker.finished.connect(self.finishedDownloading) self.worker.progress.connect(self.setStatus) self.thread.start() def finishedDownloading(self, count): self.selectRendition(self.selectedRendition) self.setStatus("") self.downloadButton.setEnabled(True) self.closeButton.setEnabled(True) if count > 0: self.parent.displayMessage( config.thisTranslation["message_installed"]) def setStatus(self, message): self.status.setText(message) QApplication.processEvents() def closeDialog(self): if self.thread: if self.thread.isRunning(): self.thread.quit() self.close()
class 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() 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() 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 FallbackActor(QObject): #: Signal emitted when the Thread is ready sig_fallback_ready = Signal() sig_set_tokens = Signal(int, dict) sig_mailbox = Signal(dict) def __init__(self, parent): QObject.__init__(self) self.stopped = False self.daemon = True self.mutex = QMutex() self.file_tokens = {} self.diff_patch = diff_match_patch() self.thread = QThread() self.moveToThread(self.thread) self.thread.started.connect(self.started) self.sig_mailbox.connect(self.handle_msg) def tokenize(self, text, offset, language, current_word): """ Return all tokens in `text` and all keywords associated by Pygments to `language`. """ valid = is_prefix_valid(text, offset, language) if not valid: return [] # Get language keywords provided by Pygments try: lexer = get_lexer_by_name(language) keywords = get_keywords(lexer) except Exception: keywords = [] keyword_set = set(keywords) keywords = [{ 'kind': CompletionItemKind.KEYWORD, 'insertText': keyword, 'label': keyword, 'sortText': keyword, 'filterText': keyword, 'documentation': '', 'provider': FALLBACK_COMPLETION } for keyword in keywords] # Get file tokens tokens = get_words(text, offset, language) tokens = [{ 'kind': CompletionItemKind.TEXT, 'insertText': token, 'label': token, 'sortText': token, 'filterText': token, 'documentation': '', 'provider': FALLBACK_COMPLETION } for token in tokens] for token in tokens: if token['insertText'] not in keyword_set: keywords.append(token) # Filter matching results if current_word is not None: current_word = current_word.lower() keywords = [ k for k in keywords if current_word in k['insertText'].lower() ] return keywords def stop(self): """Stop actor.""" with QMutexLocker(self.mutex): logger.debug("Fallback plugin stopping...") self.thread.quit() def start(self): """Start thread.""" self.thread.start() def started(self): """Thread started.""" logger.debug('Fallback plugin starting...') self.sig_fallback_ready.emit() @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 == LSPRequestTypes.DOCUMENT_DID_OPEN: self.file_tokens[file] = { 'text': msg['text'], 'offset': msg['offset'], 'language': msg['language'], } elif msg_type == LSPRequestTypes.DOCUMENT_DID_CHANGE: if file not in self.file_tokens: self.file_tokens[file] = { 'text': '', 'offset': msg['offset'], 'language': msg['language'], } diff = msg['diff'] text = self.file_tokens[file] text['offset'] = msg['offset'] text, _ = self.diff_patch.patch_apply(diff, text['text']) self.file_tokens[file]['text'] = text elif msg_type == LSPRequestTypes.DOCUMENT_DID_CLOSE: self.file_tokens.pop(file, {}) elif msg_type == LSPRequestTypes.DOCUMENT_COMPLETION: tokens = [] if file in self.file_tokens: text_info = self.file_tokens[file] tokens = self.tokenize(text_info['text'], text_info['offset'], text_info['language'], msg['current_word']) tokens = {'params': tokens} self.sig_set_tokens.emit(_id, tokens)
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 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 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 KiteClient(QObject, KiteMethodProviderMixIn): sig_response_ready = Signal(int, dict) sig_client_started = Signal(list) sig_client_not_responding = Signal() sig_perform_request = Signal(int, str, object) def __init__(self, parent, enable_code_snippets=True): QObject.__init__(self, parent) self.endpoint = None self.requests = {} self.languages = [] self.mutex = QMutex() self.opened_files = {} self.thread_started = False self.enable_code_snippets = enable_code_snippets self.thread = QThread() self.moveToThread(self.thread) self.thread.started.connect(self.started) self.sig_perform_request.connect(self.perform_request) def start(self): if not self.thread_started: self.thread.start() logger.debug('Starting Kite HTTP session...') self.endpoint = requests.Session() self.languages = self.get_languages() self.sig_client_started.emit(self.languages) def started(self): self.thread_started = True def stop(self): if self.thread_started: logger.debug('Closing Kite HTTP session...') self.endpoint.close() self.thread.quit() def get_languages(self): verb, url = KITE_ENDPOINTS.LANGUAGES_ENDPOINT success, response = self.perform_http_request(verb, url) if response is None: response = ['python'] return response def perform_http_request(self, verb, url, params=None): response = None success = False http_method = getattr(self.endpoint, verb) try: http_response = http_method(url, json=params) except Exception as error: logger.debug('Kite request error: {0}'.format(error)) return False, None success = http_response.status_code == 200 if success: try: response = http_response.json() except Exception: response = http_response.text response = None if response == '' else response return success, response def send(self, method, params, url_params): response = None if self.endpoint is not None and method in KITE_REQUEST_MAPPING: http_verb, path = KITE_REQUEST_MAPPING[method] path = path.format(**url_params) try: success, response = self.perform_http_request( http_verb, path, params) except (ConnectionRefusedError, ConnectionError): return response return response def perform_request(self, req_id, method, params): if method in self.sender_registry: logger.debug('Perform {0} request with id {1}'.format( method, req_id)) handler_name = self.sender_registry[method] handler = getattr(self, handler_name) response = handler(params) if method in self.handler_registry: converter_name = self.handler_registry[method] converter = getattr(self, converter_name) if response is not None: response = converter(response) if response is not None: self.sig_response_ready.emit(req_id, response)
class DAQ_PID(QObject): """ """ command_pid = Signal(ThreadCommand) curr_points_signal = Signal(dict) setpoints_signal = Signal(dict) emit_curr_points_sig = Signal() models = get_models() params = [ {'title': 'Models', 'name': 'models', 'type': 'group', 'expanded': True, 'visible': True, 'children': [ {'title': 'Models class:', 'name': 'model_class', 'type': 'list', 'limits': [d['name'] for d in models]}, {'title': 'Model params:', 'name': 'model_params', 'type': 'group', 'children': []}, ]}, {'title': 'Move settings:', 'name': 'move_settings', 'expanded': True, 'type': 'group', 'visible': False, 'children': [ {'title': 'Units:', 'name': 'units', 'type': 'str', 'value': ''}]}, # here only to be compatible with DAQ_Scan, the model could update it {'title': 'Main Settings:', 'name': 'main_settings', 'expanded': True, 'type': 'group', 'children': [ {'title': 'Acquisition Timeout (ms):', 'name': 'timeout', 'type': 'int', 'value': 10000}, {'title': 'epsilon', 'name': 'epsilon', 'type': 'float', 'value': 0.01, 'tooltip': 'Precision at which move is considered as done'}, {'title': 'PID controls:', 'name': 'pid_controls', 'type': 'group', 'children': [ {'title': 'Sample time (ms):', 'name': 'sample_time', 'type': 'int', 'value': 10}, {'title': 'Refresh plot time (ms):', 'name': 'refresh_plot_time', 'type': 'int', 'value': 200}, {'title': 'Output limits:', 'name': 'output_limits', 'expanded': True, 'type': 'group', 'children': [ {'title': 'Output limit (min):', 'name': 'output_limit_min_enabled', 'type': 'bool', 'value': False}, {'title': 'Output limit (min):', 'name': 'output_limit_min', 'type': 'float', 'value': 0}, {'title': 'Output limit (max):', 'name': 'output_limit_max_enabled', 'type': 'bool', 'value': False}, {'title': 'Output limit (max:', 'name': 'output_limit_max', 'type': 'float', 'value': 100}, ]}, {'title': 'Auto mode:', 'name': 'auto_mode', 'type': 'bool', 'value': False, 'readonly': True}, {'title': 'Prop. on measurement:', 'name': 'proportional_on_measurement', 'type': 'bool', 'value': False}, {'title': 'PID constants:', 'name': 'pid_constants', 'type': 'group', 'children': [ {'title': 'Kp:', 'name': 'kp', 'type': 'float', 'value': 0.1, 'min': 0}, {'title': 'Ki:', 'name': 'ki', 'type': 'float', 'value': 0.01, 'min': 0}, {'title': 'Kd:', 'name': 'kd', 'type': 'float', 'value': 0.001, 'min': 0}, ]}, ]}, ]}, ] def __init__(self, dockarea): super().__init__() self.settings = Parameter.create(title='PID settings', name='pid_settings', type='group', children=self.params) self.title = 'PyMoDAQ PID' self.Initialized_state = False self.model_class = None self._curr_points = dict([]) self._setpoints = dict([]) self.modules_manager = None self.dock_area = dockarea self.check_moving = False self.setupUI() self.enable_controls_pid(False) self.enable_controls_pid_run(False) self.emit_curr_points_sig.connect(self.emit_curr_points) def set_module_manager(self, detector_modules, actuator_modules): self.modules_manager = ModulesManager(detector_modules, actuator_modules) def ini_PID(self): if self.ini_PID_action.isChecked(): output_limits = [None, None] if self.settings.child('main_settings', 'pid_controls', 'output_limits', 'output_limit_min_enabled').value(): output_limits[0] = self.settings.child('main_settings', 'pid_controls', 'output_limits', 'output_limit_min').value() if self.settings.child('main_settings', 'pid_controls', 'output_limits', 'output_limit_max_enabled').value(): output_limits[1] = self.settings.child('main_settings', 'pid_controls', 'output_limits', 'output_limit_max').value() self.PIDThread = QThread() pid_runner = PIDRunner(self.model_class, self.modules_manager, setpoints=self.setpoints, params=dict(Kp=self.settings.child('main_settings', 'pid_controls', 'pid_constants', 'kp').value(), Ki=self.settings.child('main_settings', 'pid_controls', 'pid_constants', 'ki').value(), Kd=self.settings.child('main_settings', 'pid_controls', 'pid_constants', 'kd').value(), sample_time=self.settings.child('main_settings', 'pid_controls', 'sample_time').value() / 1000, output_limits=output_limits, auto_mode=False), ) self.PIDThread.pid_runner = pid_runner pid_runner.pid_output_signal.connect(self.process_output) pid_runner.status_sig.connect(self.thread_status) self.command_pid.connect(pid_runner.queue_command) pid_runner.moveToThread(self.PIDThread) self.PIDThread.start() self.pid_led.set_as_true() self.enable_controls_pid_run(True) else: if hasattr(self, 'PIDThread'): if self.PIDThread.isRunning(): try: self.PIDThread.quit() except Exception: pass self.pid_led.set_as_false() self.enable_controls_pid_run(False) self.Initialized_state = True def process_output(self, datas): self.output_viewer.show_data([[dat] for dat in datas['output']]) self.input_viewer.show_data([[dat] for dat in datas['input']]) self.curr_points = datas['input'] def enable_controls_pid(self, enable=False): self.ini_PID_action.setEnabled(enable) #self.setpoint_sb.setOpts(enabled=enable) def enable_controls_pid_run(self, enable=False): self.run_action.setEnabled(enable) self.pause_action.setEnabled(enable) def setupUI(self): self.dock_pid = Dock('PID controller', self.dock_area) self.dock_area.addDock(self.dock_pid) widget = QtWidgets.QWidget() widget_toolbar = QtWidgets.QWidget() verlayout = QtWidgets.QVBoxLayout() widget.setLayout(verlayout) self.toolbar_layout = QtWidgets.QGridLayout() widget_toolbar.setLayout(self.toolbar_layout) iconquit = QtGui.QIcon() iconquit.addPixmap(QtGui.QPixmap(":/icons/Icon_Library/close2.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.quit_action = QtWidgets.QPushButton(iconquit, "Quit") self.quit_action.setToolTip('Quit the application') self.toolbar_layout.addWidget(self.quit_action, 0, 0, 1, 2) self.quit_action.clicked.connect(self.quit_fun) iconini = QtGui.QIcon() iconini.addPixmap(QtGui.QPixmap(":/icons/Icon_Library/ini.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.ini_model_action = QtWidgets.QPushButton(iconini, "Init Model") self.ini_model_action.setToolTip('Initialize the chosen model') self.toolbar_layout.addWidget(self.ini_model_action, 2, 0) self.ini_model_action.clicked.connect(self.ini_model) self.model_led = QLED() self.toolbar_layout.addWidget(self.model_led, 2, 1) self.ini_PID_action = QtWidgets.QPushButton(iconini, "Init PID") self.ini_PID_action.setToolTip('Initialize the PID loop') self.toolbar_layout.addWidget(self.ini_PID_action, 2, 2) self.ini_PID_action.setCheckable(True) self.ini_PID_action.clicked.connect(self.ini_PID) self.pid_led = QLED() self.toolbar_layout.addWidget(self.pid_led, 2, 3) self.iconrun = QtGui.QIcon() self.iconrun.addPixmap(QtGui.QPixmap(":/icons/Icon_Library/run2.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.icon_stop = QtGui.QIcon() self.icon_stop.addPixmap(QtGui.QPixmap(":/icons/Icon_Library/stop.png")) self.run_action = QtWidgets.QPushButton(self.iconrun, "", None) self.run_action.setToolTip('Start PID loop') self.run_action.setCheckable(True) self.toolbar_layout.addWidget(self.run_action, 0, 2) self.run_action.clicked.connect(self.run_PID) iconpause = QtGui.QIcon() iconpause.addPixmap(QtGui.QPixmap(":/icons/Icon_Library/pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.pause_action = QtWidgets.QPushButton(iconpause, "", None) self.pause_action.setToolTip('Pause PID') self.pause_action.setCheckable(True) self.toolbar_layout.addWidget(self.pause_action, 0, 3) self.pause_action.setChecked(True) self.pause_action.clicked.connect(self.pause_PID) lab = QtWidgets.QLabel('Target Value:') self.toolbar_layout.addWidget(lab, 3, 0, 1, 2) lab1 = QtWidgets.QLabel('Current Value:') self.toolbar_layout.addWidget(lab1, 4, 0, 1, 2) # create main parameter tree self.settings_tree = ParameterTree() self.settings_tree.setParameters(self.settings, showTop=False) verlayout.addWidget(widget_toolbar) verlayout.addWidget(self.settings_tree) self.dock_output = Dock('PID output') widget_output = QtWidgets.QWidget() self.output_viewer = Viewer0D(widget_output) self.dock_output.addWidget(widget_output) self.dock_area.addDock(self.dock_output, 'right', self.dock_pid) self.dock_input = Dock('PID input') widget_input = QtWidgets.QWidget() self.input_viewer = Viewer0D(widget_input) self.dock_input.addWidget(widget_input) self.dock_area.addDock(self.dock_input, 'bottom', self.dock_output) if len(self.models) != 0: self.get_set_model_params(self.models[0]['name']) # connecting from tree self.settings.sigTreeStateChanged.connect( self.parameter_tree_changed) # any changes on the settings will update accordingly the detector self.dock_pid.addWidget(widget) def get_set_model_params(self, model_name): self.settings.child('models', 'model_params').clearChildren() models = get_models() if len(models) > 0: model_class = find_dict_in_list_from_key_val(models, 'name', model_name)['class'] params = getattr(model_class, 'params') self.settings.child('models', 'model_params').addChildren(params) def run_PID(self): if self.run_action.isChecked(): self.run_action.setIcon(self.icon_stop) self.command_pid.emit(ThreadCommand('start_PID', [])) QtWidgets.QApplication.processEvents() QtWidgets.QApplication.processEvents() self.command_pid.emit(ThreadCommand('run_PID', [self.model_class.curr_output])) else: self.run_action.setIcon(self.iconrun) self.command_pid.emit(ThreadCommand('stop_PID')) QtWidgets.QApplication.processEvents() def pause_PID(self): for setp in self.setpoints_sb: setp.setEnabled(not self.pause_action.isChecked()) self.command_pid.emit(ThreadCommand('pause_PID', [self.pause_action.isChecked()])) def stop_moves(self, overshoot): """ Foreach module of the move module object list, stop motion. See Also -------- stop_scan, DAQ_Move_main.daq_move.stop_Motion """ self.overshoot = overshoot for mod in self.modules_manager.actuators: mod.stop_Motion() def set_model(self): model_name = self.settings.child('models', 'model_class').value() self.model_class = find_dict_in_list_from_key_val(self.models, 'name', model_name)['class'](self) self.set_setpoints_buttons() self.model_class.ini_model() self.settings.child('main_settings', 'epsilon').setValue(self.model_class.epsilon) def ini_model(self): try: if self.model_class is None: self.set_model() self.modules_manager.selected_actuators_name = self.model_class.actuators_name self.modules_manager.selected_detectors_name = self.model_class.detectors_name self.enable_controls_pid(True) self.model_led.set_as_true() self.ini_model_action.setEnabled(False) except Exception as e: logger.exception(str(e)) @property def setpoints(self): return [sp.value() for sp in self.setpoints_sb] @setpoints.setter def setpoints(self, values): for ind, sp in enumerate(self.setpoints_sb): sp.setValue(values[ind]) def setpoints_external(self, values_dict): for key in values_dict: index = self.model_class.setpoints_names.index(key) self.setpoints_sb[index].setValue(values_dict[key]) @property def curr_points(self): return [sp.value() for sp in self.currpoints_sb] @curr_points.setter def curr_points(self, values): for ind, sp in enumerate(self.currpoints_sb): sp.setValue(values[ind]) def emit_curr_points(self): if self.model_class is not None: self.curr_points_signal.emit(dict(zip(self.model_class.setpoints_names, self.curr_points))) def set_setpoints_buttons(self): self.setpoints_sb = [] self.currpoints_sb = [] for ind_set in range(self.model_class.Nsetpoints): self.setpoints_sb.append(SpinBox()) self.setpoints_sb[-1].setMinimumHeight(40) font = self.setpoints_sb[-1].font() font.setPointSizeF(20) self.setpoints_sb[-1].setFont(font) self.setpoints_sb[-1].setDecimals(6) self.toolbar_layout.addWidget(self.setpoints_sb[-1], 3, 2+ind_set, 1, 1) self.setpoints_sb[-1].valueChanged.connect(self.update_runner_setpoints) self.currpoints_sb.append(SpinBox()) self.currpoints_sb[-1].setMinimumHeight(40) self.currpoints_sb[-1].setReadOnly(True) self.currpoints_sb[-1].setDecimals(6) self.currpoints_sb[-1].setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) font = self.currpoints_sb[-1].font() font.setPointSizeF(20) self.currpoints_sb[-1].setFont(font) self.toolbar_layout.addWidget(self.currpoints_sb[-1], 4, 2+ind_set, 1, 1) self.setpoints_signal.connect(self.setpoints_external) def quit_fun(self): """ """ try: try: self.PIDThread.exit() except Exception as e: print(e) areas = self.dock_area.tempAreas[:] for area in areas: area.win.close() QtWidgets.QApplication.processEvents() QThread.msleep(1000) QtWidgets.QApplication.processEvents() self.dock_area.parent().close() except Exception as e: print(e) def parameter_tree_changed(self, param, changes): """ Foreach value changed, update : * Viewer in case of **DAQ_type** parameter name * visibility of button in case of **show_averaging** parameter name * visibility of naverage in case of **live_averaging** parameter name * scale of axis **else** (in 2D pymodaq type) Once done emit the update settings signal to link the commit. =============== =================================== ================================================================ **Parameters** **Type** **Description** *param* instance of ppyqtgraph parameter the parameter to be checked *changes* tuple list Contain the (param,changes,info) list listing the changes made =============== =================================== ================================================================ """ for param, change, data in changes: path = self.settings.childPath(param) if change == 'childAdded': pass elif change == 'value': if param.name() == 'model_class': self.get_set_model_params(param.value()) elif param.name() == 'refresh_plot_time' or param.name() == 'timeout': self.command_pid.emit(ThreadCommand('update_timer', [param.name(), param.value()])) elif param.name() == 'sample_time': self.command_pid.emit(ThreadCommand('update_options', dict(sample_time=param.value()))) elif param.name() in putils.iter_children( self.settings.child('main_settings', 'pid_controls', 'output_limits'), []): output_limits = convert_output_limits( self.settings.child('main_settings', 'pid_controls', 'output_limits', 'output_limit_min').value(), self.settings.child('main_settings', 'pid_controls', 'output_limits', 'output_limit_min_enabled').value(), self.settings.child('main_settings', 'pid_controls', 'output_limits', 'output_limit_max').value(), self.settings.child('main_settings', 'pid_controls', 'output_limits', 'output_limit_max_enabled').value()) self.command_pid.emit(ThreadCommand('update_options', dict(output_limits=output_limits))) elif param.name() in putils.iter_children( self.settings.child('main_settings', 'pid_controls', 'pid_constants'), []): Kp = self.settings.child('main_settings', 'pid_controls', 'pid_constants', 'kp').value() Ki = self.settings.child('main_settings', 'pid_controls', 'pid_constants', 'ki').value() Kd = self.settings.child('main_settings', 'pid_controls', 'pid_constants', 'kd').value() self.command_pid.emit(ThreadCommand('update_options', dict(tunings=(Kp, Ki, Kd)))) elif param.name() in putils.iter_children(self.settings.child('models', 'model_params'), []): if self.model_class is not None: self.model_class.update_settings(param) elif param.name() == 'detector_modules': self.model_class.update_detector_names() elif change == 'parent': pass def update_runner_setpoints(self): self.command_pid.emit(ThreadCommand('update_setpoints', self.setpoints)) @Slot(list) def thread_status(self, status): # general function to get datas/infos from all threads back to the main """ """ pass
class 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 KiteClient(QObject, KiteMethodProviderMixIn): sig_response_ready = Signal(int, dict) sig_client_started = Signal(list) sig_client_not_responding = Signal() sig_perform_request = Signal(int, str, object) MAX_SERVER_CONTACT_RETRIES = 40 def __init__(self, parent): QObject.__init__(self, parent) self.contact_retries = 0 self.endpoint = None self.requests = {} self.languages = [] self.mutex = QMutex() self.opened_files = {} self.alive = False self.thread_started = False self.thread = QThread() self.moveToThread(self.thread) self.thread.started.connect(self.started) self.sig_perform_request.connect(self.perform_request) def __wait_http_session_to_start(self): logger.debug('Waiting Kite HTTP endpoint to be available...') _, url = KITE_ENDPOINTS.ALIVE_ENDPOINT try: code = requests.get(url).status_code except Exception: code = 500 if self.contact_retries == self.MAX_SERVER_CONTACT_RETRIES: logger.debug('Kite server is not answering') self.sig_client_not_responding.emit() elif code != 200: self.contact_retries += 1 QTimer.singleShot(250, self.__wait_http_session_to_start) elif code == 200: self.alive = True self.start_client() def start(self): if not self.thread_started: self.thread.start() self.__wait_http_session_to_start() def start_client(self): logger.debug('Starting Kite HTTP session...') self.endpoint = requests.Session() self.languages = self.get_languages() self.sig_client_started.emit(self.languages) def started(self): self.thread_started = True def stop(self): if self.thread_started: logger.debug('Closing Kite HTTP session...') self.endpoint.close() self.thread.quit() def get_languages(self): verb, url = KITE_ENDPOINTS.LANGUAGES_ENDPOINT success, response = self.perform_http_request(verb, url) return response def perform_http_request(self, verb, url, params=None): response = None success = False http_method = getattr(self.endpoint, verb) http_response = http_method(url, json=params) success = http_response.status_code == 200 if success: try: response = http_response.json() except Exception: response = http_response.text response = None if response == '' else response return success, response def send(self, method, params, url_params): response = None if self.endpoint is not None and method in KITE_REQUEST_MAPPING: if self.alive: http_verb, path = KITE_REQUEST_MAPPING[method] path = path.format(**url_params) try: success, response = self.perform_http_request( http_verb, path, params) except (ConnectionRefusedError, ConnectionError): self.alive = False self.endpoint = None self.contact_retries = 0 self.__wait_http_session_to_start() return response return response def perform_request(self, req_id, method, params): if method in self.sender_registry: logger.debug('Perform {0} request with id {1}'.format( method, req_id)) handler_name = self.sender_registry[method] handler = getattr(self, handler_name) response = handler(params) if method in self.handler_registry: converter_name = self.handler_registry[method] converter = getattr(self, converter_name) response = converter(response) if response is not None: self.sig_response_ready.emit(req_id, response)
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 KiteClient(QObject, KiteMethodProviderMixIn): sig_response_ready = Signal(int, dict) sig_client_started = Signal(list) sig_client_not_responding = Signal() sig_perform_request = Signal(int, str, object) sig_perform_status_request = Signal(str) sig_status_response_ready = Signal((str, ), (dict, )) sig_perform_onboarding_request = Signal() sig_onboarding_response_ready = Signal(str) def __init__(self, parent, enable_code_snippets=True): QObject.__init__(self, parent) self.endpoint = None self.requests = {} self.languages = [] self.mutex = QMutex() self.opened_files = {} self.opened_files_status = {} self.thread_started = False self.enable_code_snippets = enable_code_snippets self.thread = QThread() self.moveToThread(self.thread) self.thread.started.connect(self.started) self.sig_perform_request.connect(self.perform_request) self.sig_perform_status_request.connect(self.get_status) self.sig_perform_onboarding_request.connect(self.get_onboarding_file) def start(self): if not self.thread_started: self.thread.start() logger.debug('Starting Kite HTTP session...') self.endpoint = requests.Session() self.languages = self.get_languages() self.sig_client_started.emit(self.languages) def started(self): self.thread_started = True def stop(self): if self.thread_started: logger.debug('Closing Kite HTTP session...') self.endpoint.close() self.thread.quit() def get_languages(self): verb, url = KITE_ENDPOINTS.LANGUAGES_ENDPOINT success, response = self.perform_http_request(verb, url) if response is None: response = ['python'] return response def _get_onboarding_file(self): """Perform a request to get kite's onboarding file.""" verb, url = KITE_ENDPOINTS.ONBOARDING_ENDPOINT success, response = self.perform_http_request(verb, url) return response def get_onboarding_file(self): """Get onboarding file.""" onboarding_file = self._get_onboarding_file() self.sig_onboarding_response_ready.emit(onboarding_file) def _get_status(self, filename): """Perform a request to get kite status for a file.""" verb, url = KITE_ENDPOINTS.STATUS_ENDPOINT if filename: url_params = {'filename': filename} else: url_params = {'filetype': 'python'} success, response = self.perform_http_request(verb, url, url_params=url_params) return response def get_status(self, filename): """Get kite status for a given filename.""" kite_status = self._get_status(filename) if not filename or kite_status is None: kite_status = status() self.sig_status_response_ready[str].emit(kite_status) else: self.sig_status_response_ready[dict].emit(kite_status) def perform_http_request(self, verb, url, url_params=None, params=None): response = None http_method = getattr(self.endpoint, verb) try: http_response = http_method(url, params=url_params, json=params) except Exception as error: return False, None success = http_response.status_code == 200 if success: try: response = http_response.json() except Exception: response = http_response.text response = None if response == '' else response return success, response def send(self, method, params, url_params): response = None if self.endpoint is not None and method in KITE_REQUEST_MAPPING: http_verb, path = KITE_REQUEST_MAPPING[method] encoded_url_params = { key: quote(value) if isinstance(value, TEXT_TYPES) else value for (key, value) in url_params.items() } path = path.format(**encoded_url_params) try: success, response = self.perform_http_request(http_verb, path, params=params) except (ConnectionRefusedError, ConnectionError): return response return response def perform_request(self, req_id, method, params): response = None if method in self.sender_registry: logger.debug('Perform {0} request with id {1}'.format( method, req_id)) handler_name = self.sender_registry[method] handler = getattr(self, handler_name) response = handler(params) if method in self.handler_registry: converter_name = self.handler_registry[method] converter = getattr(self, converter_name) if response is not None: response = converter(response) self.sig_response_ready.emit(req_id, response or {})
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 FallbackActor(QObject): #: Signal emitted when the Thread is ready sig_fallback_ready = Signal() sig_set_tokens = Signal(int, dict) sig_mailbox = Signal(dict) def __init__(self, parent): QObject.__init__(self) self.stopped = False self.daemon = True self.mutex = QMutex() self.file_tokens = {} self.diff_patch = diff_match_patch() self.thread = QThread() self.moveToThread(self.thread) self.thread.started.connect(self.started) self.sig_mailbox.connect(self.handle_msg) def tokenize(self, text, language): """ Return all tokens in `text` and all keywords associated by Pygments to `language`. """ try: lexer = get_lexer_by_name(language) keywords = get_keywords(lexer) except Exception: keywords = [] keyword_set = set(keywords) keywords = [{ 'kind': CompletionItemKind.KEYWORD, 'insertText': keyword, 'sortText': u'zz{0}'.format(keyword[0].lower()), 'filterText': keyword, 'documentation': '' } for keyword in keywords] # logger.debug(keywords) # tokens = list(lexer.get_tokens(text)) # logger.debug(tokens) tokens = get_words(text, language) tokens = [{ 'kind': CompletionItemKind.TEXT, 'insertText': token, 'sortText': u'zz{0}'.format(token[0].lower()), 'filterText': token, 'documentation': '' } for token in tokens] for token in tokens: if token['insertText'] not in keyword_set: keywords.append(token) return keywords def stop(self): """Stop actor.""" with QMutexLocker(self.mutex): logger.debug("Fallback plugin stopping...") self.thread.quit() def start(self): """Start thread.""" self.thread.start() def started(self): """Thread started.""" logger.debug('Fallback plugin starting...') self.sig_fallback_ready.emit() @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('Got request id {0}: {1} for file {2}'.format( _id, msg_type, file)) if msg_type == LSPRequestTypes.DOCUMENT_DID_OPEN: self.file_tokens[file] = { 'text': msg['text'], 'language': msg['language'] } elif msg_type == LSPRequestTypes.DOCUMENT_DID_CHANGE: if file not in self.file_tokens: self.file_tokens[file] = { 'text': '', 'language': msg['language'] } diff = msg['diff'] text = self.file_tokens[file] text, _ = self.diff_patch.patch_apply(diff, text['text']) self.file_tokens[file]['text'] = text elif msg_type == LSPRequestTypes.DOCUMENT_DID_CLOSE: self.file_tokens.pop(file, {}) elif msg_type == LSPRequestTypes.DOCUMENT_COMPLETION: tokens = [] if file in self.file_tokens: text_info = self.file_tokens[file] tokens = self.tokenize(text_info['text'], text_info['language']) tokens = {'params': tokens} self.sig_set_tokens.emit(_id, tokens)
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()