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()
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)
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
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)
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
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)
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, Å<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=['Å<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='#')
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()
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
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)
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()
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()
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()
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
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()
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