def run(optimiser): app = QApplication(sys.argv) # Thread for running slow parts of the optimiser without pausing GUI opt_worker = OptWorker(optimiser) opt_thread = QThread() opt_worker.moveToThread(opt_thread) app.aboutToQuit.connect(opt_thread.quit) opt_thread.start() # Queue and thread for updating text field queue = Queue() sys.stdout = WriteStream(queue) window = BayesOptWindow(optimiser, opt_worker) window.show() write_thread = QThread() receiver = Receiver(queue) receiver.signal.connect(window.write_to_textfield) receiver.moveToThread(write_thread) write_thread.started.connect(receiver.run) app.aboutToQuit.connect(write_thread.quit) write_thread.start() # app.exec_() sys.exit(app.exec_())
def main(): # initialize app = QApplication([]) # create connection to the server client = Client(address=('127.0.0.1', 8888), debug=True) client.client_connect() # create a main window window = MainWindow(window_width=400, window_height=600) window.setWindowTitle("Python chat") # waiting for messages global client_worker, network_thread # TODO: refactor this network_thread = QThread() network_thread.setTerminationEnabled(True) client_worker = ClientWorker(client_socket=client.client()) client_worker.recieved_message.connect(window.recieved_message_handler) client_worker.moveToThread(network_thread) network_thread.started.connect(client_worker.start) network_thread.start() window.show() return app.exec_()
class Window(QMainWindow): def __init__(self, parent=None): super().__init__(parent) self.clicksCount = 0 self.setupUi() def setupUi(self): self.setWindowTitle("Freezing GUI") self.resize(300, 150) self.centralWidget = QWidget() self.setCentralWidget(self.centralWidget) # Create and connect widgets self.clicksLabel = QLabel("Counting: 0 clicks", self) self.clicksLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.stepLabel = QLabel("Long-Running Step: 0") self.stepLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.countBtn = QPushButton("Click me!", self) self.countBtn.clicked.connect(self.countClicks) self.longRunningBtn = QPushButton("Long-Running Task!", self) self.longRunningBtn.clicked.connect(self.runLongTask) # Set the layout layout = QVBoxLayout() layout.addWidget(self.clicksLabel) layout.addWidget(self.countBtn) layout.addStretch() layout.addWidget(self.stepLabel) layout.addWidget(self.longRunningBtn) self.centralWidget.setLayout(layout) def countClicks(self): self.clicksCount += 1 self.clicksLabel.setText(f"Counting: {self.clicksCount} clicks") def reportProgress(self, n): self.stepLabel.setText(f"Long-Running Step: {n}") def runLongTask(self): # 创建 QThread self.thread = QThread() # 创建 worker 对象 self.worker = Worker() # 将 work 移到 thread 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.thread.deleteLater) self.worker.progress.connect(self.reportProgress) # 启动线程 self.thread.start() # 结束 self.longRunningBtn.setEnabled(False) self.thread.finished.connect( lambda: self.longRunningBtn.setEnabled(True)) self.thread.finished.connect( lambda: self.stepLabel.setText("Long-Running Step: 0"))
class ConstraintCleanerPlugin: name = 'Constraint Cleaner' description = 'Cleans up duplicate constraints\nand disables redundant constraints' hidden = True def __init__(self, api: PluginApi) -> None: self.api = api def load(self) -> None: #self.action_remove_duplicate = self.api.register_menu_entry('Remove duplicate constraints', self.slot_remove_duplicate) self.action_remove_redundant = self.api.register_menu_entry( 'Remove redundant constraints', self.slot_remove_redundant) def unload(self) -> None: #self.api.remove_menu_entry(self.action_remove_duplicate) self.api.remove_menu_entry(self.action_remove_redundant) def slot_remove_duplicate(self) -> None: # TODO pass def slot_remove_redundant(self) -> None: ''' Disables all constraints that only contain redundant information and don't create more relations ''' progress_dialog = self.api.get_progress_dialog( 'Constraint Cleaner', 'Removing redundant constraints...', False) progress_dialog.show() self.thread = QThread() self.worker = RemoveRedundantWorker() self.worker.moveToThread(self.thread) self.worker.signal_progress.connect( lambda progress: progress_dialog.set_progress(progress)) self.worker.signal_done.connect( lambda: ( # https://stackoverflow.com/a/13672943 self.thread.quit(), progress_dialog.close(), QMessageBox.information( self.api.main_window, 'Constraint Cleaner', 'All redundant constraints are removed.'))) self.worker.signal_fail.connect(lambda: ( self.thread.quit(), progress_dialog.close(), QMessageBox.critical( self.api.main_window, 'Constraint Cleaner', 'Failed to add a constraint.\nSee console for more information.' ))) self.thread.started.connect(self.worker.process) self.thread.start()
class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() #ui self.ui = Ui_MainWindow() self.ui.setupUi(self) self.ui.startButton.clicked.connect(self.handleClicked) self.checkboxes = [ self.ui.checkBox, self.ui.checkBox_2, self.ui.checkBox_3, self.ui.checkBox_4, self.ui.checkBox_5, self.ui.checkBox_6 ] def handleClicked(self): if not self.ui.locationInput.text(): msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Warning) msgBox.setText("지역을 설정해주세요.") else: filter = json.load(open("mod.json"))["filter"] filter_list = [] for c in self.checkboxes: if c.isChecked(): filter_list.append(filter[c.text()]) filter = ":".join(filter_list) #thread self.thread = QThread() self.worker = Worker(self.ui.locationInput.text(), filter) 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.thread.deleteLater) self.worker.progress.connect(self.ui.progressBar.setValue) self.thread.start() self.ui.startButton.setEnabled(False) self.thread.finished.connect(self.handleFinished) def handleFinished(self): self.ui.startButton.setEnabled(True) self.ui.progressBar.setValue(0) msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Information) msgBox.setText("수집이 완료되었습니다.") msgBox.setDefaultButton(QMessageBox.Ok) ret = msgBox.exec_() if ret == QMessageBox.Ok: path = os.getcwd() + "/result" webbrowser.open('file:///' + path)
class RunThread: """ Wraps the thread and worker setting up the log signals to pass the log messages to the main window thread :param on_start: Method to call before the transformation begins :param on_finish: Method to call after the transformation ends """ def __init__(self, on_start, on_finish): self.on_start = on_start self.on_finish = on_finish self.worker = None """The controller runner""" self.thread = None """The thread the worker will be ran on""" def start(self, config, generic_log_handler, validation_log_handler): """ Starts the thread connecting the log panels to the relevant signals :param config: Config for the transformation to run :param generic_log_handler: The handler to consume all log records :param validation_log_handler: The handler to consume validation log records """ self.on_start() self.thread = QThread() self.worker = ControllerRunner(config) self.worker.moveToThread(self.thread) self.worker.finished.connect(self.on_finish) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.started.connect(self.worker.run) self.thread.finished.connect(self.thread.deleteLater) # connect the log handlers back to the main window self.worker.generic_log_record.connect(generic_log_handler.emit) self.worker.validation_log_record.connect(validation_log_handler.emit) self.thread.start()
class App(QWidget): def __init__(self): super(App, self).__init__() self.combo = None self.MainProcess = MainProcess() self.MainProcess.get_image_functions() self.fps_label = QLabel('FPS: XXX') self.initUI() self._thread = QThread(self) self.video_worker = VideoThread(self.MainProcess.list_of_filters) #self.combo.currentIndexChanged.connect(self.video_worker.new_filter_slot) self.combo.currentIndexChanged.connect(self._new_ind) self.video_worker.change_pixmap_signal.connect(self.update_image) self.video_worker.update_fps_signal.connect(self._new_fps) self.video_worker.moveToThread(self._thread) self.combo.setCurrentIndex(self.MainProcess.blank_index) # connect the buttons self.start_button.clicked.connect(self.video_worker.start) self.quit_button.clicked.connect( self.close) # force this to run on current thread self._thread.start() @Slot() def _new_ind(self, ind): self.video_worker.new_filter_slot(ind) @Slot(int) def _new_fps(self, fps): self.fps_label.setText(f'FPS: {fps:.1f}') def initUI(self): self.cam = CamImage() self.start_button = QPushButton("Start") self.quit_button = QPushButton("Quit") controls = ControlWidget() self.combo = QComboBox(self) for it in self.MainProcess.list_of_filters: self.combo.addItem(it[0]) hbox = QHBoxLayout() hbox.addWidget(controls) hbox.addStretch(1) hbuttons = QHBoxLayout() hbuttons.addWidget(self.combo) hbuttons.addWidget(self.start_button) hbuttons.addWidget(self.quit_button) vbutton = QVBoxLayout() vbutton.addLayout(hbuttons) vbutton.addWidget(self.fps_label) hbox.addLayout(vbutton) vbox = QVBoxLayout() vbox.addWidget(self.cam) vbox.addStretch(1) vbox.addLayout(hbox) self.setLayout(vbox) self.setGeometry(300, 300, 300, 150) self.setWindowTitle('Buttons') self.show() def closeEvent(self, event): logger.debug('Got Close Event') self.video_worker.stop() self._thread.wait() event.accept() @Slot(np.ndarray) def update_image(self, cv_img): """Updates the image_label with a new opencv image""" qt_img = self.convert_cv_qt(cv_img) self.cam.setPixmap(qt_img) def convert_cv_qt(self, cv_img): """Convert from an opencv image to QPixmap""" rgb_image = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape bytes_per_line = ch * w convert_to_Qt_format = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888) p = convert_to_Qt_format.scaled(self.cam.width(), self.cam.height(), Qt.KeepAspectRatio) return QPixmap.fromImage(p)
class MainWindow(QObject): def __init__(self): QObject.__init__(self) # ----------------------------------------------- # GENERAL OBJECTS: signals, mutexs, etc # ----------------------------------------------- # Static Info # Nothing to see # Signals To Send Data ## Signals for install button signalButtonInstall = Signal(bool) # when pressed signalInstalledCoreBackend = Signal(bool) # when finished task ## Signals for run button signalButtonRunCore = Signal(bool) # when pressed signalCoreBackendRunning = Signal(bool) # when finished task # ----------------------------------------------- # HANDLERS OF FRONTEND FUNCTIONS # ----------------------------------------------- # Function To Check and Start Install Core Backend @Slot() def installCoreBackend(self): """ Code that execute when btnInstall is clicked """ nameOfFunction = sys._getframe().f_code.co_name nameOfFile = __file__ print(f'[{nameOfFile}][{nameOfFunction}] Started') # emit signal of task started self.signalButtonInstall.emit(True) # Clone the repository if not os.path.isdir(PATH_CORE_BACKEND): os.mkdir(PATH_CORE_BACKEND) self.runFunctionInstallCoreBackend() else: print( f'[{nameOfFile}][{nameOfFunction}] Path {PATH_CORE_BACKEND} already exist' ) self.runFunctionInstallCoreBackendFinished() # Force finish print(f'[{nameOfFile}][{nameOfFunction}] Finished') @Slot() def runCoreBackend(self): """ Function that runs Core Python """ nameOfFunction = sys._getframe().f_code.co_name nameOfFile = __file__ print(f'[{nameOfFile}][{nameOfFunction}] Started') self.signalButtonRunCore.emit(True) self.runFunctionCoreBackend() self.signalCoreBackendRunning.emit(True) print(f'[{nameOfFile}][{nameOfFunction}] Finished') # ----------------------------------------------- # THREADS SETUP # ----------------------------------------------- def runFunctionInstallCoreBackend(self): """ Script that run a class as Thread to don't freeze main app """ nameOfFunction = sys._getframe().f_code.co_name nameOfFile = __file__ print(f'[{nameOfFile}][{nameOfFunction}] Started') # Step 1: Create worker # Step 2: Create a QThread object self.thread = QThread() # Step 3: Create a worker object self.worker = WorkerBashCommand() # Step 4: Move worker to the thread self.worker.moveToThread(self.thread) # Step 5: Connect signals and slots self.thread.started.connect( self.worker.runBashCommand(COMMAND_GIT_CLONE_CORE_BACKEND)) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect( self.runFunctionInstallCoreBackendFinished) # Step 6: Start the thread self.thread.start() print(f'[{nameOfFile}][{nameOfFunction}] Finished') def runFunctionCoreBackend(self): """ Script that run a class as Thread to don't freeze main app """ nameOfFunction = sys._getframe().f_code.co_name nameOfFile = __file__ print(f'[{nameOfFile}][{nameOfFunction}] Started') # Step 1: Create worker # Step 2: Create a QThread object self.thread2 = QThread() # Step 3: Create a worker object self.worker2 = WorkerBashCommand() # Step 4: Move worker to the thread self.worker2.moveToThread(self.thread2) # Step 5: Connect signals and slots self.thread2.started.connect( self.worker2.runBashCommand(COMMAND_RUN_CORE_BACKEND)) self.worker2.finished.connect(self.thread2.quit) self.worker2.finished.connect(self.worker2.deleteLater) self.thread2.finished.connect(self.runFunctionCoreBackendFinished) # Step 6: Start the thread self.thread2.start() print(f'[{nameOfFile}][{nameOfFunction}] Finished') # ----------------------------------------------- # END OF THREADS # ----------------------------------------------- @Slot() def runFunctionInstallCoreBackendFinished(self): """ Function that executes when a Thread finish """ # emit signal of task finished self.signalInstalledCoreBackend.emit(True) # Finish cloning core repo @Slot() def runFunctionCoreBackendFinished(self): """ Function that executes when a Thread finish """ # emit signal of task finished self.signalCoreBackendRunning.emit(True) # Finish cloning core repo
(_, type_names, path, filename) = event if filename[-3:] == 'qml' and 'IN_MODIFY' in type_names: reload = True break if reload: self.requestReload.emit() if __name__ == "__main__": app = QGuiApplication(sys.argv) engine = QQmlApplicationEngine() workerThread = QThread() workerThread.start() worker = Worker() worker.moveToThread(workerThread) master = Master() master.command.connect(worker.run) worker.requestReload.connect(master.reload) master.command.emit() # Stop application gracefully: signal.signal(signal.SIGINT, signal.SIG_DFL) status = app.exec_() worker.stop() workerThread.quit() workerThread.wait() sys.exit(status)
class EvelynDesktop(QStackedWidget): INTERVAL_SECS = 30 ALERT_SECS = 5 signal_get_ping = Signal() signal_post_history = Signal(int, QDateTime) def __init__( self, config_file: str ) -> None: super().__init__() # load config try: self.config = Config(config_file) except Exception as e: QMessageBox.critical(self, 'Config error', str(e)) QTimer.singleShot(0, self.close) return # load settings self.settings = Settings() # state self.state_key: Optional[int] = None # label widget self.label_ping = ClickableLabel('Loading ...', self.post_history) self.label_ping.setTextFormat(Qt.RichText) self.label_ping.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) layout_ping = QGridLayout() layout_ping.setContentsMargins(0, 0, 0, 0) layout_ping.addWidget(self.label_ping) self.widget_ping = QWidget() self.widget_ping.setLayout(layout_ping) self.addWidget(self.widget_ping) # alert widget self.label_alert = QLabel() self.label_alert.setWordWrap(True) self.label_alert.setAlignment(Qt.AlignCenter) self.label_alert.setStyleSheet(f'background: #dddddd;') self.addWidget(self.label_alert) # context menu self.action_report_done = QAction('Report done ...') self.action_report_done.triggered.connect(self.report_done) self.action_exit = QAction('Exit') self.action_exit.triggered.connect(self.close) self.action_frameless = QAction('Frameless window') self.action_frameless.setCheckable(True) self.action_frameless.triggered.connect(self.set_frameless_window) self.action_homepage = QAction('Open homepage') self.action_homepage.triggered.connect(self.open_homepage) self.context_menu = QMenu() self.context_menu.addAction(self.action_report_done) self.context_menu.addAction(self.action_exit) self.context_menu.addAction(self.action_frameless) self.context_menu.addAction(self.action_homepage) # threads self.thread_communication = QThread() self.thread_communication.start() # workers self.worker_communication = CommunicationWorker( netloc=self.config.netloc, base_path=self.config.base_path, api_key=self.config.api_key, guild=self.config.guild, member=self.config.member) self.worker_communication.moveToThread(self.thread_communication) # signals self.worker_communication.signal_get_ping_done.connect(self.get_ping_done) self.worker_communication.signal_post_history_done.connect(self.post_history_done) self.signal_get_ping.connect(self.worker_communication.get_ping) self.signal_post_history.connect(self.worker_communication.post_history) # get ping timer QTimer.singleShot(0, self.get_ping) self.timer_ping = QTimer() self.timer_ping.timeout.connect(self.get_ping) self.timer_ping.setTimerType(Qt.VeryCoarseTimer) self.timer_ping.start(self.INTERVAL_SECS * 1000) # switch label timer self.timer_label = QTimer() self.timer_label.timeout.connect(lambda: self.setCurrentWidget(self.widget_ping)) self.timer_label.setSingleShot(True) self.timer_label.setTimerType(Qt.CoarseTimer) # window attributes size = self.settings.get('window', 'size', type_=QSize) if size is not None: self.resize(size) pos = self.settings.get('window', 'pos', type_=QPoint) if pos is not None: self.move(pos) frameless = self.settings.get('window', 'frameless', type_=bool) if frameless is not None and frameless: QTimer.singleShot(100, self.action_frameless.trigger) self.setWindowFlag(Qt.WindowStaysOnTopHint, self.config.window_stays_on_top) self.setAttribute(Qt.WA_TranslucentBackground) self.setWindowTitle('Evelyn Reminder') def closeEvent( self, event: QCloseEvent ) -> None: # save settings with suppress_and_log_exception(): self.settings.set('window', 'size', self.size()) self.settings.set('window', 'pos', self.pos()) self.settings.set('window', 'frameless', bool(self.windowFlags() & Qt.FramelessWindowHint)) # stop communication thread with suppress_and_log_exception(): self.thread_communication.quit() self.thread_communication.wait() # done super().closeEvent(event) def contextMenuEvent( self, event: QContextMenuEvent ) -> None: self.context_menu.exec_(event.globalPos()) @Slot() def get_ping(self) -> None: logging.info('Get ping ...') self.signal_get_ping.emit() @Slot(int, str, str) def get_ping_done( self, key: int, text: str, color: str ) -> None: logging.info('Get ping done') if key == -1: self.state_key = None self.label_ping.setWordWrap(True) else: self.state_key = key self.label_ping.setWordWrap(False) self.label_ping.setText(text) self.widget_ping.setStyleSheet(f'background : {color}; ') @Slot() def post_history( self, date_time: QDateTime = QDateTime() ) -> None: # this method is called as Slot by ClickableLabel.mouseReleaseEvent() without arguments # this method is called directly by EvelynDesktop.report_done() with a date_time if self.state_key is None: return logging.info('Post history ...') self.label_alert.setText('Sending ...') self.label_alert.setStyleSheet(f'background: #dddddd;') self.setCurrentWidget(self.label_alert) self.signal_post_history.emit(self.state_key, date_time) @Slot(str, bool) def post_history_done( self, text: str, error: bool ) -> None: logging.info('Post history done') self.label_alert.setText(text) if error: self.label_alert.setStyleSheet(f'background: #dd4b4b;') self.timer_label.start(self.ALERT_SECS * 1000) # trigger instant ping update to avoid outdated info self.timer_ping.stop() self.timer_ping.start(self.INTERVAL_SECS * 1000) self.get_ping() @Slot() def report_done(self) -> None: self.timer_ping.stop() # stop ping update while dialog is open report_done_dialog = ReportDoneDialog(self) response = report_done_dialog.exec() if response != QDialog.Accepted: self.timer_ping.start(self.INTERVAL_SECS * 1000) self.get_ping() return date_time = report_done_dialog.get_date_time() self.post_history(date_time) @Slot(bool) def set_frameless_window( self, value: bool ) -> None: pos = self.pos() self.setWindowFlag(Qt.FramelessWindowHint, value) # workaround: window goes invisible otherwise self.setVisible(True) # workaround: window would move up otherwise if value: QTimer.singleShot(100, lambda: self.move(pos)) @Slot() def open_homepage(self) -> None: webbrowser.open('https://github.com/stefs/evelyn-reminder')
class MediaPlayer(): def __init__(self, songQueue, marqueeFunc, parent=None): self.parent = parent self.player = vlc.MediaPlayer() self.songQueue = songQueue self.currentSong = None self.marqueeFunc = marqueeFunc self.thread = QThread() self.worker = Worker(self) self.worker.moveToThread(self.thread) self.worker.check.connect(self.songEndCheck) self.thread.started.connect(self.worker.run) self.thread.start() def restartSong(self): self.player.set_media( vlc.Media(self.parent.songPath + self.currentSong["song_path"])) self.player.play() self.updateMarquee() def pauseSong(self): self.player.pause() def skipSong(self): if len(self.songQueue.getQueue()) == 0: return self.currentSong = self.songQueue.popSong() if self.parent.content.__class__.__name__ == "WindowQueue": self.parent.content.queueList.updateQueue() self.restartSong() def swapTrack(self): self.player.audio_set_track( (self.player.audio_get_track() % (self.player.audio_get_track_count() - 1)) + 1) def updateMarquee(self): if len(self.songQueue.getQueue()) > 0: text = [] text.append("正在播放/Now Playing:{} - {}".format( self.currentSong["artist_name"], self.currentSong["song_title"])) text.append("下一首歌/Next Song: {} - {}".format( self.songQueue.getQueue()[0]["artist_name"], self.songQueue.getQueue()[0]["song_title"])) self.marqueeFunc(text) else: self.marqueeFunc("正在播放/Now Playing:{} - {}".format( self.currentSong["artist_name"], self.currentSong["song_title"])) def start(self): self.currentSong = random.choice(DB.getSongTitles("")) self.restartSong() def songEndCheck(self): if self.player.get_state() == 6: if len(self.songQueue.getQueue()) == 0: self.start() else: self.skipSong()
class BridgeDock(QDockWidget): def __init__(self, parent, api: PluginApi) -> None: super().__init__('', parent) self.api = api self.ui = Ui_BridgeDock() self.ui.setupUi(self) self.server_thread = None self.observer = None self.modified_timer = None self.slot_server_running(False) self.ui.pushButtonStartServer.clicked.connect(self.slot_start_server) self.ui.pushButtonStopServer.clicked.connect(self.slot_stop_server) self.ui.toolButtonLoadFolder.clicked.connect( self.slot_edit_load_folder) self.ui.toolButtonSaveFolder.clicked.connect( self.slot_edit_save_folder) self.ui.labelConnectionStatus.setText('Server not yet running.') # Initially load from repo folder self.ui.lineEditLoadFolder.setText(settings.get_repo_location()) self.visibilityChanged.connect(self.slot_visibility_changed) def slot_visibility_changed(self, visible: bool) -> None: if not visible and self.server_thread is not None: self.slot_stop_server() def slot_server_running(self, running: bool) -> None: if running: self.ui.pushButtonStartServer.setVisible(False) self.ui.pushButtonStopServer.setVisible(True) else: self.ui.pushButtonStartServer.setVisible(True) self.ui.pushButtonStopServer.setVisible(False) def slot_start_server(self) -> None: if self.ui.checkBoxCopySaves.isChecked( ) and self.ui.lineEditSaveFolder.text().strip() == '': self.api.show_error( 'Entity Explorer Bridge', 'You need to set the folder where to store the copies.') return self.server_thread = QThread() self.server_worker = ServerWorker() self.server_worker.signal_connected.connect(self.slot_connected) self.server_worker.signal_disconnected.connect(self.slot_disconnected) self.server_worker.signal_error.connect(self.slot_error) self.server_worker.signal_started.connect(self.slot_server_started) self.server_worker.signal_shutdown.connect(self.slot_server_stopped) self.server_worker.moveToThread(self.server_thread) self.server_thread.started.connect(self.server_worker.process) self.server_thread.start() self.slot_server_running(True) self.set_folders_active(False) def set_folders_active(self, active: bool) -> None: self.ui.lineEditLoadFolder.setEnabled(active) self.ui.toolButtonLoadFolder.setEnabled(active) self.ui.checkBoxCopySaves.setEnabled(active) self.ui.lineEditSaveFolder.setEnabled(active) self.ui.toolButtonSaveFolder.setEnabled(active) def slot_stop_server(self) -> None: # Shutdown needs to be triggered by the server thread, so send a request requests.get('http://localhost:10243/shutdown') self.set_folders_active(True) def slot_connected(self) -> None: self.ui.labelConnectionStatus.setText( 'Connected to Entity Explorer instance.') def slot_disconnected(self) -> None: self.ui.labelConnectionStatus.setText( 'Disconnected from Entity Explorer instance.') def slot_server_started(self) -> None: self.slot_server_running(True) self.ui.labelConnectionStatus.setText( 'Server running. Please connect Entity Explorer instance.') self.start_watchdog() def slot_server_stopped(self) -> None: self.slot_server_running(False) self.server_thread.terminate() self.ui.labelConnectionStatus.setText('Server stopped.') self.stop_watchdog() def slot_error(self, error: str) -> None: self.slot_server_running(False) self.server_thread.terminate() self.api.show_error('Entity Explorer Bridge', error) def slot_edit_load_folder(self): dir = QFileDialog.getExistingDirectory( self, 'Folder in which the save states are stored by mGBA', self.ui.lineEditLoadFolder.text()) print(dir) if dir is not None: self.ui.lineEditLoadFolder.setText(dir) def slot_edit_save_folder(self): dir = QFileDialog.getExistingDirectory( self, 'Folder in which all save states should be copied', self.ui.lineEditSaveFolder.text()) if dir is not None: self.ui.lineEditSaveFolder.setText(dir) def start_watchdog(self): if self.observer is not None: print('Already observing') return patterns = [ '*.ss0', '*.ss1', '*.ss2', '*.ss3', '*.ss4', '*.ss5', '*.ss6', '*.ss7', '*.ss8', '*.ss9', '*.State' ] ignore_patterns = None ignore_directories = False case_sensitive = True self.event_handler = PatternMatchingEventHandler( patterns, ignore_patterns, ignore_directories, case_sensitive) self.event_handler.on_modified = self.on_file_modified path = self.ui.lineEditLoadFolder.text() self.observer = Observer() self.observer.schedule(self.event_handler, path, recursive=False) self.observer.start() def stop_watchdog(self): if self.observer is not None: self.observer.stop() self.observer.join() self.observer = None # https://stackoverflow.com/a/66907107 def debounce(wait_time): """ Decorator that will debounce a function so that it is called after wait_time seconds If it is called multiple times, will wait for the last call to be debounced and run only this one. """ def decorator(function): def debounced(*args, **kwargs): def call_function(): debounced._timer = None return function(*args, **kwargs) # if we already have a call to the function currently waiting to be executed, reset the timer if debounced._timer is not None: debounced._timer.cancel() # after wait_time, call the function provided to the decorator with its arguments debounced._timer = threading.Timer(wait_time, call_function) debounced._timer.start() debounced._timer = None return debounced return decorator @debounce(0.1) def on_file_modified(self, event): with open(event.src_path, 'rb') as file: bytes = file.read() self.server_worker.slot_send_save_state(event.src_path, bytes) if self.ui.checkBoxCopySaves.isChecked( ) and self.ui.lineEditSaveFolder.text(): name = os.path.basename(event.src_path) name = datetime.now().strftime('%Y-%m-%d_%H_%M_%S_%f_') + name with open( os.path.join(self.ui.lineEditSaveFolder.text(), name), 'wb') as output: output.write(bytes)
# Step 2: Create a QThread object thread = QThread() # Step 3: Create a worker object worker = Worker() # Step 4: Move worker to the thread worker.moveToThread(thread) # Step 5: Connect signals and slots thread.started.connect(worker.run) worker.finished.connect(thread.quit) worker.finished.connect(worker.deleteLater) thread.finished.connect(thread.deleteLater) # Step 6: Start the thread thread.start() app = QtWidgets.QApplication([]) app.setQuitOnLastWindowClosed(False) # Adding an icon icon = QtGui.QIcon(':/images/icon.png') # Adding item on the menu bar tray = QtWidgets.QSystemTrayIcon() tray.setIcon(icon) tray.setVisible(True) # https://gist.github.com/for-l00p/3e33305f948659313127632ad04b4311
class BridgeDock(QDockWidget): def __init__(self, parent, api: PluginApi) -> None: super().__init__('', parent) self.api = api self.ui = Ui_BridgeDock() self.ui.setupUi(self) self.server_thread = None self.symbols = None self.rom = None self.data_extractor_plugin = None self.slot_server_running(False) self.ui.pushButtonStartServer.clicked.connect(self.slot_start_server) self.ui.pushButtonStopServer.clicked.connect(self.slot_stop_server) self.ui.pushButtonUpload.clicked.connect(self.slot_upload_function) self.ui.pushButtonDownload.clicked.connect(self.slot_download_function) self.ui.pushButtonCopyJs.clicked.connect(self.slot_copy_js_code) self.ui.pushButtonGoTo.clicked.connect(self.slot_goto) self.ui.pushButtonDecompile.clicked.connect(self.slot_decompile) self.ui.pushButtonGlobalTypes.clicked.connect(self.slot_global_types) self.ui.pushButtonUploadAndDecompile.clicked.connect( self.slot_upload_and_decompile) self.enable_function_group(False) self.ui.labelConnectionStatus.setText('Server not yet running.') self.visibilityChanged.connect(self.slot_close) def slot_close(self, visibility: bool) -> None: # TODO temporarily disable until a good way to detect dock closing is found pass #if not visibility and self.server_thread is not None: # self.slot_stop_server() def slot_server_running(self, running: bool) -> None: if running: self.ui.pushButtonStartServer.setVisible(False) self.ui.pushButtonStopServer.setVisible(True) else: self.ui.pushButtonStartServer.setVisible(True) self.ui.pushButtonStopServer.setVisible(False) def slot_start_server(self) -> None: self.server_thread = QThread() self.server_worker = ServerWorker() self.server_worker.signal_connected.connect(self.slot_connected) self.server_worker.signal_disconnected.connect(self.slot_disconnected) self.server_worker.signal_error.connect(self.slot_error) self.server_worker.signal_c_code.connect(self.slot_received_c_code) self.server_worker.signal_started.connect(self.slot_server_started) self.server_worker.signal_shutdown.connect(self.slot_server_stopped) self.server_worker.signal_extract_data.connect(self.slot_extract_data) self.server_worker.signal_fetch_decompilation.connect( self.slot_fetch_decompilation) self.server_worker.signal_upload_function.connect( self.slot_download_requested) self.server_worker.moveToThread(self.server_thread) self.server_thread.started.connect(self.server_worker.process) self.server_thread.start() self.slot_server_running(True) def enable_function_group(self, enabled: bool) -> None: self.ui.lineEditFunctionName.setEnabled(enabled) self.ui.pushButtonUpload.setEnabled(enabled) self.ui.pushButtonDownload.setEnabled(enabled) self.ui.pushButtonGoTo.setEnabled(enabled) self.ui.pushButtonDecompile.setEnabled(enabled) self.ui.pushButtonUploadAndDecompile.setEnabled(enabled) def slot_stop_server(self) -> None: # Shutdown needs to be triggered by the server thread, so send a request requests.get('http://localhost:10241/shutdown') def slot_upload_function(self) -> None: self.upload_function(True) # Returns true if the user accepted the uploading def upload_function(self, include_function: bool) -> bool: # TODO try catch all of the slots? (err, asm, src, signature) = get_code(self.ui.lineEditFunctionName.text().strip(), include_function) if err: self.api.show_error('CExplore Bridge', asm) return if NO_CONFIRMS: # For pros also directly go to the function in Ghidra and apply the signature self.slot_goto() #self.apply_function_signature(self.ui.lineEditFunctionName.text().strip(), signature) if NO_CONFIRMS or self.api.show_question( 'CExplore Bridge', f'Replace code in CExplore with {self.ui.lineEditFunctionName.text().strip()}?' ): self.server_worker.slot_send_asm_code(extract_USA_asm(asm)) self.server_worker.slot_send_c_code(src) if not NO_CONFIRMS: self.api.show_message( 'CExplore Bridge', f'Uploaded code of {self.ui.lineEditFunctionName.text().strip()}.' ) return True return False def slot_download_requested(self, name: str) -> None: self.ui.lineEditFunctionName.setText(name) self.slot_download_function() def slot_download_function(self) -> None: self.enable_function_group(False) self.server_worker.slot_request_c_code() def slot_received_c_code(self, code: str) -> None: self.enable_function_group(True) (includes, header, src) = split_code(code) dialog = ReceivedDialog(self) dialog.signal_matching.connect(self.slot_store_matching) dialog.signal_nonmatching.connect(self.slot_store_nonmatching) dialog.show_code(includes, header, src) def slot_store_matching(self, includes: str, header: str, code: str) -> None: self.store(includes, header, code, True) def slot_store_nonmatching(self, includes: str, header: str, code: str) -> None: self.store(includes, header, code, False) def store(self, includes: str, header: str, code: str, matching: bool) -> None: (err, msg) = store_code(self.ui.lineEditFunctionName.text().strip(), includes, header, code, matching) if err: self.api.show_error('CExplore Bridge', msg) return if not NO_CONFIRMS: self.api.show_message( 'CExplore Bridge', f'Sucessfully replaced code of {self.ui.lineEditFunctionName.text().strip()}.' ) def slot_copy_js_code(self) -> None: QApplication.clipboard().setText( 'javascript:var script = document.createElement("script");script.src = "http://localhost:10241/static/bridge.js";document.body.appendChild(script);' ) self.api.show_message( 'CExplore Bridge', 'Copied JS code to clipboard.\nPaste it as the url to a bookmark.\nThen go open the CExplore instance and click on the bookmark to connect.' ) def slot_connected(self) -> None: self.ui.labelConnectionStatus.setText( 'Connected to CExplore instance.') self.enable_function_group(True) self.ui.pushButtonCopyJs.setVisible(False) def slot_disconnected(self) -> None: self.ui.labelConnectionStatus.setText( 'Disconnected from CExplore instance.') self.enable_function_group(False) def slot_server_started(self) -> None: self.slot_server_running(True) self.ui.labelConnectionStatus.setText( 'Server running. Please connect CExplore instance.') def slot_server_stopped(self) -> None: self.slot_server_running(False) self.server_thread.terminate() self.enable_function_group(False) self.ui.pushButtonCopyJs.setVisible(True) self.ui.labelConnectionStatus.setText('Server stopped.') def slot_error(self, error: str) -> None: self.slot_server_running(False) self.server_thread.terminate() self.enable_function_group(False) self.ui.pushButtonCopyJs.setVisible(True) self.api.show_error('CExplore Bridge', error) def slot_goto(self) -> None: try: r = requests.get('http://localhost:10242/goto/' + self.ui.lineEditFunctionName.text().strip()) if r.status_code != 200: self.api.show_error('CExplore Bridge', r.text) return except requests.exceptions.RequestException as e: self.api.show_error( 'CExplore Bridge', 'Could not reach Ghidra server. Did you start the script?') def slot_fetch_decompilation(self, name: str) -> None: self.ui.lineEditFunctionName.setText(name) self.slot_decompile() def slot_decompile(self) -> None: try: r = requests.get('http://localhost:10242/decompile/' + self.ui.lineEditFunctionName.text().strip()) if r.status_code != 200: self.api.show_error('CExplore Bridge', r.text) return result = r.text code = improve_decompilation(result) self.server_worker.slot_add_c_code(code) except requests.exceptions.RequestException as e: self.api.show_error( 'CExplore Bridge', 'Could not reach Ghidra server. Did you start the script?') except Exception as e: self.api.show_error('CExplore Bridge', 'An unknown error occured: ' + str(e)) def slot_upload_and_decompile(self) -> None: # Upload, but don't include the function. if self.upload_function(False): # Now add the decompiled function. self.slot_decompile() def slot_global_types(self) -> None: globals = find_globals() success = True for definition in globals: if not self.apply_global_type(definition): success = False break # Also apply function signatures from file if success: signatures = read_signatures_from_file() for signature in signatures: if not self.apply_function_signature(signature.function, signature.signature): success = False break if success: self.api.show_message('CExplore Bridge', 'Applied all global types.') def apply_global_type(self, definition: TypeDefinition) -> bool: try: url = 'http://localhost:10242/globalType/' + definition.name + '/' + definition.dataType + '/' + definition.length print(url) r = requests.get(url) if r.status_code != 200: self.api.show_error('CExplore Bridge', r.text) return False return True except requests.exceptions.RequestException as e: self.api.show_error( 'CExplore Bridge', 'Could not reach Ghidra server. Did you start the script?') return False def apply_function_signature(self, name: str, signature: str) -> bool: try: url = 'http://localhost:10242/functionType/' + name + '/' + signature print(url) r = requests.get(url) if r.status_code != 200: self.api.show_error('CExplore Bridge', r.text) return False return True except requests.exceptions.RequestException as e: self.api.show_error( 'CExplore Bridge', 'Could not reach Ghidra server. Did you start the script?') return False def slot_extract_data(self, text: str) -> None: if self.symbols is None: # First need to load symbols self.symbols = get_symbol_database().get_symbols(RomVariant.CUSTOM) if self.symbols is None: self.server_worker.slot_extracted_data({ 'status': 'error', 'text': 'No symbols for rom CUSTOM loaded' }) return if self.data_extractor_plugin is None: self.data_extractor_plugin = get_plugin('data_extractor', 'DataExtractorPlugin') if self.data_extractor_plugin is None: self.server_worker.slot_extracted_data({ 'status': 'error', 'text': 'Data Extractor plugin not loaded' }) return if self.rom is None: self.rom = get_rom(RomVariant.CUSTOM) if self.rom is None: self.server_worker.slot_extracted_data({ 'status': 'error', 'text': 'CUSTOM rom could not be loaded' }) return try: result = self.data_extractor_plugin.instance.extract_data( text, self.symbols, self.rom) if result is not None: self.server_worker.slot_extracted_data({ 'status': 'ok', 'text': result }) except Exception as e: traceback.print_exc() self.server_worker.slot_extracted_data({ 'status': 'error', 'text': str(e) })
class main_controller(Ui_MainWindow, QMainWindow): def __init__(self, DEBUG): super(main_controller, self).__init__() self.ui = Ui_MainWindow() self.ui.setupUi(self) self.debugging = False self.debug_init(DEBUG) self.ui.actionAbout_Qt.triggered.connect(lambda: self.open_about_qt()) self.ui.actionOpen.triggered.connect(lambda: self.open_file()) self.ui.actionClose.triggered.connect( lambda: self.prepare_destroy_tracker()) self.ui.playPauseButton.clicked.connect(lambda: self.play_pause()) self.ui.actionQuit.triggered.connect(lambda: self.close()) self.ui.actionProperty_Editor.toggled.connect(self.show_hide_pe) self.ui.actionAbout.triggered.connect(lambda: self.open_about()) #Connect playhead signals and slots self.ui.playHeadSlider.sliderMoved.connect( lambda: self.update_playHead()) self.ui.playHeadSlider.sliderReleased.connect( lambda: self.playHead_sliderUp()) self.ui.playHeadSlider.sliderPressed.connect( lambda: self.playHead_sliderDown()) self.playHeadChanged = False self.sliderIgnoreUpdates = False self.destroyOnLoad = False self.tempVideoFile = None self.tracker = None self.tracker_thread = None self.current_state = State.Nothing_Loaded self.pe = None def open_about_qt(self): QMessageBox.aboutQt(self, ui_strings.TITLE) def init_tracker(self, video_file): if self.current_state != State.Nothing_Loaded: self.prepare_destroy_tracker() self.destroyOnLoad = True self.tempVideoFile = video_file else: self.current_state = State.Initializing #Init tracker self.tracker = video_tracker(video_file, self.debugging) #Setup tracker thread self.tracker_thread = QThread(self) self.tracker.moveToThread(self.tracker_thread) #Setup Property Editor self.pe = pe_controller(self.ui.propertyEditor, self.tracker, self.debugging) #Connect signals and slots self.tracker.gen_start.connect(self.tracker.receive_signal) self.tracker.finished.connect(self.tracker_thread.quit) self.tracker.FrameSignal.connect(self.update_frames) self.tracker.stopped.connect(self.tracker_stopped) self.tracker.gen_history.connect(self.tracker.render_history) self.tracker_thread.start() self.current_state = State.Idle self.init_controls(self.tracker.video_data.num_frames) def open_file(self): video_file = str( QFileDialog.getOpenFileName(self, ui_strings.OPEN_VIDEO_FILE)[0]) if video_file != "": self.init_tracker(video_file) def convert_frame(self, frame): qImage = None if len(frame.shape) == 3: height, width, byteValue = frame.shape byteValue = byteValue * width qImage = QImage(frame, width, height, byteValue, QImage.Format_BGR888) else: #This is a mask height, width = frame.shape qImage = QImage(frame, width, height, QImage.Format_Grayscale8) return QPixmap(qImage) def update_frames(self, frame_num): if self.current_state == State.Preparing_History: self.pb.set_progress(self.pb.bar.value() + 1) else: self.debug_logger.print_debug_info(12, frame_num) self.ui.rawFrame.changePixmap( self.convert_frame(self.tracker.frame_cnt)) self.ui.maskFrame.changePixmap( self.convert_frame(self.tracker.mask)) if self.tracker.final is not None: self.ui.outputFrame.changePixmap( self.convert_frame(self.tracker.final)) if not self.sliderIgnoreUpdates: self.ui.playHeadSlider.setValue( self.tracker.video_data.current_frame) def prepare_destroy_tracker(self): if self.current_state == State.Running: self.debug_logger.print_debug_info(13) self.current_state = State.Destroying self.tracker.running = False else: self.current_state = State.Destroying self.destroy_tracker() def destroy_tracker(self): self.tracker.destroy() self.tracker = None self.tracker_thread = None self.debug_logger.print_debug_info(15) #Clear image labels self.ui.rawFrame.clearPixmap() self.ui.maskFrame.clearPixmap() self.ui.outputFrame.clearPixmap() self.current_state = State.Nothing_Loaded self.reset_controls() def tracker_stopped(self): if self.current_state == State.Destroying: self.destroy_tracker() self.init_tracker(self.tempVideoFile) self.destroyOnLoad = False self.tempVideoFile = None elif self.current_state == State.Running: self.current_state = State.Stopped self.update_play_button() elif self.current_state == State.Preparing_History: self.pb.hide() self.pb.destroy() self.pb = None self.current_state == State.Stopped self.ui.playHeadSlider.setEnabled(True) self.play_pause() elif self.current_state == State.Slider_Moved_While_Running: self.sliderIgnoreUpdates = False self.update_history() #region DEBUG_FUNCTIONS def debug_init(self, debug): self.debug_logger = logger(debug) if not debug: self.ui.menuDEBUG.setEnabled(False) self.ui.menuDEBUG.menuAction().setVisible(False) else: self.ui.actionRender_next_frame.triggered.connect( lambda: self.debug_render_next()) self.ui.actionPrint_current_state.triggered.connect( lambda: self.debug_print_cur_state()) self.ui.actionPrint_tracker_video_data.triggered.connect( lambda: self.debug_print_tracker_video_data()) self.ui.actionPrint_tracker_data.triggered.connect( lambda: self.debug_print_tracker_data()) self.ui.actionInit.triggered.connect(lambda: self.create_pb()) self.ui.actionDestroy.triggered.connect(lambda: self.destroy_pb()) self.ui.actionIncrement_5.triggered.connect( lambda: self.debug_pb_increment_5()) self.ui.actionIncrement_50.triggered.connect( lambda: self.debug_pb_increment_50()) self.ui.actionSet_Label_Message_1.triggered.connect( lambda: self.debug_pb_set_message_1()) self.ui.actionSet_Label_Message_2.triggered.connect( lambda: self.debug_pb_set_message_2()) self.ui.actionRemove_Label_Message.triggered.connect( lambda: self.pb.remove_message()) self.ui.actionPrint_real_current_frame.triggered.connect( lambda: self.debug_print_real_current_frame()) self.ui.actionShow_Object_Data_from_current_frame.triggered.connect( lambda: self.debug_show_obj_data_current_frame()) self.debug_logger.print_debug_info(0) self.debugging = True def debug_render_next(self): if not self.current_state.Running: self.debug_logger.print_debug_info(18) self.tracker.gen_start.emit(False) def debug_print_cur_state(self): self.debug_logger.print_debug_info(1, self.current_state) def debug_print_tracker_video_data(self): self.debug_logger.print_debug_info(2, self.tracker.video_data) def debug_print_tracker_data(self): self.debug_logger.print_debug_info(3, self.tracker.data) def destroy_pb(self): self.pb.hide() self.pb.destroy() self.pb = None def create_pb(self): self.pb = ProgressBarLabel(self) self.ui.verticalLayout_2.addWidget(self.pb) def debug_pb_increment_5(self): self.pb.set_progress(self.pb.bar.value() + 5) def debug_pb_increment_50(self): self.pb.set_progress(self.pb.bar.value() + 50) def debug_pb_set_message_1(self): self.pb.set_message("Hello World!") def debug_pb_set_message_2(self): self.pb.set_message("This is the second debug message") def debug_print_real_current_frame(self): self.debug_logger.print_debug_info( 23, self.tracker.get_real_current_frame()) def debug_show_obj_data_current_frame(self): objs_in_frame = self.tracker.video_data.get_object_points( self.tracker.video_data.current_frame - 1) total_objs = len(objs_in_frame) msg = QMessageBox() msg.setIcon(QMessageBox.Information) msg.setText("Total objects in frame: " + str(total_objs) + "\n\n" + "First three objects:\n" + "(" + str(objs_in_frame[0][0]) + ", " + str(objs_in_frame[0][1]) + ") Width: " + str(objs_in_frame[0][2]) + ", Height: " + str(objs_in_frame[0][3]) + "\n" + "(" + str(objs_in_frame[1][0]) + ", " + str(objs_in_frame[1][1]) + ") Width: " + str(objs_in_frame[1][2]) + ", Height: " + str(objs_in_frame[1][3]) + "\n" + "(" + str(objs_in_frame[2][0]) + ", " + str(objs_in_frame[2][1]) + ") Width: " + str(objs_in_frame[2][2]) + ", Height: " + str(objs_in_frame[2][3])) msg.setWindowTitle("Objects in current frame") msg.setStandardButtons(QMessageBox.Ok) msg.exec_() #endregion def reset_controls(self): self.ui.playHeadSlider.setValue(0) self.ui.playHeadSlider.setEnabled(False) self.update_play_button() self.ui.playPauseButton.setEnabled(False) self.pe.destroy() self.pe = None #Check if we have a progress bar try: self.pb.hide() self.pb.destroy() self.pb = None except: #Do nothing pass def closeEvent(self, event): if self.current_state is not State.Nothing_Loaded: self.current_state = State.Destroying self.destroy_tracker() def play_pause(self): if self.current_state == State.Running: self.ui.playPauseButton.setEnabled(False) self.ui.playPauseButton.setText(ui_strings.PAUSING) self.tracker.running = False else: self.current_state = State.Running self.update_play_button() if self.playHeadChanged: self.update_history() else: self.debug_logger.print_debug_info( 16, int(self.ui.playHeadSlider.value())) self.tracker.video_data.current_frame = int( self.ui.playHeadSlider.value()) self.debug_logger.print_debug_info(18) self.tracker.gen_start.emit(True) def update_play_button(self): if self.current_state == State.Running: self.ui.playPauseButton.setEnabled(True) self.ui.playPauseButton.setText(ui_strings.PAUSE) else: if self.current_state == State.Stopped: self.ui.playPauseButton.setEnabled(True) self.ui.playPauseButton.setText(ui_strings.PLAY) if self.current_state == State.Nothing_Loaded: self.ui.playPauseButton.setText(ui_strings.PLAY) def init_controls(self, video_length): self.ui.playHeadSlider.setMaximum(video_length) self.ui.playHeadSlider.setEnabled(True) self.ui.playPauseButton.setEnabled(True) def update_history(self): self.ui.playHeadSlider.setEnabled(False) self.ui.playPauseButton.setEnabled(False) self.playHeadChanged = False self.current_state = State.Preparing_History self.pb = ProgressBarLabel(self) self.ui.verticalLayout_2.addWidget(self.pb) self.pb.bar.setMaximum(self.tracker.data.history_frames) self.pb.set_message(ui_strings.PREPARE_HISTORY) self.tracker.gen_history.emit(self.ui.playHeadSlider.value()) def update_playHead(self): self.playHeadChanged = True def playHead_sliderUp(self): if self.current_state == State.Running: self.current_state = State.Slider_Moved_While_Running self.tracker.running = False else: self.sliderIgnoreUpdates = False def playHead_sliderDown(self): self.sliderIgnoreUpdates = True def show_hide_pe(self, checked): if checked: self.ui.propertyDockWidget.show() else: self.ui.propertyDockWidget.hide() def open_about(self): about = about_controller() about.exec_()
class TableClothGenerator(QMainWindow): def __init__(self, parent=None): super().__init__(parent) # Main UI settings self.setWindowTitle('Tablecloth Generator') self.setWindowIcon(QIcon('icon.ico')) self.centralWidget = QWidget() self.setCentralWidget(self.centralWidget) self.resize(350, 350) self.center() self._createMenuBar() self.MainUI() def MainUI(self): # Obtain the configs fp_config = open(THISDIR + "\\config\\config.json", "r", encoding="utf-8") self.config = json.loads(fp_config.read()) fp_config.close() # Obtain and List the teams fp_teams = open(THISDIR + "\\config\\teams.json", "r", encoding="utf-8") conf_teams = json.loads(fp_teams.read()) fp_teams.close() self.teams = conf_teams["teams"] self.players = conf_teams["players"] # Obtain all images needed to create the tablecloth self.background = Image.open(THISDIR + "\\images\\mat.png") self.table_border = Image.open(THISDIR + "\\images\\table_border.png") self.tech_lines = Image.open(THISDIR + "\\images\\technical_lines.png") # Check if there's no configuration set up # and prompt to create/import one if self.config["total_teams"] == 0: self.no_config = QMessageBox.question(self, "No configuration", "No configuration has been found. Do you wish to set up a new one?", QMessageBox.Yes | QMessageBox.No) if self.no_config == QMessageBox.Yes: self.CreateTeamsWindow() self.bg_image = self.config["image_route"] self.players_combobox = QComboBox() self.UpdatePlayersList() self.players_combobox.setEditable(True) self.players_combobox.completer()\ .setCompletionMode(QCompleter.PopupCompletion) self.players_combobox.setInsertPolicy(QComboBox.NoInsert) # Set up the GUI self.statusBar().showMessage("Remember: Rig responsibly.") # Bottom (EAST) self.label_east = QLabel(self) self.label_east.setText("<h1>East Seat</h1>") self.label_east.setAlignment(QtCore.Qt.AlignCenter) self.image_east = QLabel(self) self.image_east.setPixmap(QPixmap("images/logos/team1.png")\ .scaled(100,100)) self.image_east.setAlignment(QtCore.Qt.AlignCenter) self.search_east = QLineEdit() self.search_east.setAlignment(QtCore.Qt.AlignCenter) self.search_east.editingFinished.connect( lambda: self.searchPlayer(self.search_east.text(), self.cloth_east)) self.cloth_east = QComboBox() self.cloth_east.setModel(self.players_combobox.model()) self.cloth_east.currentIndexChanged.connect( lambda: self.SwitchImage(self.cloth_east, self.image_east)) # Right (SOUTH) self.label_south = QLabel(self) self.label_south.setText("<h1>South Seat</h1>") self.label_south.setAlignment(QtCore.Qt.AlignCenter) self.image_south = QLabel(self) self.image_south.setPixmap(QPixmap("images/logos/team1.png")\ .scaled(100,100)) self.image_south.setAlignment(QtCore.Qt.AlignCenter) self.image_south.show() self.search_south = QLineEdit() self.search_south.setAlignment(QtCore.Qt.AlignCenter) self.search_south.editingFinished.connect( lambda: self.searchPlayer(self.search_south.text(), self.cloth_south)) self.cloth_south = QComboBox() self.cloth_south.setModel(self.players_combobox.model()) self.cloth_south.currentIndexChanged.connect( lambda: self.SwitchImage(self.cloth_south, self.image_south)) # Top (WEST) self.label_west = QLabel(self) self.label_west.setText("<h1>West Seat</h1>") self.label_west.setAlignment(QtCore.Qt.AlignCenter) self.image_west = QLabel(self) self.image_west.setPixmap(QPixmap("images/logos/team1.png")\ .scaled(100,100)) self.image_west.setAlignment(QtCore.Qt.AlignCenter) self.image_west.show() self.cloth_west = QComboBox() self.search_west = QLineEdit() self.search_west.setAlignment(QtCore.Qt.AlignCenter) self.search_west.editingFinished.connect( lambda: self.searchPlayer(self.search_west.text(), self.cloth_west)) self.cloth_west.setModel(self.players_combobox.model()) self.cloth_west.currentIndexChanged.connect( lambda: self.SwitchImage(self.cloth_west, self.image_west)) # Left (NORTH) self.label_north = QLabel(self) self.label_north.setText("<h1>North Seat</h1>") self.label_north.setAlignment(QtCore.Qt.AlignCenter) self.image_north = QLabel(self) self.image_north.setPixmap(QPixmap("images/logos/team1.png")\ .scaled(100,100)) self.image_north.setAlignment(QtCore.Qt.AlignCenter) self.image_north.show() self.cloth_north = QComboBox() self.search_north = QLineEdit() self.search_north.setAlignment(QtCore.Qt.AlignCenter) self.search_north.editingFinished.connect( lambda: self.searchPlayer(self.search_north.text(), self.cloth_north)) self.cloth_north.setModel(self.players_combobox.model()) self.cloth_north.currentIndexChanged.connect( lambda: self.SwitchImage(self.cloth_north, self.image_north)) # Technical lines self.technical_lines = QCheckBox("Show Technical lines", self) # Generate button self.generate = QPushButton(self) self.generate.setText("Generate Tablecloth") self.generate.clicked.connect(self.GeneratePreview) # Add custom mat self.custom_mat = QPushButton(self) self.custom_mat.setText("Add Mat") self.custom_mat.clicked.connect(self.MatDialog) # Create the layout grid_layout = QGridLayout() grid_layout.setAlignment(QtCore.Qt.AlignCenter) grid_layout.setAlignment(QtCore.Qt.AlignTop) # Labels East, West grid_layout.addWidget(self.label_east, 1, 1) grid_layout.addWidget(self.label_west, 1, 2) # Image preview East, West grid_layout.addWidget(self.image_east, 2, 1) grid_layout.addWidget(self.image_west, 2, 2) # Search player East, West grid_layout.addWidget(self.search_east, 3, 1) grid_layout.addWidget(self.search_west, 3, 2) # Player combobox East, West grid_layout.addWidget(self.cloth_east, 4, 1) grid_layout.addWidget(self.cloth_west, 4, 2) # Labes South, North grid_layout.addWidget(self.label_south, 5, 1) grid_layout.addWidget(self.label_north, 5, 2) # Image preview South, North grid_layout.addWidget(self.image_south, 6, 1) grid_layout.addWidget(self.image_north, 6, 2) # Search player South, North grid_layout.addWidget(self.search_south, 7, 1) grid_layout.addWidget(self.search_north, 7, 2) # Player combobox South, North grid_layout.addWidget(self.cloth_south, 8, 1) grid_layout.addWidget(self.cloth_north, 8, 2) # Technical lines grid_layout.addWidget(self.technical_lines, 9, 1) # Custom mat/bg grid_layout.addWidget(self.custom_mat, 10, 1) # Generate grid_layout.addWidget(self.generate, 10, 2) self.centralWidget.setLayout(grid_layout) # Create the window self.show() def _createMenuBar(self): # Settings and stuff for the toolbar menubar = QMenuBar(self) file_menu = QMenu("&File", self) file_menu.addAction("Create Team(s)", self.CreateTeamsWindow) file_menu.addAction("Edit Team(s)", self.EditTeamsWindow) file_menu.addAction("Exit", self.close) settings_menu = QMenu("&Settings", self) settings_menu.addAction("Version", self.SeeVersion) settings_menu.addAction("Help", self.GetHelp) menubar.addMenu(file_menu) menubar.addMenu(settings_menu) self.setMenuBar(menubar) def _createProgressBar(self): self.progress_bar = QProgressBar() self.progress_bar.minimum = 0 self.progress_bar.maximum = 100 self.progress_bar.setValue(0) self.progress_bar.setTextVisible(False) self.progress_bar.setGeometry(50, 50, 10, 10) self.progress_bar.setAlignment(QtCore.Qt.AlignRight) self.progress_bar.adjustSize() self.statusBar().addPermanentWidget(self.progress_bar) self.ChangeAppStatus(False) def SwitchImage(self, cloth, image): # It shows you the team logo. No way you can miss those, right? team_id = self.SearchTeamID(cloth, True) image.setPixmap(QPixmap( "images/logos/team%d.png" % team_id).scaled(100,100)) def searchPlayer(self, text, combobox): # It even searches the player for you. What more could you want? search_index = combobox.findText(text, QtCore.Qt.MatchContains) if search_index == -1: QMessageBox.warning(self, "Error", "No player found") else: combobox.setCurrentIndex(search_index) def CreateTeamsWindow(self): self.teamcreation_wid = EditionWidget() self.teamcreation_wid.resize(400, 200) self.teamcreation_wid.setWindowTitle("Teams configuration") self.new_config = {} id_label = QLabel(self) id_label.setText("Team ID: ") self.num_id = QLabel(self) current_id = str(self.config["total_teams"] + 1) self.num_id.setText(current_id) name_label = QLabel(self) name_label.setText("Team Name:") name_label.setFocus() self.name_input = QLineEdit(self) members_label = QLabel(self) members_label.setText("Members (write and press enter):") members_input = QLineEdit(self) members_input.editingFinished.connect( lambda: self.AddMember(members_input)) self.members_list = QListWidget(self) import_image = QPushButton(self) import_image.setText("Import Team Image") import_image.clicked.connect(self.ImportTeamImage) add_team = QPushButton(self) add_team.setText("Add Team") add_team.clicked.connect( lambda: self.addTeamFunction(self.name_input.text(), self.members_list)) import_config = QPushButton(self) import_config.setText("Import configuration") import_config.clicked.connect(self.importTeamFunction) config_lay = QGridLayout() config_lay.addWidget(id_label, 1, 0) config_lay.addWidget(self.num_id, 1, 1) config_lay.addWidget(name_label, 2, 0) config_lay.addWidget(self.name_input, 2, 1) config_lay.addWidget(members_label, 3, 0) config_lay.addWidget(members_input, 3, 1) config_lay.addWidget(self.members_list, 4, 0, 2, 2) config_lay.addWidget(add_team, 6, 0) config_lay.addWidget(import_image, 6, 1) config_lay.addWidget(import_config, 7, 0, 1, 2) self.teamcreation_wid.setLayout(config_lay) self.teamcreation_wid.setWindowModality(QtCore.Qt.ApplicationModal) self.teamcreation_wid.activateWindow() self.teamcreation_wid.raise_() self.teamcreation_wid.show() def addTeamFunction(self, name, members): fp_teams = open(THISDIR + "\\config\\teams.json", "r", encoding="utf-8") current_teams = json.loads(fp_teams.read()) fp_teams.close() team = {} current_teams["teams"].append(name) current_teams["players"][name] = [str(self.members_list.item(i).text())\ for i in range(self.members_list.count())] new_team = open(THISDIR + "\\config\\teams.json", "w+", encoding="utf-8") add_config = open(THISDIR + "\\config\\config.json", "w+", encoding="utf-8") self.teams = current_teams["teams"] self.players = current_teams["players"] self.config["total_teams"] += 1 new_id = self.config["total_teams"] + 1 self.num_id.setText(str(new_id)) add_config.write(json.dumps(self.config, indent=4)) new_team.write(json.dumps(current_teams, indent=4)) new_team.close() self.name_input.clear() self.members_list.clear() self.UpdatePlayersList() def ImportTeamImage(self): image_dialog = QFileDialog(self) image_dialog = QFileDialog.getOpenFileName(filter="Images (*.png)", selectedFilter="Images (*.png)") if image_dialog[0] != "": new_team_logo = Image.open(image_dialog[0]).convert("RGBA") if new_team_logo.size != (250, 250): new_team_logo.resize((250, 250)) new_team_logo.save(THISDIR+"\\images\\logos\\team%s.png"\ % self.num_id.text()) QMessageBox.information(self, "Team Image", "Team image added.") def importTeamFunction(self): file_dialog = QFileDialog(self) file_dialog = QFileDialog.getOpenFileName( filter="Team Files (*.json *.zip)", selectedFilter="Team Files (*.json *.zip)") if file_dialog[0] != "": if is_zipfile(file_dialog[0]): with ZipFile(file_dialog[0]) as zip_import: list_of_files = zip_import.namelist() for fimp in list_of_files: if fimp.startswith('logos'): zip_import.extract(fimp, path=THISDIR+'\\images\\') imported_teams = zip_import.read('teams.json') imported_teams = imported_teams.decode('utf-8') else: imported_teams = open(file_dialog[0], "r", encoding="utf-8").read() json_teams = json.loads(imported_teams) self.teams = json_teams["teams"] self.players = json_teams["players"] new_teams = open(THISDIR + "\\config\\teams.json", "w+", encoding="utf-8") new_teams.write(json.dumps(json_teams, indent=4)) new_teams.close() old_config = open(THISDIR + "\\config\\config.json", "r", encoding="utf-8").read() old_config = json.loads(old_config) old_config["total_teams"] = len(json_teams["teams"]) self.config = old_config new_config = open(THISDIR + "\\config\\config.json", "w+", encoding="utf-8") new_config.write(json.dumps(self.config, indent=4)) new_config.close() self.UpdatePlayersList() self.image_east.setPixmap(QPixmap("images/logos/team1.png")\ .scaled(100,100)) self.cloth_east.setModel(self.players_combobox.model()) self.image_south.setPixmap(QPixmap("images/logos/team1.png")\ .scaled(100,100)) self.cloth_south.setModel(self.players_combobox.model()) self.image_west.setPixmap(QPixmap("images/logos/team1.png")\ .scaled(100,100)) self.cloth_west.setModel(self.players_combobox.model()) self.image_north.setPixmap(QPixmap("images/logos/team1.png")\ .scaled(100,100)) self.cloth_north.setModel(self.players_combobox.model()) self.statusBar().showMessage("Teams imported successfully.") self.teamcreation_wid.close() def AddMember(self, member): self.members_list.addItem(member.text()) member.clear() def EditTeamsWindow(self): self.teamedit_wid = EditionWidget() self.teamedit_wid.resize(400, 320) self.teamedit_wid.setWindowTitle("Edit Teams") self.teams_list = QComboBox(self) self.teams_list.addItem("--- Select a team ---") for team in self.teams: self.teams_list.addItem(team) self.teams_list.currentIndexChanged.connect(self.UpdateTeamInfo) team_id_label = QLabel(self) team_id_label.setText("Team ID: ") self.config_team_id = QLabel(self) team_name_label = QLabel(self) team_name_label.setText("Team name: ") self.config_team_name = QLabel(self) team_members_label = QLabel(self) team_members_label.setText("Team members: ") self.config_team_members = QListWidget(self) add_member_label = QLabel(self) add_member_label.setText("Add new member: ") add_member_input = QLineEdit(self) add_member_input.editingFinished.connect(self.AddNewMember) delete_member = QPushButton(self) delete_member.setText("Delete member") delete_member.clicked.connect(self.DeleteMember) delete_team = QPushButton(self) delete_team.setText("Delete Team") delete_team.clicked.connect(self.DeleteTeam) save_changes = QPushButton(self) save_changes.setText("Save changes") save_changes.clicked.connect(self.SaveEdits) export_config = QPushButton(self) export_config.setText("Export Configuration") export_config.clicked.connect(self.ExportTeams) config_lay = QGridLayout() config_lay.addWidget(self.teams_list, 1, 0) config_lay.addWidget(team_id_label, 2, 0) config_lay.addWidget(self.config_team_id, 2, 1) config_lay.addWidget(team_name_label, 3, 0) config_lay.addWidget(self.config_team_name, 3, 1, 1, 2) config_lay.addWidget(team_members_label, 4, 0) config_lay.addWidget(self.config_team_members, 5, 0) config_lay.addWidget(add_member_label, 6, 0) config_lay.addWidget(add_member_input, 6, 1, 1, 2) config_lay.addWidget(delete_member, 7, 0) config_lay.addWidget(delete_team, 7, 1) config_lay.addWidget(save_changes, 8, 0) config_lay.addWidget(export_config, 8, 1) self.teamedit_wid.setLayout(config_lay) self.teamedit_wid.setWindowModality(QtCore.Qt.ApplicationModal) self.teamedit_wid.activateWindow() self.teamedit_wid.raise_() self.teamedit_wid.show() def UpdateTeamInfo(self): sender = self.sender() if sender.currentIndex() > 0: team_id = sender.currentIndex() self.config_team_id.setText(str(team_id)) self.config_team_name.setText(sender.currentText()) if self.config_team_members.count() > 0: self.config_team_members.clear() self.config_team_members.addItems( self.players[sender.currentText()]) def AddNewMember(self): sender = self.sender() self.config_team_members.addItem(sender.text()) sender.clear() def DeleteMember(self): list_members = self.config_team_members.selectedItems() if len(list_members) == 0: QMessageBox.warning(self, "Error", "No player selected") else: for member in list_members: self.config_team_members.takeItem( self.config_team_members.row(member)) def DeleteTeam(self): team_id = int(self.config_team_id.text()) is_last_item = self.teams[self.teams.index( self.config_team_name.text())] == (self.teams[len(self.teams)-1]) self.teams.pop(self.teams.index(self.config_team_name.text())) self.players.pop(self.config_team_name.text()) new_teamlist = {} new_teamlist["teams"] = self.teams new_teamlist["players"] = self.players current_teams = open(THISDIR + "\\config\\teams.json", "w+", encoding="utf-8") current_teams.write(json.dumps(new_teamlist, indent=4)) current_teams.close() if is_last_item == True: self.teams_list.setCurrentIndex(1) else: self.teams_list.setCurrentIndex(team_id+1) self.teams_list.removeItem(team_id) self.UpdatePlayersList() self.cloth_east.setModel(self.players_combobox.model()) self.cloth_south.setModel(self.players_combobox.model()) self.cloth_west.setModel(self.players_combobox.model()) self.cloth_north.setModel(self.players_combobox.model()) def ExportTeams(self): export_dir = self.config["save_route"] if self.config["save_route"] \ is not None else THISDIR exported_file = QFileDialog.getSaveFileName(self, "Save File", export_dir, "Save files (*.zip)") if exported_file[0] != "": export_filename = exported_file[0] if export_filename.endswith(".zip") is False: export_filename += ".zip" files_to_export = [] files_to_export.append("config\\teams.json") for root, directories, files in os.walk(THISDIR+"\\images\\logos"): for filename in files: filepath = os.path.join(root, filename) files_to_export.append(filepath) with ZipFile(export_filename, "w") as export_zip: for exp_file in files_to_export: export_name = exp_file if exp_file.endswith(".json"): split_name = exp_file.split("\\") export_name = split_name[-1] if exp_file.endswith(".png"): split_name = exp_file.split("\\") export_name = "\\logos\\" + split_name[-1] export_zip.write(exp_file, arcname=export_name) export_zip.close() if os.path.exists(export_filename): QMessageBox.information(self, "Export", "The export was successful") def SaveEdits(self): list_members = [str(self.config_team_members.item(i).text()) for i in \ range(self.config_team_members.count())] self.players[self.config_team_name.text()] = list_members new_teamlist = {} new_teamlist["teams"] = self.teams new_teamlist["players"] = self.players current_teams = open(THISDIR + "\\config\\teams.json", "w+", encoding="utf-8") current_teams.write(json.dumps(new_teamlist, indent=4)) current_teams.close() self.teamedit_wid.close() self.statusBar().showMessage("Settings saved.") def MatDialog(self): mat_dialog = QFileDialog(self) mat_dialog = QFileDialog.getOpenFileName(filter="Images (*.png *.jpg)", selectedFilter="Images (*.png *.jpg)") if mat_dialog[0] != "": self.GenerateMat(mat_dialog[0]) def GenerateMat(self, image): self.background = image background = Image.open(self.background).resize((2048,2048))\ .convert("RGBA") self.mat_thread = QThread() east_id = self.SearchTeamID(self.cloth_east, True) south_id = self.SearchTeamID(self.cloth_south, True) west_id = self.SearchTeamID(self.cloth_west, True) north_id = self.SearchTeamID(self.cloth_north, True) if self.config["save_route"] is None: save_to_route = THISDIR else: save_to_route = self.config["save_route"] self._createProgressBar() self.mat_worker = GenerateImageThread(background, self.table_border, east_id, south_id, west_id, north_id, self.technical_lines.isChecked(), save_to_route, self.bg_image, True) self.mat_worker.moveToThread(self.mat_thread) self.mat_thread.started.connect(self.mat_worker.run) self.mat_worker.update_progress.connect(self.UpdateStatus) self.mat_worker.finished.connect(self.mat_thread.quit) self.mat_worker.finished.connect(self.mat_worker.deleteLater) self.mat_thread.finished.connect(self.mat_thread.deleteLater) self.mat_thread.finished.connect(self.MatPreviewWindow) self.mat_thread.start() def MatPreviewWindow(self): self.statusBar().showMessage('Mat preview generated.') self.statusBar().removeWidget(self.progress_bar) # Now you can go back to rigging self.ChangeAppStatus(True) self.mat_wid = QWidget() self.mat_wid.resize(600, 600) self.mat_wid.setWindowTitle("Background preview") mat_preview_title = QLabel(self) mat_preview_title.setText("Selected image (1/4 scale)") mat_preview = QLabel(self) mat_preview.setPixmap(QPixmap(tempfile.gettempdir()+"\\Table_Dif.jpg")\ .scaled(512,512)) confirm = QPushButton(self) confirm.setText("Confirm") confirm.clicked.connect( lambda: self.ChangeMatImage(self.background)) vbox = QVBoxLayout() vbox.setAlignment(QtCore.Qt.AlignCenter) vbox.addWidget(mat_preview_title) vbox.addWidget(mat_preview) vbox.addWidget(confirm) self.mat_wid.setLayout(vbox) self.mat_wid.setWindowModality(QtCore.Qt.ApplicationModal) self.mat_wid.activateWindow() self.mat_wid.raise_() self.mat_wid.show() def ChangeMatImage(self, image): new_bg = Image.open(image) if new_bg.size != (2048, 2048): new_bg = new_bg.resize((2048, 2048)) if new_bg.mode != "RGBA": new_bg = new_bg.convert("RGBA") if self.config["save_route"] is not None: new_bg.save(self.config["save_route"]+"\\images\\mat.png") self.bg_image = self.config["save_route"]+"\\images\\mat.png" else: new_bg.save(THISDIR+"\\images\\mat.png") self.bg_image = THISDIR+"\\images\\mat.png" self.background = new_bg self.config["image_route"] = self.bg_image new_file = open(THISDIR + "\\config\\config.json", "w+", encoding="utf-8") new_file.write(json.dumps(self.config, indent=4)) new_file.close() self.statusBar().showMessage('New background added.') self.statusBar().removeWidget(self.progress_bar) self.ChangeAppStatus(True) self.mat_wid.close() def GeneratePreview(self): self.preview_thread = QThread() east_id = self.SearchTeamID(self.cloth_east, True) south_id = self.SearchTeamID(self.cloth_south, True) west_id = self.SearchTeamID(self.cloth_west, True) north_id = self.SearchTeamID(self.cloth_north, True) if self.config["save_route"] is None: save_to_route = THISDIR else: save_to_route = self.config["save_route"] self._createProgressBar() self.preview_worker = GenerateImageThread(self.background, self.table_border, east_id, south_id, west_id, north_id, self.technical_lines.isChecked(), save_to_route, self.bg_image, True) self.preview_worker.moveToThread(self.preview_thread) self.preview_thread.started.connect(self.preview_worker.run) self.preview_worker.update_progress.connect(self.UpdateStatus) self.preview_worker.finished.connect(self.preview_thread.quit) self.preview_worker.finished.connect(self.preview_worker.deleteLater) self.preview_thread.finished.connect(self.preview_thread.deleteLater) self.preview_thread.finished.connect(self.PreviewWindow) self.preview_thread.start() def PreviewWindow(self): self.statusBar().showMessage('Tablecloth preview generated.') self.statusBar().removeWidget(self.progress_bar) # Now you can go back to rigging self.ChangeAppStatus(True) self.preview_wid = QWidget() self.preview_wid.resize(600, 600) self.preview_wid.setWindowTitle("Tablecloth preview") tablecloth = QPixmap(tempfile.gettempdir()+"\\Table_Dif.jpg") tablecloth_preview_title = QLabel(self) tablecloth_preview_title.setText("Tablecloth preview (1/4 scale)") tablecloth_preview = QLabel(self) tablecloth_preview.setPixmap(tablecloth.scaled(512,512)) confirm = QPushButton(self) confirm.setText("Confirm") confirm.clicked.connect(self.GenerateImage) confirm.clicked.connect(self.preview_wid.close) vbox = QVBoxLayout() vbox.setAlignment(QtCore.Qt.AlignCenter) vbox.addWidget(tablecloth_preview_title) vbox.addWidget(tablecloth_preview) vbox.addWidget(confirm) self.preview_wid.setLayout(vbox) self.preview_wid.setWindowModality(QtCore.Qt.ApplicationModal) self.preview_wid.activateWindow() self.preview_wid.raise_() self.preview_wid.show() def GeneratedDialog(self): self.statusBar().showMessage('Tablecloth generated. Happy rigging!') self.statusBar().removeWidget(self.progress_bar) # Now you can go back to rigging self.ChangeAppStatus(True) mbox = QMessageBox() mbox.setWindowTitle("Tablecloth Generator") mbox.setText("Tablecloth Generated!") mbox.setStandardButtons(QMessageBox.Ok) mbox.exec() def UpdateStatus(self, status): self.progress_bar.setValue(status) def GenerateImage(self): self.statusBar().showMessage('Generating image...') self._createProgressBar() if self.config["save_route"] is None: self.config["save_route"] = THISDIR save_to_route = QFileDialog.getExistingDirectory(self, "Where to save the image", self.config["save_route"], QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks) if self.config["save_route"] != save_to_route: temp_file = open(THISDIR + "\\config\\config.json", "r", encoding="utf-8") fp_teams = json.loads(temp_file.read()) fp_teams["save_route"] = save_to_route fp_teams["image_route"] = self.bg_image new_file = open(THISDIR + "\\config\\config.json", "w+", encoding="utf-8") new_file.write(json.dumps(fp_teams, indent=4)) new_file.close() self.background = Image.open(THISDIR + "\\images\\mat.png") self.table_border = Image.open(THISDIR + "\\images\\table_border.png") self.tech_lines = Image.open(THISDIR + "\\images\\technical_lines.png") self.thread = QThread() east_id = self.SearchTeamID(self.cloth_east, True) south_id = self.SearchTeamID(self.cloth_south, True) west_id = self.SearchTeamID(self.cloth_west, True) north_id = self.SearchTeamID(self.cloth_north, True) self.worker = GenerateImageThread(self.background, self.table_border, east_id, south_id, west_id, north_id, self.technical_lines.isChecked(), save_to_route, self.bg_image) self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run) self.worker.update_progress.connect(self.UpdateStatus) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) self.thread.finished.connect(self.GeneratedDialog) self.thread.start() def ChangeAppStatus(self, status): # True for enable, False for disable. self.cloth_east.setEnabled(status) self.search_east.setEnabled(status) self.cloth_south.setEnabled(status) self.search_south.setEnabled(status) self.cloth_west.setEnabled(status) self.search_west.setEnabled(status) self.cloth_north.setEnabled(status) self.search_north.setEnabled(status) self.generate.setEnabled(status) def SearchTeamID(self, cloth, plus_one=False): team_id = self.teams.index(cloth.itemData(cloth.currentIndex())) if plus_one: team_id += 1 return team_id def UpdatePlayersList(self): for team, members in self.players.items(): for member in members: self.players_combobox.addItem(member, team) def center(self): qr = self.frameGeometry() cp = QScreen().availableGeometry().center() qr.moveCenter(cp) def SeeVersion(self): git_url = "https://raw.githubusercontent.com/vg-mjg/tablecloth-" git_url += "generator/main/version.txt" with urllib.request.urlopen(git_url) as response: url_version = response.read().decode("utf-8") version = "Your version is up to date!" if url_version != VERSION: version = "Your version is outdated." version += "Please check the <a href='https://github.com/vg-mjg/" version += "tablecloth-generator/releases'>Github page</a>" version +=" for updates." version_message = QMessageBox(self) version_message.setWindowTitle("Checking version") version_message.setText("""<h1>Tablecloth generator</h1> <br> <b>Current Version:</b> %s<br> <b>Your Version:</b> %s<br> <i>%s</i> """ % (url_version, VERSION, version)) version_message.exec() def GetHelp(self): webbrowser.open("https://github.com/vg-mjg/tablecloth-generator/wiki")
class Controller: def __init__(self): # gui self.app = QApplication([]) self.main_window = MainWindow(controller=self) # device self.device = Device() # fps stats self.fps_timer = QTimer() self.fps_timer.timeout.connect(self.update_ui_fps) self.spf = 1 # seconds per frame self.timestamp_last_capture = 0 # acquisition thread self.continuous_acquisition = False self.worker_wait_condition = QWaitCondition() self.acquisition_worker = AcquisitionWorker(self.worker_wait_condition, device=self.device) self.acquisition_thread = QThread() self.acquisition_worker.moveToThread(self.acquisition_thread) self.acquisition_thread.started.connect(self.acquisition_worker.run) self.acquisition_worker.finished.connect(self.acquisition_thread.quit) # self.acquisition_worker.finished.connect(self.acquisition_thread.deleteLater) # self.acquisition_thread.finished.connect(self.acquisition_worker.deleteLater) self.acquisition_worker.data_ready.connect(self.data_ready_callback) self.acquisition_thread.start() # default timebase self.set_timebase("20 ms") # on app exit self.app.aboutToQuit.connect(self.on_app_exit) def run_app(self): self.main_window.show() return self.app.exec_() def get_ports_names(self): return [p.device for p in serial.tools.list_ports.comports()] def update_ui_fps(self): fps = 1 / self.spf self.main_window.control_panel.stats_panel.fps_label.setText( f"{fps:.2f} fps") def set_timebase(self, timebase: str): # send timebase to device self.device.timebase = timebase if self.is_device_connected(): self.device.write_timebase() # adjust timebase in the screen seconds_per_sample = (float(timebase.split()[0]) / 10 * { "ms": 1e-3, "us": 1e-6 }[timebase.split()[1]]) self.data_time_array = (np.arange(0, self.device.BUFFER_SIZE) * seconds_per_sample) self.main_window.screen.setXRange(0, self.device.BUFFER_SIZE * seconds_per_sample, padding=0.02) self.main_window.screen.setYRange(0, 5) def set_trigger_state(self, on): self.device.trigger_on = on if self.is_device_connected(): self.device.write_trigger_state() def set_trigger_slope(self, slope): self.device.trigger_slope = slope if self.is_device_connected(): self.device.write_trigger_slope() def connect_to_device(self, port): if port == "": QMessageBox.about( self.main_window, "Connection failed", "Could not connect to device. No port is selected.", ) elif port not in self.get_ports_names(): QMessageBox.about( self.main_window, "Connection failed", f"Could not connect to device. Port {port} not available. Refresh and try again.", ) else: self.device.connect(port) def disconnect_device(self): self.device.disconnect() def is_device_connected(self): return self.device.is_connected() def show_no_connection_message(self): QMessageBox.about( self.main_window, "Device not connected", "No device is connected. Connect a device first.", ) def oscilloscope_single_run(self): if self.device.is_connected(): self.continuous_acquisition = False self.device.clean_buffers() self.worker_wait_condition.notify_one() return True else: self.show_no_connection_message() return False def oscilloscope_continuous_run(self): if self.device.is_connected(): self.timestamp_last_capture = time.time() self.spf = 1 self.fps_timer.start(500) self.continuous_acquisition = True self.device.clean_buffers() self.worker_wait_condition.notify_one() return True else: self.show_no_connection_message() return False def oscilloscope_stop(self): self.continuous_acquisition = False self.fps_timer.stop() def data_ready_callback(self): curr_time = time.time() self.spf = 0.9 * (curr_time - self.timestamp_last_capture) + 0.1 * self.spf self.timestamp_last_capture = curr_time self.main_window.screen.update_ch(self.data_time_array, self.acquisition_worker.data) if self.continuous_acquisition == True: self.worker_wait_condition.notify_one() def on_app_exit(self): print("exiting")
class GameDisplay(QWidget): default_font = 'Sans Serif,9,-1,5,50,0,0,0,0,0' rules_path: typing.Optional[str] = None move_needed = Signal(int, np.ndarray) # active_player, board move_made = Signal(np.ndarray) # board game_ended = Signal(np.ndarray) # final_board def __init__(self, start_state: GameState): super().__init__() self.start_state = start_state self.mcts_workers: typing.Dict[int, MctsWorker] = {} self.worker_thread: typing.Optional[QThread] = None self.current_state = self.start_state self.valid_moves = self.start_state.get_valid_moves() self._show_coordinates = False self.log_display = LogDisplay() self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.is_reviewing = False @property def show_coordinates(self): return self._show_coordinates @show_coordinates.setter def show_coordinates(self, value): self._show_coordinates = value scene = self.scene() size = QSize(scene.width(), scene.height()) self.resizeEvent(QResizeEvent(size, size)) @property def mcts_players(self): return [worker.player for worker in self.mcts_workers.values()] @mcts_players.setter def mcts_players(self, players: typing.Sequence[MctsPlayer]): self.stop_workers() self.log_display = LogDisplay() self.mcts_workers = {player.player_number: MctsWorker(player) for player in players} if not self.mcts_workers: self.worker_thread = None else: self.worker_thread = QThread() for worker in self.mcts_workers.values(): worker.move_chosen.connect(self.make_move) # type: ignore worker.move_analysed.connect(self.analyse_move) # type: ignore # noinspection PyUnresolvedReferences self.move_needed.connect(worker.choose_move) # type: ignore # noinspection PyUnresolvedReferences self.move_made.connect(worker.analyse_move) # type: ignore worker.moveToThread(self.worker_thread) self.worker_thread.start() def get_player(self, player_number: int) -> typing.Optional[MctsPlayer]: worker = self.mcts_workers.get(player_number) if worker: return worker.player return None @abstractmethod def update_board(self, board: GameState): """ Update self.scene, based on the state in board. It's probably also helpful to override resizeEvent(). :param board: the state of the game to display. """ def resizeEvent(self, event: QResizeEvent): self.update_board(self.current_state) @property def credit_pairs(self) -> typing.Iterable[typing.Tuple[str, str]]: """ Return a list of label and detail pairs. These are displayed in the about box. """ return () def choose_active_text(self): active_player = self.current_state.get_active_player() if active_player in self.mcts_workers: return 'thinking' return 'to move' @Slot(int) # type: ignore def make_move(self, move: int): self.log_display.record_move(self.current_state, move) # noinspection PyUnresolvedReferences self.move_made.emit(self.current_state) # type: ignore self.current_state = self.current_state.make_move(move) self.update_board(self.current_state) if self.current_state.is_ended(): # noinspection PyUnresolvedReferences self.game_ended.emit(self.current_state) # type: ignore forced_move = self.get_forced_move() if forced_move is None: self.request_move() else: self.make_move(forced_move) def get_forced_move(self) -> typing.Optional[int]: """ Override this method if some moves should be forced. Look at self.valid_moves and self.current_board to decide. :return: move number, or None if there is no forced move. """ return None @Slot(GameState, int, list) # type: ignore def analyse_move( self, board: GameState, analysing_player: int, move_probabilities: typing.List[typing.Tuple[str, float, int, float]]): self.log_display.analyse_move(board, analysing_player, move_probabilities) def request_move(self): if self.current_state.is_ended(): return player = self.current_state.get_active_player() # noinspection PyUnresolvedReferences self.move_needed.emit(player, self.current_state) def close(self): self.stop_workers() def stop_workers(self): if self.worker_thread is not None: self.worker_thread.quit() def can_move(self): if self.is_reviewing: return False return not self.current_state.get_active_player() in self.mcts_workers
class BridgeDock(QDockWidget): def __init__(self, parent, api: PluginApi) -> None: super().__init__('', parent) self.api = api self.ui = Ui_BridgeDock() self.ui.setupUi(self) self.server_thread = None self.observer = None self.modified_timer = None self.slot_server_running(False) self.ui.pushButtonStartServer.clicked.connect(self.slot_start_server) self.ui.pushButtonStopServer.clicked.connect(self.slot_stop_server) self.ui.labelConnectionStatus.setText('Server not yet running.') self.visibilityChanged.connect(self.slot_visibility_changed) def slot_visibility_changed(self, visible: bool) -> None: if not visible and self.server_thread is not None: self.slot_stop_server() def slot_server_running(self, running: bool) -> None: if running: self.ui.pushButtonStartServer.setVisible(False) self.ui.pushButtonStopServer.setVisible(True) else: self.ui.pushButtonStartServer.setVisible(True) self.ui.pushButtonStopServer.setVisible(False) def slot_start_server(self) -> None: self.server_thread = QThread() self.server_worker = ServerWorker() self.server_worker.signal_connected.connect(self.slot_connected) self.server_worker.signal_error.connect(self.slot_error) self.server_worker.signal_started.connect(self.slot_server_started) self.server_worker.signal_shutdown.connect(self.slot_server_stopped) self.server_worker.signal_script_addr.connect(self.slot_script_addr) self.server_worker.moveToThread(self.server_thread) self.server_thread.started.connect(self.server_worker.process) self.server_thread.start() self.slot_server_running(True) def slot_stop_server(self) -> None: # Shutdown needs to be triggered by the server thread, so send a request requests.get('http://*****:*****@') or stripped.endswith(':'): output += f'{line}\n' continue if '.ifdef' in stripped: if not ifdef_stack[-1]: ifdef_stack.append(False) output += f'{line}\n' continue # TODO check variant is_usa = stripped.split(' ')[1] == 'USA' ifdef_stack.append(is_usa) output += f'{line}\n' continue if '.ifndef' in stripped: if not ifdef_stack[-1]: ifdef_stack.append(False) output += f'{line}\n' continue is_usa = stripped.split(' ')[1] == 'USA' ifdef_stack.append(not is_usa) output += f'{line}\n' continue if '.else' in stripped: if ifdef_stack[-2]: # If the outermost ifdef is not true, this else does not change the validiness of this ifdef ifdef_stack[-1] = not ifdef_stack[-1] output += f'{line}\n' continue if '.endif' in stripped: ifdef_stack.pop() output += f'{line}\n' continue if not ifdef_stack[-1]: # Not defined for this variant output += f'{line}\n' continue if current_instruction >= len(instructions): # TODO maybe even not print additional lines? output += f'{line}\n' continue addr = instructions[current_instruction].addr prefix = '' if addr == script_offset: prefix = '>' output += f'{addr:03d}| {prefix}{line}\t\n' current_instruction += 1 if stripped.startswith('SCRIPT_END'): break self.ui.labelCode.setText(output)
class ShiftabilityTesterPlugin: name = 'Shiftability Tester' description = 'Tests whether a rom with .space inside it was\nshifted correctly.' hidden = True def __init__(self, api: PluginApi) -> None: self.api = api self.locations = [] def load(self) -> None: self.action_test_shiftability = self.api.register_menu_entry( 'Test Shiftability', self.slot_test_shiftability) self.action_next_location = self.api.register_menu_entry( 'Next Location', self.slot_next_location) self.action_next_location.setShortcut(QKeySequence(Qt.Key_F4)) def unload(self) -> None: self.api.remove_menu_entry(self.action_test_shiftability) self.api.remove_menu_entry(self.action_next_location) def slot_test_shiftability(self) -> None: progress_dialog = self.api.get_progress_dialog( 'Shiftability Tester', 'Testing shiftability...', False) progress_dialog.show() self.thread = QThread() self.worker = TestShiftabilityWorker() self.worker.moveToThread(self.thread) self.worker.signal_progress.connect( lambda progress: progress_dialog.set_progress(progress)) self.worker.signal_done.connect( lambda: (self.thread.quit(), progress_dialog.close(), self.api.show_message( 'Shiftability Tester', 'Test complete. See console for more information.'))) self.worker.signal_fail.connect( lambda message: (self.thread.quit(), progress_dialog.close( ), self.api.show_error('Shiftability Tester', message))) self.worker.signal_locations.connect(self.slot_set_locations) self.thread.started.connect(self.worker.process) self.thread.start() def slot_set_locations(self, locations) -> None: self.locations = locations def slot_next_location(self) -> None: if len(self.locations) == 0: self.api.show_error( 'Shiftability Tester', 'Shiftability not tested yet or all locations visited.') return location = self.locations.pop( 0) - 2 # as we shift by 0x10000, this should be moved # TODO add this in better to the plugin api: find linked usa controller controller = None for contrl in self.api.main_window.dock_manager.hex_viewer_manager.controllers: if contrl.rom_variant == RomVariant.USA and contrl.is_linked == True: controller = contrl break if controller is None: self.api.show_error('Shiftability Tester', 'Need a USA hex viewer that is linked') return controller.update_cursor( controller.address_resolver.to_virtual(location))
class Importador(QWidget): """GUI class for the BGG -> Ludopedia importer""" enable_editables = Signal(bool) alternative_chosen = Signal(object) def __init__(self, parent=None): super().__init__(parent) self.thread = QThread() self.worker = None grid_layout = QGridLayout(self) login_group_box = self.create_login_group() data_group_box = self.create_data_group() self.enable_editables.connect(login_group_box.setEnabled) self.enable_editables.connect(data_group_box.setEnabled) self.import_button = QPushButton('Importar', self) self.import_button.setEnabled(False) self.enable_editables.connect(self.import_button.setEnabled) self.import_button.clicked.connect(self.enable_editables) self.import_button.clicked.connect(self.load_data) self.bgg_user_line_edit.textChanged.connect(self.enable_import) self.ludo_mail_line_edit.textChanged.connect(self.enable_import) self.ludo_pass_line_edit.textChanged.connect(self.enable_import) grid_layout.addWidget(login_group_box, 1, 1, 1, 2) grid_layout.addWidget(data_group_box, 2, 1, 1, 2) grid_layout.addWidget(self.import_button, 8, 2) self.log_widget = QTextEdit(self) self.log_widget.setReadOnly(True) grid_layout.addWidget(self.log_widget, 9, 1, 30, 2) def create_qlineedit(self, text): """Creates a label with the given text and an accompanying line edit""" line_edit = QLineEdit(self) line_edit_label = QLabel(text, line_edit) line_edit_label.setBuddy(line_edit) return (line_edit, line_edit_label) def create_login_group(self): """Create labels and line edits for providing BGG and ludopedia login information""" (self.bgg_user_line_edit, bgg_user_label) = self.create_qlineedit('Usuario BoardGameGeek:') (self.ludo_mail_line_edit, ludo_mail_label) = self.create_qlineedit('E-mail Ludopedia:') (self.ludo_pass_line_edit, ludo_pass_label) = self.create_qlineedit('Senha Ludopedia:') self.ludo_pass_line_edit.setEchoMode(QLineEdit.PasswordEchoOnEdit) group_box = QGroupBox('Login') grid_layout = QGridLayout(group_box) grid_layout.addWidget(bgg_user_label, 1, 1) grid_layout.addWidget(self.bgg_user_line_edit, 1, 2) grid_layout.addWidget(ludo_mail_label, 2, 1) grid_layout.addWidget(self.ludo_mail_line_edit, 2, 2) grid_layout.addWidget(ludo_pass_label, 3, 1) grid_layout.addWidget(self.ludo_pass_line_edit, 3, 2) group_box.setLayout(grid_layout) return group_box def create_data_group(self): """Creates group for holding specific choice data selection""" button_group = QButtonGroup(self) button_group.setExclusive(True) colecao_radio_button = QRadioButton('Coleção') self.partidas_radio_button = QRadioButton('Partidas') colecao_radio_button.setChecked(True) button_group.addButton(colecao_radio_button) button_group.addButton(self.partidas_radio_button) (self.min_date_picker, min_date_label) = create_date_picker('À Partir de:', self) (self.max_date_picker, max_date_label) = create_date_picker('Até:', self) self.min_date_picker.dateChanged.connect( self.max_date_picker.setMinimumDate) colecao_radio_button.toggled.connect(self.min_date_picker.setDisabled) colecao_radio_button.toggled.connect(self.max_date_picker.setDisabled) self.map_users_button = QPushButton( 'Ver mapa de usuarios BGG -> Ludopedia', self) self.map_users_button.setEnabled(False) self.map_users_button.clicked.connect(self.user_map) colecao_radio_button.toggled.connect(self.map_users_button.setDisabled) group_box = QGroupBox('Dados') grid_layout = QGridLayout(group_box) grid_layout.addWidget(colecao_radio_button, 1, 1) grid_layout.addWidget(self.partidas_radio_button, 1, 2) grid_layout.addWidget(min_date_label, 2, 1) grid_layout.addWidget(self.min_date_picker, 2, 2) grid_layout.addWidget(max_date_label, 3, 1) grid_layout.addWidget(self.max_date_picker, 3, 2) grid_layout.addWidget(self.map_users_button, 4, 1, 1, 2) group_box.setLayout(grid_layout) return group_box def enable_import(self): """Slot to toggle state of the import button""" self.import_button.setDisabled(not self.bgg_user_line_edit.text() or not self.ludo_mail_line_edit.text() or not self.ludo_pass_line_edit.text()) def log_text(self, message_type, text): """Logs the given text to the QPlainTextWidget""" current_time = QTime.currentTime().toString() if message_type == MessageType.ERROR: self.log_widget.insertHtml( f'[{current_time}] {ERROR_HTML}{text}<br>') elif message_type == MessageType.GENERIC: self.log_widget.insertHtml(f'[{current_time}] {text}<br>') elif message_type == MessageType.DEBUG and ENABLE_DEBUG: self.log_widget.insertHtml( f'[{current_time}] {DEBUG_HTML}{text}<br>') self.log_widget.moveCursor(QTextCursor.End) if ENABLE_DEBUG: print(text) def disconnect_thread(self): """Disconnect the started signal from the thread""" self.thread.started.disconnect() def configure_thread(self, worker): """Does basic thread startup and cleanup configuration""" worker.finished.connect(self.thread.quit) worker.moveToThread(self.thread) self.thread.started.connect(worker.run) worker.message.connect(self.log_text) worker.finished.connect(self.disconnect_thread) worker.exit_on_error.connect(self.thread.quit) worker.exit_on_error.connect(lambda: self.enable_editables.emit(True)) def load_data(self): """Load data from bgg""" try: (session, ludo_user_id) = self.login_ludopedia() bgg_user = self.bgg_user_line_edit.text() if self.partidas_radio_button.isChecked(): current_date = format_qdate(QDate.currentDate()) min_date = parse_date( format_qdate(self.min_date_picker.date()), current_date) max_date = parse_date( format_qdate(self.max_date_picker.date()), min_date) self.worker = BGGPlayFetcher(bgg_user, min_date, max_date) self.configure_thread(self.worker) self.worker.finished.connect(lambda plays: self.post_plays( session, plays, bgg_user, ludo_user_id)) else: self.worker = BGGColectionFetcher(bgg_user) self.configure_thread(self.worker) self.worker.finished.connect( lambda bgg_collection: self.import_collection( session, bgg_collection)) self.thread.start() except InputError: self.enable_editables.emit(True) def show_play_table(self, plays): """Shows a table with all the plays to be imported, allowing user to select some to skip""" tree_model = PlayTableModel(plays) table_widget = QTableView() table_widget.setModel(tree_model) table_widget.verticalHeader().setVisible(False) table_view_header = table_widget.horizontalHeader() table_view_header.setStretchLastSection(True) for column in range(tree_model.columnCount()): column_size = tree_model.data(tree_model.index(0, column), PlayTableModel.SIZE_ROLE) table_view_header.resizeSection(column, column_size) table_widget_dialog = QDialog(self) table_widget_dialog.setModal(True) grid_layout = QGridLayout(table_widget_dialog) grid_layout.addWidget(table_widget, 1, 1) table_widget_dialog.resize(800, 600) table_widget_dialog.exec_() skipped_plays = tree_model.get_skipped_plays() return [play for play in plays if play.id not in skipped_plays] def post_plays(self, session, plays, bgg_user, ludo_user_id): """Receives plays from the Play Fetched thread and start the Ludopedia Logger""" user_map = self.get_bgg_to_ludo_users() if bgg_user not in user_map: user_map[bgg_user] = ludo_user_id selected_plays = self.show_play_table(plays) self.worker = LudopediaPlayLogger(session, selected_plays, bgg_user, user_map) self.worker.request_search.connect( self.request_search_and_show_alternatives, Qt.BlockingQueuedConnection) self.worker.request_alternative.connect(self.request_alternative, Qt.BlockingQueuedConnection) self.alternative_chosen.connect(self.worker.receive_alternative, Qt.DirectConnection) self.configure_thread(self.worker) self.worker.finished.connect(lambda: self.enable_editables.emit(True)) self.thread.start() def user_map(self): """Slot to show user map from bgg to ludopedia""" user_map_dialog = QDialog(self) user_map_dialog.setModal(True) bgg_to_ludo = self.get_bgg_to_ludo_users() user_list = [f'{key} -> {value}' for key, value in bgg_to_ludo.items()] list_widget = QListWidget(user_map_dialog) list_widget.addItems(user_list) list_widget.setResizeMode(QListView.Adjust) list_widget.sortItems() grid_layout = QGridLayout(user_map_dialog) grid_layout.addWidget(list_widget, 1, 1) user_map_dialog.resize(400, 400) user_map_dialog.show() def login_ludopedia(self): """Logins into Ludopedia manually and returns the session and user_id""" self.log_text(MessageType.GENERIC, 'Obtendo dados do Ludopedia') payload = { 'email': self.ludo_mail_line_edit.text(), 'pass': self.ludo_pass_line_edit.text() } session = requests.Session() session_request = session.post(LUDOPEDIA_LOGIN_URL, data=payload) if 'senha incorretos' in session_request.text: self.log_text( MessageType.ERROR, 'Não foi possível logar na Ludopedia com as informações fornecidas' ) raise InputError user_re = re.search(r'id_usuario=(\d+)', session_request.text) user_id = user_re.group(1) if user_re else None return (session, user_id) def import_collection(self, session, collection): """Imports a given collection into Ludopedia""" self.worker = LudopediaCollectionLogger(session, collection) self.configure_thread(self.worker) self.worker.finished.connect(lambda: self.enable_editables.emit(True)) self.thread.start() def show_alternatives_dialog(self, bgg_play, data): """Show alternative games to use as the game to log a play""" alternatives_dialog = QInputDialog(self) alternatives_list = [ f'{item["nm_jogo"]} ({item["ano_publicacao"]})' for item in data ] alternatives_dialog.setComboBoxItems(alternatives_list) alternatives_dialog.setOption(QInputDialog.UseListViewForComboBoxItems) game_str = f'{bgg_play.game_name} ({bgg_play.year_published})' alternatives_dialog.setLabelText( f'Escolha uma alternativa para o jogo "{game_str}"') if alternatives_dialog.exec_(): selected_index = alternatives_list.index( alternatives_dialog.textValue()) return data[selected_index] return None def request_search_and_show_alternatives(self, session, bgg_play): """Request a new string to use for game search and then show results to be picked""" new_search_dialog = QInputDialog(self) game_str = f'{bgg_play.game_name} ({bgg_play.year_published})' new_search_dialog.setLabelText( f'Jogo "{game_str}" não encontrado\nBuscar por:') new_search_dialog.setInputMode(QInputDialog.TextInput) if new_search_dialog.exec_(): data = search_ludopedia_games(session, new_search_dialog.textValue()) data = self.show_alternatives_dialog(bgg_play, data) self.alternative_chosen.emit(data) def request_alternative(self, bgg_play, data): """Request an alternative from user and emit choice""" alternative = self.show_alternatives_dialog(bgg_play, data) self.alternative_chosen.emit(alternative) def get_bgg_to_ludo_users(self): """Reads usuarios.txt file to map a bgg user to its corresponding ludopedia one""" try: parser = ConfigParser() with open("usuarios.txt") as lines: lines = chain(("[top]", ), lines) parser.read_file(lines) bgg_to_ludo_user = dict(parser['top']) bgg_to_ludo_user_id = dict() for bgg_user, ludo_user in bgg_to_ludo_user.items(): if is_invalid_bgg_user(bgg_user): self.log_text( MessageType.ERROR, f'Usuário do BGG "{bgg_user}" inválido' f' no mapa de usuários') continue if ludo_user.isdigit(): bgg_to_ludo_user_id[bgg_user] = ludo_user self.log_text( MessageType.DEBUG, f'Usuário do BGG "{bgg_user}" já mapeado' f' ao id ludopedia: {ludo_user}') else: ludo_user_id = get_ludo_user_id(ludo_user) if ludo_user_id: self.log_text(MessageType.DEBUG, f'{ludo_user_id} para {ludo_user}') bgg_to_ludo_user_id[bgg_user] = ludo_user_id else: self.log_text( MessageType.ERROR, f'Falha ao buscar id de usuario da' f' ludopedia para "{ludo_user}"') return bgg_to_ludo_user_id except FileNotFoundError: self.log_error( MessageType.ERROR, 'Não foi possível encontrar o arquivo "usuarios.txt') return {}
class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.ui = Ui_MainWindow() self.ui.setupUi(self) self.adf = "" self.bbdf = "" self.andf = "" self.ofile = "" self.ofileadd = "" self.ofilerem = "" self.aboutAction = QAction('&关于', self) self.aboutText = """程序版本:1.2\[email protected]\nAll rights reserved\n2021-01-20""" self.aboutAction.triggered.connect( lambda: self.showDialog(self.aboutText)) # 添加事件 self.ui.checkBox.stateChanged.connect(self.updateState) self.ui.pushButton_jwc.clicked.connect(lambda: self.chooseFile("all")) self.ui.pushButton_bb.clicked.connect(lambda: self.chooseFile("bb")) self.ui.pushButton_jwc_new.clicked.connect( lambda: self.chooseFile("all-new")) self.ui.pushButton_start.clicked.connect(self.saveFile) self.ui.pushButton_start.clicked.connect(self.genData) self.ui.menuBar.addAction(self.aboutAction) # 初始化日志窗口 self.logger(u"程序已就绪,请选择文件进行处理。") self.logger(textHelp) self.updateSb(u"请选择文件以运行程序") def logger(self, c): return self.ui.textBrowser.append(c) def updateSb(self, c): return self.ui.statusbar.showMessage(c) def showDialog(self, text): msgbox = QMessageBox() msgbox.setWindowTitle(u'关于') msgbox.setText(text) msgbox.exec_() def updateState(self): if self.ui.checkBox.isChecked(): self.ui.lineEdit_jwc_new.setEnabled(True) self.ui.pushButton_jwc_new.setEnabled(True) self.ui.pushButton_start.clicked.disconnect(self.genData) self.ui.pushButton_start.clicked.connect(self.genComp) else: self.andf = "" self.ui.lineEdit_jwc_new.setText("") self.ui.lineEdit_jwc_new.setEnabled(False) self.ui.pushButton_jwc_new.setEnabled(False) self.ui.pushButton_start.clicked.disconnect(self.genComp) self.ui.pushButton_start.clicked.connect(self.genData) self.updateButton() def chooseFile(self, bname): fileName, _ = QFileDialog.getOpenFileName(self, "选择xlsx文件", "", "Excel文件 (*.xlsx)") if bname == "all": if fileName: self.adf = fileName self.ui.lineEdit_jwc.setText(fileName) else: self.adf = "" self.ui.lineEdit_jwc.setText("") self.updateButton() return if bname == "bb": if fileName: self.bbdf = fileName self.ui.lineEdit_bb.setText(fileName) else: self.bbdf = "" self.ui.lineEdit_bb.setText("") self.updateButton() return if bname == "all-new": if fileName: self.andf = fileName self.ui.lineEdit_jwc_new.setText(fileName) else: self.andf = "" self.ui.lineEdit_jwc_new.setText("") self.updateButton() return def saveFile(self): fileName, _ = QFileDialog.getSaveFileName(self, "选择输出文件名", "", "CVS文件 (*.csv)") if fileName: self.ofile = fileName if self.ui.checkBox.isChecked(): (pname, ext) = os.path.splitext(fileName) self.ofileadd = pname + "-新增名单" + ext self.ofilerem = pname + "-删减名单" + ext self.logger(u"新增学生名单将保存为:" + self.ofileadd) self.logger(u"删减学生名单将保存为:" + self.ofilerem) else: self.logger(u"注册名单将保存为:" + fileName) else: self.ofile = "" self.logger("用户取消操作") def updateButton(self): if self.adf and self.bbdf and not (bool(self.ui.checkBox.isChecked()) != bool(self.andf)): self.ui.pushButton_start.setEnabled(True) else: self.ui.pushButton_start.setEnabled(False) def writeFile(self, content, path): try: content.to_csv(path, index=False, header=False, encoding='utf_8_sig') except PermissionError: self.logger("文件写入失败,请检查是否已经打开了同目录下的同名文件!") @Slot() def genData(self): if self.ofile == "": return self.thread = QThread() self.worker = Worker(self.adf, self.bbdf) self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run) self.updateSb(u"正在处理数据") self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.worker.finished.connect(lambda: self.updateSb(u"处理完成")) self.worker.finished.connect( lambda: self.logger(u"数据处理完成,请检查生成的文件。\n")) self.thread.finished.connect(self.thread.deleteLater) self.worker.progress.connect(lambda p: self.ui.progressBar.setValue(p)) self.worker.output.connect(lambda c: self.logger(c)) self.worker.content.connect( lambda content: self.writeFile(content, self.ofile)) self.thread.start() self.ui.pushButton_start.setEnabled(False) self.thread.finished.connect( lambda: self.ui.pushButton_start.setEnabled(True)) @Slot() def genComp(self): if self.ofile == "": return self.file1 = pd.DataFrame() self.file2 = pd.DataFrame() def updateFile(f, id): if id == 1: self.file1 = f if id == 2: self.file2 = f self.p = 0 self.lock1 = False self.lock2 = False def checkLock(i): if i == 1: self.lock1 = True if i == 2: self.lock2 = True if self.lock1 and self.lock2: self.updateSb(u"正在处理数据") self.th3 = QThread() self.comp = Comparer(self.file1, self.file2) self.comp.moveToThread(self.th3) self.th3.started.connect(self.comp.run) self.comp.finished.connect(self.th3.quit) self.comp.finished.connect(self.comp.deleteLater) self.th3.finished.connect(self.th3.deleteLater) self.comp.progress.connect(lambda p: up(p)) self.comp.output.connect(lambda c: self.logger(c)) self.comp.content1.connect( lambda content: self.writeFile(content, self.ofileadd)) self.comp.content2.connect( lambda content: self.writeFile(content, self.ofilerem)) self.th3.finished.connect( lambda: self.ui.progressBar.setValue(100)) self.comp.finished.connect( lambda: self.logger(u"数据处理完成,请检查生成的文件。\n")) self.comp.finished.connect(lambda: self.updateSb(u"处理完成")) self.th3.finished.connect( lambda: self.ui.pushButton_start.setEnabled(True)) self.th3.start() def up(wp): self.p += 0.4 * wp self.ui.progressBar.setValue(self.p) self.th1 = QThread() self.th2 = QThread() self.worker1 = Worker(self.adf, self.bbdf) self.worker2 = Worker(self.andf, self.bbdf) self.worker1.moveToThread(self.th1) self.worker2.moveToThread(self.th2) self.th1.started.connect(self.worker1.run) self.worker1.content.connect(lambda f: updateFile(f, 1)) self.worker1.finished.connect(self.th1.quit) self.worker1.finished.connect(self.worker1.deleteLater) self.worker1.finished.connect(lambda: checkLock(1)) self.th1.finished.connect(self.th1.deleteLater) self.th2.started.connect(self.worker2.run) self.worker2.content.connect(lambda f: updateFile(f, 2)) self.worker2.finished.connect(self.th2.quit) self.worker2.finished.connect(self.worker2.deleteLater) self.worker2.finished.connect(lambda: checkLock(2)) self.th2.finished.connect(self.th2.deleteLater) self.worker1.progress.connect(lambda p: up(p)) self.worker2.progress.connect(lambda p: up(p)) # self.worker1.output.connect(lambda c: self.logger(c)) # self.worker2.output.connect(lambda c: self.logger(c)) self.th1.start() self.th2.start() self.ui.pushButton_start.setEnabled(False)