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()
Beispiel #2
0
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'])
Beispiel #3
0
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()
Beispiel #4
0
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)
Beispiel #6
0
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()
Beispiel #7
0
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()
Beispiel #8
0
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)
Beispiel #9
0
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()
Beispiel #10
0
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)
Beispiel #11
0
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()
Beispiel #12
0
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
Beispiel #13
0
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()
Beispiel #14
0
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()
Beispiel #15
0
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
                        ))
Beispiel #16
0
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()
Beispiel #18
0
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
Beispiel #19
0
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')
Beispiel #20
0
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()
Beispiel #21
0
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
Beispiel #22
0
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)
Beispiel #23
0
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)
Beispiel #24
0
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()))
Beispiel #25
0
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()
Beispiel #26
0
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)
Beispiel #28
0
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)
Beispiel #29
0
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)
Beispiel #30
0
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