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()
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 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 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 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')