Ejemplo n.º 1
0
class JobFileTransfer(QObject):
    def __init__(self, parent, finished_callback, job):
        """

        :param modules.gui_service_manager.ServiceManager parent:
        :param callable finished_callback:
        :param modules.job.Job job:
        """
        super(JobFileTransfer, self).__init__(parent)

        self.job = job

        self.work_thread = QThread()
        self.worker = FileTransferWorker(job)
        self.worker.moveToThread(self.work_thread)
        self.worker.finished.connect(finished_callback)

        self.work_thread.started.connect(self.worker.work)
        self.work_thread.finished.connect(self._finish_thread)

    def start(self):
        LOGGER.info('Starting Job file transfer thread')
        self.work_thread.start()

    def _finish_thread(self):
        LOGGER.info('Job file transfer finished. Deleting file transfer objects.')
        self.work_thread.deleteLater()
        self.deleteLater()
Ejemplo n.º 2
0
class Window(QWidget):
    def __init__(self, *args, **kwargs):
        super(Window, self).__init__(*args, **kwargs)
        layout = QVBoxLayout(self)
        self.progressBar = QProgressBar(self)
        self.progressBar.setRange(0, 100)
        layout.addWidget(self.progressBar)
        layout.addWidget(QPushButton('开启线程', self, clicked=self.onStart))

        # 当前线程id
        print('main id', QThread.currentThread())

        # 启动线程更新进度条值
        self._thread = QThread(self)
        self._worker = Worker()
        self._worker.moveToThread(self._thread)  # 移动到线程中执行
        self._thread.finished.connect(self._worker.deleteLater)
        self._thread.started.connect(self._worker.run)
        self._worker.valueChanged.connect(self.progressBar.setValue)

    def onStart(self):
        if not self._thread.isRunning():
            print('main id', QThread.currentThread())
            self._thread.start()  # 启动线程

    def closeEvent(self, event):
        if self._thread.isRunning():
            self._thread.requestInterruption()
            self._thread.quit()
            self._thread.wait()
            # 强制
            # self._thread.terminate()
        self._thread.deleteLater()
        super(Window, self).closeEvent(event)
Ejemplo n.º 3
0
class Widget(QMainWindow):
    logger.info("---------- STARTED ----------")
    stop_signal = pyqtSignal(
    )  # sinaliza que o usuário clicou em "Stop Data Colleting"
    logout_signal = pyqtSignal()

    date = datetime.datetime.strptime(RELEASE_DATE, '%Y-%m-%d')
    version = VERSION

    # Check if all files/folders are present.
    if not os.path.exists("Data"):
        os.mkdir("Data")

    if not os.path.exists("Data\\Training Data"):
        os.mkdir("Data\\Training Data")

    try:
        with open("config.json", "r") as f:
            logger.info('Loading config file')
            output = json.loads(f.read())
            used_key = output["Used key"]
            username = output['User']
            ignore_login = output['Ignore Login Popup']
            first_time_running = output['First Time Running']
            token = output['Token']

    except Exception as e:
        logger.error(e)

        output = {
            "Used key": "C",
            "User": "",
            "Ignore Login Popup": False,
            "First Time Running": True,
            "Token": ""
        }

        used_key = output["Used key"]
        username = output['User']
        ignore_login = output['Ignore Login Popup']
        first_time_running = output['First Time Running']
        token = output['Token']

        with open("config.json", 'w') as f:
            json.dump(output, f, indent=2)

        logger.info("Fixed config file")

    logger.info('Config file loaded')

    def __init__(self):
        super().__init__()

        self.initUI()

        # Starting threads for startup verification
        self.loading_dialog = Loading()

        if DEV:
            wait_time = 100
        else:
            wait_time = random.randint(3550, 4500)

        # Defining control variables:
        self.auth_done = False
        self.update_check_done = False
        self.call_update_box = False
        self.call_accnt_box = False
        self.update_available = False
        self.wait_counter = 0
        self.online = False
        self.bot_btn_clicks = 0

        # Startup processes:
        self.startup_authorize_user()
        self.startup_update_check()

        self.loading_timer = QTimer()
        self.loading_timer.timeout.connect(self.loading)
        self.loading_timer.start(wait_time)

        # Icons:
        self.setWindowIcon(QtGui.QIcon('media\\logo\\logo.ico'))
        self.dog_getting_up = QtGui.QMovie(
            'media\\animations\\Dog Getting Up4.gif')
        self.dog_running = QtGui.QMovie('media\\animations\\Dog Running4.gif')
        self.dog_idle = QtGui.QMovie('media\\animations\\Dog Idle2.gif')
        self.dog_sitting_down = QtGui.QMovie(
            "media\\animations\\Dog Sitting Down4.gif")

        self.icons_label.setMovie(self.dog_idle)
        self.icons_label_2.setMovie(self.dog_idle)
        self.dog_idle.start()

        # Setting default labels and texts:
        self.v_label.setText("v{}".format(self.version))
        self.v_label_2.setText("v{}".format(self.version))

        # Defining button/checkbox actions
        self.data_start.clicked.connect(self.data_start_action)
        self.data_stop.clicked.connect(self.data_stop_action)
        self.send_btn.clicked.connect(self.send_data)
        self.bot_btn.clicked.connect(self.bot_controller)

    def initUI(self):
        logger.info("Initializing UI")

        loadUi('designs\\MainWindow.ui', self)

        # Main Window
        self.setFixedSize(333, 493)

        # Toolbar
        self.setWindowFlags(Qt.CustomizeWindowHint | Qt.WindowCloseButtonHint
                            | Qt.WindowMinimizeButtonHint)

        # Bug Report Tab
        self.send_message_btn.clicked.connect(self.send_message)

        # Menu
        self.logout_btn = self.menu.addAction('Logout')
        self.logout_btn.triggered.connect(self.logout)
        self.logout_btn.setVisible(False)

        self.login_btn = self.menu.addAction('Login')
        self.login_btn.triggered.connect(self.runtime_login)
        self.login_btn.setVisible(False)

        visit_ranking = self.menu.addAction('Ranking')
        visit_ranking.triggered.connect(lambda: webbrowser.open(RANKING_URL))

        website = self.menu.addAction('GitHub')
        website.triggered.connect(
            lambda: webbrowser.open("https://github.com/Setti7/SVFB-GUI"))

        self.config = self.menu.addAction('Fast Config')
        self.config.triggered.connect(self.fast_config)

        logger.info("UI Initialized")

    # Loading screen controller
    def loading(self):

        if self.auth_done and self.update_check_done:
            logger.info("Loading main aplication finished")
            self.loading_timer.stop()
            self.loading_timer.deleteLater()

            self.call_accnt_box = True
            self.call_update_box = True
            self.loading_dialog.close()
            self.show()

    def fast_config(self):

        config_dialog = ChangeKey(self.used_key)
        if config_dialog.exec_():
            self.used_key = config_dialog.new_key

    # Send message
    def send_message(self):

        msg = self.message_text.toPlainText()

        if self.update_available:

            bug_box = QMessageBox()
            bug_box.setIcon(QMessageBox.Warning)
            bug_box.setText(
                "<strong>You can't send bug reports while using an outdated version!</strong>"
            )
            bug_box.setInformativeText(
                """Please click <i>Ok</i> to download the newer one. Maybe your bug is already fixed."""
            )
            bug_box.setWindowTitle("Please update before sending bug reports")
            bug_box.setWindowIcon(QtGui.QIcon('media\\logo\\logo.ico'))

            bug_box.setEscapeButton(QMessageBox.Close)
            bug_box.addButton(QMessageBox.Close)

            ok = bug_box.addButton(QMessageBox.Ok)
            ok.clicked.connect(lambda: webbrowser.open(
                "https://github.com/Setti7/SVFB-GUI/releases"))
            bug_box.exec_()

        elif len(msg) > 1000:

            self.message_status_label.clear()
            self.message_status_label.setStyleSheet("color: #dc3545;")

            self.timer_msg0 = QTimer()
            self.timer_msg0.timeout.connect(
                lambda: self.message_status_label.setText(
                    "Your message is to big!\nThe maximum is 1000 chars."))
            self.timer_msg0.timeout.connect(self.timer_msg0.stop)
            self.timer_msg0.timeout.connect(self.timer_msg0.deleteLater)
            self.timer_msg0.start(200)

        else:

            self.message_text.clear()

            try:

                data = {
                    'message': msg,
                    'user': self.username,
                    'version': self.version,
                }

                headers = {'Authorization': f'Token {self.token}'}

                response = requests.post(BUG_REPORT_URL,
                                         data=data,
                                         headers=headers)

                result = json.loads(response.text)

                if result['success']:

                    self.message_status_label.setText(
                        "Message successfully sent!")
                    self.message_status_label.setStyleSheet("color: #28a745;")

                else:
                    logger.error(
                        f"Error while sending message: {result['error']}")
                    self.message_status_label.setText(
                        "There was an error while sending!")
                    self.message_status_label.setStyleSheet("color: #dc3545;")

                self.timer_msg = QTimer()
                self.timer_msg.timeout.connect(self.message_status_label.clear)
                self.timer_msg.timeout.connect(self.timer_msg.stop)
                self.timer_msg.timeout.connect(self.timer_msg.deleteLater)
                self.timer_msg.start(5000)

            except Exception as e:
                print(e)
                logger.error(e)

    # Startup Processes and connection functions:
    def startup_update_check(self):
        logger.info("Searching for updates")
        self.check_thread = QThread()
        self.checker = CheckForUpdates()
        self.checker.moveToThread(self.check_thread)

        self.checker.update_text.connect(self.update_check_over)

        self.check_thread.started.connect(self.checker.do_work)
        self.check_thread.start()

    def update_check_over(self, update_info):
        self.checker.deleteLater()
        self.check_thread.quit()
        self.check_thread.deleteLater()
        self.check_thread.wait()

        self.update_timer = QTimer()
        self.update_timer.timeout.connect(
            lambda: self.update_message_box(update_info))
        self.update_timer.start(200)

        self.update_check_done = True

    def startup_authorize_user(self):
        logger.info("Trying to login user")

        # Thread:
        self.login_thread = QThread()
        self.login_worker = LoginWorker(self.username, self.token)
        self.login_worker.moveToThread(self.login_thread)

        self.login_worker.result.connect(self.login_control)

        self.login_worker.result.connect(self.login_worker.deleteLater)
        self.login_worker.result.connect(self.login_thread.quit)

        self.login_thread.finished.connect(self.login_thread.deleteLater)
        self.login_thread.finished.connect(self.login_thread.wait)

        self.login_thread.started.connect(self.login_worker.do_work)
        self.login_thread.start()

    # If the user clicks the retry connection button too much:
    def wait_motherfucker(self, *args):
        self.wait_counter += 1

        if self.wait_counter >= 5:
            self.wait_counter = 0
            wait = QMessageBox()
            wait.setIcon(QMessageBox.Warning)
            wait.setText(
                "<strong>I AM TRYING TO CONNECT ALREADY. STOP MASHING THAT DAMN BUTTON"
            )
            wait.setInformativeText(
                """I'm doing my best ok? Just have a little patience please."""
            )
            wait.setWindowTitle("FOR GOD'S SAKE, WAIT!")
            wait.setWindowIcon(QtGui.QIcon('media\\logo\\logo.ico'))

            close = wait.addButton(QMessageBox.Close)
            close.setText("Ok... I will stop")
            wait.setEscapeButton(QMessageBox.Close)
            wait.exec_()

    def retry_connection(self, *args):
        # *args are necessary so it can be called from the mousePressEvent

        # Label
        self.username_label.setText("Connecting")
        self.username_label.mousePressEvent = lambda x: self.wait_motherfucker(
        )

        self.username_label_2.setText("Connecting")
        self.username_label_2.mousePressEvent = lambda x: self.wait_motherfucker(
        )

        # Thread:
        self.login_thread = QThread()
        self.login_worker = LoginWorker(self.username, self.token)
        self.login_worker.moveToThread(self.login_thread)

        # Finished proccess
        self.login_worker.result.connect(self.login_worker.deleteLater)
        self.login_worker.result.connect(self.login_thread.quit)
        self.login_thread.finished.connect(self.login_thread.deleteLater)

        # Response from thread
        self.login_worker.result.connect(self.login_control)

        # Thread starting
        self.login_thread.started.connect(self.login_worker.do_work)
        self.login_thread.start()

        # Verifying new versions:
        self.check_thread = QThread()
        self.checker = CheckForUpdates()
        self.checker.moveToThread(self.check_thread)

        self.checker.update_text.connect(self.update_check_over)

        self.check_thread.started.connect(self.checker.do_work)
        self.check_thread.start()

        # As gui has already loaded, we jump through the code made to stop the message box from appearing while loading
        self.call_update_box = True

    def update_score(self, **kwargs):

        # if 'offline' in kwargs.keys():
        #     self.score_label.setText("")
        #     # self.score_label.setText("Score: Offline")
        #
        # if 'waiting' in kwargs.keys():
        #     self.score_label.setText("")
        #     # self.score_label.setText("Score: Waiting Connection")
        #
        # if 'not_logged' in kwargs.keys():
        #     self.score_label.setText("")
        #     # self.score_label.setText("Score: Not Logged")

        if 'online_score' in kwargs.keys():
            self.score_label.setVisible(True)
            self.line.setVisible(True)
            self.score_label.setText("Online Score: {}".format(
                kwargs['online_score']))

        else:
            self.score_label.setVisible(False)
            self.line.setVisible(False)

    def dog_go_idle(self):
        self.icons_label.setMovie(self.dog_idle)
        self.dog_idle.start()
        self.dog_timer2.stop()

    def dog_run(self):
        self.icons_label.setMovie(self.dog_running)
        self.dog_running.start()
        self.dog_timer.stop()

    # Notification of new version:
    def call_not_critical_update_message_box(self, *args, **kwargs):
        current_version = kwargs['current_version']
        new_version = kwargs['new_version']
        changes = kwargs['changes']

        updateBox = QMessageBox()
        updateBox.setIcon(QMessageBox.Information)
        updateBox.setText(
            """This new version has no critical changes, so you can choose to download it or not. Check the changelog below!"""
        )
        updateBox.setWindowTitle("New Update Available")
        updateBox.setWindowIcon(QtGui.QIcon('media\\logo\\logo.ico'))

        text = """Version available: {1}\n\n{2}""".format(
            current_version, new_version, changes)
        updateBox.setDetailedText(text)

        updateBox.setEscapeButton(QMessageBox.Close)
        updateBox.addButton(QMessageBox.Close)

        ok = updateBox.addButton(QMessageBox.Ok)
        ok.setText('Update')
        ok.clicked.connect(lambda: webbrowser.open(HOME_PAGE_URL))
        updateBox.exec_()

    def update_message_box(self, update_info):

        if self.call_update_box:
            self.update_timer.stop()
            self.update_timer.deleteLater()

            update = update_info['Update']
            logger.info("Update available: %s" % update)

        else:
            update = False

        if update:
            self.update_available = True

            current_version = update_info['Current Version']
            new_version = update_info['New Version']
            changes = update_info['Changes']
            critical = update_info['Critical']
            logger.info("Update critical: %s" % critical)

            if not critical:

                self.v_label.setText(
                    "v{} (<a href='#'>update to v{}</a>)".format(
                        self.version, new_version))
                self.v_label.mousePressEvent = lambda args: self.call_not_critical_update_message_box(
                    current_version=current_version,
                    new_version=new_version,
                    changes=changes,
                )

                self.v_label_2.setText(
                    "v{} (<a href='#'>update to v{}</a>)".format(
                        self.version, new_version))
                self.v_label_2.mousePressEvent = lambda args: self.call_not_critical_update_message_box(
                    current_version=current_version,
                    new_version=new_version,
                    changes=changes,
                )

            if critical:
                updateBox = QMessageBox()
                updateBox.setIcon(QMessageBox.Warning)
                updateBox.setText(
                    "<strong>Your version is super outdated and is not useful anymore!</strong>"
                )
                updateBox.setInformativeText(
                    """Please click <i>Ok</i> to download the newer one. You can also see the changelog details below! <small>(The critical change is highlighted)</small>"""
                )
                updateBox.setWindowTitle("Unsupported Version")
                updateBox.setWindowIcon(QtGui.QIcon('media\\logo\\logo.ico'))

                text = """Version available: {1}\n\n{2}""".format(
                    current_version, new_version, changes)
                updateBox.setDetailedText(text)

                updateBox.setEscapeButton(QMessageBox.Close)
                updateBox.addButton(QMessageBox.Close)
                self.v_label.setText("v{} (v{} REQUIRED)".format(
                    self.version, new_version))
                self.v_label_2.setText("v{} (v{} REQUIRED)".format(
                    self.version, new_version))

                ok = updateBox.addButton(QMessageBox.Ok)
                ok.clicked.connect(lambda: webbrowser.open(HOME_PAGE_URL))
                updateBox.exec_()
                self.close()

    # Data processes:
    def data_start_action(self):
        # Flag to indicate data collection is running
        self.data_running = True

        self.data_start.setEnabled(False)
        self.data_stop.setEnabled(True)
        self.send_btn.setEnabled(False)
        self.logout_btn.setEnabled(False)
        self.login_btn.setEnabled(False)

        # Indicating user about the key being used:
        self.send_status_label.setText(
            f'Using "{self.used_key}" key. Change it in "Fast Config".')
        self.send_status_label.setStyleSheet("color: #007bff;")
        QTimer.singleShot(3000, self.send_status_label.clear)

        # Icon
        self.icons_label.setMovie(self.dog_getting_up)
        self.dog_getting_up.start()
        self.dog_timer = QTimer()
        self.dog_timer.timeout.connect(self.dog_run)
        self.dog_timer.start(370)

        try:  # this is necessary to make the dog don't go idle if the user clicks start>stop too fast.
            self.dog_timer2.deleteLater()
        except:
            pass  # fight me

        # Thread: __init__
        logger.info("Creating data thread")
        self.thread = QThread()

        logger.info(f"Using: {self.used_key}")

        if self.used_key.upper() == "LEFT-CLICK":
            self.worker = SaveData(0x01)
        else:
            self.worker = SaveData(self.used_key)

        self.stop_signal.connect(
            self.worker.stop)  # connect stop signal to worker stop method
        self.worker.moveToThread(self.thread)

        self.worker.finished.connect(
            self.thread.quit
        )  # connect the workers finished signal to stop thread
        self.worker.finished.connect(
            self.worker.deleteLater
        )  # connect the workers finished signal to clean up worker
        self.thread.finished.connect(
            self.thread.deleteLater
        )  # connect threads finished signal to clean up thread

        self.thread.started.connect(self.worker.main)
        self.thread.finished.connect(self.worker.stop)

        self.worker.send_data.connect(self.send_data)

        self.thread.start()

    def data_stop_action(self):
        # Flag to indicate data collection is not running
        self.data_running = False

        if hasattr(self, 'bot_running'):
            if not self.bot_running:
                self.logout_btn.setEnabled(True)
                self.login_btn.setEnabled(True)
        else:
            self.logout_btn.setEnabled(True)
            self.login_btn.setEnabled(True)

        self.data_start.setEnabled(True)
        self.data_stop.setEnabled(False)

        if self.online:
            self.send_btn.setEnabled(True)

        logger.info("Data stopped")
        self.stop_signal.emit()  # emit the finished signal on stop

        # Icon
        self.dog_timer.deleteLater()
        self.icons_label.setMovie(self.dog_sitting_down)
        self.dog_sitting_down.start()

        self.dog_timer2 = QTimer()
        self.dog_timer2.timeout.connect(self.dog_go_idle)
        self.dog_timer2.start(370)

    def send_data(self):
        """
        Creates thread to send data and don't stop the execution of the program while it is uploading.
        Every time the button is clicked, it is created a new thread, that is deleted after the upload.
        """

        self.send_btn.setEnabled(False)
        if self.online:
            try:
                logger.info("Starting send data thread")
                self.send_data_thread = QThread()  # Thread criado
                self.send_data_worker = SendData(version=self.version,
                                                 token=self.token,
                                                 username=self.username)
                self.send_data_worker.moveToThread(self.send_data_thread)

                self.send_data_worker.status_code.connect(
                    self.auto_send_response_code_controller)
                self.send_data_worker.status_code.connect(
                    self.score_worker.single_check_online_score)

                self.send_data_worker.status_code.connect(
                    self.send_data_worker.deleteLater
                )  # Finished then deletes thread and worker
                self.send_data_worker.status_code.connect(
                    self.send_data_thread.quit)
                self.send_data_thread.finished.connect(
                    self.send_data_thread.deleteLater)

                self.send_data_thread.started.connect(
                    self.send_data_worker.send_data)

                self.send_data_thread.start()
                logger.info('Send data thread started')

            except Exception as e:
                logger.error("Could not start thread to send data: %s" % e)
                QMessageBox.information(
                    self, "Oops!",
                    "Could not start thread to send data: %s" % e)

        else:
            # Raises the little offline message
            self.auto_send_response_code_controller(-2)

    def auto_send_response_code_controller(self, code):
        if code == 200:
            self.send_status_label.setText("Success! Thank you for helping!")
            self.send_status_label.setStyleSheet("color: #28a745;")
            self.send_btn.setText("Send Data")

        elif code == -1:
            self.send_status_label.setText('Everything was already sent!')
            self.send_status_label.setStyleSheet("color: #dc3545;")
            self.send_btn.setText("Send Data")

        elif code == -2:
            self.send_status_label.setText(
                'Could not connect to server. Session is saved.')
            self.send_status_label.setStyleSheet("color: #ffaf00;")
            self.update_score(waiting=True)
            self.send_btn.setText("Send Data (upload pending)")

        else:
            self.send_status_label.setText("Verify your connection")
            self.send_status_label.setStyleSheet("color: #dc3545;")
            self.update_score(offline=True)

        QTimer.singleShot(5000, self.send_status_label.clear)

        if hasattr(self, 'data_running'):
            if not self.data_running:
                self.send_btn.setEnabled(True)
        else:
            self.send_btn.setEnabled(True)

    # Bot Functions
    def bot_controller(self):
        self.bot_btn_clicks += 1

        if self.bot_btn_clicks == 1:

            # Configuring control variables
            self.bot_running = True

            # Changing labels
            self.bot_btn.setText("Stop")

            # Disabling buttons that could cause problems
            self.logout_btn.setEnabled(False)
            self.login_btn.setEnabled(False)
            self.change_dataset_btn.setEnabled(False)

        else:

            # Configuring control variables
            self.bot_btn_clicks = 0
            self.bot_running = False

            # Changing labels
            self.bot_btn.setText("Start")

            # Enabling buttons
            self.change_dataset_btn.setEnabled(True)

            if hasattr(self, 'data_running'):
                if not self.data_running:
                    self.logout_btn.setEnabled(True)
                    self.login_btn.setEnabled(True)
            else:
                self.logout_btn.setEnabled(True)
                self.login_btn.setEnabled(True)

    # Account functions:
    def login_control(self, results):
        self.auth_done = True

        if "Logged" in results.keys():
            if results['Logged']:
                logger.info("User successfully logged in")

                self.user_has_logged({
                    "Username": results['Username'],
                    "Token": results['Token'],
                    "Session": results['Session']
                })

            else:
                # If user has failed to login, keep calling the function until loading stops, so it does not pop up
                # with the loading screen still on.
                self.accnt_timer = QTimer()
                self.accnt_timer.timeout.connect(self.login_error)
                self.accnt_timer.start(200)

        if "Offline" in results.keys():
            self.online = False
            self.update_score(offline=True)
            logger.warning("Offline")

            self.login_btn.setVisible(False)
            self.logout_btn.setVisible(False)

            self.username_label.mousePressEvent = self.retry_connection
            self.username_label.setText(
                '<a href="#"><span style=" text-decoration: underline; color:#0000ff;">Retry Connection</span></a>'
            )

            self.username_label_2.mousePressEvent = self.retry_connection
            self.username_label_2.setText(
                '<a href="#"><span style=" text-decoration: underline; color:#0000ff;">Retry Connection</span></a>'
            )
            self.wait_counter = 0

            if self.first_time_running:
                welcome_dialog = WelcomeDialog()
                if welcome_dialog.exec_():
                    self.first_time_running = False

    def login_error(self):
        # when the loading has finished, call the accnt manager pop up if the login failed
        if self.call_accnt_box:
            logger.info("Opening account manager")
            self.accnt_timer.stop()
            self.accnt_timer.deleteLater()

            self.login_btn.setVisible(True)
            self.logout_btn.setVisible(False)

            if not self.ignore_login:
                self.accnt_manager = AccountManager()
                self.accnt_manager.user_logged.connect(self.user_has_logged)
                self.accnt_manager.rejected.connect(self.login_rejected)

                self.accnt_manager.exec_()

            else:
                self.username_label.setText("Not logged")
                self.username_label_2.setText("Not logged")
                self.username_label.mousePressEvent = None
                self.username_label_2.mousePressEvent = None

                self.update_score(not_logged=True)

            if self.first_time_running:
                welcome_dialog = WelcomeDialog()
                if welcome_dialog.exec_():
                    self.first_time_running = False

    def login_rejected(self):

        self.username_label.setText("Not logged")
        self.username_label_2.setText("Not logged")
        self.username_label.mousePressEvent = None
        self.username_label_2.mousePressEvent = None

        self.logout_btn.setVisible(False)
        self.login_btn.setVisible(True)
        self.update_score(not_logged=True)

        with open("config.json", 'r') as f:
            output = json.loads(f.read())
            output["Ignore Login Popup"] = True

        with open("config.json", "w") as f:
            json.dump(output, f, indent=2)

    def user_has_logged(self, user_info):
        # When the user has logged in, create a score thread with its user/password to get the online score.
        # Check the online score as soon as possible.
        logger.info("User logged")
        self.username = user_info['Username']
        self.token = user_info['Token']

        # Checking if there is data to be sent
        if os.listdir('Data\\Training Data'):
            self.send_btn.setText("Send data (upload pending)")
        else:
            self.send_btn.setText("Send data")

        self.online = True
        self.username_label.setText(self.username)
        self.username_label_2.setText(self.username)

        self.send_message_btn.setEnabled(True)
        self.send_message_btn.setText("Send cool message")

        self.login_btn.setVisible(False)
        self.logout_btn.setVisible(True)

        # Fixes bug where offline user collecting data could retry connecting to the server, re-enabling the buttons.
        if hasattr(self, 'data_running'):
            if not self.data_running:
                self.send_btn.setEnabled(True)
                self.data_start.setEnabled(True)
        else:
            self.send_btn.setEnabled(True)
            self.data_start.setEnabled(True)

        # Score Thread initialization
        try:
            self.score_thread = QThread()  # Thread criado
            self.score_worker = QuickCheck(token=self.token,
                                           username=self.username)  #
            self.score_worker.moveToThread(self.score_thread)

            # If logout signal is emmitted, delete the worker and quit then delete the thread too
            self.logout_signal.connect(self.score_worker.deleteLater)
            self.logout_signal.connect(self.score_thread.quit)
            self.score_thread.finished.connect(self.score_thread.deleteLater)

            self.score_worker.online_score.connect(
                lambda ol_score: self.update_score(online_score=ol_score))

            self.score_thread.start()
            logger.info("Score thread started")
            self.score_worker.single_check_online_score()

        except Exception as e:
            logger.error("Could not start score thread: %s" % e)
            QMessageBox.information(self, "Oops!",
                                    "Could not start score thread: %s" % e)

    # When user tries to login with the login button at the menu
    def runtime_login(self):
        self.ignore_login = False
        self.login_control({'Logged': False})

    def logout(self):
        # When user logs out, emit a signal that kills the score_thread and score_worker, because the session will change
        # if the user logs in with another account.
        self.logout_signal.emit()
        logger.info("User logged out")

        self.username = None
        self.token = None

        self.username_label.setText("Not logged")
        self.username_label_2.setText("Not logged")

        self.send_btn.setText("Can't send data while offline")
        self.send_btn.setEnabled(False)
        self.send_message_btn.setEnabled(False)

        with open('config.json', 'r') as f:
            output = json.loads(f.read())
            output['User'] = self.username
            output['Token'] = self.token

        with open('config.json', 'w') as f:
            json.dump(output, f, indent=2)

        self.update_score(not_logged=True)
        self.login_control({"Logged": False})

    def closeEvent(self, event):
        stop_time = datetime.datetime.now()
        runtime = (stop_time - start_time).total_seconds()
        logger.info('---------- CLOSED. Runtime: %ss ----------' % runtime)
        event.accept()  #.ignore
Ejemplo n.º 4
0
class ProgressDialog(QDialog, FORM_CLASS):
    """
    Dialog showing progress in textfield and bar after starting a certain task with run()
    """
    def __init__(self, worker, parent=None, auto_close=False, auto_run=False):
        super().__init__(parent=parent)
        self.parent = parent
        self.setupUi(self)
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.progress_bar.setValue(0)
        self.close_button.clicked.connect(self.close)
        self.stop_button.setVisible(False)
        self.close_button.setVisible(False)
        self.auto_close = auto_close

        self.worker = worker
        self.thread = QThread(self.parent)
        self.worker.moveToThread(self.thread)

        self.thread.started.connect(self.worker.run)
        self.worker.finished.connect(self.finished)
        self.worker.error.connect(self.show_status)
        self.worker.message.connect(self.show_status)
        self.worker.counter.connect(self.progress)

        self.start_button.clicked.connect(self.run)
        self.stop_button.clicked.connect(self.stop)
        self.close_button.clicked.connect(self.close)

        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update_timer)
        if auto_run:
            self.run()

    def running(self):
        self.close_button.setVisible(True)
        self.cancelButton.setText('Stoppen')
        self.cancelButton.clicked.disconnect(self.close)

    def finished(self):
        # already gone if killed
        try:
            self.worker.deleteLater()
        except:
            pass
        self.thread.quit()
        self.thread.wait()
        self.thread.deleteLater()
        self.timer.stop()
        self.close_button.setVisible(True)
        self.stop_button.setVisible(False)
        if self.auto_close:
            self.close()

    def show_status(self, text):
        self.log_edit.appendHtml(text)
        #self.log_edit.moveCursor(QTextCursor.Down)
        scrollbar = self.log_edit.verticalScrollBar()
        scrollbar.setValue(scrollbar.maximum());

    def progress(self, progress, obj=None):
        if isinstance(progress, QVariant):
            progress = progress.toInt()[0]
        self.progress_bar.setValue(progress)

    def start_timer(self):
        self.start_time = datetime.datetime.now()
        self.timer.start(1000)

    # task needs to be overridden
    def run(self):
        self.start_timer()
        self.stop_button.setVisible(True)
        self.start_button.setVisible(False)
        self.thread.start()

    def stop(self):
        self.timer.stop()
        self.worker.kill()
        self.log_edit.appendHtml('<b> Vorgang abgebrochen </b> <br>')
        self.log_edit.moveCursor(QTextCursor.End)
        self.finished()

    def update_timer(self):
        delta = datetime.datetime.now() - self.start_time
        h, remainder = divmod(delta.seconds, 3600)
        m, s = divmod(remainder, 60)
        timer_text = '{:02d}:{:02d}:{:02d}'.format(h, m, s)
        self.elapsed_time_label.setText(timer_text)
Ejemplo n.º 5
0
class QgsFmvPlayer(QMainWindow, Ui_PlayerWindow):
    """ Video Player Class """
    def __init__(self, iface, path=None, parent=None):
        """ Constructor """
        super(QgsFmvPlayer, self).__init__(parent)
        self.setupUi(self)
        self.parent = parent
        self.iface = iface
        self.fileName = None
        self.metadataDlg = None
        self.createingMosaic = False
        self.currentInfo = 0.0

        self.RecGIF = QMovie(":/imgFMV/images/record.gif")

        self.videoWidget.customContextMenuRequested[QPoint].connect(
            self.contextMenuRequested)

        self.duration = 0
        self.playerMuted = False
        self.HasFileAudio = False

        self.player = QMediaPlayer(None, QMediaPlayer.VideoSurface)
        self.player.setNotifyInterval(1000)  # One second
        self.pass_time = 0.1
        self.playlist = QMediaPlaylist()

        #         self.player.setVideoOutput(
        #             self.videoWidget)  # Standar Surface

        self.player.setVideoOutput(
            self.videoWidget.videoSurface())  # Custom Surface

        self.player.durationChanged.connect(self.durationChanged)
        self.player.positionChanged.connect(self.positionChanged)
        self.player.mediaStatusChanged.connect(self.statusChanged)

        self.player.stateChanged.connect(self.setCurrentState)

        self.playerState = QMediaPlayer.StoppedState
        self.playFile(path)

        self.sliderDuration.setRange(0, self.player.duration() / 1000)

        self.volumeSlider.setValue(self.player.volume())
        self.volumeSlider.enterEvent = self.showVolumeTip

        if self.metadataDlg is None:
            self.metadataDlg = QgsFmvMetadata(parent=self, player=self)
            self.addDockWidget(Qt.RightDockWidgetArea, self.metadataDlg)
            self.metadataDlg.setMinimumWidth(500)
            self.metadataDlg.hide()

    def HasMetadata(self, videoPath):
        """ Check if video have Metadata or not """
        try:
            p = _spawn([
                '-i', videoPath, '-map', 'data-re', '-codec', 'copy', '-f',
                'data', '-'
            ])

            stdout_data, _ = p.communicate()

            if stdout_data == b'':
                qgsu.showUserAndLogMessage(QCoreApplication.translate(
                    "QgsFmvPlayer", "This video don't have Metadata ! : "),
                                           level=QGis.Info)
                return False

            return True
        except Exception as e:
            qgsu.showUserAndLogMessage(QCoreApplication.translate(
                "QgsFmvPlayer", "Metadata Callback Failed! : "),
                                       str(e),
                                       level=QGis.Info)

    def HasAudio(self, videoPath):
        """ Check if video have Metadata or not """
        try:
            p = _spawn([
                '-i', videoPath, '-show_streams', '-select_streams', 'a',
                '-loglevel', 'error'
            ],
                       type="ffprobe")

            stdout_data, _ = p.communicate()

            if stdout_data == b'':
                qgsu.showUserAndLogMessage(QCoreApplication.translate(
                    "QgsFmvPlayer", "This video don't have Audio ! : "),
                                           level=QGis.Info)
                return False

            return True
        except Exception as e:
            qgsu.showUserAndLogMessage(QCoreApplication.translate(
                "QgsFmvPlayer", "Audio check Failed! : "),
                                       str(e),
                                       level=QGis.Info)

    def callBackMetadata(self, currentTime, nextTime):
        """ Metadata CallBack """
        try:
            # TODO : Speed this function
            #             stdout_data = _check_output(['-i', self.fileName,
            #                         '-ss', currentTime,
            #                         '-to', nextTime,
            #                         '-f', 'data', '-'])

            t = callBackMetadataThread(cmds=[
                '-i', self.fileName, '-ss', currentTime, '-to', nextTime,
                '-map', 'data-re', '-f', 'data', '-'
            ])
            t.start()
            t.join(1)
            if t.is_alive():
                t.p.terminate()
                t.join()
            if t.stdout == b'':
                return

            for packet in StreamParser(t.stdout):
                try:
                    self.addMetadata(packet.MetadataList())
                    UpdateLayers(packet,
                                 parent=self,
                                 mosaic=self.createingMosaic)
                    self.iface.mapCanvas().refresh()
                    QApplication.processEvents()
                    return
                except Exception as e:
                    None
        except Exception as e:
            qgsu.showUserAndLogMessage(QCoreApplication.translate(
                "QgsFmvPlayer", "Metadata Callback Failed! : "),
                                       str(e),
                                       level=QGis.Info)

    def addMetadata(self, packet):
        ''' Add Metadata to List '''
        self.clearMetadata()
        row = 0
        for key in sorted(packet.keys()):
            self.metadataDlg.VManager.insertRow(row)
            self.metadataDlg.VManager.setItem(row, 0,
                                              QTableWidgetItem(str(key)))
            self.metadataDlg.VManager.setItem(
                row, 1, QTableWidgetItem(str(packet[key][0])))
            self.metadataDlg.VManager.setItem(
                row, 2, QTableWidgetItem(str(packet[key][1])))
            row += 1
        self.metadataDlg.VManager.setVisible(False)
        self.metadataDlg.VManager.resizeColumnsToContents()
        self.metadataDlg.VManager.setVisible(True)
        self.metadataDlg.VManager.verticalScrollBar().setSliderPosition(
            self.sliderPosition)

    def clearMetadata(self):
        ''' Clear Metadata List '''
        try:
            self.sliderPosition = self.metadataDlg.VManager.verticalScrollBar(
            ).sliderPosition()
            self.metadataDlg.VManager.setRowCount(0)
        except:
            None

    def saveInfoToJson(self):
        """ Save video Info to json """
        if not self.KillAllProcessors():
            return
        out_json, _ = QFileDialog.getSaveFileName(self, "Save File", "",
                                                  "Json Files (*.json)")
        if out_json == "":
            return
        try:
            self.VPProbeToJson = Converter()
            self.VPTProbeToJson = QThread()

            self.VPProbeToJson.moveToThread(self.VPTProbeToJson)

            self.VPProbeToJson.finished.connect(self.QThreadFinished)

            self.VPProbeToJson.error.connect(self.QThreadError)

            self.VPProbeToJson.progress.connect(
                self.progressBarProcessor.setValue)

            self.VPTProbeToJson.start(QThread.LowPriority)

            QMetaObject.invokeMethod(self.VPProbeToJson, 'probeToJson',
                                     Qt.QueuedConnection,
                                     Q_ARG(str, self.fileName),
                                     Q_ARG(str, out_json))

        except Exception as e:
            qgsu.showUserAndLogMessage(
                QCoreApplication.translate("QgsFmvPlayer",
                                           "Error saving Json"))
            self.QThreadFinished("probeToJson", "Closing ProbeToJson")

    def showVideoInfo(self):
        ''' Show default probe info '''
        try:

            self.VPProbe = Converter()
            self.VPTProbe = QThread()

            self.VPProbe.moveToThread(self.VPTProbe)

            self.VPProbe.finishedJson.connect(self.QThreadFinished)

            self.VPProbe.error.connect(self.QThreadError)

            self.VPProbe.progress.connect(self.progressBarProcessor.setValue)

            self.VPTProbe.start(QThread.LowPriority)

            QMetaObject.invokeMethod(self.VPProbe, 'probeShow',
                                     Qt.QueuedConnection,
                                     Q_ARG(str, self.fileName))

        except Exception as e:
            qgsu.showUserAndLogMessage(
                QCoreApplication.translate("QgsFmvPlayer", "Error Info Show"))
            self.QThreadFinished("probeShow", "Closing Probe")
        return

    def state(self):
        ''' Return Current State '''
        return self.playerState

    def setCurrentState(self, state):
        ''' Set Current State '''
        if state != self.playerState:
            self.playerState = state

            if state == QMediaPlayer.StoppedState:
                self.btn_play.setIcon(QIcon(":/imgFMV/images/play-arrow.png"))

        return

    def showColorDialog(self):
        ''' Show Color dialog '''
        self.ColorDialog = ColorDialog(parent=self)
        self.ColorDialog.setWindowFlags(Qt.Window | Qt.WindowCloseButtonHint)
        # Fail if not uncheked
        self.actionMagnifying_glass.setChecked(False)
        self.actionZoom_Rectangle.setChecked(False)
        self.ColorDialog.exec_()
        return

    def createMosaic(self, value):
        ''' Function for create Video Mosaic '''
        home = os.path.expanduser("~")

        qgsu.createFolderByName(home, "QGIS_FMV")
        homefmv = os.path.join(home, "QGIS_FMV")
        root, ext = os.path.splitext(os.path.basename(self.fileName))
        qgsu.createFolderByName(homefmv, root)
        self.createingMosaic = value
        # Create Group
        CreateGroupByName()
        return

    def contextMenuRequested(self, point):
        ''' Context Menu Video '''
        menu = QMenu()

        #         actionColors = menu.addAction(
        #             QCoreApplication.translate("QgsFmvPlayer", "Color Options"))
        #         actionColors.setShortcut("Ctrl+May+C")
        #         actionColors.triggered.connect(self.showColorDialog)

        actionMute = menu.addAction(
            QCoreApplication.translate("QgsFmvPlayer", "Mute/Unmute"))
        actionMute.setShortcut("Ctrl+May+U")
        actionMute.triggered.connect(self.setMuted)

        menu.addSeparator()
        actionAllFrames = menu.addAction(
            QCoreApplication.translate("QgsFmvPlayer", "Extract All Frames"))
        actionAllFrames.setShortcut("Ctrl+May+A")
        actionAllFrames.triggered.connect(self.ExtractAllFrames)

        actionCurrentFrames = menu.addAction(
            QCoreApplication.translate("QgsFmvPlayer",
                                       "Extract Current Frame"))
        actionCurrentFrames.setShortcut("Ctrl+May+Q")
        actionCurrentFrames.triggered.connect(self.ExtractCurrentFrame)

        menu.addSeparator()
        actionShowMetadata = menu.addAction(
            QCoreApplication.translate("QgsFmvPlayer", "Show Metadata"))
        actionShowMetadata.setShortcut("Ctrl+May+M")
        actionShowMetadata.triggered.connect(self.OpenQgsFmvMetadata)

        menu.exec_(self.mapToGlobal(point))

    # Start Snnipet FILTERS
    def grayFilter(self, value):
        self.UncheckFilters(self.sender(), value)
        self.videoWidget.SetGray(value)
        self.videoWidget.UpdateSurface()
        return

    def edgeFilter(self, value):
        self.UncheckFilters(self.sender(), value)
        self.videoWidget.SetEdgeDetection(value)
        self.videoWidget.UpdateSurface()
        return

    def invertColorFilter(self, value):
        self.UncheckFilters(self.sender(), value)
        self.videoWidget.SetInvertColor(value)
        self.videoWidget.UpdateSurface()
        return

    def autoContrastFilter(self, value):
        self.UncheckFilters(self.sender(), value)
        self.videoWidget.SetAutoContrastFilter(value)
        self.videoWidget.UpdateSurface()
        return

    def monoFilter(self, value):
        self.UncheckFilters(self.sender(), value)
        self.videoWidget.SetMonoFilter(value)
        self.videoWidget.UpdateSurface()
        return

    def magnifier(self, value):
        self.UncheckUtils(self.sender(), value)
        self.videoWidget.SetMagnifier(value)
        self.videoWidget.UpdateSurface()
        return

    def zoomRect(self, value):
        self.UncheckUtils(self.sender(), value)
        self.videoWidget.SetZoomRect(value)
        self.videoWidget.UpdateSurface()
        return

    def UncheckUtils(self, sender, value):
        #         p = self.player.position()
        #         self.player.setVideoOutput(
        #             self.videoWidget.videoSurface())  # Custom surface
        #         self.player.setPosition(p)
        QApplication.processEvents()
        name = sender.objectName()
        self.actionMagnifying_glass.setChecked(
            True if name == "actionMagnifying_glass" else False)
        self.actionZoom_Rectangle.setChecked(True if name ==
                                             "actionZoom_Rectangle" else False)

        sender.setChecked(value)
        return

    def UncheckFilters(self, sender, value):
        #         p = self.player.position()
        #         self.player.setVideoOutput(
        #             self.videoWidget.videoSurface())  # Custom surface
        #         self.player.setPosition(p)
        #         QApplication.processEvents()
        name = sender.objectName()

        self.actionGray.setChecked(True if name == "actionGray" else False)
        self.actionInvert_Color.setChecked(True if name ==
                                           "actionInvert_Color" else False)
        self.actionMono_Filter.setChecked(True if name ==
                                          "actionMono_Filter" else False)
        self.actionCanny_edge_detection.setChecked(
            True if name == "actionCanny_edge_detection" else False)
        self.actionAuto_Contrast_Filter.setChecked(
            True if name == "actionAuto_Contrast_Filter" else False)

        self.videoWidget.SetGray(True if name == "actionGray" else False)
        self.videoWidget.SetEdgeDetection(
            True if name == "actionCanny_edge_detection" else False)
        self.videoWidget.SetInvertColor(True if name ==
                                        "actionInvert_Color" else False)
        self.videoWidget.SetMonoFilter(True if name ==
                                       "actionMono_Filter" else False)
        self.videoWidget.SetAutoContrastFilter(
            True if name == "actionAuto_Contrast_Filter" else False)

        sender.setChecked(value)
        return

    # End Snnipet FILTERS

    def isMuted(self):
        ''' Is muted video property'''
        return self.playerMuted

    def setMuted(self):
        ''' Muted video '''
        if self.player.isMuted():
            self.btn_volume.setIcon(QIcon(":/imgFMV/images/volume_up.png"))
            self.player.setMuted(False)
            self.volumeSlider.setEnabled(True)
        else:
            self.btn_volume.setIcon(QIcon(":/imgFMV/images/volume_off.png"))
            self.player.setMuted(True)
            self.volumeSlider.setEnabled(False)
        return

    def stop(self):
        ''' Stop video'''
        self.player.stop()
        self.videoWidget.update()
        return

    def volume(self):
        ''' Volume Slider '''
        return self.volumeSlider.value()

    def setVolume(self, volume):
        ''' Tooltip and set value'''
        self.player.setVolume(volume)
        self.showVolumeTip(volume)
        if 0 < volume <= 30:
            self.btn_volume.setIcon(QIcon(":/imgFMV/images/volume_30.png"))
        elif 30 < volume <= 60:
            self.btn_volume.setIcon(QIcon(":/imgFMV/images/volume_60.png"))
        elif 60 < volume <= 100:
            self.btn_volume.setIcon(QIcon(":/imgFMV/images/volume_up.png"))
        elif volume == 0:
            self.btn_volume.setIcon(QIcon(":/imgFMV/images/volume_off.png"))

    def EndMedia(self):
        ''' Button end video position '''
        if self.player.isVideoAvailable():
            self.player.setPosition(self.player.duration())
            self.videoWidget.update()
        return

    def StartMedia(self):
        ''' Button start video position '''
        if self.player.isVideoAvailable():
            self.player.setPosition(0)
            self.videoWidget.update()
        return

    def forwardMedia(self):
        ''' Button forward Video '''
        forwardTime = int(self.player.position()) + 10 * 1000
        if forwardTime > int(self.player.duration()):
            forwardTime = int(self.player.duration())
        self.player.setPosition(forwardTime)

    def rewindMedia(self):
        ''' Button rewind Video '''
        rewindTime = int(self.player.position()) - 10 * 1000
        if rewindTime < 0:
            rewindTime = 0
        self.player.setPosition(rewindTime)

    def AutoRepeat(self, checked):
        ''' Button AutoRepeat Video '''
        if checked:
            self.playlist.setPlaybackMode(QMediaPlaylist.Loop)
        else:
            self.playlist.setPlaybackMode(QMediaPlaylist.Sequential)
        return

    def showVolumeTip(self, _):
        ''' Volume Slider Tooltip Trick '''
        self.style = self.volumeSlider.style()
        self.opt = QStyleOptionSlider()
        self.volumeSlider.initStyleOption(self.opt)
        rectHandle = self.style.subControlRect(self.style.CC_Slider, self.opt,
                                               self.style.SC_SliderHandle)
        self.tip_offset = QPoint(5, 15)
        pos_local = rectHandle.topLeft() + self.tip_offset
        pos_global = self.volumeSlider.mapToGlobal(pos_local)
        QToolTip.showText(pos_global,
                          str(self.volumeSlider.value()) + " %", self)

    def showMoveTip(self, currentInfo):
        ''' Player Silder Move Tooptip Trick '''
        self.style = self.sliderDuration.style()
        self.opt = QStyleOptionSlider()
        self.sliderDuration.initStyleOption(self.opt)
        rectHandle = self.style.subControlRect(self.style.CC_Slider, self.opt,
                                               self.style.SC_SliderHandle)
        self.tip_offset = QPoint(5, 15)
        pos_local = rectHandle.topLeft() + self.tip_offset
        pos_global = self.sliderDuration.mapToGlobal(pos_local)

        tStr = _seconds_to_time(currentInfo)

        QToolTip.showText(pos_global, tStr, self)

    def durationChanged(self, duration):
        ''' Duration video change signal '''
        duration /= 1000
        self.duration = duration
        self.sliderDuration.setMaximum(duration)

    def positionChanged(self, progress):
        ''' Current Video position change '''
        progress /= 1000

        if not self.sliderDuration.isSliderDown():
            self.sliderDuration.setValue(progress)

        self.updateDurationInfo(progress)

    def updateDurationInfo(self, currentInfo):
        ''' Update labels duration Info and CallBack Metadata '''
        duration = self.duration
        self.currentInfo = currentInfo
        if currentInfo or duration:

            totalTime = _seconds_to_time(duration)
            currentTime = _seconds_to_time(currentInfo)
            tStr = currentTime + " / " + totalTime

            nextTime = currentInfo + self.pass_time
            currentTimeInfo = _seconds_to_time_frac(currentInfo)
            nextTimeInfo = _seconds_to_time_frac(nextTime)
            # Metadata CallBack
            self.callBackMetadata(currentTimeInfo, nextTimeInfo)

        else:
            tStr = ""

        self.labelDuration.setText(tStr)

    def handleCursor(self, status):
        ''' Change cursor '''
        if status in (QMediaPlayer.LoadingMedia, QMediaPlayer.BufferingMedia,
                      QMediaPlayer.StalledMedia):
            self.setCursor(Qt.BusyCursor)
        else:
            self.unsetCursor()

    def statusChanged(self, status):
        ''' Signal Status video change '''
        self.handleCursor(status)
        if status == QMediaPlayer.LoadingMedia:
            self.videoAvailableChanged(False)
        elif status == QMediaPlayer.StalledMedia:
            self.videoAvailableChanged(False)
        if status == QMediaPlayer.EndOfMedia:
            self.videoAvailableChanged(True)
        elif status == QMediaPlayer.InvalidMedia:
            qgsu.showUserAndLogMessage(QCoreApplication.translate(
                "QgsFmvPlayer", self.player.errorString()),
                                       level=QGis.Warning)
            self.videoAvailableChanged(False)
        else:
            self.videoAvailableChanged(True)

    def playFile(self, videoPath):
        ''' Play file from path '''
        try:
            RemoveVideoLayers()
            RemoveGroupByName()
            self.fileName = videoPath
            self.playlist = QMediaPlaylist()
            url = QUrl.fromLocalFile(videoPath)
            self.playlist.addMedia(QMediaContent(url))
            self.playlist.setPlaybackMode(QMediaPlaylist.Sequential)
            self.player.setPlaylist(self.playlist)

            self.setWindowTitle("Playing : " +
                                os.path.basename(os.path.normpath(videoPath)))

            if self.HasMetadata(videoPath):
                CreateVideoLayers()
                self.clearMetadata()
                self.lb_cursor_coord.setText(
                    "<span style='font-size:10pt; font-weight:bold;'>Lon :</span>"
                    +
                    "<span style='font-size:9pt; font-weight:normal;'>Null</span>"
                    +
                    "<span style='font-size:10pt; font-weight:bold;'> Lat :</span>"
                    +
                    "<span style='font-size:9pt; font-weight:normal;'>Null</span>"
                )
            else:
                self.btn_GeoReferencing.setEnabled(False)

            self.HasFileAudio = True
            if not self.HasAudio(videoPath):
                self.actionAudio.setEnabled(False)
                self.actionSave_Audio.setEnabled(False)
                self.HasFileAudio = False

            self.playClicked(True)

        except Exception as e:
            qgsu.showUserAndLogMessage(QCoreApplication.translate(
                "QgsFmvPlayer", 'Open Video File : '),
                                       str(e),
                                       level=QGis.Warning)

    def ReciconUpdate(self, frame):
        self.btn_Rec.setIcon(QIcon(self.RecGIF.currentPixmap()))

    def RecordVideo(self, value):
        ''' Cut Video '''
        currentTime = _seconds_to_time(self.currentInfo)

        if value is False:
            self.endRecord = currentTime
            _, file_extension = os.path.splitext(self.fileName)
            out, _ = QFileDialog.getSaveFileName(self, "Save As", "",
                                                 file_extension)
            if not out:
                self.RecGIF.frameChanged.disconnect(self.ReciconUpdate)
                self.RecGIF.stop()
                self.btn_Rec.setIcon(QIcon(":/imgFMV/images/record.png"))
                return False

            lfn = out.lower()
            if not lfn.endswith((file_extension)):
                out += file_extension

            p = _spawn([
                '-i', self.fileName, '-ss', self.startRecord, '-to',
                self.endRecord, '-c', 'copy', out
            ])
            p.communicate()
            qgsu.showUserAndLogMessage(
                QCoreApplication.translate("QgsFmvPlayer",
                                           "Save file succesfully!"))

            self.RecGIF.frameChanged.disconnect(self.ReciconUpdate)
            self.RecGIF.stop()
            self.btn_Rec.setIcon(QIcon(":/imgFMV/images/record.png"))
        else:
            self.startRecord = currentTime
            self.RecGIF.frameChanged.connect(self.ReciconUpdate)
            self.RecGIF.start()
        return

    def videoAvailableChanged(self, available):
        ''' Buttons for video available '''
        # self.btn_Color.setEnabled(available)
        self.btn_CaptureFrame.setEnabled(available)
        self.gb_PlayerControls.setEnabled(available)
        return

    def toggleGroup(self, state):
        ''' Toggle GroupBox '''
        sender = self.sender()
        if state:
            sender.setFixedHeight(sender.sizeHint().height())
        else:
            sender.setFixedHeight(15)

    def playClicked(self, state):
        ''' Stop and Play video '''
        if self.playerState in (QMediaPlayer.StoppedState,
                                QMediaPlayer.PausedState):
            self.btn_play.setIcon(QIcon(":/imgFMV/images/pause.png"))
            self.player.play()
        elif self.playerState == QMediaPlayer.PlayingState:
            self.btn_play.setIcon(QIcon(":/imgFMV/images/play-arrow.png"))
            self.player.pause()

    def seek(self, seconds):
        '''Slider Move'''
        self.player.setPosition(seconds * 1000)
        self.showMoveTip(seconds)

    def convertVideo(self):
        '''Convert Video To Other Format '''
        if not self.KillAllProcessors():
            return
        sel = "mp4 Files (*.mp4)"
        out, _ = QFileDialog.getSaveFileName(
            self, "Save Video as...", None,
            "ogg files (*.ogg);;avi Files (*.avi);;mkv Files (*.mkv);;webm Files (*.webm);;flv Files (*.flv);;mov Files (*.mov);;mp4 Files (*.mp4);;mpg Files (*.mpg);;mp3 Files (*.mp3)",
            sel)

        if not out:
            return False

        lfn = out.lower()
        if not lfn.endswith(('.ogg', '.avi', '.mkv', '.webm', '.flv', '.mov',
                             '.mp4', '.mp3', '.mpg')):
            # The default.
            out += '.mp4'

        try:
            self.VPConverter = Converter()
            self.VPTConverter = QThread()

            self.VPConverter.moveToThread(self.VPTConverter)

            self.VPConverter.finished.connect(self.QThreadFinished)

            self.VPConverter.error.connect(self.QThreadError)

            self.VPConverter.progress.connect(
                self.progressBarProcessor.setValue)

            self.VPTConverter.start(QThread.LowPriority)

            # TODO : Make Correct format Conversion and embebed metadata
            info = self.VPConverter.probeInfo(self.fileName)
            if info is not None:
                if self.HasFileAudio:
                    audio_codec = info.audio.codec
                    audio_samplerate = info.audio.audio_samplerate
                    audio_channels = info.audio.audio_channels

                video_codec = info.video.codec
                video_width = info.video.video_width
                video_height = info.video.video_height
                video_fps = info.video.video_fps

            _, out_ext = os.path.splitext(out)

            if self.HasFileAudio:
                options = {
                    'format': out_ext[1:],
                    'audio': {
                        'codec': audio_codec,
                        'samplerate': audio_samplerate,
                        'channels': audio_channels
                    },
                    'video': {
                        'codec': video_codec,
                        'width': video_width,
                        'height': video_height,
                        'fps': video_fps
                    }
                }
            else:
                options = {
                    'format': out_ext[1:],
                    'video': {
                        'codec': video_codec,
                        'width': video_width,
                        'height': video_height,
                        'fps': video_fps
                    }
                }
            QMetaObject.invokeMethod(self.VPConverter, 'convert',
                                     Qt.QueuedConnection,
                                     Q_ARG(str,
                                           self.fileName), Q_ARG(str, out),
                                     Q_ARG(dict, options), Q_ARG(bool, False))

        except Exception as e:
            qgsu.showUserAndLogMessage(
                QCoreApplication.translate("QgsFmvPlayer",
                                           "Error converting video "))
            self.QThreadFinished("convert", "Closing convert")

    def ShowPlot(self, bitrate_data, frame_count, output=None):
        ''' Show plot,because show not work using threading '''
        matplot.figure().canvas.set_window_title(self.fileName)
        matplot.title("Stream Bitrate vs Time")
        matplot.xlabel("Time (sec)")
        matplot.ylabel("Frame Bitrate (kbit/s)")
        matplot.grid(True)
        # map frame type to color
        frame_type_color = {
            # audio
            'A': 'yellow',
            # video
            'I': 'red',
            'P': 'green',
            'B': 'blue'
        }

        global_peak_bitrate = 0.0
        global_mean_bitrate = 0.0

        # render charts in order of expected decreasing size
        for frame_type in ['I', 'P', 'B', 'A']:

            # skip frame type if missing
            if frame_type not in bitrate_data:
                continue

            # convert list of tuples to numpy 2d array
            frame_list = bitrate_data[frame_type]
            frame_array = numpy.array(frame_list)

            # update global peak bitrate
            peak_bitrate = frame_array.max(0)[1]
            if peak_bitrate > global_peak_bitrate:
                global_peak_bitrate = peak_bitrate

            # update global mean bitrate (using piecewise mean)
            mean_bitrate = frame_array.mean(0)[1]
            global_mean_bitrate += mean_bitrate * \
                (len(frame_list) / frame_count)

            # plot chart using gnuplot-like impulses
            matplot.vlines(frame_array[:, 0], [0],
                           frame_array[:, 1],
                           color=frame_type_color[frame_type],
                           label="{} Frames".format(frame_type))

        self.progressBarProcessor.setValue(90)
        # calculate peak line position (left 15%, above line)
        peak_text_x = matplot.xlim()[1] * 0.15
        peak_text_y = global_peak_bitrate + \
            ((matplot.ylim()[1] - matplot.ylim()[0]) * 0.015)
        peak_text = "peak ({:.0f})".format(global_peak_bitrate)

        # draw peak as think black line w/ text
        matplot.axhline(global_peak_bitrate, linewidth=2, color='black')
        matplot.text(peak_text_x,
                     peak_text_y,
                     peak_text,
                     horizontalalignment='center',
                     fontweight='bold',
                     color='black')

        # calculate mean line position (right 85%, above line)
        mean_text_x = matplot.xlim()[1] * 0.85
        mean_text_y = global_mean_bitrate + \
            ((matplot.ylim()[1] - matplot.ylim()[0]) * 0.015)
        mean_text = "mean ({:.0f})".format(global_mean_bitrate)

        # draw mean as think black line w/ text
        matplot.axhline(global_mean_bitrate, linewidth=2, color='black')
        matplot.text(mean_text_x,
                     mean_text_y,
                     mean_text,
                     horizontalalignment='center',
                     fontweight='bold',
                     color='black')

        matplot.legend()
        if output != "":
            matplot.savefig(output)
        else:
            matplot.show()

        self.progressBarProcessor.setValue(100)

    def CreateBitratePlot(self):
        ''' Create video Plot Bitrate Thread '''
        if not self.KillAllProcessors():
            return
        try:
            self.VPBitratePlot = CreatePlotsBitrate()
            self.VPTBitratePlot = QThread()

            self.VPBitratePlot.moveToThread(self.VPTBitratePlot)

            self.VPBitratePlot.finished.connect(self.QThreadFinished)

            self.VPBitratePlot.return_fig.connect(self.ShowPlot)

            self.VPBitratePlot.error.connect(self.QThreadError)

            self.VPBitratePlot.progress.connect(
                self.progressBarProcessor.setValue)

            self.VPTBitratePlot.start(QThread.LowPriority)

            sender = self.sender().objectName()

            if sender == "actionAudio":
                QMetaObject.invokeMethod(self.VPBitratePlot, 'CreatePlot',
                                         Qt.QueuedConnection,
                                         Q_ARG(str, self.fileName),
                                         Q_ARG(str, None), Q_ARG(str, 'audio'))

            elif sender == "actionVideo":
                QMetaObject.invokeMethod(self.VPBitratePlot, 'CreatePlot',
                                         Qt.QueuedConnection,
                                         Q_ARG(str, self.fileName),
                                         Q_ARG(str, None), Q_ARG(str, 'video'))

            elif sender == "actionSave_Audio":
                selfilter = "Portable Network Graphics (*.png)"
                fileaudio, _ = QFileDialog.getSaveFileName(
                    self, "Save Audio Bitrate Plot", "",
                    "EPS Encapsulated Postscript (*.eps);;"
                    "PGF code for LaTex (*.pgf);;"
                    "Portable document format(*pdf);;"
                    "Portable Network Graphics (*.png);;"
                    "Postscript (*.ps);;"
                    "Raw RGBA bitmap (*.raw*.rgba);;"
                    "Scalable vector graphics (*.svg*.svgz)", selfilter)
                if fileaudio == "":
                    return

                QMetaObject.invokeMethod(self.VPBitratePlot, 'CreatePlot',
                                         Qt.QueuedConnection,
                                         Q_ARG(str, self.fileName),
                                         Q_ARG(str, fileaudio),
                                         Q_ARG(str, 'audio'))

            elif sender == "actionSave_Video":
                selfilter = "Portable Network Graphics (*.png)"
                filevideo, _ = QFileDialog.getSaveFileName(
                    self, "Save Video Bitrate Plot", "",
                    "EPS Encapsulated Postscript (*.eps);;"
                    "PGF code for LaTex (*.pgf);;"
                    "Portable document format(*pdf);;"
                    "Portable Network Graphics (*.png);;"
                    "Postscript (*.ps);;"
                    "Raw RGBA bitmap (*.raw*.rgba);;"
                    "Scalable vector graphics (*.svg*.svgz)", selfilter)
                if filevideo == "":
                    return

                QMetaObject.invokeMethod(self.VPBitratePlot, 'CreatePlot',
                                         Qt.QueuedConnection,
                                         Q_ARG(str, self.fileName),
                                         Q_ARG(str, filevideo),
                                         Q_ARG(str, 'video'))

        except Exception as e:
            qgsu.showUserAndLogMessage(
                QCoreApplication.translate("QgsFmvPlayer",
                                           "Failed creating Plot Bitrate"))

    def ExtractAllFrames(self):
        """ Extract All Video Frames Thread """
        if not self.KillAllProcessors():
            return

        options = QFileDialog.DontResolveSymlinks | QFileDialog.ShowDirsOnly
        directory = QFileDialog.getExistingDirectory(
            self,
            QCoreApplication.translate("QgsFmvPlayer", "Save images"),
            '',
            options=options)

        if directory:

            self.VPExtractFrames = ExtractFramesProcessor()
            self.VPTExtractAllFrames = QThread()

            self.VPExtractFrames.moveToThread(self.VPTExtractAllFrames)
            self.VPExtractFrames.finished.connect(self.QThreadFinished)
            self.VPExtractFrames.error.connect(self.QThreadError)
            self.VPExtractFrames.progress.connect(
                self.progressBarProcessor.setValue)
            self.VPTExtractAllFrames.start(QThread.LowPriority)

            QMetaObject.invokeMethod(self.VPExtractFrames,
                                     'ExtractFrames', Qt.QueuedConnection,
                                     Q_ARG(str, directory),
                                     Q_ARG(str, self.fileName))
        return

    def ExtractCurrentFrame(self):
        """ Extract Current Frame Thread """
        image = self.videoWidget.GetCurrentFrame()
        out_image, _ = QFileDialog.getSaveFileName(
            self, "Save Current Frame", "",
            "Image File (*.png *.jpg *.bmp *.tiff)")

        if out_image == "":
            return

        if out_image:
            t = threading.Thread(target=self.SaveCapture,
                                 args=(
                                     image,
                                     out_image,
                                 ))
            t.start()
        return

    def SaveCapture(self, image, output):
        ''' Save Current Image '''
        image.save(output)
        QApplication.processEvents()
        return

    def QThreadFinished(self, process, msg, outjson=None):
        ''' Finish Threads '''
        if process == "ExtractFramesProcessor":
            self.VPExtractFrames.deleteLater()
            self.VPTExtractAllFrames.terminate()
            self.VPTExtractAllFrames.deleteLater()
        elif process == "CreatePlotsBitrate":
            self.VPBitratePlot.deleteLater()
            self.VPTBitratePlot.terminate()
            self.VPTBitratePlot.deleteLater()
        elif process == "convert":
            self.VPConverter.deleteLater()
            self.VPTConverter.terminate()
            self.VPTConverter.deleteLater()
        elif process == "probeToJson":
            self.VPProbeToJson.deleteLater()
            self.VPTProbeToJson.terminate()
            self.VPTProbeToJson.deleteLater()
        elif process == "probeShow":
            self.VPProbe.deleteLater()
            self.VPTProbe.terminate()
            self.VPTProbe.deleteLater()
            self.showVideoInfoDialog(outjson)

        QApplication.processEvents()
        self.progressBarProcessor.setValue(0)
        return

    def QThreadError(self, processor, e, exception_string):
        """ Threads Errors"""
        qgsu.showUserAndLogMessage(QCoreApplication.translate(
            "QgsFmvPlayer", processor),
                                   'Failed!\n'.format(exception_string),
                                   level=QGis.Warning)

        self.QThreadFinished(processor, "Closing Processor")

        return

    def OpenQgsFmvMetadata(self):
        """ Open Metadata Dock """
        if self.metadataDlg is None:
            self.metadataDlg = QgsFmvMetadata(parent=self, player=self)
            self.addDockWidget(Qt.RightDockWidgetArea, self.metadataDlg)
            self.metadataDlg.show()
        else:
            self.metadataDlg.show()
        return

    def KillAllProcessors(self):
        """Kill All Processors"""
        """ Extract all frames Processors """
        try:
            if self.VPTExtractAllFrames.isRunning():
                ret = qgsu.CustomMessage(
                    QCoreApplication.translate(
                        "QgsFmvPlayer", "HEY...Active background process!"),
                    QCoreApplication.translate("QgsFmvPlayer",
                                               "Do you really want close?"))
                if ret == QMessageBox.Yes:
                    self.QThreadFinished("ExtractFramesProcessor",
                                         "Closing Extract Frames Processor")
                else:
                    return False
        except:
            None
        """ Bitrates Processors"""
        try:
            if self.VPTBitratePlot.isRunning():
                ret = qgsu.CustomMessage(
                    QCoreApplication.translate(
                        "QgsFmvPlayer", "HEY...Active background process!"),
                    QCoreApplication.translate("QgsFmvPlayer",
                                               "Do you really want close?"))
                if ret == QMessageBox.Yes:
                    self.QThreadFinished("CreatePlotsBitrate",
                                         "Closing Plot Bitrate")
                else:
                    return False
        except:
            None
        """ Converter Processors """
        try:
            if self.VPTConverter.isRunning():
                ret = qgsu.CustomMessage(
                    QCoreApplication.translate(
                        "QgsFmvPlayer", "HEY...Active background process!"),
                    QCoreApplication.translate("QgsFmvPlayer",
                                               "Do you really want close?"))
                if ret == QMessageBox.Yes:
                    self.QThreadFinished("convert", "Closing convert")
                else:
                    return False
        except:
            None
        """ probeToJson Processors """
        try:
            if self.VPTProbeToJson.isRunning():
                ret = qgsu.CustomMessage(
                    QCoreApplication.translate(
                        "QgsFmvPlayer", "HEY...Active background process!"),
                    QCoreApplication.translate("QgsFmvPlayer",
                                               "Do you really want close?"))
                if ret == QMessageBox.Yes:
                    self.QThreadFinished("probeToJson", "Closing Info to Json")
                else:
                    return False
        except:
            None
        """ probeShow Processors """
        try:
            if self.VPTProbe.isRunning():
                ret = qgsu.CustomMessage(
                    QCoreApplication.translate(
                        "QgsFmvPlayer", "HEY...Active background process!"),
                    QCoreApplication.translate("QgsFmvPlayer",
                                               "Do you really want close?"))
                if ret == QMessageBox.Yes:
                    self.QThreadFinished("probeShow",
                                         "Closing Show Video Info")
                else:
                    return False
        except:
            None
        return True

    def showVideoInfoDialog(self, outjson):
        """ Show Video Information Dialog """
        view = QTreeView()
        model = QJsonModel()
        view.setModel(model)
        model.loadJsonFromConsole(outjson)

        self.VideoInfoDialog = QDialog(self)
        self.VideoInfoDialog.setWindowTitle("Video Information : " +
                                            self.fileName)
        self.VideoInfoDialog.setWindowIcon(
            QIcon(":/imgFMV/images/video_information.png"))

        self.verticalLayout = QVBoxLayout(self.VideoInfoDialog)
        self.verticalLayout.addWidget(view)
        view.expandAll()
        view.header().setSectionResizeMode(QHeaderView.ResizeToContents)

        self.VideoInfoDialog.setWindowFlags(Qt.Window
                                            | Qt.WindowCloseButtonHint)
        self.VideoInfoDialog.setObjectName("VideoInfoDialog")
        self.VideoInfoDialog.resize(500, 400)
        self.VideoInfoDialog.show()

    def closeEvent(self, evt):
        """ Close Event """
        if self.KillAllProcessors() is False:
            evt.ignore()
            return

        self.player.stop()
        self.parent._PlayerDlg = None
        self.parent.ToggleActiveFromTitle()
        RemoveVideoLayers()
        RemoveGroupByName()

        # Restore Filters State
        self.videoWidget.RestoreFilters()
        # QApplication.processEvents()
        del self.player
Ejemplo n.º 6
0
class FixateGUI(QtWidgets.QMainWindow, layout.Ui_FixateUI):
    """
    GUI Main window
    """
    # QT Signals
    # These are the thread safe signals to update UI elements
    # Multiple Choices/ OK  signal
    sig_choices_input = pyqtSignal(str, tuple)
    # Updates the test Information above the image
    sig_label_update = pyqtSignal(str, str)
    # Signal for the text user input
    sig_text_input = pyqtSignal(str)
    # Timer for abort cleanup. TODO Rethink?
    sig_timer = pyqtSignal()
    # Tree Events
    sig_tree_init = pyqtSignal(list)
    sig_tree_update = pyqtSignal(str, str)
    # Active Window
    sig_active_update = pyqtSignal(str)
    sig_active_clear = pyqtSignal()
    # History Window
    sig_history_update = pyqtSignal(str)
    sig_history_clear = pyqtSignal(str)
    # Error Window
    sig_error_update = pyqtSignal(str)
    sig_error_clear = pyqtSignal(str)
    # Image Window
    sig_image_update = pyqtSignal(str)
    sig_image_clear = pyqtSignal()

    # Progress Signals
    sig_indicator_start = pyqtSignal()
    sig_indicator_stop = pyqtSignal()
    sig_working = pyqtSignal()
    sig_progress = pyqtSignal()
    sig_finish = pyqtSignal()

    # Deprecated Replace with Active , History and Error Window signals
    output_signal = pyqtSignal(str, str)
    # Deprecated replace with Image Window signals
    update_image = pyqtSignal(str, bool)

    """Class Constructor and destructor"""

    def __init__(self, worker, application):
        super(FixateGUI, self).__init__(None)
        self.application = application
        self.register_events()
        self.setupUi(self)
        self.treeSet = False
        self.blocked = False
        self.closing = False

        # Extra GUI setup not supported in the designer
        self.TestTree.setColumnWidth(1, 90)
        self.TestTree.header().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
        self.TestTree.header().setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)

        self.base_image = ""
        self.dialog = None
        self.image_scene = QtWidgets.QGraphicsScene()
        self.ImageView.set_scene(self.image_scene)
        self.ImageView.setScene(self.image_scene)

        self.working_indicator = QtGui.QMovie(QT_GUI_WORKING_INDICATOR)
        self.WorkingIndicator.setMovie(self.working_indicator)
        self.start_indicator()

        self.status_code = -1  # Default status code used to check for unusual exit

        # Timers and Threads
        self.input_queue = Queue()
        self.abort_timer = QtCore.QTimer(self)
        self.abort_timer.timeout.connect(self.abort_check)
        self.worker = SequencerThread(worker)
        self.worker_thread = QThread()
        self.worker.moveToThread(self.worker_thread)
        self.worker_thread.started.connect(self.worker.run_thread)

        self.user_action_queue = None
        self.abort_queue = None

        # UI Binds
        self.Button_1.clicked.connect(self.button_1_click)
        self.Button_2.clicked.connect(self.button_2_click)
        self.Button_3.clicked.connect(self.button_3_click)
        self.UserInputBox.submit.connect(self.text_input_submit)

        self.bind_qt_signals()
        sys.excepthook = exception_hook  # TODO DEBUG REMOVE

    def run_sequencer(self):
        self.worker_thread.start()

    def closeEvent(self, event):
        """ This function overrides closeEvent from the MainWindow class, called in case of unusual termination"""

        event.ignore()
        self.hide()
        self.clean_up()

    def bind_qt_signals(self):
        """
        Binds the qt signals to the appropriate handlers
        :return:
        """
        # Signal Binds
        self.sig_finish.connect(self.clean_up)  # Normal termination
        self.sig_choices_input.connect(self.get_input)
        self.sig_label_update.connect(self.display_test)
        self.sig_text_input.connect(self.open_text_input)
        self.sig_timer.connect(self.start_timer)
        self.sig_tree_init.connect(self.display_tree)
        self.sig_tree_update.connect(self.update_tree)
        self.sig_progress.connect(self.progress_update)

        # New Binds
        self.sig_indicator_start.connect(self._start_indicator)
        self.sig_indicator_stop.connect(self._stop_indicator)
        self.sig_active_update.connect(self._active_update)
        self.sig_active_clear.connect(self._active_clear)
        self.sig_history_update.connect(self.history_update)
        self.sig_history_clear.connect(self.history_clear)
        self.sig_error_update.connect(self.error_update)
        self.sig_error_clear.connect(self.error_clear)
        self.sig_image_update.connect(self._image_update)
        self.sig_image_clear.connect(self._image_clear)
        # Deprecated
        # self.update_image.connect(self.display_image)
        # self.output_signal.connect(self.display_output)
        # self.working.connect(self.start_indicator)

    """Pubsub handlers for setup and teardown
       These are run in the main thread"""

    def register_events(self):
        pub.subscribe(self._seq_abort, "Sequence_Abort")
        pub.subscribe(self._user_ok, 'UI_req')
        pub.subscribe(self._user_choices, "UI_req_choices")
        pub.subscribe(self._user_input, 'UI_req_input')
        pub.subscribe(self._user_display, 'UI_display')
        pub.subscribe(self._user_display_important, "UI_display_important")
        pub.subscribe(self._user_action, 'UI_action')
        pub.subscribe(self._completion_code, 'Finish')
        # Image Window
        pub.subscribe(self.image_update, "UI_image")
        pub.subscribe(self.image_clear, "UI_image_clear")
        pub.subscribe(self.image_clear, "UI_block_end")
        # Active Window
        pub.subscribe(self.active_clear, "UI_block_end")
        # Multi Window
        pub.subscribe(self._print_test_start, 'Test_Start')
        pub.subscribe(self._print_test_seq_start, 'TestList_Start')
        pub.subscribe(self._print_test_complete, 'Test_Complete')
        pub.subscribe(self._print_comparisons, 'Check')
        pub.subscribe(self._print_errors, "Test_Exception")
        pub.subscribe(self._print_sequence_end, "Sequence_Complete")
        pub.subscribe(self._print_test_skip, 'Test_Skip')
        pub.subscribe(self._print_test_retry, 'Test_Retry')

        # Error Window

        # Working Indicator
        pub.subscribe(self.start_indicator, 'Test_Start')
        pub.subscribe(self.start_indicator, 'UI_block_end')
        pub.subscribe(self.stop_indicator, 'UI_block_start')

        return

    def unregister_events(self):
        pub.unsubAll()
        return

    """Slot handlers for thread-gui interaction
       These are run in the main thread"""

    def start_timer(self):
        self.abort_timer.start(100)

    def abort_check(self):
        if self.abort_queue is None:
            return
        try:
            self.abort_queue.get_nowait()
            self.abort_queue = None
            self.button_reset(True)
            self.abort_timer.stop()
        except Empty:
            return

    def open_text_input(self, message):
        self.ActiveEvent.append(message)
        self.ActiveEvent.verticalScrollBar().setValue(self.ActiveEvent.verticalScrollBar().maximum())
        self.Events.append(message)
        self.Events.verticalScrollBar().setValue(self.Events.verticalScrollBar().maximum())
        self.UserInputBox.setPlaceholderText("Input:")
        self.UserInputBox.setEnabled(True)
        self.UserInputBox.setFocus()

    def start_indicator(self, **kwargs):
        self.sig_indicator_start.emit()

    def _start_indicator(self):
        self.WorkingIndicator.show()
        self.working_indicator.start()

    def stop_indicator(self):
        self.sig_indicator_stop.emit()

    def _stop_indicator(self):
        self.working_indicator.stop()
        self.WorkingIndicator.hide()

    def retrieve_packaged_data(self, path):
        try:
            return pkgutil.get_data("module.loaded_tests", path)
        except FileNotFoundError:
            return b""

    def image_update(self, path):
        self.sig_image_update.emit(path)

    def _image_update(self, path):
        """
        Adds an image to the image viewer. These images can be stacked with transparent layers to form overlays
        :param path: Relative path to image within the test scripts package
        :return: None
        """
        image = QtGui.QPixmap()
        image.loadFromData(self.retrieve_packaged_data(path))
        if image.isNull():
            self.file_not_found(path)
        self.image_scene.addPixmap(image)
        self.ImageView.fitInView(0, 0, self.image_scene.width(), self.image_scene.height(), QtCore.Qt.KeepAspectRatio)

    def image_clear(self):
        self.sig_image_clear.emit()

    def _image_clear(self):
        self.image_scene.clear()

    def display_image(self, path="", overlay=False):
        if path == "" or not overlay:
            self.image_scene.clear()
            if overlay:
                image = QtGui.QPixmap()
                image.loadFromData(self.base_image)
                if image.isNull():
                    self.file_not_found(self.base_image)
            elif path == "":
                self.base_image = path
                return
            else:
                self.base_image = self.retrieve_packaged_data(path)
                image = QtGui.QPixmap()
                image.loadFromData(self.base_image)
                if image.isNull():
                    self.file_not_found(path)
            self.image_scene.addPixmap(image)
            self.ImageView.fitInView(0, 0, self.image_scene.width(), self.image_scene.height(),
                                     QtCore.Qt.KeepAspectRatio)
            return
        image = QtGui.QPixmap()
        image.loadFromData(self.retrieve_packaged_data(path))
        if image.isNull():
            self.file_not_found(path)
        self.image_scene.addPixmap(image)
        self.ImageView.fitInView(0, 0, self.image_scene.width(), self.image_scene.height(), QtCore.Qt.KeepAspectRatio)
        return

    def file_not_found(self, path):
        """
        Display warning box for an invalid image path
        :param path:
        :return:
        """

        self.dialog = QtWidgets.QMessageBox()
        self.dialog.setText("Warning: Image not Found")
        self.dialog.setInformativeText("Filename: {}".format(path))
        self.dialog.setStandardButtons(QtWidgets.QMessageBox.Ok)
        self.dialog.setDefaultButton(QtWidgets.QMessageBox.Ok)
        self.dialog.setIcon(QtWidgets.QMessageBox.Warning)
        self.dialog.exec()

    def display_tree(self, tree):

        # Make sure this function is only run once
        if self.treeSet:
            return
        self.treeSet = True

        level_stack = []
        for item in tree:
            # Check Level
            if item[0].count('.') + 1 <= len(level_stack):  # Case 1: Going out one or more levels or same level
                for _ in range(len(level_stack) - item[0].count('.')):
                    level_stack.pop()
            elif item[0].count('.') + 1 > len(level_stack):  # Case 2: Going in one or more levels
                for index in range(item[0].count('.') + 1 - len(level_stack), 0, -1):
                    split_index = item[0].split('.')
                    if index > 1:  # More than one level, append dummy items as required
                        dummy = QtWidgets.QTreeWidgetItem()
                        dummy.setText(0, '.'.join(split_index[:-(index - 1)]))
                        dummy.setText(1, 'Queued')
                        dummy.setTextAlignment(1, QtCore.Qt.AlignRight)
                        level_stack.append(dummy.clone())

            tree_item = QtWidgets.QTreeWidgetItem()
            tree_item.setText(0, item[0] + '. ' + item[1])
            tree_item.setTextAlignment(1, QtCore.Qt.AlignRight)
            tree_item.setText(1, 'Queued')

            level_stack.append(tree_item.clone())
            if len(level_stack) > 1:  # Child Add
                level_stack[-2].addChild(level_stack[-1])
            else:  # Top Level
                self.TestTree.addTopLevelItem(level_stack[-1])

    def update_tree(self, test_index, status):

        if len(test_index) == 0:
            return

        colours = get_status_colours(status)
        test_index = test_index.split('.')

        #   Find the test in the tree
        current_test = self.TestTree.findItems(test_index[0], QtCore.Qt.MatchStartsWith, 0)[0]
        while len(test_index) > 1:
            test_index[0:2] = [''.join(test_index[0] + '.' + test_index[1])]
            for child_index in range(current_test.childCount()):
                if current_test.child(child_index).text(0).startswith(test_index[0]):
                    current_test = current_test.child(child_index)
                    break

        # Update the test
        if status not in ["Aborted"]:
            for i in range(2):
                current_test.setBackground(i, colours[0])
                current_test.setForeground(i, colours[1])
            current_test.setText(1, status)
            current_test.setExpanded(True)

        # In case of an abort, update all remaining tests
        else:
            self.active_update("Aborting, please wait...")
            sub_finish = False
            original_test = current_test
            while current_test is not None:
                if current_test.text(1) in ["Queued"]:
                    for i in range(2):
                        current_test.setBackground(i, colours[0])
                        current_test.setForeground(i, colours[1])
                    current_test.setText(1, status)
                    current_test.setExpanded(False)
                if current_test.childCount() > 0 and not sub_finish:  # Go in a level
                    current_test = current_test.child(0)
                    sub_finish = False
                elif current_test.parent() is not None:
                    if current_test.parent().indexOfChild(
                            current_test) >= current_test.parent().childCount() - 1:  # Come out a level
                        sub_finish = True
                        current_test = current_test.parent()
                    else:
                        current_test = current_test.parent().child(
                            current_test.parent().indexOfChild(current_test) + 1)  # Same level
                        sub_finish = False
                else:  # Top level test, go to next test
                    current_test = self.TestTree.topLevelItem(self.TestTree.indexOfTopLevelItem(current_test) + 1)
                    sub_finish = False
            current_test = original_test

        # Check for last test in group
        while current_test.parent() is not None and (current_test.parent().indexOfChild(
                current_test) >= current_test.parent().childCount() - 1 or status in ["Aborted"]):
            parent_status = current_test.text(1)
            current_test = current_test.parent()
            for child_index in range(current_test.childCount()):  # Check status of all child tests
                check_status = current_test.child(child_index).text(1)
                if list(STATUS_PRIORITY.keys()).index(check_status) < list(STATUS_PRIORITY.keys()).index(parent_status):
                    parent_status = check_status
            colours = get_status_colours(parent_status)
            for i in range(2):
                current_test.setBackground(i, colours[0])
                current_test.setForeground(i, colours[1])
            current_test.setText(1, parent_status)
            if parent_status not in ["In Progress"]:
                current_test.setExpanded(False)

    def display_test(self, test_index, description):
        self.ActiveTest.setText("Test {}:".format(test_index))
        self.TestDescription.setText("{}".format(description))

    def active_update(self, msg, **kwargs):
        self.sig_active_update.emit(msg)

    def _active_update(self, message):
        self.ActiveEvent.append(message)
        self.ActiveEvent.verticalScrollBar().setValue(self.ActiveEvent.verticalScrollBar().maximum())

    def active_clear(self, **kwargs):
        self.sig_active_clear.emit()

    def _active_clear(self):
        self.ActiveEvent.clear()
        self.ActiveEvent.verticalScrollBar().setValue(self.ActiveEvent.verticalScrollBar().maximum())

    def history_update(self, message):
        self.Events.append(message)
        self.Events.verticalScrollBar().setValue(self.Events.verticalScrollBar().maximum())

    def history_clear(self):
        self.Events.clear()
        self.Events.verticalScrollBar().setValue(self.Events.verticalScrollBar().maximum())

    def error_update(self, message):
        self.Errors.append(message)
        self.Errors.verticalScrollBar().setValue(self.Errors.verticalScrollBar().maximum())

    def error_clear(self):
        self.Errors.clear()
        self.Errors.verticalScrollBar().setValue(self.Errors.verticalScrollBar().maximum())

    # def display_output(self, message, status):
    #     self.Events.append(message)
    #     self.Events.verticalScrollBar().setValue(self.Events.verticalScrollBar().maximum())
    #
    #     if status == "False":  # Print errors
    #         self.Errors.append(self.ActiveTest.text() + ' - ' + message[1:])
    #         self.Errors.verticalScrollBar().setValue(self.Errors.verticalScrollBar().maximum())
    #
    #     if status in ["Active", "False"]:
    #         self.ActiveEvent.append(message)
    #         self.ActiveEvent.verticalScrollBar().setValue(self.ActiveEvent.verticalScrollBar().maximum())

    def progress_update(self):
        self.ActiveEvent.clear()
        self.ProgressBar.setValue(self.worker.worker.get_current_task())
        if self.worker.worker.sequencer.tests_failed > 0 or self.worker.worker.sequencer.tests_errored > 0:
            self.ProgressBar.setStyleSheet(ERROR_STYLE)

    def get_input(self, message, choices):
        self.Events.append(message)
        self.ActiveEvent.append(message)
        self.Events.verticalScrollBar().setValue(self.Events.verticalScrollBar().maximum())
        self.ActiveEvent.verticalScrollBar().setValue(self.ActiveEvent.verticalScrollBar().maximum())
        if isinstance(choices, bool):
            pass
        elif len(choices) == 1:
            self.Button_2.setText(choices[0])
            self.Button_2.setShortcut(QtGui.QKeySequence(choices[0][0:1]))
            self.Button_2.setEnabled(True)
            self.Button_2.setDefault(True)
            self.Button_2.setFocus()
        elif len(choices) == 2:
            self.Button_1.setText(choices[0])
            self.Button_1.setShortcut(QtGui.QKeySequence(choices[0][0:1]))
            self.Button_1.setEnabled(True)
            self.Button_1.setDefault(True)
            self.Button_1.setFocus()
            self.Button_3.setText(choices[1])
            self.Button_3.setShortcut(QtGui.QKeySequence(choices[1][0:1]))
            self.Button_3.setEnabled(True)
        else:
            self.Button_1.setText(choices[0])
            self.Button_1.setShortcut(QtGui.QKeySequence(choices[0][0:1]))
            self.Button_1.setEnabled(True)
            self.Button_1.setDefault(True)
            self.Button_1.setFocus()
            self.Button_2.setText(choices[1])
            self.Button_2.setShortcut(QtGui.QKeySequence(choices[1][0:1]))
            self.Button_2.setEnabled(True)
            self.Button_3.setText(choices[2])
            self.Button_3.setShortcut(QtGui.QKeySequence(choices[2][0:1]))
            self.Button_3.setEnabled(True)

    def _seq_abort(self, exception=None):
        """
        This function ensures that sequence aborting is handled correctly if the sequencer is blocked waiting for input
        """

        # Release user input waiting loops
        if self.user_action_queue is not None:
            self.user_action_queue.put(False)
            self.user_action_queue = None
        if self.abort_queue is not None:
            self.abort_queue.put(True)
            self.abort_queue = None

        # Release sequence blocking calls
        if self.blocked:
            self.input_queue.put("ABORT_FORCE")

    def clean_up(self):
        """
        This function is the second one called for normal termination, and the first one called for unusual termination.
        Check for abnormal termination, and stop the sequencer if required; then stop and delete the thread
        """

        if self.worker_thread is None:  # This function has already run, therefore main already has the status code
            return

        # The following actions must be done in a specific order, be careful when making changes to this section
        self.abort_timer.stop()
        self.closing = True

        if self.status_code == -1:  # Unusual termination - The sequencer hasn't finished yet, stop it
            self.status_code = self.worker.worker.stop()

        self.unregister_events()  # Prevent interruption by pubsub messages

        self.worker.deleteLater()  # Schedule the thread worker for deletion
        self.worker = None  # Remove the reference to allow the GC to clean up

        self.worker_thread.exit(self.status_code)  # Exit the thread
        self.worker_thread.wait(2000)  # 2 seconds for the thread to exit
        self.worker_thread.terminate()  # Force quit the thread if it is still running, if so, this will throw a warning
        self.worker_thread.deleteLater()  # Schedule the thread for deletion
        self.worker_thread = None  # Remove the reference to allow the GC to clean up

        #   Now close the GUI thread, return to the controller in main
        self.application.exit(self.status_code)

    """User IO handlers, emit signals to trigger main thread updates via slots.
       These are run in the sequencer thread"""

    def event_output(self, message, status="True"):
        self.output_signal.emit(message, str(status))

    def gui_text_input(self, message):
        self.sig_text_input.emit(message)
        self.blocked = True
        result = self.input_queue.get(True)
        self.blocked = False
        self.sig_working.emit()
        return result

    def gui_choices(self, message, choices):
        self.sig_choices_input.emit(message, choices)
        self.blocked = True
        result = self.input_queue.get(True)
        self.blocked = False
        self.sig_working.emit()
        return result

    def gui_user_action_pass_fail(self, message, q, abort):
        """
        Non blocking user call
        :param message:
        :param q:
        :param abort:
        :return:
        """
        self.sig_choices_input.emit(message, ["PASS", "FAIL"])
        self.sig_timer.emit()
        self.user_action_queue = q
        self.abort_queue = abort

    def gui_user_action_fail(self, message, q, abort):
        self.sig_choices_input.emit(message, ["FAIL"])
        self.sig_timer.emit()
        self.user_action_queue = q
        self.abort_queue = abort

    def gui_user_input(self, message, choices=None, blocking=True):
        result = None
        if choices is not None:  # Button Prompt
            if blocking:
                self.sig_choices_input.emit(message, choices)
            else:
                self.sig_choices_input.emit(message, (choices[0],))
                self.sig_timer.emit()
        else:  # Text Prompt
            self.sig_text_input.emit(message)

        if blocking:  # Block sequencer until user responds
            self.blocked = True
            result = self.input_queue.get(True)
            self.blocked = False
            self.sig_working.emit()
        else:
            self.user_action_queue = choices[1]
            self.abort_queue = choices[2]
        return result

    """UI Event Handlers, process actions taken by the user on the GUI.
       These are run in the main thread """

    def text_input_submit(self):
        self.input_queue.put(self.UserInputBox.toPlainText())
        self.UserInputBox.clear()
        self.UserInputBox.setPlaceholderText("")
        self.UserInputBox.setEnabled(False)

    def button_1_click(self):
        self.input_queue.put(self.Button_1.text())
        self.button_reset()

    def button_2_click(self):
        if self.user_action_queue is not None:
            self.user_action_queue.put(self.Button_2.text())
            self.user_action_queue = None
            self.abort_timer.stop()
            self.abort_queue = None
        else:
            self.input_queue.put(self.Button_2.text())
        self.button_reset()

    def button_3_click(self):
        self.input_queue.put(self.Button_3.text())
        self.button_reset()

    def button_reset(self, fail_only=False):
        self.Button_2.setText("")
        self.Button_2.setEnabled(False)
        self.Button_2.setDefault(False)
        if not fail_only:
            self.Button_1.setText("")
            self.Button_3.setText("")
            self.Button_1.setEnabled(False)
            self.Button_3.setEnabled(False)
            self.Button_1.setDefault(False)
            self.Button_3.setDefault(False)

    """Thread listener, called from the sequencer thread"""

    def _completion_code(self, code):
        """This function is the first one called when the sequencer completes normally.
           Set the exit code, and signal the main thread."""
        self.status_code = code
        self.sig_finish.emit()

    """UI Callables, called from the sequencer thread"""

    def reformat_text(self, text_str, first_line_fill="", subsequent_line_fill=""):
        lines = []
        wrapper.initial_indent = first_line_fill
        wrapper.subsequent_indent = subsequent_line_fill
        for ind, line in enumerate(text_str.splitlines()):
            if ind != 0:
                wrapper.initial_indent = subsequent_line_fill
            lines.append(wrapper.fill(line))
        return '\n'.join(lines)

    # def _image(self, path, overlay):
    #     if self.closing:
    #         return
    #     self.update_image.emit(path, overlay)

    def _user_action(self, msg, q, abort):
        """
        This is for tests that aren't entirely dependant on the automated system.
        This works by monitoring a queue to see if the test completed successfully.
        Also while doing this it is monitoring if the escape key is pressed to signal to the system that the test fails.

        Use this in situations where you want the user to do something (like press all the keys on a keypad) where the
        system is automatically monitoring for success but has no way of monitoring failure.
        :param msg:
         Information for the user
        :param q:
         The queue object to put false if the user fails the test
        :param abort:
         The queue object to abort this monitoring as the test has already passed.
        :return:
        None
        """
        if self.closing:
            q.put(False)
            abort.put(True)
            return
        self.gui_user_input(self.reformat_text(msg), ("Fail", q, abort), False)

    def _user_ok(self, msg, q):
        """
        This can be replaced anywhere in the project that needs to implement the user driver
        The result needs to be put in the queue with the first part of the tuple as 'Exception' or 'Result' and the
        second part is the exception object or response object
        :param msg:
         Message for the user to understand what to do
        :param q:
         The result queue of type queue.Queue
        :return:
        """
        if self.closing:
            q.put("Result", None)
            return
        self.gui_user_input(msg, ("Continue",))
        q.put("Result", None)

    def _user_choices(self, msg, q, choices, target, attempts=5):
        """
        This can be replaced anywhere in the project that needs to implement the user driver
        Temporarily a simple input function.
        The result needs to be put in the queue with the first part of the tuple as 'Exception' or 'Result' and the
        second part is the exception object or response object
        This needs to be compatible with forced exit. Look to user action for how it handles a forced exit
        :param msg:
         Message for the user to understand what to input
        :param q:
         The result queue of type queue.Queue
        :param target:
         Optional
         Validation function to check if the user response is valid
        :param attempts:
        :return:
        """
        if self.closing:
            q.put(("Result", "ABORT_FORCE"))
            return

        for _ in range(attempts):
            # This will change based on the interface
            ret_val = self.gui_user_input(self.reformat_text(msg), choices)
            ret_val = target(ret_val, choices)
            if ret_val:
                q.put(('Result', ret_val))
                return
        q.put('Exception', UserInputError("Maximum number of attempts {} reached".format(attempts)))

    def _user_input(self, msg, q, target=None, attempts=5, kwargs=None):
        """
        This can be replaced anywhere in the project that needs to implement the user driver
        Temporarily a simple input function.
        The result needs to be put in the queue with the first part of the tuple as 'Exception' or 'Result' and the
        second part is the exception object or response object
        This needs to be compatible with forced exit. Look to user action for how it handles a forced exit
        :param msg:
         Message for the user to understand what to input
        :param q:
         The result queue of type queue.Queue
        :param target:
         Optional
         Validation function to check if the user response is valid
        :param attempts:

        :param kwargs:
        :return:
        """
        if self.closing:
            q.put(('Result', "ABORT_FORCE"))
            return

        msg = self.reformat_text(msg)
        wrapper.initial_indent = ""
        wrapper.subsequent_indent = ""
        for _ in range(attempts):
            # This will change based on the interface
            ret_val = self.gui_user_input(msg, None, True)
            if target is None or ret_val == "ABORT_FORCE":
                q.put(ret_val)
                return
            ret_val = target(ret_val, **kwargs)
            if ret_val:
                q.put(('Result', ret_val))
                return
        q.put('Exception', UserInputError("Maximum number of attempts {} reached".format(attempts)))

    def _user_display(self, msg):
        """
        :param msg:
        :return:
        """
        if self.closing:
            return

        self.history_update(self.reformat_text(msg))

    def _user_display_important(self, msg):
        """
        :param msg:
        :return:
        """
        if self.closing:
            return

        self.history_update("")
        self.history_update("!" * wrapper.width)
        self.active_update("!" * wrapper.width)
        self.history_update("")
        self.history_update(self.reformat_text(msg))
        self.active_update(self.reformat_text(msg))
        self.history_update("")
        self.history_update("!" * wrapper.width)
        self.active_update("!" * wrapper.width)

    def _print_sequence_end(self, status, passed, failed, error, skipped, sequence_status):
        if self.closing:
            return

        self.history_update("#" * wrapper.width)
        self.history_update(self.reformat_text("Sequence {}".format(sequence_status)))
        # self.history_update("Sequence {}".format(sequence_status))
        post_sequence_info = []
        if status == "PASSED":
            post_sequence_info.extend(RESOURCES["SEQUENCER"].context_data.get("_post_sequence_info_pass", []))
        elif status == "FAILED" or status == "ERROR":
            post_sequence_info.extend(RESOURCES["SEQUENCER"].context_data.get("_post_sequence_info_fail", []))
        post_sequence_info.extend(RESOURCES["SEQUENCER"].context_data.get("_post_sequence_info", []))

        if post_sequence_info:
            self.history_update("-" * wrapper.width)
            self.history_update("IMPORTANT INFORMATION")
            for itm in post_sequence_info:
                self.history_update(self.reformat_text(itm))
        self.history_update("-" * wrapper.width)
        self.history_update(self.reformat_text("Status: {}".format(status)))
        self.history_update("#" * wrapper.width)

    def _print_test_start(self, data, test_index):
        if self.closing:
            return

        self.sig_progress.emit()
        self.history_update("*" * wrapper.width)
        self.history_update(self.reformat_text("Test {}: {}".format(test_index, data.test_desc)))
        self.history_update("-" * wrapper.width)
        self.sig_label_update.emit(test_index, data.test_desc)
        self.sig_tree_update.emit(test_index, "In Progress")

    def _print_test_seq_start(self, data, test_index):
        if self.closing:
            return

        self.ProgressBar.setMaximum(self.worker.worker.get_task_count())
        self.sig_tree_init.emit(self.worker.worker.get_test_tree())
        self.sig_progress.emit()
        self._print_test_start(data, test_index)

    def _print_test_complete(self, data, test_index, status):
        if self.closing:
            return

        sequencer = RESOURCES["SEQUENCER"]
        self.history_update("-" * wrapper.width)
        self.history_update(
            self.reformat_text("Checks passed: {}, Checks failed: {}".format(sequencer.chk_pass, sequencer.chk_fail)))
        # self.history_update("Checks passed: {}, Checks failed: {}".format(sequencer.chk_pass, sequencer.chk_fail))
        self.history_update(self.reformat_text("Test {}: {}".format(test_index, status.upper())))
        # self.history_update("Test {}: {}".format(test_index, status.upper()))
        self.history_update("-" * wrapper.width)

        if status.upper() in ["ERROR", "SKIPPED"]:
            return

        if sequencer.chk_fail == 0:
            self.sig_tree_update.emit(test_index, "Passed")
        else:
            self.sig_tree_update.emit(test_index, "Failed")

    def _print_test_skip(self, data, test_index):
        if self.closing:
            return

        self.history_update("\nTest Marked as skip")
        self.sig_tree_update.emit(test_index, "Skipped")

    def _print_test_retry(self, data, test_index):
        if self.closing:
            return

        self.history_update(self.reformat_text("\nTest {}: Retry".format(test_index)))

    def _print_errors(self, exception, test_index):
        if self.closing:
            return

        if isinstance(exception, SequenceAbort):
            self.sig_tree_update.emit(test_index, "Aborted")
            status = True
        else:
            status = False
            self.sig_tree_update.emit(test_index, "Error")
        self.history_update("")
        self.history_update("!" * wrapper.width)
        self.active_update("!" * wrapper.width)
        self.history_update(
            self.reformat_text("Test {}: Exception Occurred, {} {}".format(test_index, type(exception), exception)))
        self.active_update(
            self.reformat_text("Test {}: Exception Occurred, {} {}".format(test_index, type(exception), exception)))
        self.history_update("!" * wrapper.width)
        self.active_update("!" * wrapper.width)
        # TODO self.history_update traceback into a debug log file
        if fixate.config.DEBUG:
            traceback.print_tb(exception.__traceback__, file=sys.stderr)

    def round_to_3_sig_figures(self, chk):
        """
        Tries to round elements to 3 significant figures for formatting
        :param chk:
        :return:
        """
        ret_dict = {}
        for element in ["_min", "_max", "test_val", "nominal", "tol"]:
            ret_dict[element] = getattr(chk, element, None)
            try:
                ret_dict[element] = "{:.3g}".format(ret_dict[element])
            except:
                pass
        return ret_dict

    def _print_comparisons(self, passes, chk, chk_cnt, context):
        if passes:
            status = "PASS"
        else:
            status = "FAIL"
        format_dict = self.round_to_3_sig_figures(chk)
        if chk._min is not None and chk._max is not None:
            msg = self.reformat_text(
                "\nCheck {chk_cnt}: {status} when comparing {test_val} {comparison} {_min} - {_max} : "
                "{description}".format(
                    status=status,
                    comparison=chk.target.__name__[1:].replace('_', ' '),
                    chk_cnt=chk_cnt,
                    description=chk.description, **format_dict))
            self.history_update(msg)
            if status == "FAIL":
                self.active_update(msg)
        elif chk.nominal is not None and chk.tol is not None:
            msg = self.reformat_text(
                "\nCheck {chk_cnt}: {status} when comparing {test_val} {comparison} {nominal} +- {tol}% : "
                "{description}".format(
                    status=status,
                    comparison=chk.target.__name__[1:].replace('_', ' '),
                    chk_cnt=chk_cnt,
                    description=chk.description, **format_dict))
            self.history_update(msg)
            if status == "FAIL":
                self.active_update(msg)
        elif chk._min is not None or chk._max is not None or chk.nominal is not None:
            # Grabs the first value that isn't none. Nominal takes priority
            comp_val = next(format_dict[item] for item in ["nominal", "_min", "_max"] if format_dict[item] is not None)
            msg = self.reformat_text("\nCheck {chk_cnt}: {status} when comparing {test_val} {comparison} {comp_val} : "
                                     "{description}".format(
                status=status,
                comparison=chk.target.__name__[1:].replace('_', ' '),
                comp_val=comp_val,
                chk_cnt=chk_cnt,
                description=chk.description, **format_dict))
            self.history_update(msg)
            if status == "FAIL":
                self.active_update(msg)
        else:
            if chk.test_val is not None:
                msg = self.reformat_text(
                    "\nCheck {chk_cnt}: {status}: {test_val} : {description}".format(
                        chk_cnt=chk_cnt,
                        description=chk.description,
                        status=status, **format_dict))
                self.history_update(msg)
                if status == "FAIL":
                    self.active_update(msg)
            else:
                msg = self.reformat_text(
                    "\nCheck {chk_cnt} : {status}: {description}".format(description=chk.description, chk_cnt=chk_cnt,
                                                                         status=status))
                self.history_update(msg)
                if status == "FAIL":
                    self.active_update(msg)
Ejemplo n.º 7
0
class XAnoS_Reducer(QWidget):
    """
    This widget is developed to reduce on the fly 2D SAXS data to azimuthally averaged 1D SAXS data
    """
    def __init__(self,poniFile=None,dataFile=None, darkFile=None, maskFile=None,extractedFolder='/tmp', npt=1000, azimuthalRange=(-180.0,180.0), parent=None):
        """
        poniFile is the calibration file obtained after Q-calibration
        """
        QWidget.__init__(self,parent)
        self.setup_dict=json.load(open('./SetupData/reducer_setup.txt','r'))
        if poniFile is not None:
            self.poniFile=poniFile
        else:
            self.poniFile=self.setup_dict['poniFile']
        if maskFile is not None:
            self.maskFile=maskFile
        else:
            self.maskFile=self.setup_dict['maskFile']
        self.dataFile=dataFile
        if darkFile is None:
            self.dark_corrected=False
            self.darkFile=''
        else:
            self.darkFile=darkFile
            self.dark_corrected=True
       
        self.curDir=os.getcwd()
        
        self.extractedBaseFolder=extractedFolder
        self.npt=npt
        self.set_externally=False
        #ai=AIWidget()
        #self.layout.addWidget(ai)
        self.azimuthalRange=azimuthalRange
        self.create_UI()
        if os.path.exists(self.poniFile):
            self.openPoniFile(file=self.poniFile)
        if os.path.exists(self.maskFile):
            self.openMaskFile(file=self.maskFile)   
        self.clientRunning=False     
        
        
    def create_UI(self):
        """
        Creates the widget user interface
        """
        loadUi('UI_Forms/Data_Reduction_Client.ui',self)
        self.poniFileLineEdit.setText(str(self.poniFile))
        self.maskFileLineEdit.setText(str(self.maskFile))
        self.darkFileLineEdit.setText(str(self.darkFile))
        self.extractedBaseFolderLineEdit.setText(self.extractedBaseFolder)
        self.radialPointsLineEdit.setText(str(self.npt))
        self.openDataPushButton.clicked.connect(self.openDataFiles)
        self.reducePushButton.clicked.connect(self.reduce_multiple)
        self.openDarkPushButton.clicked.connect(self.openDarkFile)
        self.openPoniPushButton.clicked.connect(lambda x: self.openPoniFile(file=None))
        self.calibratePushButton.clicked.connect(self.calibrate)
        self.maskFileLineEdit.returnPressed.connect(self.maskFileChanged)
        self.openMaskPushButton.clicked.connect(lambda x: self.openMaskFile(file=None))
        self.createMaskPushButton.clicked.connect(self.createMask)
        self.extractedFolderPushButton.clicked.connect(self.openFolder)
        self.extractedFolderLineEdit.textChanged.connect(self.extractedFolderChanged)
        self.polCorrComboBox.currentIndexChanged.connect(self.polarizationChanged)
        self.polarizationChanged()
        self.radialPointsLineEdit.returnPressed.connect(self.nptChanged)
        self.azimuthalRangeLineEdit.returnPressed.connect(self.azimuthalRangeChanged)
        self.azimuthalRangeChanged()
        #self.statusLabel.setStyleSheet("color:rgba(0,1,0,0)")
        self.imageWidget=Image_Widget(zeros((100,100)))
        self.cakedImageWidget=Image_Widget(zeros((100,100)))
        imgNumberLabel=QLabel('Image number')
        self.imgNumberSpinBox=QSpinBox()
        self.imgNumberSpinBox.setSingleStep(1)
        self.imageWidget.imageLayout.addWidget(imgNumberLabel,row=2,col=1)
        self.imageWidget.imageLayout.addWidget(self.imgNumberSpinBox,row=2,col=2)
        self.imageView=self.imageWidget.imageView.getView()
        self.plotWidget=PlotWidget()
        self.plotWidget.setXLabel('Q, &#8491;<sup>-1</sup>',fontsize=5)
        self.plotWidget.setYLabel('Intensity',fontsize=5)
        self.tabWidget.addTab(self.plotWidget,'Reduced 1D-data')
        self.tabWidget.addTab(self.imageWidget,'Masked 2D-data')
        self.tabWidget.addTab(self.cakedImageWidget,'Reduced Caked Data')
        
        self.serverAddress=self.serverAddressLineEdit.text()
        self.startClientPushButton.clicked.connect(self.startClient)
        self.stopClientPushButton.clicked.connect(self.stopClient)
        self.serverAddressLineEdit.returnPressed.connect(self.serverAddressChanged)
        
        self.startServerPushButton.clicked.connect(self.startServer)
        self.stopServerPushButton.clicked.connect(self.stopServer)
        
    def startServer(self):
        serverAddr=self.serverAddressLineEdit.text()
        dataDir=QFileDialog.getExistingDirectory(self,'Select data folder',options=QFileDialog.ShowDirsOnly)
        self.serverStatusLabel.setText('<font color="Red">Transmitting</font>')
        QApplication.processEvents()
        self.serverThread=QThread()
        self.zeromq_server=ZeroMQ_Server(serverAddr,dataDir)
        self.zeromq_server.moveToThread(self.serverThread)
        self.serverThread.started.connect(self.zeromq_server.loop)
        self.zeromq_server.messageEmitted.connect(self.updateServerMessage)
        self.zeromq_server.folderFinished.connect(self.serverDone)
        QTimer.singleShot(0,self.serverThread.start)

    
    def updateServerMessage(self,mesg):
        #self.serverStatusLabel.setText('<font color="Red">Transmitting</font>')
        self.serverMessageLabel.setText('Server sends: %s'%mesg)
        QApplication.processEvents()
        
    def serverDone(self):
        self.serverStatusLabel.setText('<font color="Green">Idle</font>')
        self.zeromq_server.socket.unbind(self.zeromq_server.socket.last_endpoint)
        self.serverThread.quit()
        self.serverThread.wait()
        self.serverThread.deleteLater()
        self.zeromq_server.deleteLater()
        
    def stopServer(self):
        try:
            self.zeromq_server.running=False
            self.serverStatusLabel.setText('<font color="Green">Idle</font>')
            self.zeromq_server.socket.unbind(self.zeromq_server.socket.last_endpoint)
            self.serverThread.quit()
            self.serverThread.wait()
            self.serverThread.deleteLater()
            self.zeromq_server.deleteLater()
        except:
            QMessageBox.warning(self,'Server Error','Start the server before stopping it')
        
    def enableClient(self,enable=True):
        self.startClientPushButton.setEnabled(enable)
        self.stopClientPushButton.setEnabled(enable)
        
    def enableServer(self,enable=True):
        self.startServerPushButton.setEnabled(enable)
        self.stopServerPushButton.setEnabled(enable)
        
        
    def startClient(self):
        if self.clientRunning:
            self.stopClient()
        else:
            self.clientFree=True
            self.clientRunning=True
            self.files=[]
            self.listenerThread = QThread()
            addr=self.clientAddressLineEdit.text()
            self.zeromq_listener = ZeroMQ_Listener(addr)
            self.zeromq_listener.moveToThread(self.listenerThread)
            self.listenerThread.started.connect(self.zeromq_listener.loop)
            self.zeromq_listener.messageReceived.connect(self.signal_received)
            QTimer.singleShot(0, self.listenerThread.start)
            QTimer.singleShot(0,self.clientReduce)
            self.clientStatusLabel.setText('<font color="red">Connected</font>')
            
    def stopClient(self):
        try:
            self.clientRunning=False
            self.clientFree=False
            self.zeromq_listener.messageReceived.disconnect()
            self.zeromq_listener.running=False
            self.listenerThread.quit()
            self.listenerThread.wait()
            self.listenerThread.deleteLater()
            self.zeromq_listener.deleteLater()
            self.clientStatusLabel.setText('<font color="green">Idle</font>')
        except:
            QMessageBox.warning(self,'Client Error', 'Please start the client first before closing.',QMessageBox.Ok)
        
        
    def serverAddressChanged(self):
        if self.clientRunning:
            self.startClient()
        
        
    def signal_received(self, message):
        self.clientMessageLabel.setText('Client receives: %s'%message)
        if 'dark.edf' not in message:
            self.files.append(message)
            
            
    def clientReduce(self):
        while self.clientFree:
            QApplication.processEvents()
            if len(self.files)>0:
                message=self.files[0]
                self.dataFiles=[message]
                self.dataFileLineEdit.setText(str(self.dataFiles))
                self.extractedBaseFolder=os.path.dirname(message)
                self.extractedFolder=os.path.join(self.extractedBaseFolder,self.extractedFolderLineEdit.text())
                if not os.path.exists(self.extractedFolder):
                    os.makedirs(self.extractedFolder)
                self.extractedBaseFolderLineEdit.setText(self.extractedBaseFolder)
                self.set_externally=True
                self.reduce_multiple()
                self.set_externally=False
                self.files.pop(0)
        
            
    def closeEvent(self, event):
        if self.clientRunning:
            self.stopClient()
        event.accept()
       
    def polarizationChanged(self):
        if self.polCorrComboBox.currentText()=='Horizontal':
            self.polarization_factor=1
        elif self.polCorrComboBox.currentText()=='Vertical':
            self.polarization_factor=-1
        elif self.polCorrComboBox.currentText()=='Circular':
            self.polarization_factor=0
        else:
            self.polarization_factor=None
            
    def createMask(self):
        """
        Opens a mask-widget to create mask file
        """
        fname=str(QFileDialog.getOpenFileName(self,'Select an image file', directory=self.curDir,filter='Image file (*.edf *.tif)')[0])
        if fname is not None or fname!='':
            img=fb.open(fname).data
            self.maskWidget=MaskWidget(img)
            self.maskWidget.saveMaskPushButton.clicked.disconnect()
            self.maskWidget.saveMaskPushButton.clicked.connect(self.save_mask)
            self.maskWidget.show()
        else:
            QMessageBox.warning(self,'File error','Please import a data file first for creating the mask',QMessageBox.Ok)
            
    def maskFileChanged(self):
        """
        Changes the mask file
        """
        maskFile=str(self.maskFileLineEdit.text())
        if str(maskFile)=='':
            self.maskFile=None
        elif os.path.exists(maskFile):
            self.maskFile=maskFile
        else:
            self.maskFile=None
            
    def save_mask(self):
        """
        Saves the entire mask combining all the shape ROIs
        """
        fname=str(QFileDialog.getSaveFileName(filter='Mask Files (*.msk)')[0])
        name,extn=os.path.splitext(fname)
        if extn=='':
            fname=name+'.msk'
        elif extn!='.msk':
            QMessageBox.warning(self,'File extension error','Please donot provide file extension other than ".msk". Thank you!')
            return
        else:
            tmpfile=fb.edfimage.EdfImage(data=self.maskWidget.full_mask_data.T,header=None)
            tmpfile.save(fname)
            self.maskFile=fname
            self.maskFileLineEdit.setText(self.maskFile)
            
    def calibrate(self):
        """
        Opens a calibartion widget to create calibration file
        """
        fname=str(QFileDialog.getOpenFileName(self,'Select calibration image',directory=self.curDir, filter='Calibration image (*.edf *.tif)')[0])
        if fname is not None:
            img=fb.open(fname).data
            if self.maskFile is not None:
                try:
                    mask=fb.open(self.maskFile).data
                except:
                    QMessageBox.warning(self,'Mask File Error','Cannot open %s.\n No masking will be done.'%self.maskFile)
                    mask=None
            else:
                mask=None
            pixel1=79.0
            pixel2=79.0
            self.calWidget=CalibrationWidget(img,pixel1,pixel2,mask=mask)
            self.calWidget.saveCalibrationPushButton.clicked.disconnect()
            self.calWidget.saveCalibrationPushButton.clicked.connect(self.save_calibration)
            self.calWidget.show()
        else:
            QMessageBox.warning(self,'File error','Please import a data file first for creating the calibration file',QMessageBox.Ok)
            
    def save_calibration(self):
        fname=str(QFileDialog.getSaveFileName(self,'Calibration file',directory=self.curDir,filter='Clibration files (*.poni)')[0])
        tfname=os.path.splitext(fname)[0]+'.poni'
        self.calWidget.applyPyFAI()
        self.calWidget.geo.save(tfname)      
        self.poniFile=tfname
        self.poniFileLineEdit.setText(self.poniFile)
        self.openPoniFile(file=self.poniFile)
        
    def openPoniFile(self,file=None):
        """
        Select and imports the calibration file
        """
        if file is None:
            self.poniFile=QFileDialog.getOpenFileName(self,'Select calibration file',directory=self.curDir,filter='Calibration file (*.poni)')[0]
            self.poniFileLineEdit.setText(self.poniFile)
        else:
            self.poniFile=file
        if os.path.exists(self.poniFile):
            self.setup_dict['poniFile']=self.poniFile
            json.dump(self.setup_dict,open('./SetupData/reducer_setup.txt','w'))
            fh=open(self.poniFile,'r')
            lines=fh.readlines()
            self.calib_data={}
            for line in lines:
                if line[0]!='#':
                    key,val=line.split(': ')
                    self.calib_data[key]=float(val)
            self.dist=self.calib_data['Distance']
            self.pixel1=self.calib_data['PixelSize1']
            self.pixel2=self.calib_data['PixelSize2']
            self.poni1=self.calib_data['Poni1']
            self.poni2=self.calib_data['Poni2']
            self.rot1=self.calib_data['Rot1']
            self.rot2=self.calib_data['Rot2']
            self.rot3=self.calib_data['Rot3']
            self.wavelength=self.calib_data['Wavelength']
            self.ai=AzimuthalIntegrator(dist=self.dist,poni1=self.poni1,poni2=self.poni2,pixel1=self.pixel1,pixel2=self.pixel2,rot1=self.rot1,rot2=self.rot2,rot3=self.rot3,wavelength=self.wavelength)
            #pos=[self.poni2/self.pixel2,self.poni1/self.pixel1]
            #self.roi=cake(pos,movable=False)
            #self.roi.sigRegionChangeStarted.connect(self.endAngleChanged)
            
            #self.imageView.addItem(self.roi)
        else:
            QMessageBox.warning(self,'File error','The calibration file '+self.poniFile+' doesnot exists.',QMessageBox.Ok)                
        
    def endAngleChanged(self,evt):
        print(evt.pos())
        
        
    def nptChanged(self):
        """
        Changes the number of radial points
        """
        try:
            self.npt=int(self.radialPointsLineEdit.text())
        except:
            QMessageBox.warning(self,'Value error', 'Please input positive integers only.',QMessageBox.Ok)
            
    def azimuthalRangeChanged(self):
        """
        Changes the azimuth angular range
        """
        try:
            self.azimuthalRange=tuple(map(float, self.azimuthalRangeLineEdit.text().split(':')))
        except:
            QMessageBox.warning(self,'Value error','Please input min:max angles in floating point numbers',QMessageBox.Ok)
        
    def openDataFile(self):
        """
        Select and imports one data file
        """
        dataFile=QFileDialog.getOpenFileName(self,'Select data file',directory=self.curDir,filter='Data file (*.edf *.tif)')[0]
        if dataFile!='':
            self.dataFile=dataFile
            self.curDir=os.path.dirname(self.dataFile)
            self.dataFileLineEdit.setText(self.dataFile)
            self.data2d=fb.open(self.dataFile).data
            if self.darkFile is not None:
                self.applyDark()
            if self.maskFile is not None:
                self.applyMask()    
            self.imageWidget.setImage(self.data2d,transpose=True)
            self.tabWidget.setCurrentWidget(self.imageWidget)
            if not self.set_externally:
                self.extractedFolder=os.path.join(self.curDir,self.extractedFolderLineEdit.text())
                if not os.path.exists(self.extractedFolder):
                    os.makedirs(self.extractedFolder)
                    
    def openDataFiles(self):
        """
        Selects and imports multiple data files
        """
        self.dataFiles=QFileDialog.getOpenFileNames(self,'Select data files', directory=self.curDir,filter='Data files (*.edf *.tif)')[0]
        if len(self.dataFiles)!=0:
            self.imgNumberSpinBox.valueChanged.connect(self.imageChanged)
            self.imgNumberSpinBox.setMinimum(0)
            self.imgNumberSpinBox.setMaximum(len(self.dataFiles)-1)
            self.dataFileLineEdit.setText(str(self.dataFiles))
            self.curDir=os.path.dirname(self.dataFiles[0])
            self.extractedBaseFolder=self.curDir
            self.extractedFolder=os.path.abspath(os.path.join(self.extractedBaseFolder,self.extractedFolderLineEdit.text()))
            if not os.path.exists(self.extractedFolder):
                os.makedirs(self.extractedFolder)
            self.extractedBaseFolderLineEdit.setText(self.extractedBaseFolder)
            self.imgNumberSpinBox.setValue(0)
            self.imageChanged()
            
    def imageChanged(self):
        self.data2d=fb.open(self.dataFiles[self.imgNumberSpinBox.value()]).data
        if self.darkFile is not None:
            self.applyDark()
        if self.maskFile is not None:
            self.applyMask()    
        self.imageWidget.setImage(self.data2d,transpose=True)
            

                  
            
                
    def applyDark(self):
        if not self.dark_corrected and self.darkFile!='':
            self.dark2d=fb.open(self.darkFile).data
            self.data2d=self.data2d-self.dark2d
            self.dark_corrected=True
                
    def applyMask(self):
        self.mask2d=fb.open(self.maskFile).data
        self.data2d=self.data2d*(1+self.mask2d)/2.0
        self.mask_applied=True

    def openDarkFile(self):
        """
        Select and imports the dark file
        """
        self.darkFile=QFileDialog.getOpenFileName(self,'Select dark file',directory=self.curDir,filter='Dark file (*.edf)')[0]
        if self.darkFile!='':
            self.dark_corrected=False
            self.darkFileLineEdit.setText(self.darkFile)
            if self.dataFile is not None:
                self.data2d=fb.open(self.dataFile).data
                self.applyDark()
        
    
    def openMaskFile(self,file=None):
        """
        Select and imports the Mask file
        """
        if file is None:
            self.maskFile=QFileDialog.getOpenFileName(self,'Select mask file',directory=self.curDir,filter='Mask file (*.msk)')[0]
        else:
            self.maskFile=file
        if self.maskFile!='':
            self.mask_applied=False
            if os.path.exists(self.maskFile):
                self.curDir=os.path.dirname(self.maskFile)
                self.maskFileLineEdit.setText(self.maskFile)
                self.setup_dict['maskFile']=self.maskFile
                self.setup_dict['poniFile']=self.poniFile
                json.dump(self.setup_dict,open('./SetupData/reducer_setup.txt','w'))
            else:
                self.openMaskFile(file=None)
            if self.dataFile is not None:
                self.applyMask()
        else:
            self.maskFile=None
            self.maskFileLineEdit.clear()
            
            
        
    def openFolder(self):
        """
        Select the folder to save the reduce data
        """
        oldfolder=self.extractedBaseFolder.text()
        folder=QFileDialog.getExistingDirectory(self,'Select extracted directory',directory=self.curDir)
        if folder!='':
            self.extractedBaseFolder=folder
            self.extractedBaseFolderLineEdit.setText(folder)
            self.extractedFolder=os.path.join(folder,self.extractedFolderLineEdit.text())
            self.set_externally=True
        else:
            self.extractedBaseFolder=oldfolder
            self.extractedBaseFolderLineEdit.setText(oldfolder)
            self.extractedFolder = os.path.join(oldfolder, self.extractedFolderLineEdit.text())
            self.set_externally = True


    def extractedFolderChanged(self,txt):
        self.extractedFolder=os.path.join(self.extractedBaseFolder,txt)
        self.set_externally=True

        
        
    def reduceData(self):
        """
        Reduces the 2d data to 1d data
        """
        if (self.dataFile is not None) and (os.path.exists(self.dataFile)):
            if (self.poniFile is not None) and (os.path.exists(self.poniFile)):
#                self.statusLabel.setText('Busy')
#                self.progressBar.setRange(0, 0)
                imageData=fb.open(self.dataFile)
                #self.data2d=imageData.data
                #if self.maskFile is not None:
                #    self.applyMask()    
                #self.imageWidget.setImage(self.data2d,transpose=True)
                #self.tabWidget.setCurrentWidget(self.imageWidget)
                
                self.header=imageData.header
                try:
                    self.ai.set_wavelength(float(self.header['Wavelength'])*1e-10)
                except:
                    self.ai.set_wavelength(self.wavelength)
                #print(self.darkFile)
                if os.path.exists(self.dataFile.split('.')[0]+'_dark.edf') and self.darkCheckBox.isChecked():
                    self.darkFile=self.dataFile.split('.')[0]+'_dark.edf'
                    dark=fb.open(self.darkFile)
                    self.darkFileLineEdit.setText(self.darkFile)
                    imageDark=dark.data                                     
                    self.header['BSDiode_corr']=max([1.0,(float(imageData.header['BSDiode'])-float(dark.header['BSDiode']))])
                    self.header['Monitor_corr']=max([1.0,(float(imageData.header['Monitor'])-float(dark.header['Monitor']))])
                    print("Dark File read from existing dark files")                    
                elif self.darkFile is not None and self.darkFile!='' and self.darkCheckBox.isChecked():
                    dark=fb.open(self.darkFile)
                    imageDark=dark.data                                     
                    self.header['BSDiode_corr']=max([1.0,(float(imageData.header['BSDiode'])-float(dark.header['BSDiode']))])
                    self.header['Monitor_corr']=max([1.0,(float(imageData.header['Monitor'])-float(dark.header['Monitor']))])
                    print("Dark File from memory subtracted")                
                else:
                    imageDark=None
                    try:
                        self.header['BSDiode_corr']=float(imageData.header['BSDiode'])
                        self.header['Monitor_corr']=float(imageData.header['Monitor'])
                        self.header['Transmission'] = float(imageData.header['Transmission'])
                    except:
                        self.normComboBox.setCurrentText('None')
                    print("No dark correction done")
                if str(self.normComboBox.currentText())=='BSDiode':
                    norm_factor=self.header['BSDiode_corr']#/self.header['Monitor_corr']#float(self.header[
                    # 'count_time'])
                elif str(self.normComboBox.currentText())=='TransDiode':
                    norm_factor=self.header['Transmission']*self.header['Monitor_corr']
                elif str(self.normComboBox.currentText())=='Monitor':
                    norm_factor=self.header['Monitor_corr']
                elif str(self.normComboBox.currentText())=='Image Sum':
                    norm_factor=sum(imageData.data)
                else:
                    norm_factor=1.0
                    
                if self.maskFile is not None:
                    imageMask=fb.open(self.maskFile).data
                else:
                    imageMask=None
#                QApplication.processEvents()
                #print(self.azimuthalRange)
                self.q,self.I,self.Ierr=self.ai.integrate1d(imageData.data,self.npt,error_model='poisson',mask=imageMask,dark=imageDark,unit='q_A^-1',normalization_factor=norm_factor,azimuth_range=self.azimuthalRange,polarization_factor=self.polarization_factor)
                self.plotWidget.add_data(self.q,self.I,yerr=self.Ierr,name='Reduced data')
                if not self.set_externally:
                    cakedI,qr,phir=self.ai.integrate2d(imageData.data,self.npt,mask=imageMask,dark=imageDark,unit='q_A^-1',normalization_factor=norm_factor,polarization_factor=self.polarization_factor)
                    self.cakedImageWidget.setImage(cakedI,xmin=qr[0],xmax=qr[-1],ymin=phir[0],ymax=phir[-1],transpose=True,xlabel='Q ', ylabel='phi ',unit=['&#8491;<sup>-1</sup>','degree'])
                    self.cakedImageWidget.imageView.view.setAspectLocked(False)
                    try:
                        self.azimuthalRegion.setRegion(self.azimuthalRange)
                    except:
                        self.azimuthalRegion=pg.LinearRegionItem(values=self.azimuthalRange,orientation=pg.LinearRegionItem.Horizontal,movable=True,bounds=[-180,180])
                        self.cakedImageWidget.imageView.getView().addItem(self.azimuthalRegion)
                        self.azimuthalRegion.sigRegionChanged.connect(self.azimuthalRegionChanged)
                self.plotWidget.setTitle(self.dataFile,fontsize=3)
#                self.progressBar.setRange(0,100)
#                self.progressBar.setValue(100)
#                self.statusLabel.setText('Idle')
#                QApplication.processEvents()
                self.saveData()
                #self.tabWidget.setCurrentWidget(self.plotWidget)
            else:
                QMessageBox.warning(self,'Calibration File Error','Data reduction failed because either no calibration file provided or the provided file or path do not exists',QMessageBox.Ok)
                
        else:
            QMessageBox.warning(self,'Data File Error','No data file provided', QMessageBox.Ok)
            
    def azimuthalRegionChanged(self):
        minp,maxp=self.azimuthalRegion.getRegion()
        self.azimuthalRangeLineEdit.setText('%.1f:%.1f'%(minp,maxp))
        self.azimuthalRange=[minp,maxp]
        self.set_externally=True
        
        
            
    def reduce_multiple(self):
        """
        Reduce multiple files
        """
        try:
            i=0
            self.progressBar.setRange(0,len(self.dataFiles))
            self.progressBar.setValue(i)
            self.statusLabel.setText('<font color="red">Busy</font>')
            for file in self.dataFiles:
                self.dataFile=file
                QApplication.processEvents()
                self.reduceData()
                i=i+1
                self.progressBar.setValue(i)
                QApplication.processEvents()
            self.statusLabel.setText('<font color="green">Idle</font>')
            self.progressBar.setValue(0)
        except:
            QMessageBox.warning(self,'File error','No data files to reduce',QMessageBox.Ok)
        
    def saveData(self):
        """
        saves the extracted data into a file
        """
        if not os.path.exists(self.extractedFolder):
            os.makedirs(self.extractedFolder)
        filename=os.path.join(self.extractedFolder,os.path.splitext(os.path.basename(self.dataFile))[0]+'.txt')
        headers='File extracted on '+time.asctime()+'\n'
        headers='Files used for extraction are:\n'
        headers+='Data file: '+self.dataFile+'\n'
        if self.darkFile is not None:
            headers+='Dark file: '+self.darkFile+'\n'
        else:
            headers+='Dark file: None\n'
        headers+='Poni file: '+self.poniFile+'\n'
        if self.maskFile is not None:
            headers+='mask file: '+self.maskFile+'\n'
        else:
            headers+='mask file: None\n'
        for key in self.header.keys():
            headers+=key+'='+str(self.header[key])+'\n'
        headers+="col_names=['Q (inv Angs)','Int','Int_err']\n"
        headers+='Q (inv Angs)\tInt\tInt_err'
        data=vstack((self.q,self.I,self.Ierr)).T
        savetxt(filename,data,header=headers,comments='#')
Ejemplo n.º 8
0
class _POSIXUserscriptRunner(_BaseUserscriptRunner):

    """Userscript runner to be used on POSIX. Uses _BlockingFIFOReader.

    The OS must have support for named pipes and select(). Commands are
    executed immediately when they arrive in the FIFO.

    Attributes:
        _reader: The _BlockingFIFOReader instance.
        _thread: The QThread where reader runs.
    """

    def __init__(self, parent=None):
        super().__init__(parent)
        self._reader = None
        self._thread = None

    def run(self, cmd, *args, env=None):
        rundir = utils.get_standard_dir(QStandardPaths.RuntimeLocation)
        # tempfile.mktemp is deprecated and discouraged, but we use it here to
        # create a FIFO since the only other alternative would be to create a
        # directory and place the FIFO there, which sucks. Since os.kfifo will
        # raise an exception anyways when the path doesn't exist, it shouldn't
        # be a big issue.
        self._filepath = tempfile.mktemp(prefix='userscript-', dir=rundir)
        os.mkfifo(self._filepath)  # pylint: disable=no-member

        self._reader = _BlockingFIFOReader(self._filepath)
        self._thread = QThread(self)
        self._reader.moveToThread(self._thread)
        self._reader.got_line.connect(self.got_cmd)
        self._thread.started.connect(self._reader.read)
        self._reader.finished.connect(self.on_reader_finished)
        self._thread.finished.connect(self.on_thread_finished)

        self._run_process(cmd, *args, env=env)
        self._thread.start()

    def on_proc_finished(self):
        """Interrupt the reader when the process finished."""
        log.procs.debug("proc finished")
        self._thread.requestInterruption()

    def on_proc_error(self, error):
        """Interrupt the reader when the process had an error."""
        super().on_proc_error(error)
        self._thread.requestInterruption()

    def on_reader_finished(self):
        """Quit the thread and clean up when the reader finished."""
        log.procs.debug("reader finished")
        self._thread.quit()
        self._reader.fifo.close()
        self._reader.deleteLater()
        super()._cleanup()
        self.finished.emit()

    def on_thread_finished(self):
        """Clean up the QThread object when the thread finished."""
        log.procs.debug("thread finished")
        self._thread.deleteLater()
Ejemplo n.º 9
0
def main(argv=None):

    # PREVENTS MESSAGING FROM THREADING PROBLEMS IN MATPLOTLIB:
    # The process has forked and you cannot use this CoreFoundation functionality safely. You MUST exec().
    # Break on __THE_PROCESS_HAS_FORKED_AND_YOU_CANNOT_USE_THIS_COREFOUNDATION_FUNCTIONALITY___YOU_MUST_EXEC__() to debug.
    #
    # This problem cause the continous appearing of Popup Windows "Python Quit Unexpectedly", with no reason.
    #
    if platform.system() == "Darwin":
        crash_report = os.popen(
            "defaults read com.apple.CrashReporter DialogType").read().strip()
        os.system("defaults write com.apple.CrashReporter DialogType none")

    try:
        if argv is None:
            argv = sys.argv

        usage = "usage: %prog [options] [workflow_file]"
        parser = optparse.OptionParser(usage=usage)

        parser.add_option("--no-discovery",
                          action="store_true",
                          help="Don't run widget discovery "
                          "(use full cache instead)")
        parser.add_option("--force-discovery",
                          action="store_true",
                          help="Force full widget discovery "
                          "(invalidate cache)")
        parser.add_option("--clear-widget-settings",
                          action="store_true",
                          help="Remove stored widget setting")
        parser.add_option("--no-welcome",
                          action="store_true",
                          help="Don't show welcome dialog.")
        parser.add_option("--no-splash",
                          action="store_true",
                          help="Don't show splash screen.")
        parser.add_option("-l",
                          "--log-level",
                          help="Logging level (0, 1, 2, 3, 4)",
                          type="int",
                          default=1)
        parser.add_option(
            "--no-redirect",
            action="store_true",
            help="Do not redirect stdout/err to canvas output view.")
        parser.add_option("--style",
                          help="QStyle to use",
                          type="str",
                          default="Fusion")
        parser.add_option("--stylesheet",
                          help="Application level CSS style sheet to use",
                          type="str",
                          default="orange.qss")
        parser.add_option("--qt",
                          help="Additional arguments for QApplication",
                          type="str",
                          default=None)

        (options, args) = parser.parse_args(argv[1:])

        levels = [
            logging.CRITICAL, logging.ERROR, logging.WARN, logging.INFO,
            logging.DEBUG
        ]

        # Fix streams before configuring logging (otherwise it will store
        # and write to the old file descriptors)
        fix_win_pythonw_std_stream()

        # Try to fix fonts on OSX Mavericks
        fix_osx_10_9_private_font()

        # File handler should always be at least INFO level so we need
        # the application root level to be at least at INFO.

        root_level = min(levels[options.log_level], logging.INFO)
        rootlogger = logging.getLogger(orangecanvas.__name__)
        rootlogger.setLevel(root_level)
        oasyslogger = logging.getLogger("oasys")
        oasyslogger.setLevel(root_level)

        # Standard output stream handler at the requested level
        stream_handler = logging.StreamHandler()
        stream_handler.setLevel(level=levels[options.log_level])
        rootlogger.addHandler(stream_handler)
        oasyslogger.addHandler(stream_handler)

        config.set_default(conf.oasysconf)
        log.info("Starting 'OASYS' application.")

        qt_argv = argv[:1]

        #     if options.style is not None:
        qt_argv += ["-style", options.style]

        if options.qt is not None:
            qt_argv += shlex.split(options.qt)

        qt_argv += args

        if options.clear_widget_settings:
            log.debug("Clearing widget settings")
            shutil.rmtree(config.widget_settings_dir(), ignore_errors=True)

        log.debug("Starting CanvasApplicaiton with argv = %r.", qt_argv)
        app = CanvasApplication(qt_argv)

        # NOTE: config.init() must be called after the QApplication constructor
        config.init()

        file_handler = logging.FileHandler(filename=os.path.join(
            config.log_dir(), "canvas.log"),
                                           mode="w")

        file_handler.setLevel(root_level)
        rootlogger.addHandler(file_handler)

        # intercept any QFileOpenEvent requests until the main window is
        # fully initialized.
        # NOTE: The QApplication must have the executable ($0) and filename
        # arguments passed in argv otherwise the FileOpen events are
        # triggered for them (this is done by Cocoa, but QApplicaiton filters
        # them out if passed in argv)

        open_requests = []

        def onrequest(url):
            log.info("Received an file open request %s", url)
            open_requests.append(url)

        app.fileOpenRequest.connect(onrequest)

        settings = QSettings()

        stylesheet = options.stylesheet
        stylesheet_string = None

        if stylesheet != "none":
            if os.path.isfile(stylesheet):
                stylesheet_string = open(stylesheet, "rb").read()
            else:
                if not os.path.splitext(stylesheet)[1]:
                    # no extension
                    stylesheet = os.path.extsep.join([stylesheet, "qss"])

                pkg_name = orangecanvas.__name__
                resource = "styles/" + stylesheet

                if pkg_resources.resource_exists(pkg_name, resource):
                    stylesheet_string = \
                        pkg_resources.resource_string(pkg_name, resource).decode()

                    base = pkg_resources.resource_filename(pkg_name, "styles")

                    pattern = re.compile(
                        r"^\s@([a-zA-Z0-9_]+?)\s*:\s*([a-zA-Z0-9_/]+?);\s*$",
                        flags=re.MULTILINE)

                    matches = pattern.findall(stylesheet_string)

                    for prefix, search_path in matches:
                        QDir.addSearchPath(prefix,
                                           os.path.join(base, search_path))
                        log.info("Adding search path %r for prefix, %r",
                                 search_path, prefix)

                    stylesheet_string = pattern.sub("", stylesheet_string)

                else:
                    log.info("%r style sheet not found.", stylesheet)

        # Add the default canvas_icons search path
        dirpath = os.path.abspath(os.path.dirname(orangecanvas.__file__))
        QDir.addSearchPath("canvas_icons", os.path.join(dirpath, "icons"))

        canvas_window = OASYSMainWindow()
        canvas_window.setWindowIcon(config.application_icon())

        if stylesheet_string is not None:
            canvas_window.setStyleSheet(stylesheet_string)

        if not options.force_discovery:
            reg_cache = cache.registry_cache()
        else:
            reg_cache = None

        widget_registry = qt.QtWidgetRegistry()

        widget_discovery = config.widget_discovery(
            widget_registry, cached_descriptions=reg_cache)
        menu_registry = conf.menu_registry()

        want_splash = \
            settings.value("startup/show-splash-screen", True, type=bool) and \
            not options.no_splash

        if want_splash:
            pm, rect = config.splash_screen()
            splash_screen = SplashScreen(pixmap=pm, textRect=rect)
            splash_screen.setFont(QFont("Helvetica", 12))
            color = QColor("#FFD39F")

            def show_message(message):
                splash_screen.showMessage(message, color=color)

            widget_registry.category_added.connect(show_message)

        log.info("Running widget discovery process.")

        cache_filename = os.path.join(config.cache_dir(),
                                      "widget-registry.pck")

        if options.no_discovery:
            widget_registry = pickle.load(open(cache_filename, "rb"))
            widget_registry = qt.QtWidgetRegistry(widget_registry)
        else:
            if want_splash:
                splash_screen.show()
            widget_discovery.run(config.widgets_entry_points())
            if want_splash:
                splash_screen.hide()
                splash_screen.deleteLater()

            # Store cached descriptions
            cache.save_registry_cache(widget_discovery.cached_descriptions)
            with open(cache_filename, "wb") as f:
                pickle.dump(WidgetRegistry(widget_registry), f)

        set_global_registry(widget_registry)

        canvas_window.set_widget_registry(widget_registry)
        canvas_window.set_menu_registry(menu_registry)

        # automatic save

        automatic_saver_thread = QThread()
        automatic_saver = SaveWorkspaceObj(canvas_window, )
        automatic_saver.moveToThread(automatic_saver_thread)
        automatic_saver.finished.connect(automatic_saver_thread.quit)
        automatic_saver_thread.started.connect(automatic_saver.long_running)
        automatic_saver_thread.finished.connect(app.exit)
        automatic_saver_thread.start()

        canvas_window.show()
        canvas_window.raise_()


        want_welcome = True or \
            settings.value("startup/show-welcome-screen", True, type=bool) \
            and not options.no_welcome

        app.setStyle(QStyleFactory.create('Fusion'))
        #app.setStyle(QStyleFactory.create('Macintosh'))
        #app.setStyle(QStyleFactory.create('Windows'))

        # Process events to make sure the canvas_window layout has
        # a chance to activate (the welcome dialog is modal and will
        # block the event queue, plus we need a chance to receive open file
        # signals when running without a splash screen)
        app.processEvents()

        app.fileOpenRequest.connect(canvas_window.open_scheme_file)

        close_app = False

        if open_requests:
            if "pydevd.py" in str(
                    open_requests[0].path()):  # PyCharm Debugger on
                open_requests = []

        if want_welcome and not args and not open_requests:
            if not canvas_window.welcome_dialog():
                log.info("Welcome screen cancelled; closing application")
                close_app = True

        elif args:
            log.info("Loading a scheme from the command line argument %r",
                     args[0])
            canvas_window.load_scheme(args[0])
        elif open_requests:
            log.info("Loading a scheme from an `QFileOpenEvent` for %r",
                     open_requests[-1])
            canvas_window.load_scheme(open_requests[-1].toLocalFile())

        stdout_redirect = \
            settings.value("output/redirect-stdout", True, type=bool)

        stderr_redirect = \
            settings.value("output/redirect-stderr", True, type=bool)

        # cmd line option overrides settings / no redirect is possible
        # under ipython
        if options.no_redirect or running_in_ipython():
            stderr_redirect = stdout_redirect = False

        output_view = canvas_window.output_view()

        if stdout_redirect:
            stdout = TextStream()
            stdout.stream.connect(output_view.write)
            if sys.stdout is not None:
                # also connect to original fd
                stdout.stream.connect(sys.stdout.write)
        else:
            stdout = sys.stdout

        if stderr_redirect:
            error_writer = output_view.formated(color=Qt.red)
            stderr = TextStream()
            stderr.stream.connect(error_writer.write)
            if sys.stderr is not None:
                # also connect to original fd
                stderr.stream.connect(sys.stderr.write)
        else:
            stderr = sys.stderr

        if stderr_redirect:
            sys.excepthook = ExceptHook()
            sys.excepthook.handledException.connect(output_view.parent().show)

        if not close_app:
            with redirect_stdout(stdout), redirect_stderr(stderr):
                log.info("Entering main event loop.")
                try:
                    status = app.exec_()
                except BaseException:
                    log.error("Error in main event loop.", exc_info=True)

            canvas_window.deleteLater()
            app.processEvents()
            app.flush()
            del canvas_window
        else:
            status = False

        if automatic_saver_thread.isRunning():
            automatic_saver_thread.deleteLater()

        # Collect any cycles before deleting the QApplication instance
        gc.collect()

        del app

        # RESTORE INITIAL USER SETTINGS
        if platform.system() == "Darwin":
            os.system("defaults write com.apple.CrashReporter DialogType " +
                      crash_report)

        return status
    except Exception as e:
        # RESTORE INITIAL USER SETTINGS
        if platform.system() == "Darwin":
            os.system("defaults write com.apple.CrashReporter DialogType " +
                      crash_report)

        raise e
Ejemplo n.º 10
0
class Preferences(QDialog):
    # Signal to warn that the window is closed
    settingsClosed = pyqtSignal()

    def __init__(self, parent=None):
        super(Preferences, self).__init__(parent)

        # Main container
        # This contains a grid
        main_box = QVBoxLayout(self)
        main_box.setContentsMargins(200, 50, 200, 100)

        # The grid contains two containers
        # left container and right container
        grid = QGridLayout()

        # Left Container
        left_container = QVBoxLayout()
        left_container.setContentsMargins(0, 0, 0, 0)

        # General
        group_gral = QGroupBox(self.tr("General"))
        box_gral = QVBoxLayout(group_gral)
        # Updates
        btn_updates = QPushButton(self.tr("Check for updates"))
        box_gral.addWidget(btn_updates)
        # Language
        group_language = QGroupBox(self.tr("Language"))
        box = QVBoxLayout(group_language)
        # Find .qm files in language path
        available_langs = file_manager.get_files_from_folder(
            settings.LANGUAGE_PATH)

        languages = ["English"] + available_langs
        self._combo_lang = QComboBox()
        box.addWidget(self._combo_lang)
        self._combo_lang.addItems(languages)
        self._combo_lang.currentIndexChanged[int].connect(
            self._change_lang)
        if PSetting.LANGUAGE:
            self._combo_lang.setCurrentText(PSetting.LANGUAGE)
        box.addWidget(QLabel(self.tr("(Requires restart)")))

        # Add widgets
        left_container.addWidget(group_gral)
        left_container.addWidget(group_language)
        left_container.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding,
                                           QSizePolicy.Expanding))

        # Right Container
        right_container = QVBoxLayout()
        right_container.setContentsMargins(0, 0, 0, 0)

        # Editor
        editor_group = QGroupBox(self.tr("Editor Configurations"))
        box_editor = QHBoxLayout(editor_group)
        # Current line
        self._highlight_current_line = QCheckBox(
            self.tr("Highlight Current Line"))
        self._highlight_current_line.setChecked(
            PSetting.HIGHLIGHT_CURRENT_LINE)
        self._highlight_current_line.stateChanged[int].connect(
            self.__current_line_value_changed)
        box_editor.addWidget(self._highlight_current_line)
        # Matching paren
        self._matching_paren = QCheckBox(self.tr("Matching Parenthesis"))
        self._matching_paren.setChecked(
            PSetting.MATCHING_PARENTHESIS)
        self._matching_paren.stateChanged[int].connect(
            self.__set_enabled_matching_parenthesis)
        box_editor.addWidget(self._matching_paren)
        # Font group
        font_group = QGroupBox(self.tr("Font"))
        font_grid = QGridLayout(font_group)
        font_grid.addWidget(QLabel(self.tr("Family")), 0, 0)
        self._combo_font = QFontComboBox()
        self._combo_font.setCurrentFont(PSetting.FONT)
        font_grid.addWidget(self._combo_font, 0, 1)
        font_grid.addWidget(QLabel(self.tr("Point Size")), 1, 0)
        self._combo_font_size = QComboBox()
        fdb = QFontDatabase()
        combo_sizes = fdb.pointSizes(PSetting.FONT.family())
        current_size_index = combo_sizes.index(
            PSetting.FONT.pointSize())

        self._combo_font_size.addItems([str(f) for f in combo_sizes])
        self._combo_font_size.setCurrentIndex(current_size_index)
        font_grid.addWidget(self._combo_font_size, 1, 1)

        right_container.addWidget(editor_group)
        right_container.addWidget(font_group)
        right_container.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding,
                                            QSizePolicy.Expanding))

        # Add widgets
        grid.addLayout(left_container, 0, 0)
        grid.addLayout(right_container, 0, 1)
        main_box.addLayout(grid)

        # Button close and reset
        hbox = QHBoxLayout()
        hbox.setSpacing(20)
        hbox.addItem(QSpacerItem(1, 0, QSizePolicy.Expanding))
        btn_cancel = QPushButton(self.tr("Back"))
        hbox.addWidget(btn_cancel)
        btn_reset = QPushButton(self.tr("Reset Configurations"))
        hbox.addWidget(btn_reset)
        main_box.addLayout(hbox)

        # Overlay
        self.overlay = overlay_widget.OverlayWidget(self)
        self.overlay.hide()

        # Effect and animations
        self.effect = QGraphicsOpacityEffect()
        self.setGraphicsEffect(self.effect)
        duration, x = 180, 150  # Animation duration
        # Animation start
        # Opacity animation
        self.opacity_animation_s = QPropertyAnimation(self.effect, b"opacity")
        self.opacity_animation_s.setDuration(duration)
        self.opacity_animation_s.setStartValue(0.0)
        self.opacity_animation_s.setEndValue(1.0)
        # X animation
        self.x_animation_s = QPropertyAnimation(self, b"geometry")
        self.x_animation_s.setDuration(duration)
        self.x_animation_s.setStartValue(QRect(x, 0, parent.width(),
                                               parent.height()))
        self.x_animation_s.setEndValue(QRect(0, 0, parent.width(),
                                             parent.height()))
        # Animation end
        # Opacity animation
        self.opacity_animation_e = QPropertyAnimation(self.effect, b"opacity")
        self.opacity_animation_e.setDuration(duration)
        self.opacity_animation_e.setStartValue(1.0)
        self.opacity_animation_e.setEndValue(0.0)
        # X animation
        self.x_animation_e = QPropertyAnimation(self, b"geometry")
        self.x_animation_e.setDuration(duration)
        self.x_animation_e.setStartValue(QRect(0, 0, parent.width(),
                                               parent.height()))
        self.x_animation_e.setEndValue(QRect(-x, 0, parent.width(),
                                             parent.height()))

        # Group animation start
        self.group_animation_s = QParallelAnimationGroup()
        self.group_animation_s.addAnimation(self.opacity_animation_s)
        self.group_animation_s.addAnimation(self.x_animation_s)

        # Group animation end
        self.group_animation_e = QParallelAnimationGroup()
        self.group_animation_e.addAnimation(self.opacity_animation_e)
        self.group_animation_e.addAnimation(self.x_animation_e)

        # Connections
        self.group_animation_e.finished.connect(
            self._on_group_animation_finished)
        btn_cancel.clicked.connect(self.close)
        btn_reset.clicked.connect(self._reset_settings)
        btn_updates.clicked.connect(self._check_for_updates)
        # self.thread.finished.connect(self._on_thread_finished)
        self._combo_font.currentFontChanged.connect(
            self._change_font)
        self._combo_font_size.currentTextChanged.connect(
            self._change_font_size)

    def __current_line_value_changed(self, value):
        qs = QSettings(settings.SETTINGS_PATH, QSettings.IniFormat)
        qs.setValue('highlight_current_line', value)
        PSetting.HIGHLIGHT_CURRENT_LINE = value

    def __set_enabled_matching_parenthesis(self, value):
        qs = QSettings(settings.SETTINGS_PATH, QSettings.IniFormat)
        qs.setValue("matching_parenthesis", value)
        PSetting.MATCHING_PARENTHESIS = value

    def _change_font(self, font):
        # FIXME: un quilombo esto
        central = Pireal.get_service("central")
        mcontainer = central.get_active_db()
        if mcontainer is not None:
            query_widget = mcontainer.query_container.currentWidget()
            if query_widget is not None:
                weditor = query_widget.get_editor()
                if weditor is not None:
                    qs = QSettings(settings.SETTINGS_PATH, QSettings.IniFormat)
                    weditor.set_font(font)
                    qs.setValue("font", font)

    def _change_font_size(self, size):
        # FIXME: un quilombo esto
        font = self._combo_font.currentFont()
        font.setPointSize(int(size))
        central = Pireal.get_service("central")
        mcontainer = central.get_active_db()
        if mcontainer is not None:
            query_widget = mcontainer.query_container.currentWidget()
            if query_widget is not None:
                weditor = query_widget.get_editor()
                if weditor is not None:
                    qs = QSettings(settings.SETTINGS_PATH, QSettings.IniFormat)
                    weditor.set_font(font)
                    qs.setValue("font", font)

    def showEvent(self, event):
        super(Preferences, self).showEvent(event)
        self.group_animation_s.start()

    def resizeEvent(self, event):
        self.overlay.resize(self.size())
        event.accept()

    def done(self, result):
        self.res = result
        self.group_animation_e.start()

    def _on_group_animation_finished(self):
        super(Preferences, self).done(self.res)
        self.settingsClosed.emit()

    def _check_for_updates(self):
        # Thread
        self._thread = QThread()
        self._updater = updater.Updater()
        self._updater.moveToThread(self._thread)
        self._thread.started.connect(self._updater.check_updates)
        self._updater.finished.connect(self.__on_thread_update_finished)
        # Show overlay widget
        self.overlay.show()
        # Start thread
        self._thread.start()

    def __on_thread_update_finished(self):
        # Hide overlay widget
        self.overlay.hide()
        self._thread.quit()
        msg = QMessageBox(self)
        if not self._updater.error:
            if self._updater.version:
                version = self._updater.version
                msg.setWindowTitle(self.tr("New version available!"))
                msg.setText(self.tr("Check the web site to "
                                    "download <b>Pireal {}</b>".format(
                                        version)))
                download_btn = msg.addButton(self.tr("Download!"),
                                             QMessageBox.YesRole)
                msg.addButton(self.tr("Cancel"),
                              QMessageBox.RejectRole)
                msg.exec_()
                r = msg.clickedButton()
                if r == download_btn:
                    webbrowser.open_new(
                        "http://centaurialpha.github.io/pireal")
            else:
                msg.setWindowTitle(self.tr("Information"))
                msg.setText(self.tr("Last version installed"))
                msg.addButton(self.tr("Ok"),
                              QMessageBox.AcceptRole)
                msg.exec_()
        else:
            msg.critical(self, self.tr("Error"),
                         self.tr("Connection error"))

        self._thread.deleteLater()
        self._updater.deleteLater()

    def _reset_settings(self):
        """ Remove all settings """

        msg = QMessageBox(self)
        msg.setWindowTitle(self.tr("Reset Settings"))
        msg.setText(self.tr("Are you sure you want to clear all settings?"))
        msg.setIcon(QMessageBox.Question)
        msg.addButton(self.tr("No"), QMessageBox.NoRole)
        yes_btn = msg.addButton(self.tr("Yes"),
                                QMessageBox.YesRole)
        msg.exec_()
        r = msg.clickedButton()
        if r == yes_btn:
            QSettings(settings.SETTINGS_PATH, QSettings.IniFormat).clear()
            self.close()

    def _change_lang(self, index):
        lang = self._combo_lang.itemText(index)
        qs = QSettings(settings.SETTINGS_PATH, QSettings.IniFormat)
        qs.setValue('language', lang)
Ejemplo n.º 11
0
class Preferences(QDialog):
    settingsClosed = pyqtSignal()

    def __init__(self, parent=None):
        QDialog.__init__(self, parent)
        self.__need_restart = False
        box = QVBoxLayout(self)
        box.setContentsMargins(0, 0, 0, 0)
        view = QQuickWidget()
        view.setResizeMode(QQuickWidget.SizeRootObjectToView)
        qml = os.path.join(settings.QML_PATH, "Preferences.qml")
        view.setSource(QUrl.fromLocalFile(qml))
        box.addWidget(view)

        self.__root = view.rootObject()
        # Lista de idiomas para el Combo qml
        available_langs = file_manager.get_files_from_folder(
            settings.LANGUAGE_PATH)
        langs = ["English"] + available_langs
        self.__root.addLangsToCombo(langs)

        self.__root.setCurrentLanguage(CONFIG.get("language"))

        font = CONFIG.get("fontFamily")
        size = CONFIG.get("fontSize")
        if font is None:
            font, size = CONFIG._get_font()

        self.__root.setFontFamily(font, size)

        self.__root.setInitialStates(
            CONFIG.get("highlightCurrentLine"),
            CONFIG.get("matchParenthesis"))

        # Conexiones
        self.__root.close.connect(lambda: self.settingsClosed.emit())
        self.__root.resetSettings.connect(self.__reset_settings)
        self.__root.checkForUpdates.connect(self.__check_for_updates)
        self.__root.changeLanguage.connect(self.__change_language)
        self.__root.stateCurrentLineChanged[bool].connect(
            self.__on_state_current_line_changed)
        self.__root.stateMatchingParenChanged[bool].connect(
            self.__on_state_matching_parenthesis_changed)
        self.__root.needChangeFont.connect(self.__change_font)

    @pyqtSlot()
    def __change_font(self):
        font = CONFIG.get("fontFamily")
        size = CONFIG.get("fontSize")
        if font is None:
            font, size = CONFIG._get_font()
        font, ok = QFontDialog.getFont(QFont(font, size), self)
        if ok:
            CONFIG.set_value("fontFamily", font.family())
            CONFIG.set_value("fontSize", font.pointSize())
            central = Pireal.get_service("central")
            mcontainer = central.get_active_db()
            if mcontainer is not None:
                query_widget = mcontainer.query_container.currentWidget()
                if query_widget is not None:
                    weditor = query_widget.get_editor()
                    if weditor is not None:
                        weditor.set_font(font.family(), font.pointSize())
            # Cambio el texto en la interfáz QML
            self.__root.setFontFamily(font.family(), font.pointSize())

    @pyqtSlot(bool)
    def __on_state_current_line_changed(self, state):
        CONFIG.set_value("highlightCurrentLine", state)

    @pyqtSlot(bool)
    def __on_state_matching_parenthesis_changed(self, state):
        CONFIG.set_value("matchParenthesis", state)

    @pyqtSlot('QString')
    def __change_language(self, lang):
        qsettings = QSettings(settings.SETTINGS_PATH, QSettings.IniFormat)
        current_lang = qsettings.value('language', 'English')
        if current_lang != lang:
            qsettings.setValue('language', lang)
            self.__need_restart = True

    @pyqtSlot()
    def __check_for_updates(self):
        # Thread
        self._thread = QThread()
        self._updater = updater.Updater()
        self._updater.moveToThread(self._thread)
        self._thread.started.connect(self._updater.check_updates)
        self._updater.finished.connect(self.__on_thread_update_finished)
        # Start thread
        self._thread.start()

    @pyqtSlot()
    def __on_thread_update_finished(self):
        self._thread.quit()
        msg = QMessageBox(self)
        if not self._updater.error:
            if self._updater.version:
                version = self._updater.version
                msg.setWindowTitle(self.tr("New version available!"))
                msg.setText(self.tr("Check the web site to "
                                    "download <b>Pireal {}</b>".format(
                                        version)))
                download_btn = msg.addButton(self.tr("Download!"),
                                             QMessageBox.YesRole)
                msg.addButton(self.tr("Cancel"),
                              QMessageBox.RejectRole)
                msg.exec_()
                r = msg.clickedButton()
                if r == download_btn:
                    webbrowser.open_new(
                        "http://centaurialpha.github.io/pireal")
            else:
                # Cierro BusyIndicator de qml
                self.__root.threadFinished()
                msg.setWindowTitle(self.tr("Information"))
                msg.setText(self.tr("Last version installed"))
                msg.addButton(self.tr("Ok"),
                              QMessageBox.AcceptRole)
                msg.exec_()
        else:
            msg.critical(self, self.tr("Error"),
                         self.tr("Connection error"))

        self._thread.deleteLater()
        self._updater.deleteLater()
        self.__root.threadFinished()

    @pyqtSlot()
    def __reset_settings(self):
        """ Remove all settings """

        msg = QMessageBox(self)
        msg.setWindowTitle(self.tr("Reset Settings"))
        msg.setText(self.tr("Are you sure you want to clear all settings?"))
        msg.setIcon(QMessageBox.Question)
        msg.addButton(self.tr("No"), QMessageBox.NoRole)
        yes_btn = msg.addButton(self.tr("Yes"),
                                QMessageBox.YesRole)
        msg.exec_()
        r = msg.clickedButton()
        if r == yes_btn:
            QSettings(settings.SETTINGS_PATH, QSettings.IniFormat).clear()
Ejemplo n.º 12
0
class _POSIXUserscriptRunner(_BaseUserscriptRunner):

    """Userscript runner to be used on POSIX. Uses _BlockingFIFOReader.

    The OS must have support for named pipes and select(). Commands are
    executed immediately when they arrive in the FIFO.

    Attributes:
        _reader: The _BlockingFIFOReader instance.
        _thread: The QThread where reader runs.
    """

    def __init__(self, win_id, parent=None):
        super().__init__(win_id, parent)
        self._reader = None
        self._thread = None

    def run(self, cmd, *args, env=None):
        rundir = standarddir.get(QStandardPaths.RuntimeLocation)
        try:
            # tempfile.mktemp is deprecated and discouraged, but we use it here
            # to create a FIFO since the only other alternative would be to
            # create a directory and place the FIFO there, which sucks. Since
            # os.kfifo will raise an exception anyways when the path doesn't
            # exist, it shouldn't be a big issue.
            self._filepath = tempfile.mktemp(prefix='userscript-', dir=rundir)
            os.mkfifo(self._filepath)  # pylint: disable=no-member
        except OSError as e:
            message.error(self._win_id, "Error while creating FIFO: {}".format(
                e))
            return

        self._reader = _BlockingFIFOReader(self._filepath)
        self._thread = QThread(self)
        self._reader.moveToThread(self._thread)
        self._reader.got_line.connect(self.got_cmd)
        self._thread.started.connect(self._reader.read)
        self._reader.finished.connect(self.on_reader_finished)
        self._thread.finished.connect(self.on_thread_finished)

        self._run_process(cmd, *args, env=env)
        self._thread.start()

    def on_proc_finished(self):
        """Interrupt the reader when the process finished."""
        log.procs.debug("proc finished")
        self._thread.requestInterruption()

    def on_proc_error(self, error):
        """Interrupt the reader when the process had an error."""
        super().on_proc_error(error)
        self._thread.requestInterruption()

    def on_reader_finished(self):
        """Quit the thread and clean up when the reader finished."""
        log.procs.debug("reader finished")
        self._thread.quit()
        self._reader.fifo.close()
        self._reader.deleteLater()
        super()._cleanup()
        self.finished.emit()

    def on_thread_finished(self):
        """Clean up the QThread object when the thread finished."""
        log.procs.debug("thread finished")
        self._thread.deleteLater()
Ejemplo n.º 13
0
class RmExplorerWindow(QMainWindow):

    def __init__(self):

        super().__init__()

        self.settings = Settings()
        self.updateFromSettings()

        self.statusBar()
        self.makeMenus()

        self.dirsList = QListWidget(self)
        self.dirsList.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.dirsList.itemDoubleClicked.connect(self.dirsListItemDoubleClicked)
        self.dirsList.setContextMenuPolicy(Qt.CustomContextMenu)
        self.dirsList.customContextMenuRequested.connect(self.dirsListContextMenuRequested)

        self.filesList = QListWidget(self)
        self.filesList.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.filesList.itemDoubleClicked.connect(self.filesListItemDoubleClicked)
        self.filesList.setContextMenuPolicy(Qt.CustomContextMenu)
        self.filesList.customContextMenuRequested.connect(self.filesListContextMenuRequested)

        self.curDirLabel = QLabel(self)

        browserLayout = QGridLayout()
        browserLayout.addWidget(QLabel('Folders:'), 0, 0)
        browserLayout.addWidget(QLabel('Files:'), 0, 1)
        browserLayout.addWidget(self.dirsList, 1, 0)
        browserLayout.addWidget(self.filesList, 1, 1)

        mainLayout = QVBoxLayout()
        mainLayout.addLayout(browserLayout)
        mainLayout.addWidget(self.curDirLabel)

        centralWidget = QWidget(self)
        centralWidget.setLayout(mainLayout)
        self.setCentralWidget(centralWidget)

        self.curDir = ''
        self.curDirName = ''
        self.curDirParents = []
        self.curDirParentsNames = []
        self.dirIds = []
        self.dirNames = []
        self.fileIds = []
        self.goToDir('', '')

        self.currentWarning = ''
        self.hasRaised = None

        self.progressWindow = None
        self.downloadFilesWorker = None
        self.uploadDocsWorker = None
        self.backupDocsWorker = None
        self.restoreDocsWorker = None
        self.taskThread = None

        self._masterKey = None

        self.setWindowTitle(constants.AppName)


    ###################
    # General methods #
    ###################

    def updateFromSettings(self):
        """Call this whenever settings are changed"""

        socket.setdefaulttimeout(self.settings.value('HTTPShortTimeout', type=float))


    def makeMenus(self):

        menubar = self.menuBar()

        # Explorer menu
        uploadDocsAct = QAction('&Upload documents', self)
        uploadDocsAct.setShortcut('Ctrl+U')
        uploadDocsAct.setStatusTip('Upload documents from the computer to the tablet.')
        uploadDocsAct.triggered.connect(self.uploadDocs)
        #
        dlAllAct = QAction('&Download all', self)
        dlAllAct.setShortcut('Ctrl+D')
        dlAllAct.setStatusTip('Download all files to a local folder.')
        dlAllAct.triggered.connect(self.downloadAll)
        #
        refreshAct = QAction('&Refresh', self)
        refreshAct.setShortcut('Ctrl+R')
        refreshAct.setStatusTip('Refresh folders and files lists.')
        refreshAct.triggered.connect(self.refreshLists)
        #
        settingsAct = QAction('&Settings', self)
        settingsAct.setShortcut('Ctrl+S')
        settingsAct.setStatusTip('%s settings' % constants.AppName)
        settingsAct.triggered.connect(self.editSettings)
        #
        exitAct = QAction('&Exit', self)
        exitAct.setShortcut('Ctrl+Q')
        exitAct.setStatusTip('Exit %s.' % constants.AppName)
        exitAct.triggered.connect(qApp.quit)
        #
        explorerMenu = menubar.addMenu('&Explorer')
        explorerMenu.addAction(uploadDocsAct)
        explorerMenu.addAction(dlAllAct)
        explorerMenu.addAction(refreshAct)
        explorerMenu.addSeparator()
        explorerMenu.addAction(settingsAct)
        explorerMenu.addSeparator()
        explorerMenu.addAction(exitAct)

        # SSH menu
        backupDocsAct = QAction('&Backup documents', self)
        backupDocsAct.setStatusTip('Backup all notebooks, documents, ebooks and bookmarks to a folder on this computer.')
        backupDocsAct.triggered.connect(self.backupDocs)
        #
        restoreDocsAct = QAction('&Restore documents', self)
        restoreDocsAct.setStatusTip('Restore documents on the tablet from a backup on this computer.')
        restoreDocsAct.triggered.connect(self.restoreDocs)
        #
        sshMenu = menubar.addMenu('&SSH')
        sshMenu.addAction(backupDocsAct)
        sshMenu.addAction(restoreDocsAct)

        # About menu
        aboutAct = QAction(constants.AppName, self)
        aboutAct.setStatusTip("Show %s's About box." % constants.AppName)
        aboutAct.triggered.connect(self.about)
        #
        aboutQtAct = QAction('Qt', self)
        aboutQtAct.setStatusTip("Show Qt's About box.")
        aboutQtAct.triggered.connect(qApp.aboutQt)
        #
        explorerMenu = menubar.addMenu('&About')
        explorerMenu.addAction(aboutAct)
        explorerMenu.addAction(aboutQtAct)

        # Context menu of the directories QListWidget
        self.dirsListContextMenu = QMenu(self)
        downloadDirsAct = self.dirsListContextMenu.addAction('&Download')
        downloadDirsAct.triggered.connect(self.downloadDirsClicked)

        # Context menu of the files QListWidget
        self.filesListContextMenu = QMenu(self)
        downloadFilesAct = self.filesListContextMenu.addAction('&Download')
        downloadFilesAct.triggered.connect(self.downloadFilesClicked)


    def goToDir(self, dirId, dirName):

        try:
            collections, docs = tools.listDir(dirId, self.settings)
        except (urllib.error.URLError, socket.timeout) as e:
            msg = getattr(e, 'reason', 'timeout')
            QMessageBox.critical(self, constants.AppName,
                                 'Could not go to directory "%s": URL error:\n%s' % (dirId, msg))
            return

        if dirId != self.curDir:
            # We are either moving up or down one level
            if len(self.curDirParents) == 0 or self.curDirParents[-1] != dirId:
                # Moving down
                self.curDirParents.append(self.curDir)
                self.curDirParentsNames.append(self.curDirName)
            else:
                # Moving up
                self.curDirParents.pop()
                self.curDirParentsNames.pop()
            self.curDir = dirId
            self.curDirName = dirName
        if self.curDirParents:
            path = '%s/%s' % ('/'.join(self.curDirParentsNames), self.curDirName)
        else:
            path = '/'
        self.curDirLabel.setText(path)

        # Update dirsList and filesList
        self.dirsList.clear()
        self.filesList.clear()

        if dirId != '':
            self.dirIds = [self.curDirParents[-1]]
            self.dirNames = [self.curDirParentsNames[-1]]
            self.dirsList.addItem('..')
        else:
            self.dirIds = []
            self.dirNames = []
        self.fileIds = []

        for id_, name in collections:
            self.dirIds.append(id_)
            self.dirNames.append(name)
            self.dirsList.addItem(name)
        for id_, name in docs:
            self.fileIds.append(id_)
            self.filesList.addItem(name)


    def downloadFile(self, basePath, fileDesc, mode):

        if not os.path.isdir(basePath):
            raise OSError('Not a directory: %s' % basePath)

        fid, destRelPath = fileDesc
        self.statusBar().showMessage('Downloading %s...' % os.path.split(destRelPath)[1])
        try:
            tools.downloadFile(fid, basePath, destRelPath, mode, self.settings)
        except (urllib.error.URLError, socket.timeout) as e:
            msg = getattr(e, 'reason', 'timeout')
            QMessageBox.error(self, constants.AppName,
                                'URL error: %s. Aborted.' % msg)
            self.statusBar().showMessage('Download error.',
                                         constants.StatusBarMsgDisplayDuration)
        else:
            self.statusBar().showMessage('Download finished.',
                                         constants.StatusBarMsgDisplayDuration)


    def downloadDirs(self, dirs):

        def listFiles(ext, baseFolderId, baseFolderPath, filesList):
            url = self.settings.value('listFolderURL', type=str) % baseFolderId
            try:
                res = urllib.request.urlopen(url)
                data = res.read().decode(constants.HttpJsonEncoding)
            except (urllib.error.URLError, socket.timeout) as e:
                warningBox = QMessageBox(self)
                msg = getattr(e, 'reason', 'timeout')
                warningBox.setText('URL error: %s. Aborted.' % msg)
                warningBox.setIcon(QMessageBox.Warning)
                warningBox.exec()
                self.statusBar().showMessage('Download error.',
                                             constants.StatusBarMsgDisplayDuration)
                return
            data = json.loads(data)
            for elem in data:
                if elem['Type'] == 'DocumentType':
                    path = '%s.%s' % (os.path.join(baseFolderPath, elem['VissibleName']), ext) # yes, "Vissible"
                    filesList.append((elem['ID'], path))
                elif elem['Type'] == 'CollectionType':
                    listFiles(ext, elem['ID'],
                              os.path.join(baseFolderPath, elem['VissibleName']),
                              filesList)

        dialog = SaveOptsDialog(self.settings, self)
        if dialog.exec() == QDialog.Accepted:
            mode = dialog.getSaveMode()
            ext = mode
            # Ask for destination folder
            folder = QFileDialog.getExistingDirectory(self,
                                                      'Save directory',
                                                      self.settings.value('lastDir', type=str),
                                                      QFileDialog.ShowDirsOnly
                                                      | QFileDialog.DontResolveSymlinks)
            if folder:
                self.settings.setValue('lastDir', os.path.split(folder)[0])
                # Construct files list
                dlList = []
                for dir_id, dir_name in dirs:
                    listFiles(ext, dir_id, dir_name, dlList)

                self.progressWindow = ProgressWindow(self)
                self.progressWindow.setWindowTitle("Downloading...")
                self.progressWindow.nSteps = len(dlList)
                self.progressWindow.open()

                self.settings.sync()
                self.currentWarning = ''
                self.downloadFilesWorker = DownloadFilesWorker(folder,
                                                               dlList,
                                                               mode)
                self.taskThread = QThread()
                self.downloadFilesWorker.moveToThread(self.taskThread)
                self.taskThread.started.connect(self.downloadFilesWorker.start)
                self.downloadFilesWorker.notifyProgress.connect(self.progressWindow.updateStep)
                self.downloadFilesWorker.finished.connect(self.onDownloadFilesFinished)
                self.downloadFilesWorker.warning.connect(self.warningRaised)
                self.taskThread.start()
            else:
                self.statusBar().showMessage('Cancelled.',
                                             constants.StatusBarMsgDisplayDuration)


    def downloadFiles(self, files):

        dialog = SaveOptsDialog(self.settings, self)
        if dialog.exec() == QDialog.Accepted:
            mode = dialog.getSaveMode()
            ext = mode
            # Ask for destination folder
            folder = QFileDialog.getExistingDirectory(self,
                                                      'Save directory',
                                                      self.settings.value('lastDir', type=str),
                                                      QFileDialog.ShowDirsOnly
                                                      | QFileDialog.DontResolveSymlinks)
            if folder:
                self.settings.setValue('lastDir', os.path.split(folder)[0])
                # Construct files list
                dlList = tuple((id_, os.path.join(folder, '%s.%s' % (name, ext)))
                               for id_, name in files)

                self.progressWindow = ProgressWindow(self)
                self.progressWindow.setWindowTitle("Downloading...")
                self.progressWindow.nSteps = len(dlList)
                self.progressWindow.open()

                self.settings.sync()
                self.currentWarning = ''
                self.downloadFilesWorker = DownloadFilesWorker(folder,
                                                               dlList,
                                                               mode)
                self.taskThread = QThread()
                self.downloadFilesWorker.moveToThread(self.taskThread)
                self.taskThread.started.connect(self.downloadFilesWorker.start)
                self.downloadFilesWorker.notifyProgress.connect(self.progressWindow.updateStep)
                self.downloadFilesWorker.finished.connect(self.onDownloadFilesFinished)
                self.downloadFilesWorker.warning.connect(self.warningRaised)
                self.taskThread.start()
            else:
                self.statusBar().showMessage('Cancelled.',
                                             constants.StatusBarMsgDisplayDuration)


    def backupDocs(self):

        # Destination folder
        defaultDir = (self.settings.value('lastSSHBackupDir', type=str)
                      or self.settings.value('lastDir', type=str))
        folder = QFileDialog.getExistingDirectory(self,
                                                  'Save directory',
                                                  defaultDir,
                                                  QFileDialog.ShowDirsOnly
                                                  | QFileDialog.DontResolveSymlinks)
        if not folder:
            self.statusBar().showMessage('Cancelled.',
                                         constants.StatusBarMsgDisplayDuration)
            return

        self.settings.setValue('lastSSHBackupDir', os.path.split(folder)[0])

        if not self.settings.unlockMasterKeyInteractive(self):
            self.statusBar().showMessage('Cancelled.',
                                         constants.StatusBarMsgDisplayDuration)
            return

        self.progressWindow = ProgressWindow(self)
        self.progressWindow.setWindowTitle("Downloading backup...")
        self.progressWindow.open()

        self.settings.sync()
        self.currentWarning = ''
        self.backupDocsWorker = BackupDocsWorker(folder, self.settings._masterKey)

        self.taskThread = QThread()
        self.backupDocsWorker.moveToThread(self.taskThread)
        self.taskThread.started.connect(self.backupDocsWorker.start)
        self.backupDocsWorker.notifyNSteps.connect(self.progressWindow.updateNSteps)
        self.backupDocsWorker.notifyProgress.connect(self.progressWindow.updateStep)
        self.backupDocsWorker.finished.connect(self.onBackupDocsFinished)
        self.backupDocsWorker.warning.connect(self.warningRaised)
        self.taskThread.start()


    def restoreDocs(self):

        # Confirm user has a backup folder
        tabletDir = self.settings.value('TabletDocumentsDir')
        msg = "To restore a backup, you need a previous copy on your computer of the tablet's \"%s\" folder. Ensure the backup you select was made with a tablet having the same software version as the device on which you want to restore the files.\n\n" % tabletDir
        msg += "Do you have such a backup and want to proceed to the restoration?"
        reply = QMessageBox.question(self, constants.AppName, msg)
        if reply == QMessageBox.No:
            self.statusBar().showMessage('Cancelled.',
                                         constants.StatusBarMsgDisplayDuration)
            return

        # Source folder
        defaultDir = (self.settings.value('lastSSHBackupDir', type=str)
                      or self.settings.value('lastDir', type=str))
        folder = QFileDialog.getExistingDirectory(self,
                                                  'Backup directory',
                                                  defaultDir,
                                                  QFileDialog.ShowDirsOnly
                                                  | QFileDialog.DontResolveSymlinks)
        if not folder:
            self.statusBar().showMessage('Cancelled.',
                                         constants.StatusBarMsgDisplayDuration)
            return
        self.settings.setValue('lastSSHBackupDir', os.path.split(folder)[0])

        # Basic check that the folder contents looks like a backup
        success, msg = tools.isValidBackupDir(folder)
        if not success:
            QMessageBox.warning(self, constants.AppName, '%s\nAborting.' % msg)
            self.statusBar().showMessage('Cancelled.',
                                         constants.StatusBarMsgDisplayDuration)
            return

        if not self.settings.unlockMasterKeyInteractive(self):
            self.statusBar().showMessage('Cancelled.',
                                         constants.StatusBarMsgDisplayDuration)
            return

        # Last chance to cancel!
        msg = "%s is now ready to restore the documents. Please check that the tablet is turned on, unlocked and that Wifi is enabled. Make sure no file is open and do not use the tablet during the upload.\n\n" % constants.AppName
        msg += "When the upload finishes, please reboot the tablet.\n\n"
        msg += "To restore documents, contents on the tablet will first be deleted. By continuing, you acknowledge that you take the sole responsibility for any possible data loss or damage caused to the tablet that may result from using %s.\n\n" % constants.AppName
        msg += "Do you want to continue?"
        reply = QMessageBox.question(self, constants.AppName, msg)
        if reply == QMessageBox.No:
            self.statusBar().showMessage('Cancelled.',
                                         constants.StatusBarMsgDisplayDuration)
            return

        self.progressWindow = ProgressWindow(self)
        self.progressWindow.setWindowTitle("Restoring backup...")
        self.progressWindow.open()

        self.settings.sync()
        self.hasRaised = False
        self.restoreDocsWorker = RestoreDocsWorker(folder, self.settings._masterKey)

        self.taskThread = QThread()
        self.restoreDocsWorker.moveToThread(self.taskThread)
        self.taskThread.started.connect(self.restoreDocsWorker.start)
        self.restoreDocsWorker.notifyNSteps.connect(self.progressWindow.updateNSteps)
        self.restoreDocsWorker.notifyProgress.connect(self.progressWindow.updateStep)
        self.restoreDocsWorker.finished.connect(self.onRestoreDocsFinished)
        self.restoreDocsWorker.error.connect(self.errorRaised)
        self.taskThread.start()


    #########
    # Slots #
    #########

    def refreshLists(self):

        self.goToDir(self.curDir, self.curDirName)


    def dirsListItemDoubleClicked(self, item):

        idx = self.dirsList.currentRow()
        self.goToDir(self.dirIds[idx], self.dirNames[idx])


    def dirsListContextMenuRequested(self, pos):

        if len(self.dirsList.selectedItems()) > 0:
            self.dirsListContextMenu.exec(self.dirsList.mapToGlobal(pos))


    def filesListContextMenuRequested(self, pos):

        if len(self.filesList.selectedItems()) > 0:
            self.filesListContextMenu.exec(self.filesList.mapToGlobal(pos))


    def filesListItemDoubleClicked(self, item):

        fid = self.fileIds[self.filesList.currentRow()]
        dialog = SaveOptsDialog(self.settings, self)
        if dialog.exec() == QDialog.Accepted:
            mode = dialog.getSaveMode()
            ext = mode
            filename = '%s.%s' % (item.text(), ext)

            # Ask for file destination
            result = QFileDialog.getSaveFileName(self,
                                                 'Save %s' % ext.upper(),
                                                 os.path.join(self.settings.value('lastDir', type=str),
                                                              filename),
                                                 '%s file (*.%s)' % (ext.upper(), ext))
            if result[0]:
                dest_path = result[0] if result[0].endswith('.%s' % ext) else '%s.%s' % (result[0], ext)
                parts = os.path.split(dest_path)
                self.settings.setValue('lastDir', parts[0])
                self.downloadFile(parts[0], (fid, parts[1]), ext)
            else:
                self.statusBar().showMessage('Cancelled.',
                                             constants.StatusBarMsgDisplayDuration)
        else:
            self.statusBar().showMessage('Cancelled.',
                                         constants.StatusBarMsgDisplayDuration)


    def downloadFilesClicked(self):

        items = self.filesList.selectionModel().selectedIndexes()
        files = tuple((self.fileIds[i.row()],
                       self.filesList.item(i.row()).text()) for i in items)
        self.downloadFiles(files)


    def downloadDirsClicked(self):

        items = self.dirsList.selectionModel().selectedIndexes()
        dirs = tuple((self.dirIds[i.row()], self.dirNames[i.row()]) for i in items)
        self.downloadDirs(dirs)


    def downloadAll(self):

        self.downloadDirs((('', ''),))


    def uploadDocs(self):

        defaultDir = (self.settings.value('lastDir', type=str))
        paths = QFileDialog.getOpenFileNames(self,
                                             'Select files to upload',
                                             defaultDir,
                                             'Documents (*.pdf *.epub)')[0]
        nFiles = len(paths)
        if nFiles == 0:
            self.statusBar().showMessage('Cancelled.',
                                         constants.StatusBarMsgDisplayDuration)
            return

        self.settings.setValue('lastDir', os.path.split(paths[0])[0])

        self.progressWindow = ProgressWindow(self)
        self.progressWindow.setWindowTitle("Uploading documents...")
        self.progressWindow.nSteps = nFiles
        self.progressWindow.open()

        self.settings.sync()
        self.currentWarning = ''
        self.uploadDocsWorker = UploadDocsWorker(paths)

        self.taskThread = QThread()
        self.uploadDocsWorker.moveToThread(self.taskThread)
        self.taskThread.started.connect(self.uploadDocsWorker.start)
        self.uploadDocsWorker.notifyNSteps.connect(self.progressWindow.updateNSteps)
        self.uploadDocsWorker.notifyProgress.connect(self.progressWindow.updateStep)
        self.uploadDocsWorker.finished.connect(self.onUploadDocsFinished)
        self.uploadDocsWorker.warning.connect(self.warningRaised)
        self.taskThread.start()


    def warningRaised(self, msg):

        self.currentWarning = msg


    def errorRaised(self, msg):

        self.hasRaised = True
        QMessageBox.critical(self, constants.AppName,
                             'Error:\n%s\nAborted.' % msg)


    def onDownloadFilesFinished(self):

        self.progressWindow.hide()

        self.taskThread.started.disconnect(self.downloadFilesWorker.start)
        self.downloadFilesWorker.notifyProgress.disconnect(self.progressWindow.updateStep)
        self.downloadFilesWorker.warning.disconnect(self.warningRaised)
        self.downloadFilesWorker.finished.disconnect(self.onDownloadFilesFinished)

        self.progressWindow.deleteLater()

        # Not sure that the following is entirely safe.  For example, what if a
        # new thread is created before the old objects are actually deleted?
        self.taskThread.quit()
        self.downloadFilesWorker.deleteLater()
        self.taskThread.deleteLater()
        self.taskThread.wait()

        if self.currentWarning:
            QMessageBox.warning(self, constants.AppName,
                                'Errors were encountered:\n%s' % self.currentWarning)
        self.statusBar().showMessage('Finished downloading files.',
                                     constants.StatusBarMsgDisplayDuration)


    def onUploadDocsFinished(self):

        self.progressWindow.hide()

        self.taskThread.started.disconnect(self.uploadDocsWorker.start)
        self.uploadDocsWorker.notifyNSteps.disconnect(self.progressWindow.updateNSteps)
        self.uploadDocsWorker.notifyProgress.disconnect(self.progressWindow.updateStep)
        self.uploadDocsWorker.warning.disconnect(self.warningRaised)
        self.uploadDocsWorker.finished.disconnect(self.onUploadDocsFinished)

        self.progressWindow.deleteLater()

        # Not sure that the following is entirely safe.  For example, what if a
        # new thread is created before the old objects are actually deleted?
        self.taskThread.quit()
        self.uploadDocsWorker.deleteLater()
        self.taskThread.deleteLater()
        self.taskThread.wait()

        if self.currentWarning:
            QMessageBox.warning(self, constants.AppName,
                                'Errors were encountered:\n%s' % self.currentWarning)
        self.refreshLists()
        self.statusBar().showMessage('Finished uploading files.',
                                     constants.StatusBarMsgDisplayDuration)


    def editSettings(self):

        dialog = SettingsDialog(self.settings, self)
        if dialog.exec() == QDialog.Accepted:
            self.updateFromSettings()
            self.statusBar().showMessage('Settings updated.',
                                         constants.StatusBarMsgDisplayDuration)


    def onBackupDocsFinished(self):

        self.progressWindow.hide()

        self.taskThread.started.disconnect(self.backupDocsWorker.start)
        self.backupDocsWorker.warning.disconnect(self.warningRaised)
        self.backupDocsWorker.finished.disconnect(self.onBackupDocsFinished)
        self.backupDocsWorker.notifyNSteps.disconnect(self.progressWindow.updateNSteps)
        self.backupDocsWorker.notifyProgress.disconnect(self.progressWindow.updateStep)

        self.progressWindow.deleteLater()

        self.taskThread.quit()
        self.backupDocsWorker.deleteLater()
        self.taskThread.deleteLater()
        self.taskThread.wait()

        if self.currentWarning:
            QMessageBox.warning(self, constants.AppName,
                                'Errors were encountered:\n%s' % self.currentWarning)
        else:
            QMessageBox.information(self, constants.AppName,
                                    'Backup was created successfully!')
        self.statusBar().showMessage('Finished downloading backup.',
                                     constants.StatusBarMsgDisplayDuration)


    def onRestoreDocsFinished(self):

        self.progressWindow.hide()

        self.taskThread.started.disconnect(self.restoreDocsWorker.start)
        self.restoreDocsWorker.error.disconnect(self.errorRaised)
        self.restoreDocsWorker.finished.disconnect(self.onRestoreDocsFinished)
        self.restoreDocsWorker.notifyNSteps.disconnect(self.progressWindow.updateNSteps)
        self.restoreDocsWorker.notifyProgress.disconnect(self.progressWindow.updateStep)

        self.progressWindow.deleteLater()

        self.taskThread.quit()
        self.restoreDocsWorker.deleteLater()
        self.taskThread.deleteLater()
        self.taskThread.wait()

        if not self.hasRaised:
            QMessageBox.information(self, constants.AppName,
                                    'Backup was restored successfully! Please reboot the tablet now.')

            self.statusBar().showMessage('Finished restoring backup.',
                                         constants.StatusBarMsgDisplayDuration)


    def about(self):

        msg = """<b>pyrmexplorer: Explorer for Remarkable tablets</b><br/><br/>
Version %s<br/><br/>
Copyright (C) 2019 Nicolas Bruot (<a href="https://www.bruot.org/hp/">https://www.bruot.org/hp/</a>)<br/><br/>

Some parts of this software are copyright other contributors. Refer to the individual source files for details.<br/><br/>

pyrmexplorer is released under the terms of the GNU General Public License (GPL) v3.<br/><br/>

The source code is available at <a href=\"https://github.com/bruot/pyrmexplorer/\">https://github.com/bruot/pyrmexplorer/</a>.<br/><br/>
"""
        msg = msg % __version__
        msgBox = QMessageBox(self)
        msgBox.setText(msg)
        msgBox.exec()
Ejemplo n.º 14
0
class HeapTrace(object):
    def __init__(self, mainWindow):
        self.mainWindow = mainWindow
        self.reader = None
        self.thread = None
        self.proc = None
        self.blocks = []

    def run(self):
        self.log = []
        self.bits = int(settings.get('new trace', 'BITS'))
        self.mainWindow.textLog.clear()
        invocation = PopenAndCall(getcmd(self.bits), shell=False)
        invocation.finished.connect(self.on_proc_finished)
        invocation.started.connect(self.on_proc_started)
        invocation.start(self)

    def kill(self):
        if self.thread:
            self.thread.quit()
        if self.proc:
            self.proc.terminate()
        try:
            os.kill(self.pin_proc, signal.SIGKILL)
        except Exception:
            pass

    def find_block_by_addr(self, addr):
        for i, block in enumerate(self.heapView.layoutHeapView.children()):
            print block
            if block.base_addr == addr:
                return i, block
        return None, None

    def on_got_heap_op(self, packet):
        if packet.code == heap_op_type_t.PKT_FREE:
            i, freed = self.find_block_by_addr(packet.args[0])
            if freed == None:
                print "Hacking is not nice."
            else:
                freed.new_packet(packet)
        else:
            i, old = self.find_block_by_addr(packet.return_value)
            if not old:
                block = Block(packet)
                self.heapView.push_new_block(block)
                self.blocks.append(block)
            else:
                if old.packet.chunk.size != packet.chunk.size:
                    self.heapView.layoutHeapView.removeWidget(old)
                else:
                    old.new_packet(packet)

        self.log.append(packet)
        self.mainWindow.textLog.append(packet.text_dump() + "\n")

    def on_proc_started(self):
        self.mainWindow.status("Process started")
        self.events = Queue(maxsize=0)
        self.reader = PinCommunication('localhost', 12345, self.bits, self.events)
        self.thread = QThread()
        self.reader.moveToThread(self.thread)
        self.reader.got_heap_op.connect(self.on_got_heap_op)
        self.reader.pin_PID.connect(self.on_pin_pid_received)
        self.thread.started.connect(self.reader.event_loop)
        self.reader.finished.connect(self.on_reader_finished)
        self.thread.finished.connect(self.on_thread_finished)
        self.thread.start()

    def on_pin_pid_received(self, pid):
        self.pin_proc = pid
        self.heapView = HeapWindow(self.mainWindow)
        self.heapView.show()

    def on_proc_finished(self):
        if self.thread:
            self.thread.quit()
        self.proc = None
        self.kill()
        self.mainWindow.status("Process finished ({} lines)".format(len(self.log)))

    def on_reader_finished(self):
        self.kill()
        self.reader.deleteLater()

    def on_thread_finished(self):
        self.thread.deleteLater()
        self.thread = None
Ejemplo n.º 15
0
class Pireal(QMainWindow):

    """
    Main Window class

    This class is responsible for installing all application services.
    """

    __SERVICES = {}
    __ACTIONS = {}

    # The name of items is the connection text
    TOOLBAR_ITEMS = [
        'create_database',
        'open_database',
        'save_database',
        '',  # Is a separator!
        'new_query',
        'open_query',
        'save_query',
        '',
        'undo_action',
        'redo_action',
        'cut_action',
        'copy_action',
        'paste_action',
        '',
        'create_new_relation',
        'remove_relation',
        'edit_relation',
        '',
        'execute_queries'
    ]

    def __init__(self):
        QMainWindow.__init__(self)
        self.setWindowTitle(self.tr("Pireal"))
        self.setMinimumSize(700, 500)

        # Load window geometry
        qsettings = QSettings(settings.SETTINGS_PATH, QSettings.IniFormat)
        window_maximized = qsettings.value('window_max', True, type=bool)
        if window_maximized:
            self.showMaximized()
        else:
            size = qsettings.value('window_size')
            self.resize(size)
            position = qsettings.value('window_pos')
            self.move(position)
        # Toolbar
        self.toolbar = QToolBar(self)
        self.toolbar.setIconSize(QSize(22, 22))
        self.toolbar.setMovable(False)
        self.addToolBar(self.toolbar)

        # Menu bar
        menubar = self.menuBar()
        self.__load_menubar(menubar)
        # Load notification widget after toolbar actions
        notification_widget = Pireal.get_service("notification")
        self.toolbar.addWidget(notification_widget)
        # Message error
        self._msg_error_widget = message_error.MessageError(self)
        # Central widget
        central_widget = Pireal.get_service("central")
        central_widget.databaseSaved.connect(notification_widget.show_text)
        central_widget.querySaved.connect(notification_widget.show_text)
        self.setCentralWidget(central_widget)
        central_widget.add_start_page()

        # Check for updates
        self._thread = QThread()
        self._updater = updater.Updater()
        self._updater.moveToThread(self._thread)
        self._thread.started.connect(self._updater.check_updates)
        self._updater.finished.connect(self.__on_thread_update_finished)
        self._thread.start()
        notification_widget.show_text(
            self.tr("Checking for updates..."), time_out=0)

        # Install service
        Pireal.load_service("pireal", self)

    @classmethod
    def get_service(cls, service):
        """ Return the instance of a loaded service """

        return cls.__SERVICES.get(service, None)

    @classmethod
    def load_service(cls, name, instance):
        """ Load a service providing the service name and the instance """

        cls.__SERVICES[name] = instance

    @classmethod
    def get_action(cls, name):
        """ Return the instance of a loaded QAction """

        return cls.__ACTIONS.get(name, None)

    @classmethod
    def load_action(cls, name, action):
        """ Load a QAction """

        cls.__ACTIONS[name] = action

    def __load_menubar(self, menubar):
        """
        This method installs the menubar and toolbar, menus and QAction's,
        also connects to a slot each QAction.
        """

        from src.gui import menu_actions
        from src import keymap

        # Keymap
        kmap = keymap.KEYMAP
        # Toolbar items
        toolbar_items = {}

        central = Pireal.get_service("central")

        # Load menu bar
        for item in menu_actions.MENU:
            menubar_item = menu_actions.MENU[item]
            menu_name = menubar_item['name']
            items = menubar_item['items']
            menu = menubar.addMenu(menu_name)
            for menu_item in items:
                if isinstance(menu_item, str):
                    # Is a separator
                    menu.addSeparator()
                else:
                    action = menu_item['name']
                    obj, connection = menu_item['slot'].split(':')
                    if obj.startswith('central'):
                        obj = central
                    else:
                        obj = self
                    qaction = menu.addAction(action)
                    # Icon name is connection
                    icon = QIcon(":img/%s" % connection)
                    qaction.setIcon(icon)

                    # Install shortcuts
                    shortcut = kmap.get(connection, None)
                    if shortcut is not None:
                        qaction.setShortcut(shortcut)

                    # Items for toolbar
                    if connection in Pireal.TOOLBAR_ITEMS:
                        toolbar_items[connection] = qaction

                    # The name of QAction is the connection
                    Pireal.load_action(connection, qaction)
                    slot = getattr(obj, connection, None)
                    if isinstance(slot, Callable):
                        qaction.triggered.connect(slot)

        # Install toolbar
        self.__install_toolbar(toolbar_items)
        # Disable some actions
        self.set_enabled_db_actions(False)
        self.set_enabled_relation_actions(False)
        self.set_enabled_query_actions(False)
        self.set_enabled_editor_actions(False)

    def __install_toolbar(self, toolbar_items):
        for action in Pireal.TOOLBAR_ITEMS:
            qaction = toolbar_items.get(action, None)
            if qaction is not None:
                self.toolbar.addAction(qaction)
            else:
                self.toolbar.addSeparator()

    def __show_status_message(self, msg):
        status = Pireal.get_service("status")
        status.show_message(msg)

    def __on_thread_update_finished(self):
        self._thread.quit()
        # Clear notificator
        notification_widget = Pireal.get_service("notification")
        notification_widget.clear()

        msg = QMessageBox(self)
        if not self._updater.error:
            if self._updater.version:
                version = self._updater.version
                msg.setWindowTitle(self.tr("New version available!"))
                msg.setText(self.tr("Check the web site to "
                                    "download <b>Pireal {}</b>".format(
                                        version)))
                download_btn = msg.addButton(self.tr("Download!"),
                                             QMessageBox.YesRole)
                msg.addButton(self.tr("Cancel"),
                              QMessageBox.RejectRole)
                msg.exec_()
                r = msg.clickedButton()
                if r == download_btn:
                    webbrowser.open_new(
                        "http://centaurialpha.github.io/pireal")
        self._thread.deleteLater()
        self._updater.deleteLater()

    def change_title(self, title):
        self.setWindowTitle("Pireal " + '[' + title + ']')

    def set_enabled_db_actions(self, value):
        """ Public method. Enables or disables db QAction """

        actions = [
            'new_query',
            'open_query',
            'close_database',
            'save_database',
            'save_database_as',
            'load_relation'
        ]

        for action in actions:
            qaction = Pireal.get_action(action)
            qaction.setEnabled(value)

    def set_enabled_relation_actions(self, value):
        """ Public method. Enables or disables relation's QAction """

        actions = [
            'create_new_relation',
            'remove_relation',
            'edit_relation'
        ]

        for action in actions:
            qaction = Pireal.get_action(action)
            qaction.setEnabled(value)

    def set_enabled_query_actions(self, value):
        """ Public method. Enables or disables queries QAction """

        actions = [
            'execute_queries',
            'save_query'
        ]

        for action in actions:
            qaction = Pireal.get_action(action)
            qaction.setEnabled(value)

    def set_enabled_editor_actions(self, value):
        """ Public slot. Enables or disables editor actions """

        actions = [
            'undo_action',
            'redo_action',
            'copy_action',
            'cut_action',
            'paste_action',
            'zoom_in',
            'zoom_out',
            'comment',
            'uncomment'
        ]

        for action in actions:
            qaction = Pireal.get_action(action)
            qaction.setEnabled(value)

    def about_qt(self):
        """ Show about qt dialog """

        QMessageBox.aboutQt(self)

    def about_pireal(self):
        """ Show the bout Pireal dialog """

        from src.gui.dialogs import about_dialog
        dialog = about_dialog.AboutDialog(self)
        dialog.exec_()

    def report_issue(self):
        """ Open in the browser the page to create new  issue """

        webbrowser.open("http://github.com/centaurialpha/pireal/issues/new")

    def show_hide_menubar(self):
        """ Change visibility of menu bar """

        if self.menuBar().isVisible():
            self.menuBar().hide()
        else:
            self.menuBar().show()

    def show_hide_toolbar(self):
        """ Change visibility of tool bar """

        if self.toolbar.isVisible():
            self.toolbar.hide()
        else:
            self.toolbar.show()

    def show_error_message(self, text, syntax_error=True):
        self._msg_error_widget.show_msg(text, syntax_error)
        self._msg_error_widget.show()

    def closeEvent(self, event):
        qsettings = QSettings(settings.SETTINGS_PATH, QSettings.IniFormat)
        # Save window geometry
        if self.isMaximized():
            qsettings.setValue('window_max', True)
        else:
            qsettings.setValue('window_max', False)
            qsettings.setValue('window_pos', self.pos())
            qsettings.setValue('window_size', self.size())

        central_widget = Pireal.get_service("central")
        # Save recent databases
        qsettings.setValue('recent_databases',
                           central_widget.recent_databases)
        db = central_widget.get_active_db()
        if db is not None:
            # Save splitters size
            db.save_sizes()
            # Databases unsaved
            if db.modified:
                msg = QMessageBox(self)
                msg.setIcon(QMessageBox.Question)
                msg.setWindowTitle(self.tr("Some changes where not saved"))
                msg.setText(
                    self.tr("Do you want to save changes to the database?"))
                cancel_btn = msg.addButton(self.tr("Cancel"),
                                           QMessageBox.RejectRole)
                msg.addButton(self.tr("No"),
                              QMessageBox.NoRole)
                yes_btn = msg.addButton(self.tr("Yes"),
                                        QMessageBox.YesRole)
                msg.exec_()
                r = msg.clickedButton()
                if r == yes_btn:
                    central_widget.save_database()
                if r == cancel_btn:
                    event.ignore()
            # Query files
            unsaved_editors = central_widget.get_unsaved_queries()
            if unsaved_editors:
                msg = QMessageBox(self)
                msg.setIcon(QMessageBox.Question)
                msg.setWindowTitle(self.tr("Unsaved Queries"))
                text = '\n'.join([editor.name for editor in unsaved_editors])
                msg.setText(self.tr("{files}<br><br>Do you want to "
                                    "save them?".format(files=text)))
                cancel_btn = msg.addButton(self.tr("Cancel"),
                                           QMessageBox.RejectRole)
                msg.addButton(self.tr("No"),
                              QMessageBox.NoRole)
                yes_btn = msg.addButton(self.tr("Yes"),
                                        QMessageBox.YesRole)
                msg.exec_()
                if msg.clickedButton() == yes_btn:
                    for editor in unsaved_editors:
                        central_widget.save_query(editor)
                if msg.clickedButton() == cancel_btn:
                    event.ignore()
Ejemplo n.º 16
0
class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent, Qt.Window)
        self.setObjectName('main_window')
        self.params = PARAMS
        self.load_config()
        self.menubar = self.menuBar()
        self.root = QTabWidget(self)
        self.configurator = LoggerConfigurator(self.root)
        self.console = LoggerConsole(self.root)
        self.worker = None
        self.worker_thread = None
        self.init_ui()
        self.update_start_button()

    def init_ui(self):
        # Setting window geometry
        self.setWindowTitle('JIRA work logger')
        self.setWindowIcon(QIcon('gui/misc/clock-icon.ico'))

        # Setting menu bar
        app_menu = self.menubar.addMenu('Help')
        exit_action = QAction('Exit', self)
        exit_action.setShortcut('Ctrl+Q')
        exit_action.triggered.connect(qApp.quit)
        app_menu.addAction(exit_action)

        # Setting root frame
        self.root.addTab(self.configurator, 'Logger Setup')
        self.root.addTab(self.console, 'Logger Output')
        self.setCentralWidget(self.root)

    def setup_worker_thread(self):
        self.worker = LogWorker(self.params)
        self.worker_thread = QThread()
        self.worker.moveToThread(self.worker_thread)

        # Assign signals to slots
        self.worker.msg.connect(self.console.print_msg)
        self.worker.warn.connect(self.console.print_warn)
        self.worker.err.connect(self.console.print_err)
        self.worker_thread.started.connect(self.worker.execute_logging)
        self.worker_thread.finished.connect(self.stop_worker_thread)

    def execute_autologging(self):
        get_main_window().findChild(
            QWidget, 'main_buttons',
            Qt.FindChildrenRecursively).start_btn.setDisabled(True)
        self.read_params()
        self.setup_worker_thread()
        self.root.setCurrentIndex(1)
        qApp.processEvents()
        self.worker_thread.start()

    def stop_worker_thread(self):
        self.console.print_msg('Worker thread has been stopped')
        self.worker_thread.deleteLater()
        get_main_window().findChild(
            QWidget, 'main_buttons',
            Qt.FindChildrenRecursively).start_btn.setEnabled(True)
        qApp.processEvents()

    def update_start_button(self):
        self.read_params()
        start_btn = self.findChild(QWidget, 'main_buttons',
                                   Qt.FindChildrenRecursively).start_btn

        if not [param for param in MANDATORY_PARAMS if not self.params[param]]:
            if True in list(self.params['tasks_filter'].values()):
                start_btn.setEnabled(True)
                return

        start_btn.setDisabled(True)

    def read_params(self):
        """Reading params from widgets across Configurator"""
        # JIRA settings
        jira_widget = self.findChild(QWidget, 'jira_settings',
                                     Qt.FindChildrenRecursively)
        self.params['jira_host'] = jira_widget.host_ln.text()
        self.params['jira_user'] = jira_widget.user_ln.text()
        self.params['jira_pass'] = jira_widget.pass_ln.text()

        # Tasks filter settings
        tasks_filter_widget = self.findChild(QWidget, 'tasks_filter',
                                             Qt.FindChildrenRecursively)
        self.params['tasks_filter'][
            'user_assignee'] = tasks_filter_widget.is_assignee.isChecked()
        self.params['tasks_filter'][
            'user_validator'] = tasks_filter_widget.is_validator.isChecked()
        self.params['tasks_filter'][
            'user_creator'] = tasks_filter_widget.is_creator.isChecked()

        # Working days settings
        days_widget = self.findChild(QWidget, 'days_config',
                                     Qt.FindChildrenRecursively)
        self.params['work_days'] = days_widget.weekdays
        self.params['target_hrs'] = days_widget.target_hrs.value()
        self.params['daily_tasks'] = tasks_string_to_dict(
            days_widget.daily_tasks.text())
        self.params['tasks_comment'] = days_widget.tasks_comment.text()
        self.params['daily_only'] = days_widget.daily_only.isChecked()
        self.params['ignore_tasks'] = tasks_string_to_list(
            days_widget.ignore_tasks.text())

        # Date settings
        date_widget = self.findChild(QWidget, 'dates_selector',
                                     Qt.FindChildrenRecursively)
        self.params['from_date'] = date_widget.from_cal.selectedDate(
        ).toString(Qt.ISODate)
        self.params['to_date'] = date_widget.to_cal.selectedDate().toString(
            Qt.ISODate)

    def load_config(self):
        config_path = Path(CONFIG_FILE)

        if config_path.exists():
            self.params.update(
                yaml.load(config_path.read_text(), Loader=yaml.FullLoader))
            return