class Backend(QObject): setval = Signal(QtCharts.QXYSeries) def __init__(self, parent=None): QObject.__init__(self, parent) self._serie = None @Slot(QtCharts.QXYSeries) # expose QML serie to Python def exposeserie(self, serie): self._serie = serie # print(serie) # print("QML serie exposed to Python") @Slot(str) def startthread(self, text): self.WorkerThread = QThread() self.worker = Worker1(self._serie) self.WorkerThread.started.connect(self.worker.run) self.worker.finished.connect(self.end) self.worker.set_val.connect(self.setval) self.worker.moveToThread(self.WorkerThread) # Move the Worker object to the Thread object self.WorkerThread.start() @Slot(str) def stopthread(self, text): self.worker.stop() print("CLOSING THREAD") def end(self): self.WorkerThread.quit() self.WorkerThread.wait() msgBox = QMessageBox() msgBox.setText("THREAD CLOSED") msgBox.exec()
class Torrents2Tab(QWidget): newtorrents = Signal(int) def __init__(self): QWidget.__init__(self) layout = QVBoxLayout(self) self.splitter = QSplitter(self) self.list = QTreeView(self) self.list.setSortingEnabled(True) self.model = NewTorrentModel() proxy = QSortFilterProxyModel() proxy.setSourceModel(self.model) self.list.setModel(proxy) self.splitter.addWidget(self.list) self.t = QTableWidget(0, 4, self) self.splitter.addWidget(self.t) layout.addWidget(self.splitter) self.setLayout(layout) self.ds = DataSource() self.worker = UpdateTorrentWorker() self.worker_thread = QThread() self.worker_thread.started.connect(self.worker.run) self.worker.finished.connect(self.worker_thread.quit) self.worker.moveToThread(self.worker_thread) self.worker_thread.start() self.worker.processed.connect(self.processed) def finish(self): self.worker.finish() self.worker_thread.quit() self.worker_thread.wait() def processed(self, topic): self.model.add_topic(topic['published'])
class WeatherDownloader(QObject): downloadComplete = Signal() downloadFailed = Signal() def __init_(self, parent=None): super().__init__(self, parent) @Slot def downloadForecast(self): logging.info("Request to download the forecast from YR.NO") self._thread = QThread() self._worker = Worker() self._worker.moveToThread(self._thread) # setup signals-slots self._worker.downloadComplete.connect(self.when_complete) self._worker.downloadFailed.connect(self.when_failed) self._thread.started(self._worker.do_work) self._thread.finished(self._thread.deleteLater) def when_complete(self): logging.info("Download complete, freeing resources") self._thread.quit() # emit the result self.downloadComplete.emit() def when_failed(self): logging.warning("Download failed, freeing resources") self._thread.quit() # emit the result self.downloadFailed.emit()
class _PluginWorker(QObject): finished = Signal() def __init__(self): super().__init__() self._thread = QThread() self.moveToThread(self._thread) self._function = None self._args = None self._kwargs = None def start(self, function, *args, **kwargs): self._thread.started.connect(self._do_work) self._function = function self._args = args self._kwargs = kwargs self._thread.start() @Slot() def _do_work(self): self._function(*self._args, **self._kwargs) self.finished.emit() def clean_up(self): self._thread.quit() self._thread.wait() self.deleteLater()
class Form(QMainWindow): def __init__(self, splash): super(Form, self).__init__() self.resize(800, 600) self.splash = splash self.load_thread = QThread() self.load_worker = LoadData() self.load_worker.moveToThread(self.load_thread) self.load_thread.started.connect(self.load_worker.run) self.load_worker.message_signal.connect(self.set_message) self.load_worker.finished.connect(self.load_worker_finished) self.load_thread.start() while self.load_thread.isRunning(): QtWidgets.QApplication.processEvents() # 不断刷新,保证动画流畅 self.load_thread.deleteLater() def load_worker_finished(self): self.load_thread.quit() self.load_thread.wait() def set_message(self, message): self.splash.showMessage(message, Qt.AlignLeft | Qt.AlignBottom, Qt.white)
class LoggingWorker(QObject): """ QObject living in a separate QThread, logging everything it receiving. Intended to be an attached Stm32pio project class property. Stringifies log records using DispatchingFormatter and passes them via Signal interface so they can be conveniently received by any Qt entity. Also, the level of the message is attaching so the reader can interpret them differently. Can be controlled by two threading.Event's: stopped - on activation, leads to thread termination can_flush_log - use this to temporarily save the logs in an internal buffer while waiting for some event to occurs (for example GUI widgets to load), and then flush them when time has come """ sendLog = Signal(str, int) def __init__(self, logger: logging.Logger, parent: QObject = None): super().__init__(parent=parent) self.buffer = collections.deque() self.stopped = threading.Event() self.can_flush_log = threading.Event() self.logging_handler = BufferedLoggingHandler(self.buffer) logger.addHandler(self.logging_handler) self.logging_handler.setFormatter( stm32pio.util.DispatchingFormatter( f"%(levelname)-8s %(funcName)-{stm32pio.settings.log_fieldwidth_function}s %(message)s", special=stm32pio.util.special_formatters)) self.thread = QThread() self.moveToThread(self.thread) self.thread.started.connect(self.routine) self.thread.start() def routine(self) -> None: """ The worker constantly querying the buffer on the new log messages availability. """ while not self.stopped.wait(timeout=0.050): if self.can_flush_log.is_set(): if len(self.buffer): record = self.buffer.popleft() self.sendLog.emit(self.logging_handler.format(record), record.levelno) module_logger.debug('exit logging worker') self.thread.quit()
class LoggingWorker(QObject): """ QObject living in a separate QThread, logging everything it receiving. Intended to be an attached ProjectListItem property. Stringifies log records using global BuffersDispatchingHandler instance (its stm32pio.util.DispatchingFormatter, to be precise) and passes them via Qt Signal interface so they can be conveniently received by any Qt entity. Also, the level of the message is attaching so the reader can interpret them differently. Can be controlled by two threading.Event's: stopped - on activation, leads to thread termination can_flush_log - use this to temporarily save the logs in an internal buffer while waiting for some event to occurs (for example GUI widgets to load), and then flush them when the time has come """ sendLog = Signal(str, int) def __init__(self, project_id: ProjectID, parent: QObject = None): super().__init__(parent=parent) self.project_id = project_id self.buffer = collections.deque() projects_logger_handler.buffers[ project_id] = self.buffer # register our buffer self.stopped = threading.Event() self.can_flush_log = threading.Event() self.thread = QThread() self.moveToThread(self.thread) self.thread.started.connect(self.routine) self.thread.start() def routine(self) -> None: """ The worker constantly querying the buffer on the new log messages availability """ while not self.stopped.wait(timeout=0.050): if self.can_flush_log.is_set() and len(self.buffer): record = self.buffer.popleft() self.sendLog.emit(projects_logger_handler.format(record), record.levelno) # TODO: maybe we should flush all remaining logs before termination projects_logger_handler.buffers.pop( self.project_id) # unregister our buffer module_logger.debug( f"exit LoggingWorker of project id {self.project_id}") self.thread.quit()
class RssFeedReader(QObject): """ Loads the RSS feed from the network async, every complete download is announced as the emit of the signal """ rssFeedLoadedFromSource = Signal() rssFeedComplete = Signal() def __init__(self, parent=None): super().__init__(parent) self._rss = [] @Slot() def refreshRssFeed(self): """ Refresh the RSS data from the servers """ logging.info("Worker thread starting") self._thread = QThread() self.worker = Worker() # connect the signals with slots or re-emit the signals self._thread.started.connect(self.worker.do_work) self._thread.finished.connect(self._thread.deleteLater) self.worker.rssSourceLoaded.connect(self.rssFeedLoadedFromSource.emit) self.worker.rssDownloadComplete.connect(self.downloadComplete) # move the worker to its own thread self.worker.moveToThread(self._thread) self._thread.start() logging.info("Worker thread started via QThread") @Slot() def downloadComplete(self): logging.info("Download complete, stopping thread") self._rss = self.worker.get_list() self._thread.quit() self.rssFeedComplete.emit() def get_list(self): return self._rss rssList = Property('QVariantList', get_list)
class InitialWidget(QWidget, Ui_Form): def __init__(self, mainwindow): super(self.__class__, self).__init__() self.setupUi(self) self.setStyleSheet(qss) self.mainwindow = mainwindow self.progressBar.setFixedHeight(10) self.thread = QThread() self.job = InitJob() self.job.sig_progress.connect(self.show_progress) self.job.moveToThread(self.thread) self.thread.started.connect(self.job.box_init) self.thread.start() def show_progress(self, pro, msg): self.progressBar.setValue(pro) self.msg.setText(msg) if pro == -1: self.mainwindow.home_handler() self.thread.quit() self.close()
class QueueConsumer(QObject): on_result = Signal(object) def __init__(self, queue: Queue): super(QueueConsumer, self).__init__() self._queue = queue self._worker = None self._thread: QThread = None def start(self): self._worker = QueueConsumerWorker(self._queue) self._thread = QThread() self._worker.moveToThread(self._thread) self._worker.finished.connect(self._thread.quit) self._worker.error.connect(self._handle_exception) self._worker.on_result.connect(self._on_result) self._thread.started.connect(self._worker.run) self._thread.start() def stop(self): if not (self._worker and self._thread): raise RuntimeError("not started yet") self._worker.stop() if self._thread.isRunning(): self._thread.quit() self._thread.wait() @Slot() def _handle_exception(self, e): if isinstance(e, RQAmsHelperException): e.exec_msg_box() else: logger.error("consumer failed: " + str(e)) logger.info("restart consumer") self.stop() self.start() @Slot() def _on_result(self, result): self.on_result.emit(result)
class TimeWidget(QWidget, Ui_TimeWidget): def __init__(self, parent=None): super(TimeWidget, self).__init__(parent) self.setupUi(self) self.btnStart.clicked.connect(self.start) self.btnStop.clicked.connect(self.stop) self.setupThread() self.show() def setupThread(self): self.worker = TimeDisplayer() self.thread = QThread() self.worker.moveToThread(self.thread) self.worker.timeUpdated.connect(self.lblTime.setText) self.thread.started.connect(self.worker.run) self.thread.finished.connect(self.worker.stop) def start(self): self.thread.start() def stop(self): self.thread.quit()
class JoystickSink(QObject): def __init__(self, jsdev): super().__init__() self.jsdev = jsdev self.thread = QThread() self.moveToThread(self.thread) self.thread.started.connect(self.create) self.thread.start() def create(self): self.timer = QTimer() self.timer.timeout.connect(self.readFromDevice, Qt.QueuedConnection) self.timer.setInterval(0) self.timer.start() def stop(self): self.timer.stop() self.thread.quit() def readFromDevice(self): try: events = self.jsdev.read() except: pass
class ImageStreamServer(QObject): """ Server class that will asynchronously listen for images coming from the vehicle and emit signals when images are received. """ # Emitted when a new image is received image_received = Signal() def __init__(self) -> None: super().__init__() self._worker = ImageStreamWorker() self._thread = QThread(self) self._worker.image_received.connect(self.image_received_slot) self._worker.moveToThread(self._thread) self._thread.started.connect(self._worker.do_work) @Slot() def image_received_slot(self): # Just pass on the signal self.image_received.emit() def get_last_image(self): return self._worker.get_last_image() def streaming(self): return self._worker.streaming() def start(self): self._thread.start() def stop(self): logging.info("Stopping image server") self._worker._running = False self._thread.quit() self._thread.wait()
class UserTab(QWidget): def __init__(self): QWidget.__init__(self) layout = QVBoxLayout(self) self.text = QPlainTextEdit() layout.addWidget(self.text) self.setLayout(layout) self.ds = DataSource() self.worker = UpdateUserWorker() self.worker_thread = QThread() self.worker_thread.started.connect(self.worker.run) self.worker.finished.connect(self.worker_thread.quit) self.worker.moveToThread(self.worker_thread) self.worker_thread.start() self.worker.processed.connect(self.processed) @Slot(dict) def processed(self, user): self.text.appendPlainText('USER: {}'.format(str(user))) def finish(self): self.worker.finish() self.worker_thread.quit() self.worker_thread.wait()
class AztecDiamondRenderer(QOpenGLWidget): HOLE_BORDER = QColor(210, 210, 210) DOMINO_BORDER = QColor(30, 30, 30) ARROWS = QColor(120, 120, 100) square_colors = { (board.BLACK, board.NO_COLOR): QColor(0x101010), (board.WHITE, board.NO_COLOR): QColor(0x101010) } for color, value in { board.GRAY: (120, 120, 120), board.RED: (190, 90, 90), board.YELLOW: (190, 190, 90), board.GREEN: (90, 190, 90), board.BLUE: (90, 90, 190), }.items(): square_colors[board.BLACK, color] = QColor(*map(lambda n: max(0, n - 10), value)) square_colors[board.WHITE, color] = QColor(*map(lambda n: min(255, n + 10), value)) boardChanged = Signal(QSize) skipaheadProgress = Signal(int) skipaheadComplete = Signal() class _SkipAheadWorker(QObject): progressed = Signal(int) completed = Signal(board.Board) def __init__(self, board, n, parent=None): super().__init__(parent=parent) self.board = board self.n = n @Slot() def run(self): for i in range(self.n): self.board.advance_magic() self.board.fill_holes(self.board.get_holes()) self.progressed.emit(i) self.completed.emit(self.board) def __init__(self, parent=None): super().__init__(parent=parent) self._worker = None self._worker_thread = None self.board = board.Board(2) self.holes = [] self.base_square_size = 500 self.hole_borders_enabled = True self.domino_borders_enabled = False self.domino_arrows_enabled = True self.checkerboard_enabled = True self.setMinimumSize(self.base_square_size, self.base_square_size) policy = self.sizePolicy() policy.setHeightForWidth(True) self.setSizePolicy(policy) self.boardChanged.connect(self.recalculate_holes) self.boardChanged.emit(self.minimumSize()) def recalculate_holes(self): self.holes = self.board.get_holes() def advance_magic(self, repaint=True): self.board.advance_magic() self.boardChanged.emit(self.minimumSize()) if repaint: self.repaint() def skip_ahead(self, n): self._worker_thread = QThread(self) self._worker = self._SkipAheadWorker(deepcopy(self.board), n - 1) self._worker_thread.started.connect(self._worker.run) self._worker.progressed.connect(self.skipaheadProgress.emit) self._worker.completed.connect(self._on_worker_complete) self._worker.moveToThread(self._worker_thread) self._worker_thread.start(QThread.LowPriority) @Slot(board.Board) def _on_worker_complete(self, board): self.board = board self._worker = None self._worker_thread.quit() self.advance_magic(repaint=False) self.recalculate_holes() self.fill_holes() self.skipaheadComplete.emit() self.repaint() def fill_holes(self): self.board.fill_holes(self.holes) self.repaint() def paintEvent(self, event): if not self.board: return self.base_square_size = min(self.size().width(), self.size().height()) painter = QPainter(self) painter.setPen(Qt.NoPen) painter.setBrush(self.palette().color(self.backgroundRole())) painter.drawRect(self.rect()) board_radius = len(self.board.data) // 2 square_size = self.base_square_size / board_radius / 2 offset_x = self.size().width() // 2 offset_y = self.size().height() // 2 for y in range(-board_radius, board_radius): for x in range(-board_radius, board_radius): if (color := self.board.get_square_color(x, y)) != board.NO_COLOR: painter.setBrush(QBrush(self.square_colors[ self.board.get_square_parity(x, y) if self.checkerboard_enabled else board.BLACK, color ])) painter.drawRect(QRectF( x * square_size + offset_x, y * square_size + offset_y, square_size + 0.5, square_size + 0.5 )) if self.domino_borders_enabled: painter.setPen(QPen(QBrush(self.DOMINO_BORDER), max(1, square_size / 20))) painter.setBrush(Qt.NoBrush) for y in range(-board_radius, board_radius): for x in range(-board_radius, board_radius): if self.board.get_square_parity(x, y) == board.BLACK and (other := self.board.get_square_neighbor(x, y)): painter.drawRect(QRectF( min(x, other[0]) * square_size + offset_x, min(y, other[1]) * square_size + offset_y, max(other[0] - x + 1, x - other[0] + 1) * square_size, max(other[1] - y + 1, y - other[1] + 1) * square_size ))
class AbstractBuildRunner(QObject): """ Base class to run a build. Create the required test runner and build manager, along with a thread that should be used for blocking tasks. """ running_state_changed = Signal(bool) worker_created = Signal(object) worker_class = None def __init__(self, mainwindow): QObject.__init__(self) self.mainwindow = mainwindow self.thread = None self.worker = None self.pending_threads = [] self.test_runner = None self.download_manager = None self.options = None self.stopped = False def init_worker(self, fetch_config, options): """ Create and initialize the worker. Should be subclassed to configure the worker, and should return the worker method that should start the work. """ self.options = options # global preferences global_prefs = get_prefs() self.global_prefs = global_prefs # apply the global prefs now apply_prefs(global_prefs) fetch_config.set_base_url(global_prefs["archive_base_url"]) download_dir = global_prefs["persist"] if not download_dir: download_dir = self.mainwindow.persist persist_limit = PersistLimit( abs(global_prefs["persist_size_limit"]) * 1073741824) self.download_manager = GuiBuildDownloadManager( download_dir, persist_limit) self.test_runner = GuiTestRunner() self.thread = QThread() # options for the app launcher launcher_kwargs = {} for name in ("profile", "preferences"): if name in options: value = options[name] if value: launcher_kwargs[name] = value # add add-ons paths to the app launcher launcher_kwargs["addons"] = options["addons"] self.test_runner.launcher_kwargs = launcher_kwargs launcher_kwargs["cmdargs"] = [] if options["profile_persistence"] in ("clone-first", "reuse") or options["profile"]: launcher_kwargs["cmdargs"] += ["--allow-downgrade"] # Thunderbird will fail to start if passed an URL arg if options.get("url") and fetch_config.app_name != "thunderbird": launcher_kwargs["cmdargs"] += [options["url"]] self.worker = self.worker_class(fetch_config, self.test_runner, self.download_manager) # Move self.bisector in the thread. This will # allow to the self.bisector slots (connected after the move) # to be automatically called in the thread. self.worker.moveToThread(self.thread) self.worker_created.emit(self.worker) def start(self, fetch_config, options): action = self.init_worker(fetch_config, options) assert callable(action), "%s should be callable" % action self.thread.start() # this will be called in the worker thread. QTimer.singleShot(0, action) # an action = instance of mozregression usage, so send # a usage ping (if telemetry is disabled, it will automatically # be discarded) send_telemetry_ping("gui", fetch_config.app_name) self.stopped = False self.running_state_changed.emit(True) @Slot() def stop(self, wait=True): self.stopped = True if self.options: if self.options["profile"] and self.options[ "profile_persistence"] == "clone-first": self.options["profile"].cleanup() if self.download_manager: self.download_manager.cancel() if self.thread: self.thread.quit() if wait: if self.download_manager: self.download_manager.wait(raise_if_error=False) if self.thread: # wait for thread(s) completion - this is the case when # user close the application self.thread.wait() for thread in self.pending_threads: thread.wait() self.thread = None elif self.thread: # do not block, just keep track of the thread - we got here # when user uses the stop button. self.pending_threads.append(self.thread) self.thread.finished.connect(self._remove_pending_thread) if self.test_runner: self.test_runner.finish(None) self.running_state_changed.emit(False) log("Stopped") @Slot() def _remove_pending_thread(self): for thread in self.pending_threads[:]: if thread.isFinished(): self.pending_threads.remove(thread)
class GraphLayoutGenerator(QObject): """Computes the layout for the Entity Graph View.""" finished = Signal(object, object) started = Signal() progressed = Signal(int) stopped = Signal() blocked = Signal(bool) msg = Signal(str) def __init__(self, vertex_count, src_inds, dst_inds, spread, heavy_positions=None, iterations=12, weight_exp=-2): super().__init__() if vertex_count == 0: vertex_count = 1 if heavy_positions is None: heavy_positions = dict() self._show_previews = False self.vertex_count = vertex_count self.src_inds = src_inds self.dst_inds = dst_inds self.spread = spread self.heavy_positions = heavy_positions self.iterations = max(3, round(iterations * (1 - len(heavy_positions) / self.vertex_count))) self.weight_exp = weight_exp self.initial_diameter = (self.vertex_count ** (0.5)) * self.spread self._state = _State.SLEEPING self._thread = QThread() self.moveToThread(self._thread) qApp.aboutToQuit.connect(self._thread.quit) # pylint: disable=undefined-variable self.started.connect(self.get_coordinates) self.finished.connect(self.clean_up) def show_progress_widget(self, parent): widget = ProgressBarWidget(self) parent.layout().addWidget(widget) widget.show() def clean_up(self): self.deleteLater() self._thread.quit() def is_running(self): return self._state == _State.RUNNING @Slot(bool) def stop(self, _checked=False): self._state = _State.STOPPED self.clean_up() self.stopped.emit() @Slot(bool) def set_show_previews(self, checked): self._show_previews = checked def emit_finished(self, x, y): if self._state == _State.STOPPED: return self.finished.emit(x, y) def start(self): self._thread.start() self.started.emit() def shortest_path_matrix(self): """Returns the shortest-path matrix. """ if not self.src_inds: # Introduce fake pair of links to help 'spreadness' self.src_inds = [self.vertex_count, self.vertex_count] self.dst_inds = [np.random.randint(0, self.vertex_count), np.random.randint(0, self.vertex_count)] self.vertex_count += 1 dist = np.zeros((self.vertex_count, self.vertex_count)) src_inds = arr(self.src_inds) dst_inds = arr(self.dst_inds) try: dist[src_inds, dst_inds] = dist[dst_inds, src_inds] = self.spread except IndexError: pass start = 0 slices = [] iteration = 0 self.msg.emit("Step 1 of 2: Computing shortest-path matrix...") while start < self.vertex_count: if self._state == _State.STOPPED: return self.progressed.emit(iteration) stop = min(self.vertex_count, start + math.ceil(self.vertex_count / 10)) slice_ = dijkstra(dist, directed=False, indices=range(start, stop)) slices.append(slice_) start = stop iteration += 1 matrix = np.vstack(slices) # Remove infinites and zeros matrix[matrix == np.inf] = self.spread * self.vertex_count ** (0.5) matrix[matrix == 0] = self.spread * 1e-6 return matrix def sets(self): """Returns sets of vertex pairs indices. """ sets = [] for n in range(1, self.vertex_count): pairs = np.zeros((self.vertex_count - n, 2), int) # pairs on diagonal n pairs[:, 0] = np.arange(self.vertex_count - n) pairs[:, 1] = pairs[:, 0] + n mask = np.mod(range(self.vertex_count - n), 2 * n) < n s1 = pairs[mask] s2 = pairs[~mask] if s1.any(): sets.append(s1) if s2.any(): sets.append(s2) return sets @Slot() def get_coordinates(self): """Computes and returns x and y coordinates for each vertex in the graph, using VSGD-MS.""" self._state = _State.RUNNING if self.vertex_count <= 1: x, y = np.array([0.0]), np.array([0.0]) self.emit_finished(x, y) self.stop() return matrix = self.shortest_path_matrix() mask = np.ones((self.vertex_count, self.vertex_count)) == 1 - np.tril( np.ones((self.vertex_count, self.vertex_count)) ) # Upper triangular except diagonal np.random.seed(0) layout = np.random.rand(self.vertex_count, 2) * self.initial_diameter - self.initial_diameter / 2 heavy_ind_list = list() heavy_pos_list = list() for ind, pos in self.heavy_positions.items(): heavy_ind_list.append(ind) heavy_pos_list.append([pos["x"], pos["y"]]) heavy_ind = arr(heavy_ind_list) heavy_pos = arr(heavy_pos_list) if heavy_ind.any(): layout[heavy_ind, :] = heavy_pos weights = matrix ** self.weight_exp # bus-pair weights (lower for distant buses) maxstep = 1 / np.min(weights[mask]) minstep = 1 / np.max(weights[mask]) lambda_ = np.log(minstep / maxstep) / (self.iterations - 1) # exponential decay of allowed adjustment sets = self.sets() # construct sets of bus pairs self.msg.emit("Step 2 of 2: Generating layout...") for iteration in range(self.iterations): if self._state == _State.STOPPED: return if self._show_previews: x, y = layout[:, 0], layout[:, 1] self.emit_finished(x, y) self.progressed.emit(iteration) step = maxstep * np.exp(lambda_ * iteration) # how big adjustments are allowed? rand_order = np.random.permutation( self.vertex_count ) # we don't want to use the same pair order each iteration for s in sets: v1, v2 = rand_order[s[:, 0]], rand_order[s[:, 1]] # arrays of vertex1 and vertex2 # current distance (possibly accounting for system rescaling) dist = ((layout[v1, 0] - layout[v2, 0]) ** 2 + (layout[v1, 1] - layout[v2, 1]) ** 2) ** 0.5 r = (matrix[v1, v2] - dist)[:, None] * (layout[v1] - layout[v2]) / dist[:, None] / 2 # desired change dx1 = r * np.minimum(1, weights[v1, v2] * step)[:, None] dx2 = -dx1 layout[v1, :] += dx1 # update position layout[v2, :] += dx2 if heavy_ind.any(): layout[heavy_ind, :] = heavy_pos x, y = layout[:, 0], layout[:, 1] self.emit_finished(x, y) self.stop()
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) return worker and worker.player @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 MainWindow(QMainWindow): def __init__(self, parent=None): logthread('MainWindow.__init__') QMainWindow.__init__(self, parent) self.widget = loadUi('window.ui', self) self.setFixedSize(self.size()) self.thread = QThread() self.thread.started.connect(self.threadStarted) self.thread.finished.connect(self.threadFinished) self.worker = Worker() self.worker.moveToThread(self.thread) self.thread.start() self.worker.qtextedit.connect(self.qtextedit_addtext) self.worker.stop.connect(self.endNow) self.worker.progress.connect(self.updateProgressBar) @Slot() def on_pbStart_clicked(self): logthread('MainWindow.on_pbStart_clicked') self.widget.pbrProgress.setRange(0, 0) self.teErrors.setText("") self.worker.start.emit() @Slot() def on_pbStop_clicked(self): logthread('MainWindow.on_pbStop_clicked') self.widget.pbrProgress.setRange(0, 1) self.worker.stop.emit() @Slot(str) def qtextedit_addtext(self, text): logthread('MainWindow.qtextedit_addtext args-{}'.format(str(text))) self.teErrors.setText(str(text)) @Slot() def endNow(self): logthread('MainWindow.endNow') self.worker.continueWork = False @Slot(int) def updateProgressBar(self, progress): logthread('MainWindow.updateProgressBar') self.widget.pbrProgress.setValue(progress) @Slot() def on_actionProject1_triggered(self): logthread('MainWindow.on_actionProject1_triggered') from projectsettings1 import SettingsProject1 dialogSettingsProject1 = SettingsProject1() dialogSettingsProject1.exec_() @Slot() def on_actionProject2_triggered(self): logthread('MainWindow.on_actionProject2_triggered') from projectsettings2 import SettingsProject2 dialogSettingsProject2 = SettingsProject2() dialogSettingsProject2.exec_() def closeEvent(self, event: QCloseEvent): logthread('MainWindow.closeEvent') self.worker.continueWork = False self.thread.quit() self.thread.wait() def threadStarted(self): logthread('MainWindow.threadStarted') def threadFinished(self): logthread('MainWindow.threadFinished')
class SpineEngineWorker(QObject): finished = Signal() _dag_execution_started = Signal(list) _node_execution_started = Signal(object, object) _node_execution_finished = Signal(object, object, object, bool, bool) _event_message_arrived = Signal(object, str, str, str) _process_message_arrived = Signal(object, str, str, str) def __init__(self, engine_server_address, engine_data, dag, dag_identifier, project_items): """ Args: engine_server_address (str): Address of engine server if any engine_data (dict): engine data dag (DirectedGraphHandler) dag_identifier (str) project_items (dict): mapping from project item name to :class:`ProjectItem` """ super().__init__() self._engine_data = engine_data self._engine_mngr = make_engine_manager(engine_server_address) self.dag = dag self.dag_identifier = dag_identifier self._engine_final_state = "UNKNOWN" self._executing_items = [] self._project_items = project_items self.event_messages = {} self.process_messages = {} self.successful_executions = [] self._thread = QThread() self.moveToThread(self._thread) self._thread.started.connect(self.do_work) @property def engine_data(self): """Engine data dictionary.""" return self._engine_data def get_engine_data(self): """Returns the engine data. Together with ``self.set_engine_data()`` it can be used to modify the workflow after it's initially created. We use it at the moment for creating Julia sysimages. Returns: dict """ return copy.deepcopy(self._engine_data) def set_engine_data(self, engine_data): """Sets the engine data. Args: engine_data (dict): New data """ self._engine_data = engine_data @Slot(object, str, str) def _handle_event_message_arrived(self, item, filter_id, msg_type, msg_text): self.event_messages.setdefault(msg_type, []).append(msg_text) @Slot(object, str, str) def _handle_process_message_arrived(self, item, filter_id, msg_type, msg_text): self.process_messages.setdefault(msg_type, []).append(msg_text) def stop_engine(self): self._engine_mngr.stop_engine() def engine_final_state(self): return self._engine_final_state def thread(self): return self._thread def _connect_log_signals(self, silent): if silent: self._event_message_arrived.connect( self._handle_event_message_arrived) self._process_message_arrived.connect( self._handle_process_message_arrived) return self._dag_execution_started.connect(_handle_dag_execution_started) self._node_execution_started.connect(_handle_node_execution_started) self._node_execution_finished.connect(_handle_node_execution_finished) self._event_message_arrived.connect(_handle_event_message_arrived) self._process_message_arrived.connect(_handle_process_message_arrived) def start(self, silent=False): """Connects log signals. Args: silent (bool, optional): If True, log messages are not forwarded to the loggers but saved in internal dicts. """ self._connect_log_signals(silent) self._dag_execution_started.emit(list(self._project_items.values())) self._thread.start() @Slot() def do_work(self): """Does the work and emits finished when done.""" self._engine_mngr.run_engine(self._engine_data) while True: event_type, data = self._engine_mngr.get_engine_event() self._process_event(event_type, data) if event_type == "dag_exec_finished": self._engine_final_state = data break self.finished.emit() def _process_event(self, event_type, data): handler = { "exec_started": self._handle_node_execution_started, "exec_finished": self._handle_node_execution_finished, "event_msg": self._handle_event_msg, "process_msg": self._handle_process_msg, "standard_execution_msg": self._handle_standard_execution_msg, "kernel_execution_msg": self._handle_kernel_execution_msg, }.get(event_type) if handler is None: return handler(data) def _handle_standard_execution_msg(self, msg): item = self._project_items[msg["item_name"]] if msg["type"] == "execution_failed_to_start": msg_text = f"Program <b>{msg['program']}</b> failed to start: {msg['error']}" self._event_message_arrived.emit(item, msg["filter_id"], "msg_error", msg_text) elif msg["type"] == "execution_started": self._event_message_arrived.emit( item, msg["filter_id"], "msg", f"\tStarting program <b>{msg['program']}</b>") self._event_message_arrived.emit( item, msg["filter_id"], "msg", f"\tArguments: <b>{msg['args']}</b>") self._event_message_arrived.emit( item, msg["filter_id"], "msg_warning", "\tExecution is in progress. See messages below (stdout&stderr)" ) def _handle_kernel_execution_msg(self, msg): item = self._project_items[msg["item_name"]] language = msg["language"].capitalize() if msg["type"] == "kernel_started": console_requested_signal = { "julia": item.julia_console_requested, "python": item.python_console_requested, }.get(msg["language"]) if console_requested_signal is not None: console_requested_signal.emit(msg["filter_id"], msg["kernel_name"], msg["connection_file"]) elif msg["type"] == "kernel_spec_not_found": msg_text = ( f"\tUnable to find specification for {language} kernel <b>{msg['kernel_name']}</b>. " f"Go to Settings->Tools to select a valid {language} kernel.") self._event_message_arrived.emit(item, msg["filter_id"], "msg_error", msg_text) elif msg["type"] == "execution_failed_to_start": msg_text = f"\tExecution on {language} kernel <b>{msg['kernel_name']}</b> failed to start: {msg['error']}" self._event_message_arrived.emit(item, msg["filter_id"], "msg_error", msg_text) elif msg["type"] == "execution_started": self._event_message_arrived.emit( item, msg["filter_id"], "msg", f"\tStarting program on {language} kernel <b>{msg['kernel_name']}</b>" ) self._event_message_arrived.emit( item, msg["filter_id"], "msg_warning", f"See {language} Console for messages.") def _handle_process_msg(self, data): self._do_handle_process_msg(**data) def _do_handle_process_msg(self, item_name, filter_id, msg_type, msg_text): item = self._project_items[item_name] self._process_message_arrived.emit(item, filter_id, msg_type, msg_text) def _handle_event_msg(self, data): self._do_handle_event_msg(**data) def _do_handle_event_msg(self, item_name, filter_id, msg_type, msg_text): item = self._project_items[item_name] self._event_message_arrived.emit(item, filter_id, msg_type, msg_text) def _handle_node_execution_started(self, data): self._do_handle_node_execution_started(**data) def _do_handle_node_execution_started(self, item_name, direction): """Starts item icon animation when executing forward.""" item = self._project_items[item_name] self._executing_items.append(item) self._node_execution_started.emit(item, direction) def _handle_node_execution_finished(self, data): self._do_handle_node_execution_finished(**data) def _do_handle_node_execution_finished(self, item_name, direction, state, success, skipped): item = self._project_items[item_name] if success and not skipped: self.successful_executions.append((item, direction, state)) self._executing_items.remove(item) self._node_execution_finished.emit(item, direction, state, success, skipped) def clean_up(self): for item in self._executing_items: self._node_execution_finished.emit(item, None, None, False, False) self._thread.quit() self._thread.wait() self.deleteLater()
class Worker(QObject): """ A worker to construct export settings for a database. Attributes: thread (QThread): the thread the worker executes in """ database_unavailable = Signal(str) """Emitted when opening the database fails.""" errored = Signal(str, "QVariant") """Emitted when an error occurs.""" finished = Signal(str, "QVariant") """Emitted when the worker has finished.""" # LoggerInterface signals msg = Signal(str, str) msg_warning = Signal(str, str) msg_error = Signal(str, str) def __init__(self, database_url): """ Args: database_url (str): database's URL """ super().__init__() self.thread = QThread() self.moveToThread(self.thread) self._database_url = str(database_url) self._previous_settings = None self._previous_indexing_settings = None self._previous_indexing_domains = None self._previous_merging_settings = None self.thread.started.connect(self._fetch_settings) @Slot() def _fetch_settings(self): """Constructs settings and parameter index settings.""" result = _Result(*self._read_settings()) if result.set_settings is None: return if self._previous_settings is not None: updated_settings = deepcopy(self._previous_settings) updated_settings.update(result.set_settings) updated_indexing_settings, updated_indexing_domains = self._update_indexing_settings( updated_settings, result.indexing_settings ) if updated_indexing_settings is None: return updated_merging_settings, updated_merging_domains = self._update_merging_settings(updated_settings) if updated_merging_settings is None: return result.set_settings = updated_settings result.indexing_settings = updated_indexing_settings result.indexing_domains = updated_indexing_domains result.merging_settings = updated_merging_settings result.merging_domains = updated_merging_domains self.finished.emit(self._database_url, result) self.thread.quit() def set_previous_settings( self, previous_settings, previous_indexing_settings, previous_indexing_domains, previous_merging_settings ): """ Makes worker update existing settings instead of just making new ones. Args: previous_settings (gdx.SetSettings): existing set settings previous_indexing_settings (dict): existing indexing settings previous_indexing_domains (list) existing indexing domains previous_merging_settings (dict): existing merging settings """ self._previous_settings = previous_settings self._previous_indexing_settings = previous_indexing_settings self._previous_indexing_domains = previous_indexing_domains self._previous_merging_settings = previous_merging_settings def _read_settings(self): """Reads fresh gdx settings from the database.""" try: database_map = DatabaseMapping(self._database_url) except SpineDBAPIError as error: self.database_unavailable.emit(self._database_url) return None, None, None try: time_stamp = latest_database_commit_time_stamp(database_map) settings = gdx.make_set_settings(database_map) logger = _Logger(self._database_url, self) indexing_settings = gdx.make_indexing_settings(database_map, logger) except gdx.GdxExportException as error: self.errored.emit(self._database_url, error) return None, None, None finally: database_map.connection.close() return time_stamp, settings, indexing_settings def _update_indexing_settings(self, updated_settings, new_indexing_settings): """Updates the parameter indexing settings according to changes in the database.""" updated_indexing_settings = gdx.update_indexing_settings( self._previous_indexing_settings, new_indexing_settings, updated_settings ) indexing_domain_names = list() for indexing_setting in updated_indexing_settings.values(): if indexing_setting.indexing_domain is not None: indexing_domain_names.append(indexing_setting.indexing_domain.name) updated_indexing_domains = [ domain for domain in self._previous_indexing_domains if domain.name in indexing_domain_names ] for indexing_domain in updated_indexing_domains: metadata = gdx.SetMetadata(gdx.ExportFlag.FORCED_EXPORTABLE, True) updated_settings.add_or_replace_domain(indexing_domain, metadata) return updated_indexing_settings, updated_indexing_domains def _update_merging_settings(self, updated_settings): """Updates the parameter merging settings according to changes in the database""" try: database_map = DatabaseMapping(self._database_url) except SpineDBAPIError as error: self.errored.emit(self._database_url, error) return None, None try: updated_merging_settings = gdx.update_merging_settings( self._previous_merging_settings, updated_settings, database_map ) except gdx.GdxExportException as error: self.errored.emit(self._database_url, error) return None, None finally: database_map.connection.close() updated_merging_domains = list(map(gdx.merging_domain, updated_merging_settings.values())) for domain in updated_merging_domains: metadata = gdx.SetMetadata(gdx.ExportFlag.FORCED_EXPORTABLE, True) updated_settings.add_or_replace_domain(domain, metadata) return updated_merging_settings, updated_merging_domains
class SpineDBFetcher(QObject): """Fetches content from a Spine database and 'sends' them to another thread (via a signal-slot mechanism of course), so contents can be processed in that thread without affecting the UI.""" finished = Signal(object) _fetch_completed = Signal() _alternatives_fetched = Signal(object) _scenarios_fetched = Signal(object) _scenarios_alternatives_fetched = Signal(object) _object_classes_fetched = Signal(object) _objects_fetched = Signal(object) _relationship_classes_fetched = Signal(object) _relationships_fetched = Signal(object) _entity_groups_fetched = Signal(object) _parameter_definitions_fetched = Signal(object) _parameter_definition_tags_fetched = Signal(object) _parameter_values_fetched = Signal(object) _parameter_value_lists_fetched = Signal(object) _parameter_tags_fetched = Signal(object) _features_fetched = Signal(object) _tools_fetched = Signal(object) _tool_features_fetched = Signal(object) _tool_feature_methods_fetched = Signal(object) def __init__(self, db_mngr, listener): """Initializes the fetcher object. Args: db_mngr (SpineDBManager) listener (SpineDBEditor) """ super().__init__() self._db_mngr = db_mngr self._listener = listener self._thread = QThread() # NOTE: by moving this to another thread, all the slots defined below are called on that thread too self.moveToThread(self._thread) self._thread.start() self.connect_signals() def connect_signals(self): """Connects signals.""" self._fetch_completed.connect(self._emit_finished) self._alternatives_fetched.connect(self._receive_alternatives_fetched) self._scenarios_fetched.connect(self._receive_scenarios_fetched) self._scenarios_alternatives_fetched.connect(self._receive_scenarios_alternatives_fetched) self._object_classes_fetched.connect(self._receive_object_classes_fetched) self._objects_fetched.connect(self._receive_objects_fetched) self._relationship_classes_fetched.connect(self._receive_relationship_classes_fetched) self._relationships_fetched.connect(self._receive_relationships_fetched) self._entity_groups_fetched.connect(self._receive_entity_groups_fetched) self._parameter_definitions_fetched.connect(self._receive_parameter_definitions_fetched) self._parameter_definition_tags_fetched.connect(self._receive_parameter_definition_tags_fetched) self._parameter_values_fetched.connect(self._receive_parameter_values_fetched) self._parameter_value_lists_fetched.connect(self._receive_parameter_value_lists_fetched) self._parameter_tags_fetched.connect(self._receive_parameter_tags_fetched) self._features_fetched.connect(self._receive_features_fetched) self._tools_fetched.connect(self._receive_tools_fetched) self._tool_features_fetched.connect(self._receive_tool_features_fetched) self._tool_feature_methods_fetched.connect(self._receive_tool_feature_methods_fetched) def fetch(self, db_maps): """Fetches items from the database and emit fetched signals. """ self._listener.setCursor(QCursor(Qt.BusyCursor)) self._listener.silenced = True object_classes = {x: self._db_mngr.get_object_classes(x) for x in db_maps} self._object_classes_fetched.emit(object_classes) relationship_classes = {x: self._db_mngr.get_relationship_classes(x) for x in db_maps} self._relationship_classes_fetched.emit(relationship_classes) parameter_definitions = {x: self._db_mngr.get_parameter_definitions(x) for x in db_maps} self._parameter_definitions_fetched.emit(parameter_definitions) parameter_definition_tags = {x: self._db_mngr.get_parameter_definition_tags(x) for x in db_maps} self._parameter_definition_tags_fetched.emit(parameter_definition_tags) objects = {x: self._db_mngr.get_objects(x) for x in db_maps} self._objects_fetched.emit(objects) relationships = {x: self._db_mngr.get_relationships(x) for x in db_maps} self._relationships_fetched.emit(relationships) entity_groups = {x: self._db_mngr.get_entity_groups(x) for x in db_maps} self._entity_groups_fetched.emit(entity_groups) parameter_values = {x: self._db_mngr.get_parameter_values(x) for x in db_maps} self._parameter_values_fetched.emit(parameter_values) parameter_value_lists = {x: self._db_mngr.get_parameter_value_lists(x) for x in db_maps} self._parameter_value_lists_fetched.emit(parameter_value_lists) parameter_tags = {x: self._db_mngr.get_parameter_tags(x) for x in db_maps} self._parameter_tags_fetched.emit(parameter_tags) alternatives = {x: self._db_mngr.get_alternatives(x) for x in db_maps} self._alternatives_fetched.emit(alternatives) scenarios = {x: self._db_mngr.get_scenarios(x) for x in db_maps} self._scenarios_fetched.emit(scenarios) scenario_alternatives = {x: self._db_mngr.get_scenario_alternatives(x) for x in db_maps} self._scenarios_alternatives_fetched.emit(scenario_alternatives) features = {x: self._db_mngr.get_features(x) for x in db_maps} self._features_fetched.emit(features) tools = {x: self._db_mngr.get_tools(x) for x in db_maps} self._tools_fetched.emit(tools) tool_features = {x: self._db_mngr.get_tool_features(x) for x in db_maps} self._tool_features_fetched.emit(tool_features) tool_feature_methods = {x: self._db_mngr.get_tool_feature_methods(x) for x in db_maps} self._tool_feature_methods_fetched.emit(tool_feature_methods) self._fetch_completed.emit() def clean_up(self): self._listener.silenced = False self._listener.unsetCursor() self.quit() def quit(self): self._thread.quit() self._thread.wait() @Slot(object) def _receive_alternatives_fetched(self, db_map_data): self._db_mngr.cache_items("alternative", db_map_data) self._listener.receive_alternatives_fetched(db_map_data) @Slot(object) def _receive_scenarios_fetched(self, db_map_data): self._db_mngr.cache_items("scenario", db_map_data) self._listener.receive_scenarios_fetched(db_map_data) @Slot(object) def _receive_scenarios_alternatives_fetched(self, db_map_data): self._db_mngr.cache_items("scenario_alternative", db_map_data) @Slot(object) def _receive_object_classes_fetched(self, db_map_data): self._db_mngr.cache_items("object_class", db_map_data) self._db_mngr.update_icons(db_map_data) self._listener.receive_object_classes_fetched(db_map_data) @Slot(object) def _receive_objects_fetched(self, db_map_data): self._db_mngr.cache_items("object", db_map_data) self._listener.receive_objects_fetched(db_map_data) @Slot(object) def _receive_relationship_classes_fetched(self, db_map_data): self._db_mngr.cache_items("relationship_class", db_map_data) self._listener.receive_relationship_classes_fetched(db_map_data) @Slot(object) def _receive_relationships_fetched(self, db_map_data): self._db_mngr.cache_items("relationship", db_map_data) self._listener.receive_relationships_fetched(db_map_data) @Slot(object) def _receive_entity_groups_fetched(self, db_map_data): self._db_mngr.cache_items("entity_group", db_map_data) self._listener.receive_entity_groups_fetched(db_map_data) @Slot(object) def _receive_parameter_definitions_fetched(self, db_map_data): self._db_mngr.cache_items("parameter_definition", db_map_data) self._listener.receive_parameter_definitions_fetched(db_map_data) @Slot(object) def _receive_parameter_definition_tags_fetched(self, db_map_data): self._db_mngr.cache_items("parameter_definition_tag", db_map_data) @Slot(object) def _receive_parameter_values_fetched(self, db_map_data): self._db_mngr.cache_items("parameter_value", db_map_data) self._listener.receive_parameter_values_fetched(db_map_data) @Slot(object) def _receive_parameter_value_lists_fetched(self, db_map_data): self._db_mngr.cache_items("parameter_value_list", db_map_data) self._listener.receive_parameter_value_lists_fetched(db_map_data) @Slot(object) def _receive_parameter_tags_fetched(self, db_map_data): self._db_mngr.cache_items("parameter_tag", db_map_data) self._listener.receive_parameter_tags_fetched(db_map_data) @Slot(object) def _receive_features_fetched(self, db_map_data): self._db_mngr.cache_items("feature", db_map_data) self._listener.receive_features_fetched(db_map_data) @Slot(object) def _receive_tools_fetched(self, db_map_data): self._db_mngr.cache_items("tool", db_map_data) self._listener.receive_tools_fetched(db_map_data) @Slot(object) def _receive_tool_features_fetched(self, db_map_data): self._db_mngr.cache_items("tool_feature", db_map_data) self._listener.receive_tool_features_fetched(db_map_data) @Slot(object) def _receive_tool_feature_methods_fetched(self, db_map_data): self._db_mngr.cache_items("tool_feature_method", db_map_data) self._listener.receive_tool_feature_methods_fetched(db_map_data) @Slot() def _emit_finished(self): self.finished.emit(self)
class FilesystemMonitor(QObject): """ Class provides all functions needed to work with filesystem in scope of project """ max_file_name_length = MAX_FILE_NAME_LEN - 5 selective_sync_conflict_suffix = "selective sync conflict" started = pyqtSignal() stopped = pyqtSignal() process_offline = pyqtSignal(bool) def __init__(self, root, events_processing_delay, copies_storage, get_sync_dir_size, conflict_file_suffix='', tracker=None, storage=None, excluded_dirs=(), parent=None, max_relpath_len=3096, db_file_created_cb=None): QObject.__init__(self, parent=parent) freeze_support() self._tracker = tracker self._root = root self._path_converter = PathConverter(self._root) self._storage = storage if storage else Storage( self._path_converter, db_file_created_cb) self._copies_storage = copies_storage self._copies_storage.delete_copy.connect(self.on_delete_copy) self.possibly_sync_folder_is_removed = \ self._storage.possibly_sync_folder_is_removed self.db_or_disk_full = self._storage.db_or_disk_full self._get_sync_dir_size = get_sync_dir_size self._conflict_file_suffix = conflict_file_suffix self._rsync = Rsync _hide_files = HIDDEN_FILES _hide_dirs = HIDDEN_DIRS self._clean_recent_copies() self._actions = FsEventActions( self._root, events_processing_delay=events_processing_delay, path_converter=self._path_converter, storage=self._storage, copies_storage=self._copies_storage, rsync=self._rsync, tracker=self._tracker, parent=None, max_relpath_len=max_relpath_len, ) self._watch = WatchdogHandler(root=FilePath(self._root).longpath, hidden_files=_hide_files, hidden_dirs=_hide_dirs) self._download_watch = WatchdogHandler(root=FilePath( self._root).longpath, hidden_files=_hide_files, hidden_dirs=_hide_dirs, patterns=['*.download'], is_special=True) self._observer = ObserverWrapper(self._storage, self._get_sync_dir_size, self._tracker, parent=None) self._observer.event_handled.connect( self._observer.on_event_is_handled_slot) self._actions.event_passed.connect( lambda ev: self._observer.event_handled.emit(ev, False)) self._actions.event_suppressed.connect( lambda ev: self._observer.event_handled.emit(ev, True)) # Add FS root for events tracking self._observer.schedule(self._watch, root) self._local_processor = LocalProcessor(self._root, self._storage, self._path_converter, self._tracker) self.event_is_arrived = self._local_processor.event_is_arrived self._quiet_processor = QuietProcessor(self._root, self._storage, self._path_converter, self.Exceptions) self._files_list = FilesList(self._storage, self._root) self._thread = QThread() self._thread.started.connect(self._on_thread_started) self._actions.moveToThread(self._thread) self._observer.moveToThread(self._thread) self._watch.event_is_arrived.connect(self._on_event_arrived) self._download_watch.event_is_arrived.connect(self._on_event_arrived) self._actions.event_passed.connect(self._local_processor.process) self._local_events_flag = False self._actions.event_passed.connect(self._set_local_events_flag) self.error_happens = self._actions.error_happens self.no_disk_space = self._actions.no_disk_space self.idle = self._actions.idle self.working = self._actions.working self.file_added_to_ignore = self._actions.file_added_to_ignore self.file_removed_from_ignore = self._actions.file_removed_from_ignore self.file_added_to_indexing = self._actions.file_added_to_indexing self.file_removed_from_indexing = self._actions.file_removed_from_indexing self.file_added = self._actions.file_added self.file_modified = self._actions.file_modified self.file_deleted = Signal(str) self._actions.file_deleted.connect(self.file_deleted) self._quiet_processor.file_deleted.connect(self.file_deleted) self._quiet_processor.file_modified.connect(self.file_modified) self.file_moved = self._quiet_processor.file_moved self._actions.file_moved.connect(lambda o, n: self.file_moved(o, n)) self.access_denied = self._quiet_processor.access_denied self.file_list_changed = self._files_list.file_list_changed self.file_added.connect(self._files_list.on_file_added) self.file_deleted.connect(self._files_list.on_file_deleted) self.file_moved.connect(self._files_list.on_file_moved) self.file_modified.connect(self._files_list.on_file_modified) self.idle.connect(self._files_list.on_idle) self.process_offline.connect(self._observer.process_offline_changes) self.copy_added = Signal(str) self._actions.copy_added.connect(self.copy_added) self._actions.rename_file.connect(self._rename_file) self.special_file_event = Signal( str, # path int, # event type str) # new path self._special_files = list() self._excluded_dirs = list(map(FilePath, excluded_dirs)) self._online_processing_allowed = False self._online_modifies_processing_allowed = False self._paths_with_modify_quiet = set() def on_initial_sync_finished(self): logger.debug("on_initial_sync_finished") self._actions.on_initial_sync_finished() if not self._actions.get_fs_events_count() \ and not self._observer.is_processing_offline: self.idle.emit() def _on_processed_offline_changes(self): logger.debug("_on_processed_offline_changes") if not self._actions.get_fs_events_count(): self.idle.emit() def on_initial_sync_started(self): logger.debug("on_initial_sync_started") self._actions.on_initial_sync_started() self._online_processing_allowed = False self._online_modifies_processing_allowed = False def start_online_processing(self): logger.debug("start_online_processing") if not self._online_processing_allowed: logger.debug("start_online_processing, emit process_offline") self.process_offline.emit(self._online_modifies_processing_allowed) self._online_processing_allowed = True def start_online_modifies_processing(self): logger.debug("start_online_modifies_processing") if not self._online_modifies_processing_allowed: logger.debug( "start_online_modifies_processing, emit process_offline") self.process_offline.emit(True) self._online_modifies_processing_allowed = True def get_root(self): return self._root def root_exists(self): return op.isdir(self._root) def _on_thread_started(self): logger.info("Start monitoring of '%s'", self._root) self._observer.offline_event_occured.connect(self._on_event_arrived) self._observer.processed_offline_changes.connect( self._on_processed_offline_changes) self.started.emit() self._actions.start.emit() self._observer.start.emit() self._local_events_flag = False @benchmark def start(self): logger.debug("start") self._observer.set_active() if self._thread.isRunning(): self._on_thread_started() else: self._thread.start() self._files_list.start() def stop(self): logger.info("stopped monitoring") try: self._observer.offline_event_occured.disconnect( self._on_event_arrived) except RuntimeError: logger.warning("Can't disconnect offline_event_occured") try: self._observer.processed_offline_changes.disconnect( self._on_processed_offline_changes) except RuntimeError: logger.warning("Can't disconnect processed_offline_changes") self._actions.stop() self._observer.stop() self._files_list.stop() self.stopped.emit() def quit(self): self.stop() self._thread.quit() self._thread.wait() def is_processing(self, file_path): return self._actions.is_processing( self._path_converter.create_abspath(file_path)) def is_known(self, file_path): if file_path.endswith(FILE_LINK_SUFFIX): file_path = file_path[:-len(FILE_LINK_SUFFIX)] return self._storage.get_known_file(file_path) is not None def process_offline_changes(self): if self._local_events_flag: self.process_offline.emit(self._online_modifies_processing_allowed) self._local_events_flag = False def _set_local_events_flag(self, fs_event): if not fs_event.is_offline: self._local_events_flag = True def clean_storage(self): self._storage.clean() delete_file_links(self._root) def clean_copies(self, with_files=True): self._copies_storage.clean(with_files=with_files) def move_files_to_copies(self): with self._storage.create_session(read_only=False, locked=True) as session: files_with_hashes = session\ .query(File.relative_path, File.file_hash) \ .filter(File.is_folder == 0) \ .all() copies_dir = get_copies_dir(self._root) for (file, hashsum) in files_with_hashes: hash_path = op.join(copies_dir, hashsum) file_path = self._path_converter.create_abspath(file) if not op.exists(hash_path): try: os.rename(file_path, hash_path) except Exception as e: logger.error("Error moving file to copy: %s", e) remove_file(file_path) abs_path = FilePath(self._root).longpath folders_plus_hidden = [ self._path_converter.create_abspath(f) for f in os.listdir(abs_path) if f not in HIDDEN_DIRS ] for folder in folders_plus_hidden: if not op.isdir(folder): continue try: remove_dir(folder) except Exception as e: logger.error("Error removing dir '%s' (%s)", folder, e) logger.info("Removed all files and folders") self._storage.clean() def clean(self): files = self._storage.get_known_files() for file in files: try: remove_file(file) except Exception as e: logger.error("Error removing file '%s' (%s)", file, e) folders = self._storage.get_known_folders() for folder in sorted(folders, key=len): try: remove_dir(folder) except Exception as e: logger.error("Error removing dir '%s' (%s)", folder, e) logger.info("Removed all files and folders") self._storage.clean() def accept_delete(self, path, is_directory=False, events_file_id=None, is_offline=True): ''' Processes file deletion @param path Name of file relative to sync directory [unicode] ''' full_path = self._path_converter.create_abspath(path) object_type = 'directory' if is_directory else 'file' logger.debug("Deleting '%s' %s...", path, object_type) if is_directory: self._quiet_processor.delete_directory(full_path, events_file_id) else: self._quiet_processor.delete_file(full_path, events_file_id, is_offline) self.file_removed_from_indexing.emit(FilePath(full_path), True) logger.info("'%s' %s is deleted", path, object_type) def set_patch_uuid(self, patch_path, diff_file_uuid): shutil.move(patch_path, self.get_patch_path(diff_file_uuid)) def get_patch_path(self, diff_file_uuid): return os.path.join(get_patches_dir(self._root), diff_file_uuid) def create_directory(self, path, events_file_id): full_path = self._path_converter.create_abspath(path) try: self._quiet_processor.create_directory( full_path, events_file_id=events_file_id, wrong_file_id=self.Exceptions.WrongFileId) except AssertionError: self._on_event_arrived( FsEvent(DELETE, op.dirname(full_path), True, is_offline=True, quiet=True)) raise def apply_patch(self, filename, patch, new_hash, old_hash, events_file_id): ''' Applies given patch for the file specified @param filename Name of file relative to sync directory [unicode] @param patch Patch data [dict] ''' full_fn = self._path_converter.create_abspath(filename) try: self._apply_patch(full_fn, patch, new_hash, old_hash, events_file_id=events_file_id) except AssertionError: self._on_event_arrived( FsEvent(DELETE, op.dirname(full_fn), True, is_offline=True, quiet=True)) raise def accept_move(self, src, dst, is_directory=False, events_file_id=None, is_offline=True): src_full_path = self._path_converter.create_abspath(src) dst_full_path = self._path_converter.create_abspath(dst) try: object_type = 'directory' if is_directory else 'file' logger.debug("Moving '%s' %s to '%s'...", src, object_type, dst) if is_directory: self._quiet_processor.move_directory( src_full_path, dst_full_path, events_file_id, self.Exceptions.FileAlreadyExists, self.Exceptions.FileNotFound, wrong_file_id=self.Exceptions.WrongFileId) else: self._quiet_processor.move_file( src_full_path, dst_full_path, events_file_id, self.Exceptions.FileAlreadyExists, self.Exceptions.FileNotFound, wrong_file_id=self.Exceptions.WrongFileId, is_offline=is_offline) logger.info("'%s' %s is moved to '%s'", src, object_type, dst) self.file_removed_from_indexing.emit(FilePath(src_full_path), True) except AssertionError: self._on_event_arrived( FsEvent(DELETE, op.dirname(dst_full_path), True, is_offline=True, quiet=True)) raise def change_events_file_id(self, old_id, new_id): self._storage.change_events_file_id(old_id, new_id) class Exceptions(object): """ User-defined exceptions are stored here """ class FileNotFound(Exception): """ File doesn't exist exception""" def __init__(self, file): self.file = file def __str__(self): return repr(self.file) class FileAlreadyExists(Exception): """ File already exists exception (for move) """ def __init__(self, path): self.path = path def __str__(self): return "File already exists {}".format(self.path) class AccessDenied(Exception): """ Access denied exception (for move or delete) """ def __init__(self, path): self.path = path def __str__(self): return "Access denied for {}".format(self.path) class WrongFileId(Exception): """ Wrong file if exception """ def __init__(self, path, file_id_expected=None, file_id_got=None): self.path = path self.file_id_expected = file_id_expected self.file_id_got = file_id_got def __str__(self): return "Wrong file id for {}. Expected id {}. Got id {}".format( self.path, self.file_id_expected, self.file_id_got) class CopyDoesNotExists(Exception): def __init__(self, hash): self.hash = hash def __str__(self): return "Copy with hash {} does not exists".format(self.hash) def _apply_patch(self, filename, patch, new_hash, old_hash, silent=True, events_file_id=None): start_time = time.time() patch_size = os.stat(patch).st_size success = False try: patched_new_hash, old_hash = self._quiet_processor.patch_file( filename, patch, silent=silent, events_file_id=events_file_id, wrong_file_id=self.Exceptions.WrongFileId) assert patched_new_hash == new_hash success = True self.copy_added.emit(new_hash) except Rsync.AlreadyPatched: success = True except: raise finally: if self._tracker: try: file_size = os.stat(filename).st_size except OSError: file_size = 0 duration = time.time() - start_time self._tracker.monitor_patch_accept(file_size, patch_size, duration, success) def generate_conflict_file_name(self, filename, is_folder=False, name_suffix=None, with_time=True): orig_filename = filename directory, filename = op.split(filename) original_ext = '' if is_folder: original_name = filename else: # consider ext as 2 '.'-delimited last filename substrings # if they don't contain spaces dots_list = filename.split('.') name_parts_len = len(dots_list) for k in range(1, min(name_parts_len, 3)): if ' ' in dots_list[-k]: break original_ext = '.{}{}'.format(dots_list[k], original_ext) name_parts_len -= 1 original_name = '.'.join(dots_list[:name_parts_len]) index = 0 if name_suffix is None: name_suffix = self._conflict_file_suffix date_today = date.today().strftime('%d-%m-%y') if with_time else '' suffix = '({} {})'.format(name_suffix, date_today) while len(bytes(suffix.encode('utf-8'))) > \ int(self.max_file_name_length / 3): suffix = suffix[int(len(suffix) / 2):] name = '{}{}{}'.format(original_name, suffix, original_ext) while True: to_cut = len(bytes(name.encode('utf-8'))) - \ self.max_file_name_length if to_cut <= 0: break if len(original_name) > to_cut: original_name = original_name[:-to_cut] else: remained = to_cut - len(original_name) + 1 original_name = original_name[:1] if remained < len(original_ext): original_ext = original_ext[remained:] else: original_ext = original_ext[int(len(original_ext) / 2):] name = '{}{}{}'.format(original_name, suffix, original_ext) while op.exists( self._path_converter.create_abspath( FilePath(op.join(directory, name)))): index += 1 name = '{}{} {}{}'.format(original_name, suffix, index, original_ext) conflict_file_name = FilePath(op.join(directory, name)) logger.info( "Generated conflict file name: %s, original name: %s, " "is_folder: %s, name_suffix: %s, with_time: %s", conflict_file_name, orig_filename, is_folder, name_suffix, with_time) return conflict_file_name def move_file(self, src, dst, is_offline=True): src_full_path = self._path_converter.create_abspath(src) dst_full_path = self._path_converter.create_abspath(dst) is_offline = True if op.isdir(src_full_path) else is_offline src_hard_path = self._quiet_processor.get_hard_path( src_full_path, is_offline) dst_hard_path = self._quiet_processor.get_hard_path( dst_full_path, is_offline) if not op.exists(src_hard_path): raise self.Exceptions.FileNotFound(src_full_path) elif op.exists(dst_hard_path): raise self.Exceptions.FileAlreadyExists(dst_full_path) dst_parent_folder_path = op.dirname(dst_full_path) if not op.exists(dst_parent_folder_path): self._on_event_arrived( FsEvent(DELETE, dst_parent_folder_path, True, is_offline=True, quiet=True)) try: os.rename(src_hard_path, dst_hard_path) except OSError as e: logger.warning("Can't move file (dir) %s. Reason: %s", src_full_path, e) if e.errno == errno.EACCES: self._quiet_processor.access_denied() raise self.Exceptions.AccessDenied(src_full_path) else: raise e def copy_file(self, src, dst, is_directory=False, is_offline=True): is_offline = True if is_directory else is_offline src_full_path = self._path_converter.create_abspath(src) dst_full_path = self._path_converter.create_abspath(dst) src_hard_path = self._quiet_processor.get_hard_path( src_full_path, is_offline) dst_hard_path = self._quiet_processor.get_hard_path( dst_full_path, is_offline) if not op.exists(src_hard_path): raise self.Exceptions.FileNotFound(src_full_path) if is_directory: shutil.copytree(src_full_path, dst_full_path) else: common.utils.copy_file(src_hard_path, dst_hard_path) def restore_file_from_copy(self, file_name, copy_hash, events_file_id, search_by_id=False): try: old_hash = self._quiet_processor.create_file_from_copy( file_name, copy_hash, silent=True, events_file_id=events_file_id, search_by_id=search_by_id, wrong_file_id=self.Exceptions.WrongFileId, copy_does_not_exists=self.Exceptions.CopyDoesNotExists) except AssertionError: self._on_event_arrived( FsEvent(DELETE, op.dirname( self._path_converter.create_abspath(file_name)), True, is_offline=True, quiet=True)) raise return old_hash def create_file_from_copy(self, file_name, copy_hash, events_file_id, search_by_id=False): self.restore_file_from_copy(file_name, copy_hash, events_file_id=events_file_id, search_by_id=search_by_id) @benchmark def make_copy_from_existing_files(self, copy_hash): self._quiet_processor.make_copy_from_existing_files(copy_hash) def create_empty_file(self, file_name, file_hash, events_file_id, search_by_id=False, is_offline=True): try: self._quiet_processor.create_empty_file( file_name, file_hash, silent=True, events_file_id=events_file_id, search_by_id=search_by_id, wrong_file_id=self.Exceptions.WrongFileId, is_offline=is_offline) except AssertionError: self._on_event_arrived( FsEvent(DELETE, op.dirname( self._path_converter.create_abspath(file_name)), True, is_offline=True, quiet=True)) raise def on_delete_copy(self, hash, with_signature=True): if not hash: logger.error("Invalid hash '%s'", hash) return copy = op.join(get_copies_dir(self._root), hash) try: remove_file(copy) logger.info("File copy deleted %s", copy) if not with_signature: return signature = op.join(get_signatures_dir(self._root), hash) remove_file(signature) logger.info("File copy signature deleted %s", signature) except Exception as e: logger.error( "Can't delete copy. " "Possibly sync folder is removed %s", e) self.possibly_sync_folder_is_removed() def delete_old_signatures(self, delete_all=False): logger.debug("Deleting old signatures...") self._quiet_processor.delete_old_signatures( get_signatures_dir(self._root), delete_all) def path_exists(self, path, is_offline=True): full_path = self._path_converter.create_abspath(path) hard_path = self._quiet_processor.get_hard_path(full_path, is_offline) return op.exists(hard_path) def rename_excluded(self, rel_path): logger.debug("Renaming excluded dir %s", rel_path) new_path = self.generate_conflict_file_name( rel_path, name_suffix=self.selective_sync_conflict_suffix, with_time=False) self.move_file(rel_path, new_path) def _rename_file(self, abs_path): rel_path = self._path_converter.create_relpath(abs_path) new_path = self.generate_conflict_file_name(rel_path, is_folder=False, name_suffix="", with_time=True) self.move_file(rel_path, new_path) def db_file_exists(self): return self._storage.db_file_exists() def _clean_recent_copies(self): mask = op.join(get_copies_dir(self._root), "*.recent_copy_[0-9]*") recent_copies = glob.glob(mask) list(map(os.remove, recent_copies)) def add_special_file(self, path): self._special_files.append(path) watch = None if not (path in FilePath(self._root)): watch = self._download_watch self._observer.add_special_file(path, watch) def remove_special_file(self, path): logger.debug("Removing special file %s...", path) if not (path in FilePath(self._root)): self._observer.remove_special_file(path) try: self._special_files.remove(path) except ValueError: logger.warning("Can't remove special file %s from list %s", path, self._special_files) def change_special_file(self, old_file, new_file): self.add_special_file(new_file) self.remove_special_file(old_file) def _on_event_arrived(self, fs_event, is_special=False): logger.debug( "Event arrived %s, special %s, online_processing_allowed: %s, " "online_modifies_processing_allowed: %s", fs_event, is_special, self._online_processing_allowed, self._online_modifies_processing_allowed) if is_special or fs_event.src in self._special_files: self.special_file_event.emit(fs_event.src, fs_event.event_type, fs_event.dst) elif fs_event.is_offline or self._online_processing_allowed: if not self._online_modifies_processing_allowed and \ not fs_event.is_offline and fs_event.event_type == MODIFY: return elif fs_event.src in self._paths_with_modify_quiet \ and fs_event.event_type in (CREATE, MODIFY): fs_event.is_offline = True fs_event.quiet = True path = fs_event.src if fs_event.event_type == CREATE \ else fs_event.dst if fs_event.event_type == MOVE else "" name = op.basename(path) parent_path = op.dirname(path) stripped_name = name.strip() if stripped_name != name: new_path = op.join(parent_path, stripped_name) if op.exists(new_path): new_path = self.generate_conflict_file_name( new_path, is_folder=fs_event.is_dir, name_suffix="", with_time=True) logger.debug("Renaming '%s' to '%s'...", path, new_path) os.rename(FilePath(path).longpath, FilePath(new_path).longpath) path = new_path if fs_event.event_type == CREATE: fs_event.src = new_path elif fs_event.event_type == MOVE: fs_event.dst = new_path hidden_dir = FilePath( self._path_converter.create_abspath(HIDDEN_DIRS[0])) if fs_event.event_type == MOVE: if FilePath(fs_event.src) in hidden_dir or \ op.basename(fs_event.src).startswith('._'): fs_event.event_type = CREATE fs_event.src = fs_event.dst fs_event.dst = None elif FilePath(fs_event.dst) in hidden_dir or \ op.basename(fs_event.dst).startswith('._'): fs_event.event_type = DELETE fs_event.dst = None if FilePath(fs_event.src) in hidden_dir or \ op.basename(fs_event.src).startswith('._'): return if FilePath(path) in self._excluded_dirs: self.rename_excluded(self._path_converter.create_relpath(path)) else: self._actions.add_new_event(fs_event) def get_long_paths(self): return self._actions.get_long_paths() def set_excluded_dirs(self, excluded_dirs): self._excluded_dirs = list(map(FilePath, excluded_dirs)) def remove_dir_from_excluded(self, directory): try: self._excluded_dirs.remove(directory) except Exception as e: logger.warning("Can't remove excluded dir %s from %s. Reason: %s", directory, self._excluded_dirs, e) def sync_events_file_id(self, file_path, events_file_id, is_folder): self._quiet_processor.sync_events_file_id(file_path, events_file_id, is_folder) def sync_events_file_id_by_old_id(self, events_file_id, old_events_file_id): self._quiet_processor.sync_events_file_id_by_old_id( events_file_id, old_events_file_id) def set_collaboration_folder_icon(self, folder_name): set_custom_folder_icon('collaboration', self._root, folder_name) def reset_collaboration_folder_icon(self, folder_name): reset_custom_folder_icon(self._root, folder_name, resource_name='collaboration') def reset_all_collaboration_folder_icons(self): root_folders = [ f for f in os.listdir(self._root) if op.isdir(self._path_converter.create_abspath(f)) ] logger.debug("root_folders %s", root_folders) list(map(self.reset_collaboration_folder_icon, root_folders)) def get_excluded_dirs_to_change(self, excluded_dirs, src_path, dst_path=None): src_path = FilePath(src_path) if dst_path: dst_path = FilePath(dst_path) excluded_dirs = list(map(FilePath, excluded_dirs)) dirs_to_add = [] dirs_to_delete = list(filter(lambda ed: ed in src_path, excluded_dirs)) if dst_path is not None and \ not is_contained_in_dirs(dst_path, excluded_dirs): # we have to add new excluded dirs only if folder is not moved # to excluded dir l = len(src_path) dirs_to_add = [dst_path + d[l:] for d in dirs_to_delete] logger.debug( "get_excluded_dirs_to_change. " "excluded_dirs %s, src_path %s, dst_path %s, " "dirs_to_delete %s, dirs_to_add %s", excluded_dirs, src_path, dst_path, dirs_to_delete, dirs_to_add) return dirs_to_delete, dirs_to_add def change_excluded_dirs(self, dirs_to_delete, dirs_to_add): for directory in dirs_to_delete: self.remove_dir_from_excluded(directory) for directory in dirs_to_add: self._excluded_dirs.append(directory) def clear_excluded_dirs(self): self._excluded_dirs = [] def get_fs_events_count(self): return self._actions.get_fs_events_count() def force_create_copies(self): self._storage.clear_files_hash_mtime() self.delete_old_signatures(delete_all=True) self._local_events_flag = True self.process_offline_changes() def get_file_list(self): return self._files_list.get() def get_actual_events_file_id(self, path, is_folder=None): abs_path = self._path_converter.create_abspath(path) file = self._storage.get_known_file(abs_path, is_folder=is_folder) return file.events_file_id if file else None def is_directory(self, path): abs_path = self._path_converter.create_abspath(path) return op.isdir(abs_path) def set_waiting(self, to_wait): self._actions.set_waiting(to_wait) def set_path_quiet(self, path): logger.debug("Setting path %s quiet...", path) self._paths_with_modify_quiet.add(FilePath(path)) def clear_paths_quiet(self): logger.debug("Clearing quiet paths...") self._paths_with_modify_quiet.clear() def delete_files_with_empty_events_file_ids(self): if self._storage.delete_files_with_empty_events_file_ids(): self.working.emit() def is_file_in_storage(self, events_file_id): return self._storage.get_known_file_by_id(events_file_id)
class View(QMainWindow): def __init__(self, model): super().__init__() self.setWindowTitle("OpenHRV") self.setWindowIcon(QIcon(":/logo.png")) self.setGeometry(50, 50, 1750, 850) self.model = model self.signals = ViewSignals() self.scanner = SensorScanner() self.scanner_thread = QThread(self) self.scanner.moveToThread(self.scanner_thread) self.scanner.mac_update.connect(self.model.set_mac_addresses) self.sensor = SensorClient() self.sensor_thread = QThread(self) self.sensor.moveToThread(self.sensor_thread) self.sensor.ibi_update.connect(self.model.set_ibis_buffer) self.sensor_thread.started.connect(self.sensor.run) self.redis_publisher = RedisPublisher() self.redis_publisher_thread = QThread(self) self.redis_publisher.moveToThread(self.redis_publisher_thread) self.model.ibis_buffer_update.connect(self.redis_publisher.publish) self.model.mean_hrv_update.connect(self.redis_publisher.publish) self.model.mac_addresses_update.connect(self.redis_publisher.publish) self.model.pacer_rate_update.connect(self.redis_publisher.publish) self.model.hrv_target_update.connect(self.redis_publisher.publish) self.model.biofeedback_update.connect(self.redis_publisher.publish) self.signals.annotation.connect(self.redis_publisher.publish) self.redis_publisher_thread.started.connect( self.redis_publisher.monitor.start) self.redis_logger = RedisLogger() self.redis_logger_thread = QThread(self) self.redis_logger.moveToThread(self.redis_logger_thread) self.redis_logger_thread.finished.connect( self.redis_logger.save_recording) self.redis_logger.recording_status.connect(self.show_recording_status) self.ibis_plot = pg.PlotWidget() self.ibis_plot.setBackground("w") self.ibis_plot.setLabel("left", "Inter-Beat-Interval (msec)", **{"font-size": "25px"}) self.ibis_plot.setLabel("bottom", "Seconds", **{"font-size": "25px"}) self.ibis_plot.showGrid(y=True) self.ibis_plot.setYRange(300, 1500, padding=0) self.ibis_plot.setMouseEnabled(x=False, y=False) self.ibis_signal = pg.PlotCurveItem() pen = pg.mkPen(color=(0, 191, 255), width=7.5) self.ibis_signal.setPen(pen) self.ibis_signal.setData(self.model.ibis_seconds, self.model.ibis_buffer) self.ibis_plot.addItem(self.ibis_signal) self.mean_hrv_plot = pg.PlotWidget() self.mean_hrv_plot.setBackground("w") self.mean_hrv_plot.setLabel("left", "HRV (msec)", **{"font-size": "25px"}) self.mean_hrv_plot.setLabel("bottom", "Seconds", **{"font-size": "25px"}) self.mean_hrv_plot.showGrid(y=True) self.mean_hrv_plot.setYRange(0, 600, padding=0) self.mean_hrv_plot.setMouseEnabled(x=False, y=False) colorgrad = QLinearGradient(0, 0, 0, 1) # horizontal gradient colorgrad.setCoordinateMode(QGradient.ObjectMode) colorgrad.setColorAt(0, pg.mkColor("g")) colorgrad.setColorAt(.5, pg.mkColor("y")) colorgrad.setColorAt(1, pg.mkColor("r")) brush = QBrush(colorgrad) self.mean_hrv_plot.getViewBox().setBackgroundColor(brush) self.mean_hrv_signal = pg.PlotCurveItem() pen = pg.mkPen(color="w", width=7.5) self.mean_hrv_signal.setPen(pen) self.mean_hrv_signal.setData(self.model.mean_hrv_seconds, self.model.mean_hrv_buffer) self.mean_hrv_plot.addItem(self.mean_hrv_signal) self.pacer_plot = pg.PlotWidget() self.pacer_plot.setBackground("w") self.pacer_plot.setAspectLocked(lock=True, ratio=1) self.pacer_plot.setMouseEnabled(x=False, y=False) self.pacer_plot.disableAutoRange() self.pacer_plot.setXRange(-1, 1, padding=0) self.pacer_plot.setYRange(-1, 1, padding=0) self.pacer_plot.hideAxis("left") self.pacer_plot.hideAxis("bottom") self.pacer_disc = pg.PlotCurveItem() brush = pg.mkBrush(color=(135, 206, 250)) self.pacer_disc.setBrush(brush) self.pacer_disc.setFillLevel(1) self.pacer_plot.addItem(self.pacer_disc) self.pacer_rate = QSlider(Qt.Horizontal) self.pacer_rate.setTracking(False) self.pacer_rate.setRange( 0, 6) # transformed to bpm [4, 7], step .5 by model self.pacer_rate.valueChanged.connect(self.model.set_breathing_rate) self.pacer_rate.setSliderPosition(4) # corresponds to 6 bpm self.pacer_label = QLabel(f"Rate: {self.model.breathing_rate}") self.pacer_toggle = QCheckBox("Show pacer", self) self.pacer_toggle.setChecked(True) self.pacer_toggle.stateChanged.connect(self.toggle_pacer) self.hrv_target_label = QLabel(f"Target: {self.model.hrv_target}") self.hrv_target = QSlider(Qt.Horizontal) self.hrv_target.setRange(50, 600) self.hrv_target.setSingleStep(10) self.hrv_target.valueChanged.connect(self.model.set_hrv_target) self.hrv_target.setSliderPosition(self.model.hrv_target) self.mean_hrv_plot.setYRange(0, self.model.hrv_target, padding=0) self.scan_button = QPushButton("Scan") self.scan_button.clicked.connect(self.scanner.scan) self.mac_menu = QComboBox() self.connect_button = QPushButton("Connect") self.connect_button.clicked.connect(self.connect_sensor) self.start_recording_button = QPushButton("Start") self.start_recording_button.clicked.connect( self.redis_logger.start_recording) self.save_recording_button = QPushButton("Save") self.save_recording_button.clicked.connect( self.redis_logger.save_recording) self.annotation = QComboBox() self.annotation.setEditable(True) self.annotation.setDuplicatesEnabled(False) self.annotation.addItems([ "start_baseline", "end_baseline", "start_bf", "end_bf", "start_nobf", "end_nobf" ]) self.annotation.setMaxCount( 10) # user can configure up to 4 additional custom annotations self.annotation_button = QPushButton("Annotate") self.annotation_button.clicked.connect(self.emit_annotation) self.central_widget = QWidget() self.setCentralWidget(self.central_widget) self.recording_status_label = QLabel("Status:") self.recording_statusbar = QProgressBar() self.recording_statusbar.setRange(0, 1) self.vlayout0 = QVBoxLayout(self.central_widget) self.hlayout0 = QHBoxLayout() self.hlayout0.addWidget(self.ibis_plot, stretch=80) self.hlayout0.addWidget(self.pacer_plot, stretch=20) self.vlayout0.addLayout(self.hlayout0) self.vlayout0.addWidget(self.mean_hrv_plot) self.hlayout1 = QHBoxLayout() self.device_config = QFormLayout() self.device_config.addRow(self.scan_button, self.mac_menu) self.device_config.addRow(self.connect_button) self.device_panel = QGroupBox("ECG Devices") self.device_panel.setLayout(self.device_config) self.hlayout1.addWidget(self.device_panel, stretch=25) self.hrv_config = QFormLayout() self.hrv_config.addRow(self.hrv_target_label, self.hrv_target) self.hrv_panel = QGroupBox("HRV Settings") self.hrv_panel.setLayout(self.hrv_config) self.hlayout1.addWidget(self.hrv_panel, stretch=25) self.pacer_config = QFormLayout() self.pacer_config.addRow(self.pacer_label, self.pacer_rate) self.pacer_config.addRow(self.pacer_toggle) self.pacer_panel = QGroupBox("Breathing Pacer") self.pacer_panel.setLayout(self.pacer_config) self.hlayout1.addWidget(self.pacer_panel, stretch=25) self.recording_config = QGridLayout() self.recording_config.addWidget(self.start_recording_button, 0, 0) self.recording_config.addWidget(self.save_recording_button, 0, 1) self.recording_config.addWidget(self.recording_statusbar, 0, 2) self.recording_config.addWidget(self.annotation, 1, 0, 1, 2) # row, column, rowspan, columnspan self.recording_config.addWidget(self.annotation_button, 1, 2) self.recording_panel = QGroupBox("Recording") self.recording_panel.setLayout(self.recording_config) self.hlayout1.addWidget(self.recording_panel, stretch=25) self.vlayout0.addLayout(self.hlayout1) self.model.ibis_buffer_update.connect(self.plot_ibis) self.model.mean_hrv_update.connect(self.plot_hrv) self.model.mac_addresses_update.connect(self.list_macs) self.model.pacer_disk_update.connect(self.plot_pacer_disk) self.model.pacer_rate_update.connect(self.update_pacer_label) self.model.hrv_target_update.connect(self.update_hrv_target) self.scanner_thread.start() self.sensor_thread.start() self.redis_publisher_thread.start() self.redis_logger_thread.start() def closeEvent(self, event): """Properly shut down all threads.""" print("Closing threads...") self.scanner_thread.quit() self.scanner_thread.wait() self.sensor_thread.quit( ) # since quit() only works if the thread has a running event loop... asyncio.run_coroutine_threadsafe( self.sensor.stop(), self.sensor.loop ) # ...the event loop must only be stopped AFTER quit() has been called! self.sensor_thread.wait() self.redis_publisher_thread.quit() self.redis_publisher_thread.wait() self.redis_logger_thread.quit() self.redis_logger_thread.wait() def connect_sensor(self): mac = self.mac_menu.currentText() if not valid_mac(mac): print("Invalid MAC.") return asyncio.run_coroutine_threadsafe(self.sensor.connect_client(mac), self.sensor.loop) def plot_ibis(self, ibis): self.ibis_signal.setData(self.model.ibis_seconds, ibis[1]) def plot_hrv(self, hrv): self.mean_hrv_signal.setData(self.model.mean_hrv_seconds, hrv[1]) def list_macs(self, macs): self.mac_menu.clear() self.mac_menu.addItems(macs[1]) def plot_pacer_disk(self, coordinates): self.pacer_disc.setData(*coordinates[1]) def update_pacer_label(self, rate): self.pacer_label.setText(f"Rate: {rate[1]}") def update_hrv_target(self, target): self.mean_hrv_plot.setYRange(0, target[1], padding=0) self.hrv_target_label.setText(f"Target: {target[1]}") def toggle_pacer(self): visible = self.pacer_plot.isVisible() self.pacer_plot.setVisible(not visible) def show_recording_status(self, status): self.recording_statusbar.setRange( 0, status) # indicates busy state if progress is 0 def emit_annotation(self): self.signals.annotation.emit( ("eventmarker", self.annotation.currentText()))
class RssTab(QWidget): new_torrents = Signal(int) def __init__(self): QWidget.__init__(self) layout = QVBoxLayout(self) self.splitter = QSplitter(self) self.cats = QTreeView(self) self.cats.setSortingEnabled(True) self.cat_model = RssCategoryModel() proxy = QSortFilterProxyModel() proxy.setSourceModel(self.cat_model) self.cats.setModel(proxy) self.splitter.addWidget(self.cats) self.t = QTableWidget(0, 4, self) self.splitter.addWidget(self.t) self.stats = [QLabel('{}'.format(datetime.now())) for _ in range(8)] stat: QLabel for stat in self.stats: stat.setFont(QFont(pointSize=14)) layout.addWidget(stat, 0, Qt.AlignTop) layout.addWidget(self.splitter, 0, Qt.AlignTop) self.ds = DataSource() self.f_model = ForumsModel(self.ds.get_forums()) self.forums = QTableView(self) self.forums.setModel(self.f_model) self.forums.resizeColumnsToContents() layout.addWidget(self.forums, 10, Qt.AlignTop) self.setLayout(layout) self.worker = RssWorker() self.worker_thread = QThread() self.worker_thread.started.connect(self.worker.run) self.worker.finished.connect(self.worker_thread.quit) self.worker.moveToThread(self.worker_thread) self.worker_thread.start() self.worker.processed.connect(self.processed) self.worker.current.connect(self.current) @Slot(str) def current(self, topic): for i in range(len(self.stats) - 1): self.stats[i].setText(self.stats[i + 1].text()) self.stats[len(self.stats) - 1].setText('{0} - {1}'.format( datetime.now(), topic)) @Slot(int, int) def processed(self, forum_id, torrents): print('\t\t\tRSS: ' + str(forum_id) + ', ' + str(torrents)) forum = self.ds.get_forum(forum_id) print('\t\t\tRSS FORUM: ' + str(forum)) cat = self.ds.get_category(forum['category']) print('\t\t\tRSS CAT: ' + str(cat)) self.cat_model.addCategory(cat['title'], torrents) def finish(self): self.worker.finish() self.worker_thread.quit() self.worker_thread.wait()
class MyWidget(QWidget): MAX_SHOWN_LINES = 10 PROCESS_NUMBER = 2 STOP_THREADS = False # Files APP_DIR = 'app' HTML = '.html' XML = '.xml' TXT = '.txt' EXTENSIONS = [TXT, XML, HTML] def __init__(self): QWidget.__init__(self) # Configuration from CLI interface self.file_names = [] self.disable_logging = False self.result_filter = None self.filters = [] self.configuration = [] # Internal subprocess attributes # Shared queues, first for steps and second for processes self.queues = None self.worker = None self.processes = [] self.thread = None self.show_steps_thread = None self.web_views = [] # Create Menu bar self.setWindowTitle("Sec&Po testing framework") self.main_menu = QMenuBar() self.file_menu = self.main_menu.addMenu("File") self.configure_menu = self.main_menu.addMenu("Configure") # Set actions file_actions = self.create_file_actions() for action in file_actions: self.file_menu.addAction(action) manage_actions = self.create_management_actions() for action in manage_actions: self.configure_menu.addAction(action) # Create main window self.selected_files = QLabel( "Here will be printed some of the selected files") self.finish_button = QPushButton("&Run security and" " portability testing") # Connecting the signal self.finish_button.clicked.connect(self.run_testing) self.results_label = QLabel("No results") self.show_results_button = QPushButton("&Show results") self.show_results_button.clicked.connect(self.load_html_result) # Create file dialog self.file_dialog = QFileDialog(self) # fixme: debug for pycharm # self.file_dialog.setOption(QFileDialog.DontUseNativeDialog, True) self.file_dialog.setFileMode(QFileDialog.ExistingFiles) file_filters = ["All files files (*)"] for value in ProgramTypes: file_filters.append(value.name + " files (") for program_filters in next(iter(value.value.values())): file_filters[len(file_filters) - 1] += "*" + program_filters\ + " " file_filters[len(file_filters) - 1] += ")" self.file_dialog.setNameFilters(file_filters) self.file_dialog.setViewMode(QFileDialog.Detail) # Add proxy model to change default behaviour or file selector # todo: does not work with files only directories. # However selecting of both must be overridden with own QFileDialog # proxy = ProxyModel(self.file_dialog) # self.file_dialog.setProxyModel(proxy) # Set layout of elements self._set_layout() def _set_layout(self): self.layout = QFormLayout() self.layout.setFormAlignment(Qt.AlignHCenter | Qt.AlignTop) self.layout.setFieldGrowthPolicy(QFormLayout.FieldsStayAtSizeHint) self.layout.setVerticalSpacing(150) self.layout.setHorizontalSpacing(220) self.layout.setLabelAlignment(Qt.AlignRight) self.layout.addRow(self.main_menu) self.finish_button.setFixedSize(250, 65) self.layout.addRow(self.selected_files, self.finish_button) self.show_results_button.setFixedSize(250, 65) self.layout.addRow(self.results_label, self.show_results_button) self.setLayout(self.layout) def create_file_actions(self): openAct = QAction(QIcon("images/folder.png"), self.tr("&Open..."), self) openAct.setShortcuts(QKeySequence.Open) openAct.setStatusTip( self.tr("Open an existing file, " "files or directories")) openAct.triggered.connect(self.open) addFilter = QAction(QIcon("images/filter.png"), self.tr("&Add filter"), self) # addFilter.setShortcuts() addFilter.setStatusTip( self.tr("Select filter files to apply " " result filtering")) addFilter.triggered.connect(self.add_filter) return [openAct, addFilter] def create_management_actions(self): manageAct = QAction(QIcon("images/settings.png"), self.tr("&Manage filters"), self) # manageAct.setShortcuts(QKeySequence.Open) manageAct.setStatusTip(self.tr("Manage added filters")) manageAct.triggered.connect(self.manage_filters) docker_conf_action = QAction(QIcon("images/docker.png"), self.tr("&Docker"), self) # docker_conf_action.setShortcuts() docker_conf_action.setStatusTip(self.tr("Docker")) docker_conf_action.triggered.connect(self.docker_conf) apparmor_conf_action = QAction(QIcon("images/apparmor.png"), self.tr("&Apparmor"), self) # apparmor_conf_action.setShortcuts() apparmor_conf_action.setStatusTip(self.tr("Apparmor")) apparmor_conf_action.triggered.connect(self.apparmor_conf) seccomp_conf_action = QAction(QIcon("images/lock.png"), self.tr("&Seccomp"), self) # docker_conf_action.setShortcuts() seccomp_conf_action.setStatusTip(self.tr("Seccomp")) seccomp_conf_action.triggered.connect(self.seccomp_conf) return [ manageAct, docker_conf_action, apparmor_conf_action, seccomp_conf_action ] def add_filter(self): pass def manage_filters(self): output = subprocess.check_output(['secpo', '--list-filters']) dialog = QDialog(self) label = QLabel("Managed filters are: {}".format( output.decode('utf-8'))) # dialog.set widgets = QWidget() layout = QHBoxLayout() layout.addWidget(dialog) layout.addWidget(label) widgets.setLayout(layout) widgets.show() dialog.exec() pass def docker_conf(self): pass def apparmor_conf(self): pass def seccomp_conf(self): pass def check_result_dir(self, file_name): result_dir = file_name.parent / self.APP_DIR if result_dir.exists(): self.results_label.setText("Results present") def open(self): output = 'No files chosen!' self.file_names = [] cnt = 0 # fixme: running code from pycharm causes to not use # regular path system but symlinks # https://stackoverflow.com/questions/57646908/pyqt5-qfiledialog-is-not-returning-correct-paths-in-ubuntu if self.file_dialog.exec(): self.file_names = self.file_dialog.selectedFiles() if self.file_names: output = '' for file_name in self.file_names: if cnt < self.MAX_SHOWN_LINES and file_name: # Correct the path absolute_path = pathlib.Path(file_name) self.check_result_dir(absolute_path) output += str(pathlib.Path(absolute_path.parts [len(absolute_path.parts) - 2])\ / pathlib.Path(absolute_path.name)) + '\n' cnt += 1 self.selected_files.setText(output + "\n") def open_single_result_view(self, result_file): if isinstance(self.web_views[-1], QWebEngineView): self.web_views[-1].load(QUrl(result_file.as_uri())) self.web_views[-1].showMaximized() elif isinstance(self.web_views[-1], QTextBrowser): text_doc = QTextDocument() self.web_views[-1].setDocument(text_doc) self.web_views[-1].setSource( QUrl(result_file.as_uri(), QUrl.ParsingMode.TolerantMode)) self.web_views[-1].show() def load_html_result(self): for file in self.file_names: root_dir = pathlib.Path(file).parent root_dir /= self.APP_DIR if root_dir.exists(): for extension in self.EXTENSIONS: result_files = root_dir.glob('**/*' + extension) for result_file in result_files: if extension == self.HTML or extension == self.XML: self.web_views.append(QWebEngineView()) elif extension == self.TXT: self.web_views.append(QTextBrowser()) self.open_single_result_view(result_file) def show_steps(self, progress): while True: time.sleep(0.5) if not self.queues[0].empty(): steps = self.queues[0].get() progress.setValue(steps.value) if steps.value == len(self.file_names) * 2: self.thread.quit() self.check_result_dir(pathlib.Path(self.file_names[0])) break if self.STOP_THREADS: break def remove_processes(self): self.processes = [] def cancel(self): for process in self.processes: self.queues[1].put(process.pid) time.sleep(5) # Set stop threads to true to indicate end of program self.STOP_THREADS = True if self.thread: self.thread.quit() self.processes = [] def run_testing(self): if not self.file_names: self.selected_files.setText( "No files were selected. Go to menu and " "select files to test.") return # Create new queues every run self.queues = [ multiprocessing.Queue(maxsize=self.PROCESS_NUMBER), multiprocessing.Queue(maxsize=self.PROCESS_NUMBER) ] self.STOP_THREADS = False progress = QProgressDialog("Starting Docker and VMs", "Abort start", 0, len(self.file_names) * 2, self) progress.canceled.connect(self.cancel) progress.setWindowModality(Qt.WindowModal) # Create Qthread to show progress self.thread = QThread() self.worker = Worker(self, self.file_names, self.queues) self.worker.moveToThread(self.thread) # Custom signals connected to own functions for progress dialog self.show_steps_thread = threading.Thread(target=self.show_steps, args=(progress, )) self.show_steps_thread.start() # Thread start and stop signals connected with slots self.thread.started.connect(self.worker.run) self.thread.finished.connect(self.remove_processes) # Start thread and force to show progress dialog self.thread.start() progress.forceShow() def closeEvent(self, event: PySide2.QtGui.QCloseEvent): event.ignore() super(MyWidget, self).closeEvent(event) # Indicate end of program if self.thread: self.thread.quit() self.thread.wait() for process in self.processes: if process: if os.name == 'nt': process.send_signal(signal.CTRL_C_EVENT) else: process.send_signal(signal.SIGINT)
class MainWindow(QMainWindow): """ Class that describes main window of the app """ def signals(self, main_window_ui): """ Connect signals from ui """ self.connect(main_window_ui.import_text, SIGNAL("triggered()"), self.import_text) self.connect(main_window_ui.open_document, SIGNAL("triggered()"), self.open_document) self.connect(main_window_ui.save_document, SIGNAL("triggered()"), self.save_document) self.main_window_ui.aspects_list.itemClicked.connect( self.select_aspect) self.main_window_ui.uncheck_all_aspects_btn.clicked.connect( self.aspect_tree.uncheck_all_aspects) self.main_window_ui.select_all_aspects_btn.clicked.connect( self.aspect_tree.select_all_aspects) self.main_window_ui.show_selected_aspects_btn.clicked.connect( self.show_selected_aspects) self.main_window_ui.stop_list_tree.stop_words_updated.connect( self.restart_aspect_searching) def __init__(self): """ Constructor of widget """ main_window = QMainWindow() self.main_window_ui = Ui_MainWindow() self.main_window_ui.setupUi(main_window) QMainWindow.__init__(self) Ui_MainWindow.setupUi(self.main_window_ui, self) self.signals(self.main_window_ui) self.highlighting_thread = None self.aspect_finding_thread = None self.parse_document_thread = None self.document_loader_thread = None self.parsers = {} self.document = None self.preview_document = None self.presenter = None self.worker = None self.html_presenter = None self.main_window_ui.save_document.setDisabled(True) self.preview_window = None self.__hide_progress_bar_panel() self.__cur_file_name = None self.__windows_title = 'prose-rhythm-detector' self.setWindowTitle(self.__windows_title) self.main_window_ui.show_selected_aspects_btn.setEnabled(False) @Slot() def save_document(self): """ Save the document to a *.prd file""" file_name = QFileDialog.getSaveFileName(self, dir=self.tr("../"), filter=self.tr("*.prd")) if not file_name[0]: return DocumentSaver(self.document).save(file_name[0]) @Slot() def import_text(self): """ Creates new document from a specified text file """ file_name = self.__show_open_file_dialog("Выберите файл с текстом") if file_name[0] == '': return self.preview_document = PreviewDocument( self.__import_plain_text_from_file(file_name)) if not self.preview_document.chapters: return if self.__open_preview_window(): self.__add_file_name_to_title(self.__cur_file_name) self.__clean_up_main_page() self.main_window_ui.save_document.setDisabled(True) self.main_window_ui.show_selected_aspects_btn.setEnabled(False) self.__start_parse_document_thread(self.preview_document) def __add_file_name_to_title(self, file_name: str): self.setWindowTitle('{0} ({1})'.format(self.__windows_title, file_name)) def __import_plain_text_from_file(self, filename: str) -> str: """ Reads the text from the specified file """ self.__cur_file_name = os.path.basename(filename[0]) return TextParser().parse_plain_text(filename[0]) def __open_preview_window(self): """ Open preview document window """ self.preview_window = PreviewWindow(self.preview_document) self.preview_window.setModal(True) self.preview_window.show() return self.preview_window.exec_() == 1 def __add_stop_words_to_stop_word_tree(self): """ Adds the stop words from the documents to the stop list tree widget """ self.main_window_ui.stop_list_tree.load_stop_words(self.document) def __start_parse_document_thread(self, preview_document): self.__enable_input_menu_items(False) self.__init_and_show_progress_bar("Обработка текста") self.parse_document_thread = QThread() self.worker = DocumentParserWorker(preview_document, self.parsers) self.worker.moveToThread(self.parse_document_thread) self.worker.finished.connect(self.__on_parse_document_finished) self.worker.error.connect(self.__on_error) self.parse_document_thread.started.connect(self.worker.start) self.parse_document_thread.start() @Slot(object) def __on_parse_document_finished(self, document: Document): self.parse_document_thread.quit() self.document = document self.__start_aspect_finding_thread(self.document) def __start_aspect_finding_thread(self, document): self.__init_and_show_progress_bar("Поиск аспектов") self.aspect_finding_thread = QThread() self.worker = AspectFinderWorker(document) self.worker.moveToThread(self.aspect_finding_thread) self.worker.finished.connect(self.__on_aspect_search_finished) self.worker.error.connect(self.__on_error) self.worker.update_progress.connect( self.__update_aspect_search_progress) self.aspect_finding_thread.started.connect(self.worker.start) self.aspect_finding_thread.start() @Slot(int, int) def __update_aspect_search_progress(self, processed, total): self.__progress_bar_label().setText( f"Поиск аспектов {processed} из {total}") @Slot() def __on_aspect_search_finished(self): """This method describes actions when the aspect search was finished""" try: if self.aspect_finding_thread: self.aspect_finding_thread.quit() self.__set_content_in_main_window() self.main_window_ui.save_document.setEnabled(True) self.__enable_input_menu_items(True) except Exception: traceback.print_exc() self.__on_error() def __set_content_in_main_window(self): """Set a content to main window""" self.presenter = DocumentPresenter(self.document) self.html_presenter = DocumentHtmlPresenter(self.document) self.__add_aspect_types_to_aspect_tree() self.__add_stop_words_to_stop_word_tree() self.show_selected_aspects() def select_aspect(self, item: FeatureListItem): """ fast-forward text to the selected aspect in the document""" self.main_window_ui.text_content.setFocus() cursor = self.main_window_ui.text_content.textCursor() cursor.setPosition( self.presenter.word_start(item.aspect().context_begin())) cursor.movePosition( QTextCursor.NextCharacter, QTextCursor.KeepAnchor, self.presenter.word_end(item.aspect().context_end()) - self.presenter.word_start(item.aspect().context_begin())) self.main_window_ui.text_content.setTextCursor(cursor) @Slot() def show_selected_aspects(self): """ Adds aspects with selected types to the aspect list""" self.aspect_list().clear() aspects = self.aspects_with_selected_types() for aspect in aspects: self.aspect_list().addItem(FeatureListItem(aspect, self.presenter)) self.__start_aspect_highlighting_thread(aspects) def __start_aspect_highlighting_thread(self, aspects): self.main_window_ui.show_selected_aspects_btn.setEnabled(False) self.__init_and_show_progress_bar("Подготовка текста для отображения") self.highlighting_thread = QThread() self.worker = PresenterWorker(self.html_presenter, aspects) self.worker.moveToThread(self.highlighting_thread) self.worker.finished.connect(self.__add_text) self.worker.error.connect(self.__on_error) self.highlighting_thread.started.connect(self.worker.start) self.highlighting_thread.start() @Slot() def __on_error(self): """This method describes actions when an error appears""" self.__stop_all_worker_threads() self.__hide_progress_bar_panel() self.main_window_ui.show_selected_aspects_btn.setEnabled(False) self.__enable_input_menu_items(True) error_dialog = QErrorMessage() error_dialog.showMessage('Во время обработки текста произошла ошибка!') error_dialog.exec_() def __stop_all_worker_threads(self): """ Stops all worker threads if they are run """ self.highlighting_thread.quit() if self.highlighting_thread else None self.aspect_finding_thread.quit( ) if self.aspect_finding_thread else None self.parse_document_thread.quit( ) if self.parse_document_thread else None self.document_loader_thread.quit( ) if self.document_loader_thread else None @Slot(str) def __add_text(self, text): self.main_window_ui.text_content.setHtml(text) self.__hide_progress_bar_panel() self.highlighting_thread.quit() self.main_window_ui.show_selected_aspects_btn.setEnabled(True) def aspects_with_selected_types(self) -> list: """ :return: aspects with selected types """ aspects = list() for aspect_item in self.aspect_tree.selected_aspect_types(): if aspect_item.has_children(): for child in aspect_item.selected_child_aspect_types(): aspects.extend( self.document.features_with_type_and_transcription( child.aspect_type, child.sound)) else: aspects.extend( self.document.features_with_type(aspect_item.aspect_type)) return aspects def __add_aspect_types_to_aspect_tree(self): """ Adds the aspect types and his count to the aspect types tree """ self.__add_grammatical_types_to_aspect_tree() self.__add_phonetic_types_to_aspect_tree() def __add_grammatical_types_to_aspect_tree(self): """ Adds the grammatical aspect types and his count to the aspect types tree """ for feature_type in Feature.GRAMMATICAL_TYPES: feature_count = len(self.document.features_with_type(feature_type)) self.aspect_tree.add_top_level_aspect(feature_type, feature_count) def __add_phonetic_types_to_aspect_tree(self): """ Adds the phonetic aspect types and his count to the aspect types tree """ phonetic_types = { 'assonance': assonance_sounds(self.document.lang), 'alliteration': alliteration_sounds(self.document.lang) } for feature_type, sounds in phonetic_types.items(): feature_count = len(self.document.features_with_type(feature_type)) aspect_top_item = self.aspect_tree.add_top_level_aspect( feature_type, feature_count) for sound in sounds: sound_count = len( self.document.features_with_type_and_transcription( feature_type, sound)) aspect_top_item.addChild( AspectSoundTreeWidgetItem(feature_type, sound_count, sound, aspect_top_item)) def __hide_progress_bar_panel(self): self.__progress_bar_label().setVisible(False) self.__progress_bar().setVisible(False) def __enable_input_menu_items(self, value): self.main_window_ui.open_document.setEnabled(value) self.main_window_ui.import_text.setEnabled(value) def __init_and_show_progress_bar(self, label: str = ''): self.__progress_bar().setValue(0) self.__progress_bar().setMaximum(0) self.__progress_bar().setMinimum(0) self.__progress_bar_label().setText(label) self.__progress_bar_label().setVisible(True) self.__progress_bar().setVisible(True) def __progress_bar(self) -> QProgressBar: return self.main_window_ui.progress_bar def __progress_bar_label(self) -> QLabel: return self.main_window_ui.progress_label @Slot() def open_document(self): """ Open the document """ file_name = self.__show_open_file_dialog("Выберите документ", "*.prd") if not file_name[0]: return self.__start_document_loader_thread(file_name[0]) def __start_document_loader_thread(self, file_name): self.__enable_input_menu_items(False) self.__init_and_show_progress_bar("Загрузка документа") self.document_loader_thread = QThread() self.worker = DocumentLoaderWorker(file_name, self.parsers) self.worker.moveToThread(self.document_loader_thread) self.worker.finished.connect(self.__on_document_load_finished) self.worker.error.connect(self.__on_error) self.document_loader_thread.started.connect(self.worker.start) self.document_loader_thread.start() @Slot(object) def __on_document_load_finished(self, document: Document): """This method describes actions when the document was loaded""" self.document_loader_thread.quit() self.document = document self.__on_aspect_search_finished() def __show_open_file_dialog(self, title, file_type="*.txt"): """ Open the dialog to select the name of file :param title: the title of the open file dialog :param file_type: the type of the file (sample "*.txt, *.json") :return a selected file """ file_name = QFileDialog.getOpenFileName(self, self.tr(title), self.tr("../"), self.tr(file_type)) return file_name def aspect_list(self) -> QListWidget: """ :return the aspect_list object of QListWidget class where store aspects""" return self.main_window_ui.aspects_list @property def aspect_tree(self) -> AspectTreeWidget: """ :return: the feature_tree object of the AspectTreeWidget class where stored aspect types""" return self.main_window_ui.aspect_tree def __clean_up_main_page(self): self.main_window_ui.text_content.clear() self.aspect_list().clear() self.aspect_tree.clear() self.main_window_ui.stop_list_tree.clear() @Slot(dict) def restart_aspect_searching(self, new_stop_words: dict): """ Restarts aspect searching after stop-word editing """ self.document.set_stop_words(new_stop_words) self.document.features.clear() self.__clean_up_main_page() self.main_window_ui.save_document.setDisabled(True) self.main_window_ui.show_selected_aspects_btn.setEnabled(False) self.__start_aspect_finding_thread(self.document)
class SpineDBFetcher(QObject): """Handles signals from DB manager and channels them to listeners.""" object_classes_fetched = Signal(object) objects_fetched = Signal(object) relationship_classes_fetched = Signal(object) relationships_fetched = Signal(object) parameter_definitions_fetched = Signal(object) parameter_values_fetched = Signal(object) parameter_value_lists_fetched = Signal(object) parameter_tags_fetched = Signal(object) def __init__(self, db_mngr, listener, *db_maps): """Initializes the fetcher object. Args: db_mngr (SpineDBManager) listener (DataStoreForm) db_maps (DiffDatabaseMapping) """ super().__init__() self.db_mngr = db_mngr self.listener = listener self.db_maps = db_maps self._thread = QThread() self.moveToThread(self._thread) self._thread.start() self.connect_signals() def connect_signals(self): """Connects signals.""" self.object_classes_fetched.connect( self.receive_object_classes_fetched) self.objects_fetched.connect(self.receive_objects_fetched) self.relationship_classes_fetched.connect( self.receive_relationship_classes_fetched) self.relationships_fetched.connect(self.receive_relationships_fetched) self.parameter_definitions_fetched.connect( self.receive_parameter_definitions_fetched) self.parameter_values_fetched.connect( self.receive_parameter_values_fetched) self.parameter_value_lists_fetched.connect( self.receive_parameter_value_lists_fetched) self.parameter_tags_fetched.connect( self.receive_parameter_tags_fetched) self.destroyed.connect(lambda: self.clean_up()) # pylint: disable=unnecessary-lambda qApp.aboutToQuit.connect(self._thread.quit) # pylint: disable=undefined-variable def run(self): self.listener.setCursor(QCursor(Qt.BusyCursor)) self.listener.silenced = True object_classes = { x: self.db_mngr.get_object_classes(x) for x in self.db_maps } relationship_classes = { x: self.db_mngr.get_relationship_classes(x) for x in self.db_maps } parameter_definitions = { x: self.db_mngr.get_parameter_definitions(x) for x in self.db_maps } objects = {x: self.db_mngr.get_objects(x) for x in self.db_maps} relationships = { x: self.db_mngr.get_relationships(x) for x in self.db_maps } parameter_values = { x: self.db_mngr.get_parameter_values(x) for x in self.db_maps } parameter_value_lists = { x: self.db_mngr.get_parameter_value_lists(x) for x in self.db_maps } parameter_tags = { x: self.db_mngr.get_parameter_tags(x) for x in self.db_maps } self.object_classes_fetched.emit(object_classes) self.relationship_classes_fetched.emit(relationship_classes) self.parameter_definitions_fetched.emit(parameter_definitions) self.objects_fetched.emit(objects) self.relationships_fetched.emit(relationships) self.parameter_values_fetched.emit(parameter_values) self.parameter_value_lists_fetched.emit(parameter_value_lists) self.parameter_tags_fetched.emit(parameter_tags) self.deleteLater() def clean_up(self): self._thread.quit() self.listener.silenced = False self.listener.unsetCursor() @Slot(object) def receive_object_classes_fetched(self, db_map_data): self.db_mngr.cache_items("object class", db_map_data) self.db_mngr.update_icons(db_map_data) self.listener.receive_object_classes_fetched(db_map_data) @Slot(object) def receive_objects_fetched(self, db_map_data): self.db_mngr.cache_items("object", db_map_data) self.listener.receive_objects_fetched(db_map_data) @Slot(object) def receive_relationship_classes_fetched(self, db_map_data): self.db_mngr.cache_items("relationship class", db_map_data) self.listener.receive_relationship_classes_fetched(db_map_data) @Slot(object) def receive_relationships_fetched(self, db_map_data): self.db_mngr.cache_items("relationship", db_map_data) self.listener.receive_relationships_fetched(db_map_data) @Slot(object) def receive_parameter_definitions_fetched(self, db_map_data): self.db_mngr.cache_items("parameter definition", db_map_data) self.listener.receive_parameter_definitions_fetched(db_map_data) @Slot(object) def receive_parameter_values_fetched(self, db_map_data): self.db_mngr.cache_items("parameter value", db_map_data) self.listener.receive_parameter_values_fetched(db_map_data) @Slot(object) def receive_parameter_value_lists_fetched(self, db_map_data): self.db_mngr.cache_items("parameter value list", db_map_data) self.listener.receive_parameter_value_lists_fetched(db_map_data) @Slot(object) def receive_parameter_tags_fetched(self, db_map_data): self.db_mngr.cache_items("parameter tag", db_map_data) self.listener.receive_parameter_tags_fetched(db_map_data)
class MainApplication(QApplication): def __init__(self, argv): super().__init__(argv) self.setApplicationName('ThisApplication') self.setStyle('Fusion') self.setPalette(DarkPalette()) self.setStyleSheet(""" QPushButton::disabled { background-color: #303030; color: #2e2e2e; } """) self.log = ConsoleLogger.new(__name__) self.r = Resources() g.init(self.log, self.r) self.main = MainWindow() self.main.setWindowIcon(self.r.iconApp) self.main.show() self.exceptionDialog = None sys.excepthook = self.handleGlobalException self.thread = QThread() self.job = BackgroundJob() if True: self.job.crashed.connect(self.onThreadCrash) self.job.moveToThread(self.thread) self.job.finished.connect(self.thread.quit) self.thread.started.connect(self.job.start) self.thread.finished.connect(self.quit) self.aboutToQuit.connect(self.onUserQuit) self.thread.start() else: self.job.run() def onUserQuit(self): if self.thread and self.job: self.job.stop() self.thread.quit() self.thread.wait() def onThreadCrash(self, s): lines = ['A problem has occurred and this application must exit.', ''] g.log.critical( '>>> ENCOUNTERED AN UNHANDLED EXCEPTION IN WORKER THREAD!') for line in s.splitlines(): g.log.critical(line) lines.append(line) g.log.critical( '>>> THIS ERROR IS FATAL. THE APPLICATION MUST BE RESTARTED.') lines.append('') lines.append( 'Please copy and paste the text from this message box to report the issue.' ) QMessageBox.critical(None, 'Error', '\n'.join(lines)) self.exit(1) @staticmethod def handleGlobalException(exc_type, exc_val, exc_tb): lines = ['A problem has occurred and this application must exit.', ''] g.log.critical('>>> ENCOUNTERED AN UNHANDLED APPLICATION EXCEPTION!') for exc_line in traceback.format_exception(exc_type, exc_val, exc_tb): for line in exc_line.splitlines(): g.log.critical(line) lines.append(line) g.log.critical( '>>> THIS ERROR MAY BE FATAL. THE APPLICATION MAY NOT WORK AS EXPECTED UNTIL RESTARTED.' ) lines.append('') lines.append( 'Please copy and paste the text from this message box to report the issue.' ) QMessageBox.critical(None, 'Error', '\n'.join(lines)) @classmethod def run(cls): app = cls(sys.argv) result = app.exec_() sys.exit(result)
class MainWindow(QMainWindow, Ui_MainWindow): _TV_FOLDERS_ITEM_MAP = {} # signals _dir_load_start = Signal(object) _dir_watcher_start = Signal() _loader_load_scandir = Signal(int, int, int, object) _is_watcher_running = False _update_mgr = None def __init__(self, parent=None): super(MainWindow, self).__init__(parent) self.setupUi(self) self.actiongrp_thumbs_size = QtWidgets.QActionGroup(self) self.actiongrp_thumbs_size.addAction(self.action_small_thumbs) self.actiongrp_thumbs_size.addAction(self.action_normal_thumbs) self.actiongrp_thumbs_caption = QtWidgets.QActionGroup(self) self.actiongrp_thumbs_caption.addAction(self.action_caption_none) self.actiongrp_thumbs_caption.addAction(self.action_caption_filename) self.action_caption_none.setChecked(True) # threads self._dir_watcher_thread = QThread() self._img_loader_thread = QThread() # helpers self._meta_files_mgr = MetaFilesManager() self._meta_files_mgr.connect() self._watch = Watcher() self._img_loader = ImageLoader() self.listView_thumbs = ThumbsListView(self.frame_thumbs) self.listView_thumbs.setFrameShape(QtWidgets.QFrame.NoFrame) self.vlayout_frame_thumbs.addWidget(self.listView_thumbs) # connections self._setup_connections() # Populates the folder list self._setup_scan_dir_list_model() # Properties widget self.vlayout_properties = QtWidgets.QVBoxLayout( self.toolbox_metadata_properties) self.vlayout_properties.setContentsMargins(0, 0, 0, 0) self.vlayout_properties.setSpacing(0) self.properties_widget = PropertiesWidget( self.toolbox_metadata_properties) self.vlayout_properties.addWidget(self.properties_widget) self.statusBar().showMessage("Ready") # settings self.hslider_thumb_size.setValue( settings.get(SettingType.UI_THUMBS_SIZE, 128, 'int')) thumb_caption_type = settings.get( SettingType.UI_THUMBS_CAPTION_DISPLAY_MODE, Thumb_Caption_Type.NoCaption.name) if thumb_caption_type == Thumb_Caption_Type.FileName.name: self.action_caption_filename.setChecked(True) if settings.get(SettingType.UI_METADATA_SHOW_PROPS, False, 'bool'): self.toolBox_metadata.setCurrentIndex(0) self.toolbutton_properties.setChecked(True) self.toolbutton_tags.setChecked(False) self.action_properties.setChecked(True) self.action_tags.setChecked(False) self.frame_metadata.show() elif settings.get(SettingType.UI_METADATA_SHOW_TAGS, False, 'bool'): self.toolBox_metadata.setCurrentIndex(1) self.toolbutton_properties.setChecked(False) self.toolbutton_tags.setChecked(True) self.action_properties.setChecked(False) self.action_tags.setChecked(True) self.frame_metadata.show() else: self.frame_metadata.hide() # Set event filters self.btn_slideshow.installEventFilter(self) self._curr_img_serial = 1 def resizeEvent(self, event): if event.spontaneous(): # Begin loading the currently selected dir self._make_default_dir_list_selection() cur_sel_ids = self.get_current_selection_ids() if 'sd_id' in cur_sel_ids: self._load_dir_images(cur_sel_ids['sd_id']) # Start the dir watcher thread self.init_watch_thread() # Start the loader thread self.init_loader_thread() def closeEvent(self, event): LOGGER.debug('Shutting down gracefully....') self._meta_files_mgr.disconnect() settings.persist_to_disk() self._dir_watcher_thread.quit() self._dir_watcher_thread.wait() self._img_loader_thread.quit() self._img_loader_thread.wait() def _make_default_dir_list_selection(self): folders_index = self._dirs_list_model.indexFromItem( self._TV_FOLDERS_ITEM_MAP[0]) if self._dirs_list_model.rowCount(folders_index) > 0: self._dirs_list_selection_model.select( self._dirs_list_model.index(0, 0).child(0, 0), QItemSelectionModel.Select | QItemSelectionModel.Rows) def _setup_connections(self): # Menu # File self.action_add_folder.triggered.connect( self.action_folder_manager_clicked) self.action_rescan.triggered.connect(self._run_watcher) self.action_file_locate.triggered.connect( self.handle_action_file_locate_triggered) self.action_exit.triggered.connect(self.action_exit_clicked) # View self.action_small_thumbs.triggered.connect( self.handle_action_small_thumbs_triggered) self.action_normal_thumbs.triggered.connect( self.handle_action_normal_thumbs_triggered) self.action_properties.triggered.connect( self.action_properties_clicked) self.action_tags.triggered.connect(self.action_tags_clicked) self.action_slideshow.triggered.connect(self.start_slideshow) self.action_caption_none.triggered.connect( self.handle_action_thumbnail_caption_none_triggered) self.action_caption_filename.triggered.connect( self.handle_action_thumbnail_caption_filename_triggered) # Folder self.action_folder_slideshow.triggered.connect(self.start_slideshow) self.action_folder_locate.triggered.connect( self.handle_action_folder_locate_triggered) # Picture self.action_picture_properties.triggered.connect( self.show_image_properties) # Tools self.action_settings.triggered.connect( self.handle_action_settings_triggered) self.action_folder_manager.triggered.connect( self.action_folder_manager_clicked) # Help self.action_check_updates.triggered.connect( self._handle_check_for_updates_clicked) # Btns self.btn_slideshow.clicked.connect(self.start_slideshow) self.hslider_thumb_size.valueChanged.connect( self.on_hslider_thumb_size_value_changed) # Watcher self._dir_watcher_start.connect(self._watch.watch_all) self._watch.new_img_found.connect(self.on_new_img_found) self._watch.watch_all_done.connect(self.on_watch_all_done) self._watch.dir_added_or_updated.connect(self.on_dir_added_or_updated) self._watch.dir_empty_or_deleted.connect(self.on_dir_empty_deleted) self._watch.watch_empty_or_deleted_done.connect( self.on_watch_dir_empty_deleted_done) # Loader self._loader_load_scandir.connect(self._img_loader.load_scandir) # self._loader_load_scanimg.connect(self._img_loader.load_scanimg) self._img_loader.load_scan_dir_info_success.connect( self._handle_load_scan_dir_info_success) # self._img_loader.load_images_success.connect( # self._handle_load_images_sucess) self._img_loader.load_images_success.connect( self.listView_thumbs.render_thumbs) # Tree View self.treeView_scandirs.clicked.connect( self.on_scan_dir_treeView_clicked) # Thumbs view self.listView_thumbs.clicked.connect(self.on_thumb_clicked) self.listView_thumbs.empty_area_clicked.connect( self.on_thumb_listview_empty_area_clicked) self.listView_thumbs.load_dir_images_for_scroll_up.connect( self.on_load_dir_images_for_scroll_up) self.listView_thumbs.load_dir_images_for_scroll_down.connect( self.on_load_dir_images_for_scroll_down) self.buttonGroup_metadata.buttonClicked.connect( self.on_buttongroup_metadata_clicked) self.txtbox_search.textEdited.connect(self.handle_search) def init_watch_thread(self): self._watch.moveToThread(self._dir_watcher_thread) self._dir_watcher_thread.start() self._run_watcher() LOGGER.debug('Watcher thread started.') def init_loader_thread(self): self._img_loader.moveToThread(self._img_loader_thread) self._img_loader_thread.start() LOGGER.debug('Loader thread started.') def _run_watcher(self): self._is_watcher_running = True self.action_rescan.setEnabled(False) self._dir_watcher_start.emit() def _populate_dirs_tree_view(self, parent_key, folders): parent_item = self._TV_FOLDERS_ITEM_MAP[parent_key] if folders: for idx, dir in enumerate(folders): item_title = "%s(%s)" % (dir['name'], dir['img_count']) item = QStandardItem(item_title) item.setData(dir['id'], QtCore.Qt.UserRole + 1) item.setSizeHint(QSize(item.sizeHint().width(), 24)) item.setIcon(QIcon(':/images/icon_folder')) parent_item.appendRow(item) if parent_key == 0: self._TV_FOLDERS_ITEM_MAP[dir['id']] = item self.treeView_scandirs.expandAll() def _setup_scan_dir_list_model(self): self._dirs_list_model = QStandardItemModel() self._dirs_list_selection_model = QItemSelectionModel( self._dirs_list_model) self._thumbs_view_model = QStandardItemModel() self._thumbs_selection_model = QItemSelectionModel( self._thumbs_view_model) self._dirs_list_model.setColumnCount(1) # self._dirs_list_model.setRowCount(len(scan_dirs)) self._root_tree_item = self._dirs_list_model.invisibleRootItem() # FOLDERS item folder_item = QStandardItem("Folders") folder_item_font = QFont() folder_item_font.setBold(True) folder_item.setFont(folder_item_font) folder_item.setSizeHint(QSize(folder_item.sizeHint().width(), 24)) self._root_tree_item.appendRow(folder_item) self._TV_FOLDERS_ITEM_MAP[0] = folder_item self.treeView_scandirs.setModel(self._dirs_list_model) self.treeView_scandirs.setSelectionModel( self._dirs_list_selection_model) # self.treeView_scandirs.setRootIsDecorated(False) self.listView_thumbs.setModel(self._thumbs_view_model) self.listView_thumbs.setSelectionModel(self._thumbs_selection_model) scan_dirs = self._meta_files_mgr.get_scan_dirs() self._populate_dirs_tree_view(0, scan_dirs) def _repopulate_scan_dir_list_model(self): self._clear_folders_tree_view() scan_dirs = self._meta_files_mgr.get_scan_dirs() self._populate_dirs_tree_view(0, scan_dirs) def _populate_search_tree_view(self, results): if 'search' not in self._TV_FOLDERS_ITEM_MAP: self._root_tree_item = self._dirs_list_model.invisibleRootItem() # FOLDERS item search_item = QStandardItem("Search") search_item_font = QFont() search_item_font.setBold(True) search_item.setFont(search_item_font) search_item.setSizeHint(QSize(search_item.sizeHint().width(), 24)) self._root_tree_item.insertRow(0, search_item) self._TV_FOLDERS_ITEM_MAP['search'] = search_item self._populate_dirs_tree_view('search', results) def eventFilter(self, widget, event): if widget.objectName() == 'btn_slideshow': if event.type() == QtCore.QEvent.Enter: self.label_thumbs_toolbar_tooltip.setText( "Play Fullscreen Slideshow") elif event.type() == QtCore.QEvent.Leave: self.label_thumbs_toolbar_tooltip.setText("") return QtWidgets.QWidget.eventFilter(self, widget, event) def handle_search(self, search_term): self._clear_search() if search_term != '': searches = self._meta_files_mgr.search_scan_dirs(search_term) self._populate_search_tree_view(searches) else: self._remove_search_tree_view() def handle_action_file_locate_triggered(self): curr_sel_ids = self.get_current_selection_ids() if 'sd_id' in curr_sel_ids and 'si_id' in curr_sel_ids: dr_img = self._meta_files_mgr.get_image_from_id( curr_sel_ids['si_id'], curr_sel_ids['sd_id']) explorer_process = QtCore.QProcess() explorer_process.setProgram('explorer.exe') explorer_process.setArguments([ '/select,%s' % QtCore.QDir.toNativeSeparators(dr_img['abspath']) ]) explorer_process.startDetached() def handle_action_folder_locate_triggered(self): curr_sel_ids = self.get_current_selection_ids() if 'sd_id' in curr_sel_ids: dr_sd = self._meta_files_mgr.get_scan_dir(curr_sel_ids['sd_id']) explorer_process = QtCore.QProcess() explorer_process.setProgram('explorer.exe') explorer_process.setArguments([ '/select,%s' % QtCore.QDir.toNativeSeparators(dr_sd['abspath']) ]) explorer_process.startDetached() def handle_action_small_thumbs_triggered(self): if self.action_small_thumbs.isChecked(): self.hslider_thumb_size.triggerAction( self.hslider_thumb_size.SliderToMinimum) def handle_action_normal_thumbs_triggered(self): if self.action_normal_thumbs.isChecked(): slider_value = self.hslider_thumb_size.value() if slider_value < 128: self.hslider_thumb_size.triggerAction( self.hslider_thumb_size.SliderPageStepAdd) elif slider_value > 128 and slider_value <= 192: self.hslider_thumb_size.triggerAction( self.hslider_thumb_size.SliderPageStepSub) elif slider_value > 192 and slider_value <= 256: self.hslider_thumb_size.triggerAction( self.hslider_thumb_size.SliderPageStepSub) self.hslider_thumb_size.triggerAction( self.hslider_thumb_size.SliderPageStepSub) def handle_action_thumbnail_caption_none_triggered(self): if self.action_caption_none.isChecked(): settings.save(SettingType.UI_THUMBS_CAPTION_DISPLAY_MODE, Thumb_Caption_Type.NoCaption.name) curr_sel_ids = self.get_current_selection_ids() if 'sd_id' in curr_sel_ids: self._load_dir_images(curr_sel_ids['sd_id']) def handle_action_thumbnail_caption_filename_triggered(self): if self.action_caption_filename.isChecked(): settings.save(SettingType.UI_THUMBS_CAPTION_DISPLAY_MODE, Thumb_Caption_Type.FileName.name) curr_sel_ids = self.get_current_selection_ids() if 'sd_id' in curr_sel_ids: self._load_dir_images(curr_sel_ids['sd_id']) def action_folder_manager_clicked(self): self.folder_mgr_window = FolderManagerWindow(self) self.folder_mgr_window.accepted.connect( self._on_folder_manager_window_accepted) self.folder_mgr_window.setModal(True) self.folder_mgr_window.show() def _on_folder_manager_window_accepted(self): self._repopulate_scan_dir_list_model() self._run_watcher() def action_properties_clicked(self): if self.action_properties.isChecked(): curr_sel_ids = self.get_current_selection_ids() if 'sd_id' in curr_sel_ids and 'si_id' in curr_sel_ids: img_props = self._meta_files_mgr.get_img_properties( curr_sel_ids['si_id'], curr_sel_ids['sd_id']) self.properties_widget.setup_properties(img_props) self.toolBox_metadata.setCurrentIndex(0) self.frame_metadata.show() self.toolbutton_properties.setChecked(True) self.toolbutton_tags.setChecked(False) self.action_tags.setChecked(False) settings.save(SettingType.UI_METADATA_SHOW_PROPS, True) settings.save(SettingType.UI_METADATA_SHOW_TAGS, False) else: self.frame_metadata.hide() self.toolbutton_properties.setChecked(False) settings.save(SettingType.UI_METADATA_SHOW_PROPS, False) settings.save(SettingType.UI_METADATA_SHOW_TAGS, False) # Forces the list view to re-render after the metadata window is hidden self._thumbs_view_model.layoutChanged.emit() def action_tags_clicked(self): if self.action_tags.isChecked(): self.toolBox_metadata.setCurrentIndex(1) self.frame_metadata.show() self.toolbutton_tags.setChecked(True) self.toolbutton_properties.setChecked(False) self.action_properties.setChecked(False) settings.save(SettingType.UI_METADATA_SHOW_PROPS, False) settings.save(SettingType.UI_METADATA_SHOW_TAGS, True) else: self.frame_metadata.hide() self.toolbutton_tags.setChecked(False) settings.save(SettingType.UI_METADATA_SHOW_PROPS, False) settings.save(SettingType.UI_METADATA_SHOW_TAGS, False) # Forces the list view to re-render after the metadata window is hidden self._thumbs_view_model.layoutChanged.emit() def show_image_properties(self): curr_sel_ids = self.get_current_selection_ids() if 'sd_id' in curr_sel_ids and 'si_id' in curr_sel_ids: img_props = self._meta_files_mgr.get_img_properties( curr_sel_ids['si_id'], curr_sel_ids['sd_id']) self.properties_widget.setup_properties(img_props) self.toolBox_metadata.setCurrentIndex(0) self.frame_metadata.show() self.toolbutton_properties.setChecked(True) self.toolbutton_tags.setChecked(False) self.action_properties.setChecked(True) self.action_tags.setChecked(False) def handle_action_settings_triggered(self): self.settings_window = SettingsWindow(self) self.settings_window.setModal(True) self.settings_window.show() def on_buttongroup_metadata_clicked(self, button): if button.objectName() == 'toolbutton_tags': self.action_tags.trigger() elif button.objectName() == 'toolbutton_properties': self.action_properties.trigger() def on_hslider_thumb_size_value_changed(self, value): if self.hslider_thumb_size.value() == 64: self.action_small_thumbs.setChecked(True) elif self.hslider_thumb_size.value() == 128: self.action_normal_thumbs.setChecked(True) else: self.action_small_thumbs.setChecked(False) self.action_normal_thumbs.setChecked(False) self.listView_thumbs.setIconSize(QSize(value, value)) self.listView_thumbs.setGridSize(QSize(value + 20, value + 20)) settings.save(SettingType.UI_THUMBS_SIZE, value) def _handle_check_for_updates_clicked(self): if not self._update_mgr: self._update_mgr = UpdateManager() self._update_mgr.get_updates() def _load_dir_images(self, sd_id): self._clear_thumbs() load_count = self.listView_thumbs.get_visible_thumb_count( self.hslider_thumb_size.value() + 20) print("load count %d" % load_count) self._loader_load_scandir.emit(sd_id, 0, load_count, ScrollDirection.Down) QScroller.grabGesture(self.listView_thumbs.viewport(), QScroller.LeftMouseButtonGesture) def _load_dir_images_for_scroll_up(self, sd_id, serial, count): # self._clear_thumbs() load_count = self.listView_thumbs.get_visible_thumb_count( self.hslider_thumb_size.value() + 20) self._loader_load_scandir.emit(sd_id, serial, count, ScrollDirection.Up) QScroller.grabGesture(self.listView_thumbs.viewport(), QScroller.LeftMouseButtonGesture) def _load_dir_images_for_scroll_down(self, sd_id, serial, count): # self._clear_thumbs() load_count = self.listView_thumbs.get_visible_thumb_count( self.hslider_thumb_size.value() + 20) self._loader_load_scandir.emit(sd_id, serial, count, ScrollDirection.Down) QScroller.grabGesture(self.listView_thumbs.viewport(), QScroller.LeftMouseButtonGesture) @Slot(int, int) def on_load_dir_images_for_scroll_up(self, serial, count): cur_sel_ids = self.get_current_selection_ids() if 'sd_id' in cur_sel_ids: self._load_dir_images_for_scroll_up(cur_sel_ids['sd_id'], serial, count) @Slot(int, int) def on_load_dir_images_for_scroll_down(self, serial, count): cur_sel_ids = self.get_current_selection_ids() if 'sd_id' in cur_sel_ids: self._load_dir_images_for_scroll_down(cur_sel_ids['sd_id'], serial, count) def _handle_load_scan_dir_info_success(self, dir_info): self.lbl_dir_name.setText(dir_info['name']) @Slot(object, int) def _handle_load_images_sucess(self, images, scrollDirection): img_count = len(images) print('Recevied %s images' % img_count) LOGGER.debug('Recevied %s images' % img_count) for img in images: img['thumb'] = QImage.fromData(img['thumb']) item = QStandardItem() thumb_caption_type = settings.get( SettingType.UI_THUMBS_CAPTION_DISPLAY_MODE, Thumb_Caption_Type.NoCaption.name) if thumb_caption_type == Thumb_Caption_Type.FileName.name: item.setText(img['name']) item.setData(img['id'], QtCore.Qt.UserRole + 1) item.setData(img['serial'], QtCore.Qt.UserRole + 2) item.setIcon(QIcon(QPixmap.fromImage(img['thumb']))) item.setText(str(img['serial'])) if scrollDirection == ScrollDirection.Up: self._thumbs_view_model.insertRow(0, item) if scrollDirection == ScrollDirection.Down: self._thumbs_view_model.appendRow(item) def _clear_thumbs(self): self._thumbs_view_model.clear() self.lbl_dir_name.setText('') def _tv_add_scan_dir(self, dir_info, highlight=False): item_title = "%s(%s)" % (dir_info['name'], dir_info['img_count']) item = QStandardItem(item_title) item.setData(dir_info['id'], QtCore.Qt.UserRole + 1) item.setSizeHint(QSize(item.sizeHint().width(), 24)) item.setIcon(QIcon(':/images/icon_folder')) if highlight: bold_font = QFont() bold_font.setBold(True) item.setFont(bold_font) folder_item = self._TV_FOLDERS_ITEM_MAP[0] folder_item.appendRow(item) self._TV_FOLDERS_ITEM_MAP[dir_info['id']] = item @Slot() def start_slideshow(self): selected = self.treeView_scandirs.selectedIndexes() sd_id = selected[0].data(QtCore.Qt.UserRole + 1) img_serial = 1 thumb_selected = self.listView_thumbs.selectedIndexes() if len(thumb_selected) > 0: img_serial = thumb_selected[0].data(QtCore.Qt.UserRole + 2) if sd_id > 0: self._slideshow = SlideshowWindow(sd_id, img_serial) self._slideshow.setWindowFlags(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.FramelessWindowHint) self._slideshow.showFullScreen() @Slot(QModelIndex) def on_thumb_clicked(self, mindex): selected = self.treeView_scandirs.selectedIndexes() sd_id = selected[0].data(QtCore.Qt.UserRole + 1) si_id = mindex.data(QtCore.Qt.UserRole + 1) img_props = self._meta_files_mgr.get_img_properties(si_id, sd_id) self.lbl_selection_summary.setText( self.get_thumb_selection_summary(img_props)) if self.action_properties.isChecked(): self.properties_widget.setup_properties(img_props) @Slot() def on_thumb_listview_empty_area_clicked(self): selected_thumb = self.listView_thumbs.selectedIndexes() if len(selected_thumb) == 0: selected = self.treeView_scandirs.selectedIndexes() sd_id = selected[0].data(QtCore.Qt.UserRole + 1) if sd_id: props = self._meta_files_mgr.get_dir_properties(sd_id) self.lbl_selection_summary.setText( self.get_dir_selection_summary(props)) def get_dir_selection_summary(self, props): img_count = props['img_count'] modified = props['modified'].toString('dd MMMM yyyy') size = props['size'] return "%s pictures %s %s on disk" % (img_count, modified, size) def get_thumb_selection_summary(self, props): filename = props['filename'] modified = props['DateTime'] if 'DateTime' in props else '' dimensions = props['dimensions'] filesize = props['filesize'] return "%s %s %s %s" % (filename, modified, dimensions, filesize) @Slot(QModelIndex) def on_scan_dir_treeView_clicked(self, index): sd_id = index.data(QtCore.Qt.UserRole + 1) # Categories tree nodes will not contain 'data' if sd_id: item = self._TV_FOLDERS_ITEM_MAP[sd_id] bold_font = QFont() bold_font.setBold(False) item.setFont(bold_font) props = self._meta_files_mgr.get_dir_properties(sd_id) self.lbl_selection_summary.setText( self.get_dir_selection_summary(props)) self._load_dir_images(sd_id) @Slot(object) def on_new_img_found(self, img_info): self.statusBar().showMessage("Found new image: %s - %s" % (img_info['dir'], img_info['filename'])) @Slot(object) def on_dir_added_or_updated(self, dir_info): if dir_info['id'] in self._TV_FOLDERS_ITEM_MAP: item = self._TV_FOLDERS_ITEM_MAP[dir_info['id']] item_title = "%s(%s)" % (dir_info['name'], dir_info['img_count']) item.setText(item_title) bold_font = QFont() bold_font.setBold(True) item.setFont(bold_font) else: self._tv_add_scan_dir(dir_info, True) @Slot(object) def on_dir_empty_deleted(self, dir_info): if dir_info['id'] in self._TV_FOLDERS_ITEM_MAP: item = self._TV_FOLDERS_ITEM_MAP[dir_info['id']] item_index = self._dirs_list_model.indexFromItem(item) parent_index = self._dirs_list_model.indexFromItem( self._TV_FOLDERS_ITEM_MAP[0]) self._dirs_list_model.removeRow(item_index.row(), parent_index) self._TV_FOLDERS_ITEM_MAP.pop(dir_info['id']) @Slot() def on_watch_dir_empty_deleted_done(self): # Here we make sure a folder is always selected if one more folders # ever get deleted by the watcher thread. If no folders exits, just # display an empty thumbs list cur_sel_ids = self.get_current_selection_ids() print(cur_sel_ids) if 'sd_id' not in cur_sel_ids: self._make_default_dir_list_selection() cur_sel_ids = self.get_current_selection_ids() if 'sd_id' in cur_sel_ids: self._load_dir_images(cur_sel_ids['sd_id']) else: self._clear_thumbs() @Slot(object, object) def on_watch_all_done(self, elapsed, suffix): self.statusBar().clearMessage() self._is_watcher_running = False self.action_rescan.setEnabled(True) self.statusBar().showMessage("Folder scan completed in %.2f %s" % (elapsed, suffix)) def get_current_selection_ids(self): selected_ids = {} selected = self.treeView_scandirs.selectedIndexes() if len(selected) <= 0: return selected_ids selected_ids['sd_id'] = selected[0].data(QtCore.Qt.UserRole + 1) selected_thumb = self.listView_thumbs.selectedIndexes() if len(selected_thumb) <= 0: return selected_ids selected_ids['si_id'] = selected_thumb[0].data(QtCore.Qt.UserRole + 1) return selected_ids def action_exit_clicked(self): self.close() def _clear_search(self): if 'search' in self._TV_FOLDERS_ITEM_MAP: search_item = self._TV_FOLDERS_ITEM_MAP['search'] search_item.removeRows(0, search_item.rowCount()) def _clear_folders_tree_view(self): folder_item = self._TV_FOLDERS_ITEM_MAP[0] self._TV_FOLDERS_ITEM_MAP.clear() self._TV_FOLDERS_ITEM_MAP[0] = folder_item folder_item.removeRows(0, folder_item.rowCount()) def _remove_search_tree_view(self): if 'search' in self._TV_FOLDERS_ITEM_MAP: root_tree_item = self._dirs_list_model.invisibleRootItem() root_tree_item.removeRow(0) self._TV_FOLDERS_ITEM_MAP.pop('search') def is_watcher_running(self): return self._is_watcher_running