def get_metadata(self, gal=None): thread = QThread(self) thread.setObjectName("App.get_metadata") fetch_instance = fetch.Fetch() if gal: galleries = [gal] else: if gui_constants.CONTINUE_AUTO_METADATA_FETCHER: galleries = [g for g in self.manga_list_view.gallery_model._data if not g.exed] else: galleries = self.manga_list_view.gallery_model._data if not galleries: self.notification_bar.add_text("Looks like we've already gone through all galleries!") return None fetch_instance.galleries = galleries self.notification_bar.begin_show() fetch_instance.moveToThread(thread) def done(status): self.notification_bar.end_show() fetch_instance.deleteLater() fetch_instance.GALLERY_PICKER.connect(self._web_metadata_picker) fetch_instance.GALLERY_EMITTER.connect(self.manga_list_view.replace_edit_gallery) fetch_instance.AUTO_METADATA_PROGRESS.connect(self.notification_bar.add_text) thread.started.connect(fetch_instance.auto_web_metadata) fetch_instance.FINISHED.connect(done) thread.finished.connect(thread.deleteLater) thread.start()
def start_msg_thread(self, runningObjectParam, onRun = None, onDone = None, onErrorMsg = None, onSuccessMsg = None ): if self.running: return self.running = True thr = QThread() thrName = 'EndicataLabToolMainWnd start_msg_thread[%d]' % (len( self.threadPool ) + 1) print thrName thr.setObjectName( thrName ) self.threadPool.append( thr ) self.runningObject = runningObjectParam self.runningObject.moveToThread(thr) if onRun: thr.started.connect( onRun ) else: thr.started.connect( self.runningObject.run ) self.runningObject.finished.connect( self.onUnlock ) if onDone: self.runningObject.done.connect( onDone ) if onErrorMsg: self.runningObject.errorMsg.connect( onErrorMsg ) if onSuccessMsg: self.runningObject.successMsg.connect( onSuccessMsg ) thr.start()
def start_thread(self): # self.log.append('starting thread') self.button_start.setDisabled(True) self.button_stop.setEnabled(True) self.__worker_done = 0 self.__thread = [] # for idx in range(self.NUM_THREADS): idx = 0 worker = Worker(idx, self.args) thread = QThread() thread.setObjectName('thread_' + str(idx)) self.__thread.append( (thread, worker)) # need to store worker too otherwise will be gc'd worker.moveToThread(thread) # get progress messages from worker: worker.sig_step.connect(self.on_worker_step) worker.sig_done.connect(self.on_worker_done) # worker.sig_msg.connect(self.log.append) worker.sig_image.connect(self.on_image) # control worker: self.sig_abort_worker.connect(worker.abort) # get read to start worker: # self.sig_start.connect(worker.work) # needed due to PyCharm debugger bug (!); comment out next line thread.started.connect(worker.work) thread.start( ) # this will emit 'started' and start thread's event loop
def initFig(self, show_progress=True): """ Initial data load from Figshare :return: """ # Get the list of articles from collections = Collections(self.token) articles = collections.get_articles(self.collection_id) n_articles = len(articles) worker = ArticleLoadWorker(self.app, self.token, self.parent, self.collection_id, articles) thread = QThread() thread.setObjectName('thread_article_load') self.__threads.append((thread, worker)) worker.moveToThread(thread) worker.sig_step.connect(self.add_to_tree) worker.sig_done.connect(self.update_search_field) worker.sig_done.connect(self.enable_fields) thread.started.connect(worker.work) thread.start() self.articles = articles self.article_ids = set() for article in articles: self.article_ids.add(article['id'])
def start_threads(self): self.log.append('starting {} threads'.format(self.NUM_THREADS)) self.button_start_threads.setDisabled(True) self.button_stop_threads.setEnabled(True) self.__workers_done = 0 self.__threads = [] for idx in range(self.NUM_THREADS): worker = Worker(idx, app) thread = QThread(self) # remember self otherwise second run will fail thread.setObjectName('thread_' + str(idx)) self.__threads.append((thread, worker)) # need to store worker too otherwise will be gc'd worker.moveToThread(thread) # get progress messages from worker: worker.sig_step_demo.connect(self.on_worker_step) worker.sig_done_demo.connect(self.on_worker_done) worker.sig_msg_demo.connect(self.log.append) # control worker: self.sig_abort_workers.connect(worker.abort) # get read to start worker: # self.sig_start.connect(worker.work) # needed due to PyCharm debugger bug (!); comment out next line thread.started.connect(worker.work) thread.start() # this will emit 'started' and start thread's event loop
def startCom(self, comPort: str, baudRate: int): ''' Gestion des communications serie et des timers dans des threads distincts ''' # TODO : Ajout possibilité de connection réseau # Windows : pseudo serial over TCP driver => rien a faire # Linux : use socat : # sudo socat pty,link=/dev/ttyTCP0,raw tcp:127.0.0.1:1386 # comPort = "ttyTCP0" (si dans /dev) # attention aux droits d'accès ! # comPort = "/tmp/ttyTCP0" (chemin absolu) self.sig_debug.emit("grblCom.startCom(self, {}, {})".format( comPort, baudRate)) self.sig_log.emit( logSeverity.info.value, 'grblCom: Starting grblComSerial thread on {}.'.format(comPort)) newComSerial = grblComSerial(self.decode, comPort, baudRate, self.__pooling) thread = QThread() thread.setObjectName('grblComSerial') self.__threads.append( (thread, newComSerial)) # need to store worker too otherwise will be gc'd newComSerial.moveToThread(thread) # Connecte les signaux provenant du grblComSerial newComSerial.sig_log.connect(self.sig_log.emit) newComSerial.sig_connect.connect(self.on_sig_connect) newComSerial.sig_init.connect(self.on_sig_init) newComSerial.sig_ok.connect(self.sig_ok.emit) newComSerial.sig_error.connect(self.sig_error.emit) newComSerial.sig_alarm.connect(self.sig_alarm.emit) newComSerial.sig_status.connect(self.on_sig_status) newComSerial.sig_config.connect(self.sig_config.emit) newComSerial.sig_data.connect(self.sig_data.emit) newComSerial.sig_emit.connect(self.sig_emit.emit) newComSerial.sig_recu.connect(self.sig_recu.emit) newComSerial.sig_debug.connect(self.sig_debug.emit) # Signaux de pilotage a envoyer au thread self.sig_abort.connect(newComSerial.abort) self.sig_gcodeInsert.connect(newComSerial.gcodeInsert) self.sig_gcodePush.connect(newComSerial.gcodePush) self.sig_realTimePush.connect(newComSerial.realTimePush) self.sig_clearCom.connect(newComSerial.clearCom) self.sig_startPooling.connect(newComSerial.startPooling) self.sig_stopPooling.connect(newComSerial.stopPooling) self.sig_resetSerial.connect(newComSerial.resetSerial) # Start the thread... thread.started.connect(newComSerial.run) thread.start( ) # this will emit 'started' and start thread's event loop # Memorise le communicateur self.__Com = newComSerial
class AsyncCharacterQueries(QObject): notify_update = pyqtSignal() finished = pyqtSignal() def __init__(self, shared_queue: Queue, shared_dict: dict): QObject.__init__(self) self.esi_core = None self.query_engine = None self.query_list = shared_queue self.shared_dict = shared_dict self.objThread = QThread() self.running = True # self.setDaemon(True) @pyqtSlot() def work(self) -> None: """ :return: """ logging.info("Character query service started.") self.esi_core = EsiCore() self.query_engine = EsiQuery(self.esi_core) try: while self.running: char_id = self.query_list.get(block=True) response = self.query_engine.get_character_public_information( char_id) self.shared_dict[char_id] = response self.notify_update.emit() finally: logging.warning("Character name query service terminated.") @pyqtSlot() def exit(self): logging.info("Character query service has exited.") def start(self): """ Convenience function starting the thread for the ASync character name service :return: """ self.moveToThread(self.objThread) self.finished.connect(self.objThread.quit) self.objThread.started.connect(self.work) self.objThread.finished.connect(self.exit) self.objThread.setObjectName("AsyncCharacterThread") self.objThread.start() def abort(self): self.sig_msg.emit('Worker #{} notified to abort'.format(self.__id)) self.__abort = True
class MyWidget(QWidget): NUM_THREADS = 5 sig_start = pyqtSignal() # needed only due to PyCharm debugger bug (!) sig_abort_workers = pyqtSignal() def __init__(self): super().__init__() self.setWindowTitle("Thread Example") form_layout = QVBoxLayout() self.setLayout(form_layout) self.resize(400, 800) self.button_start_threads = QPushButton() self.button_start_threads.clicked.connect(self.start_threads) self.button_start_threads.setText("Start thread") form_layout.addWidget(self.button_start_threads) self.log = QTextEdit() form_layout.addWidget(self.log) QThread.currentThread().setObjectName( 'main') # threads can be named, useful for log output self.__workers_done = None self.__threads = None def start_threads(self): self.log.append('Starting thread') self.button_start_threads.setDisabled(True) # self.button_stop_threads.setEnabled(True) self.worker = Worker() self.thread = QThread() self.thread.setObjectName('thread_') # self.__threads.append((thread, worker)) # need to store worker too otherwise will be gc'd self.worker.moveToThread(self.thread) # get progress messages from worker: self.worker.sig_done.connect(self.on_worker_done) self.worker.sig_msg.connect(self.log.append) # thread.started.connect(worker.work) # control worker: # get read to start worker: self.sig_start.connect( self.worker.work ) # needed due to PyCharm debugger bug (!); comment out next line self.thread.start( ) # this will emit 'started' and start thread's event loop self.sig_start.emit() # needed due to PyCharm debugger bug (!)''' @pyqtSlot() def on_worker_done(self): self.log.append("Work done")
def add_gallery(gallery_list): def append_to_model(x): self.manga_list_view.gallery_model.insertRows(x, None, len(x)) class A(QObject): done = pyqtSignal() prog = pyqtSignal(int) def __init__(self, obj, parent=None): super().__init__(parent) self.obj = obj self.galleries = [] def add_to_db(self): gui_constants.NOTIF_BAR.begin_show() gui_constants.NOTIF_BAR.add_text("Populating database...") for y, x in enumerate(self.obj): gui_constants.NOTIF_BAR.add_text( "Populating database {}/{}".format(y + 1, len(self.obj)) ) gallerydb.add_method_queue(gallerydb.GalleryDB.add_gallery_return, False, x) self.galleries.append(x) y += 1 self.prog.emit(y) append_to_model(self.galleries) gui_constants.NOTIF_BAR.end_show() self.done.emit() loading.progress.setMaximum(len(gallery_list)) a_instance = A(gallery_list) thread = QThread(self) thread.setObjectName("Database populate") def loading_show(): loading.setText("Populating database.\nPlease wait...") loading.show() def loading_hide(): loading.close() self.manga_list_view.gallery_model.ROWCOUNT_CHANGE.emit() def del_later(): try: a_instance.deleteLater() except NameError: pass a_instance.moveToThread(thread) a_instance.prog.connect(loading.progress.setValue) thread.started.connect(loading_show) thread.started.connect(a_instance.add_to_db) a_instance.done.connect(loading_hide) a_instance.done.connect(del_later) thread.finished.connect(thread.deleteLater) thread.start()
class MainWindow(QWidget): def __init__(self): super(MainWindow, self).__init__() uic.loadUi(qtCreatorFile, self) self.threads = [] mainThreadID = int(QThread.currentThread().currentThreadId()) print("MainThread ID: {}".format(mainThreadID)) QThread.currentThread().setObjectName("Thread_Main") print("MainThread name is: {}".format( QThread.currentThread().objectName())) self.pb1.clicked.connect(self.pb1Action) self.pb1Object = UpdateLogger(2) self.pb1Thread = QThread() self.pb1Thread.setObjectName("Thread_pb1") self.threads.append(self.pb1Thread) self.pb1Object.moveToThread(self.pb1Thread) self.pb1Object.finished.connect(self.updateLogger) self.pb1Object.update.connect(self.updateLogger) self.pb1Thread.started.connect(self.pb1Object.running) self.pb2.clicked.connect(self.pb2Action) self.pb2Object = UpdateLogger(3) self.pb2Thread = QThread() self.pb2Thread.setObjectName("Thread_pb2") self.threads.append(self.pb2Thread) self.pb2Object.moveToThread(self.pb2Thread) print(self.pb2Object.threadName) self.pb2Object.finished.connect(self.updateLogger) self.pb2Object.update.connect(self.updateLogger) self.pb2Thread.started.connect(self.pb2Object.running) for t in self.threads: print(t.objectName()) self.pb3.clicked.connect(self.pb3Action) def pb1Action(self): self.pb1Thread.start() self.pb1.setEnabled(False) def pb2Action(self): self.pb2Thread.start() self.pb2.setEnabled(False) def pb3Action(self): self.tbLog.append(self.le3.text()) @pyqtSlot(str) def updateLogger(self, txt="??????"): self.tbLog.append(txt)
def start_up(self): def normalize_first_time(): settings.set(2, "Application", "first time level") def done(): self.manga_list_view.gallery_model.init_data() if gui_constants.ENABLE_MONITOR and gui_constants.MONITOR_PATHS and all(gui_constants.MONITOR_PATHS): self.init_watchers() if gui_constants.FIRST_TIME_LEVEL != 2: normalize_first_time() if gui_constants.FIRST_TIME_LEVEL < 2: class FirstTime(file_misc.BasePopup): def __init__(self, parent=None): super().__init__(parent) main_layout = QVBoxLayout() info_lbl = QLabel( "Hi there! Some big changes are about to occur!\n" + "Please wait.. This will take at most a few minutes.\n" + "If not then try restarting the application." ) info_lbl.setAlignment(Qt.AlignCenter) main_layout.addWidget(info_lbl) prog = QProgressBar(self) prog.setMinimum(0) prog.setMaximum(0) prog.setTextVisible(False) main_layout.addWidget(prog) main_layout.addWidget(QLabel("Note: This popup will close itself when everything is ready")) self.main_widget.setLayout(main_layout) ft_widget = FirstTime(self) log_i("Invoking first time level 2") bridge = gallerydb.Bridge() thread = QThread(self) thread.setObjectName("Startup") bridge.moveToThread(thread) thread.started.connect(bridge.rebuild_galleries) bridge.DONE.connect(ft_widget.close) bridge.DONE.connect(self.setEnabled) bridge.DONE.connect(done) bridge.DONE.connect(bridge.deleteLater) thread.finished.connect(thread.deleteLater) thread.start() ft_widget.adjustSize() ft_widget.show() self.setEnabled(False) else: done()
def web_metadata(self, url, btn_widget, pgr_widget): self.link_lbl.setText(url) f = fetch.Fetch() thread = QThread(self) thread.setObjectName('Gallerydialog web metadata') btn_widget.hide() pgr_widget.show() def status(stat): def do_hide(): try: pgr_widget.hide() btn_widget.show() except RuntimeError: pass if stat: do_hide() else: danger = """QProgressBar::chunk { background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0,stop: 0 #FF0350,stop: 0.4999 #FF0020,stop: 0.5 #FF0019,stop: 1 #FF0000 ); border-bottom-right-radius: 5px; border-bottom-left-radius: 5px; border: .px solid black;}""" pgr_widget.setStyleSheet(danger) QTimer.singleShot(3000, do_hide) f.deleteLater() def gallery_picker(gallery, title_url_list, q): self.parent_widget._web_metadata_picker(gallery, title_url_list, q, self) try: dummy_gallery = self.make_gallery(self.gallery) except AttributeError: dummy_gallery = self.make_gallery(gallerydb.Gallery(), False) if not dummy_gallery: status(False) return None dummy_gallery.link = url f.galleries = [dummy_gallery] f.moveToThread(thread) f.GALLERY_PICKER.connect(gallery_picker) f.GALLERY_EMITTER.connect(self.set_web_metadata) thread.started.connect(f.auto_web_metadata) thread.finished.connect(thread.deleteLater) f.FINISHED.connect(status) thread.start()
def add_gallery(gallery_list): def append_to_model(x): self.manga_list_view.sort_model.insertRows(x, None, len(x)) self.manga_list_view.sort_model.init_search( self.manga_list_view.sort_model.current_term) class A(QObject): done = pyqtSignal() prog = pyqtSignal(int) def __init__(self, obj, parent=None): super().__init__(parent) self.obj = obj self.galleries = [] def add_to_db(self): for y, x in enumerate(self.obj): gallerydb.add_method_queue( gallerydb.GalleryDB.add_gallery_return, False, x) self.galleries.append(x) y += 1 self.prog.emit(y) append_to_model(self.galleries) self.done.emit() loading.progress.setMaximum(len(gallery_list)) self.a_instance = A(gallery_list) thread = QThread(self) thread.setObjectName('Database populate') def loading_show(numb): if loading.isHidden(): loading.show() loading.setText('Populating database ({}/{})\nPlease wait...'.format( numb, loading.progress.maximum())) loading.progress.setValue(numb) loading.show() def loading_hide(): loading.close() self.manga_list_view.gallery_model.ROWCOUNT_CHANGE.emit() self.a_instance.moveToThread(thread) self.a_instance.prog.connect(loading_show) thread.started.connect(self.a_instance.add_to_db) self.a_instance.done.connect(loading_hide) self.a_instance.done.connect(self.a_instance.deleteLater) #a_instance.add_to_db() thread.finished.connect(thread.deleteLater) thread.start()
def startCom(self, comPort: str, baudRate: int): ''' Gestion des communications serie et des timers dans des threads distincts ''' self.sig_debug.emit("grblCom.startCom(self, {}, {})".format(comPort, baudRate)) self.sig_log.emit(logSeverity.info.value, 'grblCom: Starting grblComSerial thread.') newComSerial = grblComSerial(comPort, baudRate, self.__pooling) thread = QThread() thread.setObjectName('grblComSerial') self.__threads.append((thread, newComSerial)) # need to store worker too otherwise will be gc'd newComSerial.moveToThread(thread) # Connecte les signaux provenant du grblComSerial newComSerial.sig_log.connect(self.sig_log.emit) newComSerial.sig_connect.connect(self.on_sig_connect) newComSerial.sig_init.connect(self.on_sig_init) newComSerial.sig_ok.connect(self.sig_ok.emit) newComSerial.sig_error.connect(self.sig_error.emit) newComSerial.sig_alarm.connect(self.sig_alarm.emit) newComSerial.sig_status.connect(self.on_sig_status) newComSerial.sig_config.connect(self.sig_config.emit) newComSerial.sig_data.connect(self.sig_data.emit) newComSerial.sig_emit.connect(self.sig_emit.emit) newComSerial.sig_recu.connect(self.sig_recu.emit) newComSerial.sig_debug.connect(self.sig_debug.emit) # Signaux de pilotage a envoyer au thread self.sig_abort.connect(newComSerial.abort) self.sig_gcodeInsert.connect(newComSerial.gcodeInsert) self.sig_gcodePush.connect(newComSerial.gcodePush) self.sig_realTimePush.connect(newComSerial.realTimePush) self.sig_clearCom.connect(newComSerial.clearCom) self.sig_startPooling.connect(newComSerial.startPooling) self.sig_stopPooling.connect(newComSerial.stopPooling) # Start the thread... thread.started.connect(newComSerial.run) thread.start() # this will emit 'started' and start thread's event loop # Memorise le communicateur self.__Com = newComSerial
def _setup_worker_thread(self) -> typing.Tuple[ModelWorker, QThread]: """ Create the worker thread used to perform long running operations in the background. """ worker = ModelWorker(self) worker_thread = QThread(self) worker_thread.setObjectName("AsynchronousImageWorker") worker.moveToThread(worker_thread) logger.debug("Created worker thread.") self.open_command_line_given_images.connect( worker.open_command_line_given_images) self.open_images.connect(worker.open_images) self.save_and_close_all_images.connect( worker.save_and_close_all_images) self.close_image.connect(worker.close_image) logger.debug("Connected signals to offload to the worker thread.") worker_thread.start() return worker, worker_thread
def button_start_action(self): """Slot for buttonStart clicked signal""" self.progressBar.setRange(0, 0) self.buttonStart.setEnabled(False) self.buttonClose.setEnabled(False) worker = Worker(20, self.__app) thread = QThread(self) thread.setObjectName("products_http") self.__threads.append((thread, worker)) worker.moveToThread(thread) worker.sig_status.connect(self.on_status) worker.sig_done.connect(self.on_done) try: thread.started.connect( worker.import_products_http(self.products, self.settings)) thread.start() except TypeError as t: if DBG: printit( " ->products -> http\n ->exception handled: {}".format(t))
def start_workers(self): self.start_btn.setDisabled(True) self.stop_btn.setEnabled(True) self.txt_print.clear() self.txt_print.append("识别中..") self.__workers_done = 0 self.__threads = [] self.thread_ID=["txt_print","Camera_label","wait_label"] for idx in range(len(self.thread_ID)): worker = Worker(idx) thread = QThread() thread.setObjectName(self.thread_ID[idx]) self.__threads.append((thread,worker)) worker.moveToThread(thread) worker.sig_working.connect(self.on_woker_working) worker.sig_finished.connect(self.on_worker_finished) self.sig_abort_workers.connect(worker.abort) thread.started.connect(worker.work) thread.start()
def startThreads(self): global conversion_serial self.indicateThreadsRunning(True) self.setStatusBar("Working...") if self.queue.empty(): self.setStatusBar( "{} Files converted. You can add more, or exit.".format( self.convertedCount ) ) self.indicateThreadsRunning(False) # self.setStatusBar("Ready.") try: while (not self.queue.empty()) and (len(self.working) < self.NUM_THREADS): info = self.queue.get_nowait() # self.addResponse('Starting conversion for "{}".'.format(info['basename'])) conversion_serial += 1 worker_id = conversion_serial worker = ConversionWorker(worker_id, info) thread = QThread() thread.setObjectName("{}".format(id)) self.working[worker_id] = ( info, worker, thread, id, ) # need to store worker and thread too otherwise will be gc'd worker.moveToThread(thread) # get progress messages from worker: worker.sig_done.connect(self.onWorkerDone) worker.sig_msg.connect(self.addResponse) # in case we need to stop the worker: self.sig_abort_workers.connect(worker.abort) # start the worker: thread.started.connect(worker.doConversion) thread.start() # this will emit 'started' and start thread's event loop except queue.Empty: pass # When the queue is empty, we just stop creating threads.
def start_workers(self): self.txt_log.append('Starting {} threads'.format(self.NUM_THREADS)) self.btn_start.setDisabled(True) self.btn_stop.setEnabled(True) self.__workers_done = 0 self.__threads = [] for idx in range(self.NUM_THREADS): # 启动3个线程 worker = Worker(idx) thread = QThread() thread.setObjectName('thread_' + str(idx)) self.__threads.append((thread, worker)) worker.moveToThread(thread) worker.sig_working.connect(self.on_worker_working) worker.sig_finished.connect(self.on_worker_finished) worker.sig_output.connect(self.txt_log.append) self.sig_abort_workers.connect(worker.abort) thread.started.connect(worker.work) thread.start()
def on_open_selection_clicked(self): """ Called when the open selection button is clicked. Either open or closes the metadata window :return: """ # Retrieve a set of file paths file_paths = self.get_selection_set() # Locally reference the Article List Widget article_tree = self.parent.data_articles_window.article_tree # Prevent the user from editting, searching, etc. while new articles are created. article_tree.disable_fields() # Create an article creation worker worker = ArticleCreationWorker(self.token, self.parent, file_paths) # Create the thread. load_articles_thread = QThread() load_articles_thread.setObjectName('local_articles_thread') self.__threads.append((load_articles_thread, worker)) # Add the worker to the thread worker.moveToThread(load_articles_thread) # Connect signals from worker worker.sig_step.connect(article_tree.add_to_tree) worker.sig_step.connect( lambda article_id: article_tree.add_to_articles(article_id)) worker.sig_done.connect(article_tree.enable_fields) worker.sig_done.connect(article_tree.update_search_field) worker.sig_done.connect(self.parent.data_articles_window.check_edit) load_articles_thread.started.connect(worker.work) # Begin worker thread load_articles_thread.start()
def button_start_action(self): """Slot for buttonStart clicked signal""" self.progressBar.setRange(0, 0) worker = Worker(10, self.__app) thread = QThread(self) thread.setObjectName("customers_http") self.__threads.append((thread, worker)) worker.moveToThread(thread) worker.sig_status.connect(self.on_status) worker.sig_done.connect(self.on_done) try: """ customers object is used by the worker to insert data into customer table employees object is used to fetch the customer file settings object is used to check access """ thread.started.connect( worker.import_customers_http(self.customers, self.employees, self.settings)) thread.start() except TypeError as t: if DBG: printit( " ->customers -> http\n ->exception handled: {}".format(t))
class GalleryDialog(QWidget): """ A window for adding/modifying gallery. Pass a list of QModelIndexes to edit their data or pass a path to preset path """ def __init__(self, parent, arg=None): super().__init__(parent, Qt.Dialog) self.setAttribute(Qt.WA_DeleteOnClose) self.setAutoFillBackground(True) self.parent_widget = parent m_l = QVBoxLayout() self.main_layout = QVBoxLayout() dummy = QWidget(self) scroll_area = QScrollArea(self) scroll_area.setWidgetResizable(True) scroll_area.setFrameStyle(scroll_area.StyledPanel) dummy.setLayout(self.main_layout) scroll_area.setWidget(dummy) m_l.addWidget(scroll_area, 3) final_buttons = QHBoxLayout() final_buttons.setAlignment(Qt.AlignRight) m_l.addLayout(final_buttons) self.done = QPushButton("Done") self.done.setDefault(True) cancel = QPushButton("Cancel") final_buttons.addWidget(cancel) final_buttons.addWidget(self.done) self._multiple_galleries = False self._edit_galleries = [] def new_gallery(): self.setWindowTitle('Add a new gallery') self.newUI() self.commonUI() self.done.clicked.connect(self.accept) cancel.clicked.connect(self.reject) if arg: if isinstance(arg, (list, gallerydb.Gallery)): if isinstance(arg, gallerydb.Gallery): self.setWindowTitle('Edit gallery') self._edit_galleries.append(arg) else: self.setWindowTitle('Edit {} galleries'.format(len(arg))) self._multiple_galleries = True self._edit_galleries.extend(arg) self.commonUI() self.setGallery(arg) self.done.clicked.connect(self.accept_edit) cancel.clicked.connect(self.reject_edit) elif isinstance(arg, str): new_gallery() self.choose_dir(arg) else: new_gallery() log_d('GalleryDialog: Create UI: successful') self.setLayout(m_l) if self._multiple_galleries: self.resize(500, 480) else: self.resize(500, 600) frect = self.frameGeometry() frect.moveCenter(QDesktopWidget().availableGeometry().center()) self.move(frect.topLeft()) self._fetch_inst = fetch.Fetch() self._fetch_thread = QThread(self) self._fetch_thread.setObjectName("GalleryDialog metadata thread") self._fetch_inst.moveToThread(self._fetch_thread) self._fetch_thread.started.connect(self._fetch_inst.auto_web_metadata) def commonUI(self): if not self._multiple_galleries: f_web = QGroupBox("Metadata from the Web") f_web.setCheckable(False) self.main_layout.addWidget(f_web) web_main_layout = QVBoxLayout() web_info = misc.ClickedLabel( "Which gallery URLs are supported? (hover)", parent=self) web_info.setToolTip(app_constants.SUPPORTED_METADATA_URLS) web_info.setToolTipDuration(999999999) web_main_layout.addWidget(web_info) web_layout = QHBoxLayout() web_main_layout.addLayout(web_layout) f_web.setLayout(web_main_layout) def basic_web(name): return QLabel(name), QLineEdit(), QPushButton( "Get metadata"), QProgressBar() url_lbl, self.url_edit, url_btn, url_prog = basic_web("URL:") url_btn.clicked.connect(lambda: self.web_metadata( self.url_edit.text(), url_btn, url_prog)) url_prog.setTextVisible(False) url_prog.setMinimum(0) url_prog.setMaximum(0) web_layout.addWidget(url_lbl, 0, Qt.AlignLeft) web_layout.addWidget(self.url_edit, 0) web_layout.addWidget(url_btn, 0, Qt.AlignRight) web_layout.addWidget(url_prog, 0, Qt.AlignRight) self.url_edit.setPlaceholderText( "Insert supported gallery URLs or just press the button!") url_prog.hide() f_gallery = QGroupBox("Gallery Info") f_gallery.setCheckable(False) self.main_layout.addWidget(f_gallery) gallery_layout = QFormLayout() f_gallery.setLayout(gallery_layout) def checkbox_layout(widget): if self._multiple_galleries: l = QHBoxLayout() l.addWidget(widget.g_check) widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) l.addWidget(widget) return l else: widget.g_check.setChecked(True) widget.g_check.hide() return widget def add_check(widget): widget.g_check = QCheckBox(self) return widget self.title_edit = add_check(QLineEdit()) self.author_edit = add_check(QLineEdit()) author_completer = misc.GCompleter(self, False, True, False) author_completer.setCaseSensitivity(Qt.CaseInsensitive) self.author_edit.setCompleter(author_completer) self.descr_edit = add_check(QTextEdit()) self.descr_edit.setAcceptRichText(True) self.lang_box = add_check(QComboBox()) self.lang_box.addItems(app_constants.G_LANGUAGES) self.lang_box.addItems(app_constants.G_CUSTOM_LANGUAGES) self.rating_box = add_check(QSpinBox()) self.rating_box.setMaximum(5) self.rating_box.setMinimum(0) self._find_combobox_match(self.lang_box, app_constants.G_DEF_LANGUAGE, 0) tags_l = QVBoxLayout() tag_info = misc.ClickedLabel( "How do i write namespace & tags? (hover)", parent=self) tag_info.setToolTip( "Ways to write tags:\n\nNormal tags:\ntag1, tag2, tag3\n\n" + "Namespaced tags:\nns1:tag1, ns1:tag2\n\nNamespaced tags with one or more" + " tags under same namespace:\nns1:[tag1, tag2, tag3], ns2:[tag1, tag2]\n\n" + "Those three ways of writing namespace & tags can be combined freely.\n" + "Tags are seperated by a comma, NOT whitespace.\nNamespaces will be capitalized while tags" + " will be lowercased.") tag_info.setToolTipDuration(99999999) tags_l.addWidget(tag_info) self.tags_edit = add_check(misc.CompleterTextEdit()) self.tags_edit.setCompleter(misc.GCompleter(self, False, False)) self.tags_append = QCheckBox("Append tags", self) self.tags_append.setChecked(False) if not self._multiple_galleries: self.tags_append.hide() if self._multiple_galleries: self.tags_append.setChecked(app_constants.APPEND_TAGS_GALLERIES) tags_ml = QVBoxLayout() tags_ml.addWidget(self.tags_append) tags_ml.addLayout(checkbox_layout(self.tags_edit), 5) tags_l.addLayout(tags_ml, 3) else: tags_l.addWidget(checkbox_layout(self.tags_edit), 5) self.tags_edit.setPlaceholderText( "Press Tab to autocomplete (Ctrl + E to show popup)") self.type_box = add_check(QComboBox()) self.type_box.addItems(app_constants.G_TYPES) self._find_combobox_match(self.type_box, app_constants.G_DEF_TYPE, 0) #self.type_box.currentIndexChanged[int].connect(self.doujin_show) #self.doujin_parent = QLineEdit() #self.doujin_parent.setVisible(False) self.status_box = add_check(QComboBox()) self.status_box.addItems(app_constants.G_STATUS) self._find_combobox_match(self.status_box, app_constants.G_DEF_STATUS, 0) self.pub_edit = add_check(QDateEdit()) self.pub_edit.setCalendarPopup(True) self.pub_edit.setDate(QDate.currentDate()) self.path_lbl = misc.ClickedLabel("") self.path_lbl.setWordWrap(True) self.path_lbl.clicked.connect(lambda a: utils.open_path(a, a) if a else None) link_layout = QHBoxLayout() self.link_lbl = add_check(QLabel("")) self.link_lbl.setWordWrap(True) self.link_edit = QLineEdit() link_layout.addWidget(self.link_edit) if self._multiple_galleries: link_layout.addLayout(checkbox_layout(self.link_lbl)) else: link_layout.addWidget(checkbox_layout(self.link_lbl)) self.link_edit.hide() self.link_btn = QPushButton("Modify") self.link_btn.setFixedWidth(50) self.link_btn2 = QPushButton("Set") self.link_btn2.setFixedWidth(40) self.link_btn.clicked.connect(self.link_modify) self.link_btn2.clicked.connect(self.link_set) link_layout.addWidget(self.link_btn) link_layout.addWidget(self.link_btn2) self.link_btn2.hide() rating_ = checkbox_layout(self.rating_box) lang_ = checkbox_layout(self.lang_box) if self._multiple_galleries: rating_.insertWidget(0, QLabel("Rating:")) lang_.addLayout(rating_) lang_l = lang_ else: lang_l = QHBoxLayout() lang_l.addWidget(lang_) lang_l.addWidget(QLabel("Rating:"), 0, Qt.AlignRight) lang_l.addWidget(rating_) gallery_layout.addRow("Title:", checkbox_layout(self.title_edit)) gallery_layout.addRow("Author:", checkbox_layout(self.author_edit)) gallery_layout.addRow("Description:", checkbox_layout(self.descr_edit)) gallery_layout.addRow("Language:", lang_l) gallery_layout.addRow("Tags:", tags_l) gallery_layout.addRow("Type:", checkbox_layout(self.type_box)) gallery_layout.addRow("Status:", checkbox_layout(self.status_box)) gallery_layout.addRow("Publication Date:", checkbox_layout(self.pub_edit)) gallery_layout.addRow("Path:", self.path_lbl) gallery_layout.addRow("URL:", link_layout) self.title_edit.setFocus() def resizeEvent(self, event): self.tags_edit.setFixedHeight(event.size().height() // 8) self.descr_edit.setFixedHeight(event.size().height() // 12.5) return super().resizeEvent(event) def _find_combobox_match(self, combobox, key, default): f_index = combobox.findText(key, Qt.MatchFixedString) if f_index != -1: combobox.setCurrentIndex(f_index) return True else: combobox.setCurrentIndex(default) return False def setGallery(self, gallery): "To be used for when editing a gallery" if isinstance(gallery, gallerydb.Gallery): self.gallery = gallery if not self._multiple_galleries: self.url_edit.setText(gallery.link) self.title_edit.setText(gallery.title) self.author_edit.setText(gallery.artist) self.descr_edit.setText(gallery.info) self.rating_box.setValue(gallery.rating) self.tags_edit.setText(utils.tag_to_string(gallery.tags)) if not self._find_combobox_match(self.lang_box, gallery.language, 1): self._find_combobox_match(self.lang_box, app_constants.G_DEF_LANGUAGE, 1) if not self._find_combobox_match(self.type_box, gallery.type, 0): self._find_combobox_match(self.type_box, app_constants.G_DEF_TYPE, 0) if not self._find_combobox_match(self.status_box, gallery.status, 0): self._find_combobox_match(self.status_box, app_constants.G_DEF_STATUS, 0) gallery_pub_date = "{}".format(gallery.pub_date).split(' ') try: self.gallery_time = datetime.strptime(gallery_pub_date[1], '%H:%M:%S').time() except IndexError: pass qdate_pub_date = QDate.fromString(gallery_pub_date[0], "yyyy-MM-dd") self.pub_edit.setDate(qdate_pub_date) self.link_lbl.setText(gallery.link) self.path_lbl.setText(gallery.path) elif isinstance(gallery, list): g = gallery[0] if all(map(lambda x: x.title == g.title, gallery)): self.title_edit.setText(g.title) self.title_edit.g_check.setChecked(True) if all(map(lambda x: x.artist == g.artist, gallery)): self.author_edit.setText(g.artist) self.author_edit.g_check.setChecked(True) if all(map(lambda x: x.info == g.info, gallery)): self.descr_edit.setText(g.info) self.descr_edit.g_check.setChecked(True) if all(map(lambda x: x.tags == g.tags, gallery)): self.tags_edit.setText(utils.tag_to_string(g.tags)) self.tags_edit.g_check.setChecked(True) if all(map(lambda x: x.language == g.language, gallery)): if not self._find_combobox_match(self.lang_box, g.language, 1): self._find_combobox_match(self.lang_box, app_constants.G_DEF_LANGUAGE, 1) self.lang_box.g_check.setChecked(True) if all(map(lambda x: x.rating == g.rating, gallery)): self.rating_box.setValue(g.rating) self.rating_box.g_check.setChecked(True) if all(map(lambda x: x.type == g.type, gallery)): if not self._find_combobox_match(self.type_box, g.type, 0): self._find_combobox_match(self.type_box, app_constants.G_DEF_TYPE, 0) self.type_box.g_check.setChecked(True) if all(map(lambda x: x.status == g.status, gallery)): if not self._find_combobox_match(self.status_box, g.status, 0): self._find_combobox_match(self.status_box, app_constants.G_DEF_STATUS, 0) self.status_box.g_check.setChecked(True) if all(map(lambda x: x.pub_date == g.pub_date, gallery)): gallery_pub_date = "{}".format(g.pub_date).split(' ') try: self.gallery_time = datetime.strptime( gallery_pub_date[1], '%H:%M:%S').time() except IndexError: pass qdate_pub_date = QDate.fromString(gallery_pub_date[0], "yyyy-MM-dd") self.pub_edit.setDate(qdate_pub_date) self.pub_edit.g_check.setChecked(True) if all(map(lambda x: x.link == g.link, gallery)): self.link_lbl.setText(g.link) self.link_lbl.g_check.setChecked(True) def newUI(self): f_local = QGroupBox("Directory/Archive") f_local.setCheckable(False) self.main_layout.addWidget(f_local) local_layout = QHBoxLayout() f_local.setLayout(local_layout) choose_folder = QPushButton("From Directory") choose_folder.clicked.connect(lambda: self.choose_dir('f')) local_layout.addWidget(choose_folder) choose_archive = QPushButton("From Archive") choose_archive.clicked.connect(lambda: self.choose_dir('a')) local_layout.addWidget(choose_archive) self.file_exists_lbl = QLabel() local_layout.addWidget(self.file_exists_lbl) self.file_exists_lbl.hide() def choose_dir(self, mode): """ Pass which mode to open the folder explorer in: 'f': directory 'a': files Or pass a predefined path """ self.done.show() self.file_exists_lbl.hide() if mode == 'a': name = QFileDialog.getOpenFileName(self, 'Choose archive', filter=utils.FILE_FILTER) name = name[0] elif mode == 'f': name = QFileDialog.getExistingDirectory(self, 'Choose folder') elif mode: if os.path.exists(mode): name = mode else: return None if not name: return head, tail = os.path.split(name) name = os.path.join(head, tail) parsed = utils.title_parser(tail) self.title_edit.setText(parsed['title']) self.author_edit.setText(parsed['artist']) self.path_lbl.setText(name) if not parsed['language']: parsed['language'] = app_constants.G_DEF_LANGUAGE l_i = self.lang_box.findText(parsed['language']) if l_i != -1: self.lang_box.setCurrentIndex(l_i) if gallerydb.GalleryDB.check_exists(name): self.file_exists_lbl.setText( '<font color="red">Gallery already exists.</font>') self.file_exists_lbl.show() # check galleries gs = 1 if name.endswith(utils.ARCHIVE_FILES): gs = len(utils.check_archive(name)) elif os.path.isdir(name): g_dirs, g_archs = utils.recursive_gallery_check(name) gs = len(g_dirs) + len(g_archs) if gs == 0: self.file_exists_lbl.setText( '<font color="red">Invalid gallery source.</font>') self.file_exists_lbl.show() self.done.hide() if app_constants.SUBFOLDER_AS_GALLERY: if gs > 1: self.file_exists_lbl.setText( '<font color="red">More than one galleries detected in source! Use other methods to add.</font>' ) self.file_exists_lbl.show() self.done.hide() def check(self): if not self._multiple_galleries: if len(self.title_edit.text()) is 0: self.title_edit.setFocus() self.title_edit.setStyleSheet( "border-style:outset;border-width:2px;border-color:red;") return False elif len(self.author_edit.text()) is 0: self.author_edit.setText("Unknown") if len(self.path_lbl.text()) == 0 or self.path_lbl.text( ) == 'No path specified': self.path_lbl.setStyleSheet("color:red") self.path_lbl.setText('No path specified') return False return True def reject(self): if self.check(): msgbox = QMessageBox() msgbox.setText( "<font color='red'><b>Noo oniichan! You were about to add a new gallery.</b></font>" ) msgbox.setInformativeText("Do you really want to discard?") msgbox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) msgbox.setDefaultButton(QMessageBox.No) if msgbox.exec() == QMessageBox.Yes: self.close() else: self.close() def web_metadata(self, url, btn_widget, pgr_widget): if not self.path_lbl.text(): return self.link_lbl.setText(url) btn_widget.hide() pgr_widget.show() def status(stat): def do_hide(): try: pgr_widget.hide() btn_widget.show() except RuntimeError: pass if stat: do_hide() else: danger = """QProgressBar::chunk { background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0,stop: 0 #FF0350,stop: 0.4999 #FF0020,stop: 0.5 #FF0019,stop: 1 #FF0000 ); border-bottom-right-radius: 5px; border-bottom-left-radius: 5px; border: .px solid black;}""" pgr_widget.setStyleSheet(danger) QTimer.singleShot(3000, do_hide) def gallery_picker(gallery, title_url_list, q): self.parent_widget._web_metadata_picker(gallery, title_url_list, q, self) try: dummy_gallery = self.make_gallery(self.gallery, False) except AttributeError: dummy_gallery = self.make_gallery(gallerydb.Gallery(), False, True) if not dummy_gallery: status(False) return None dummy_gallery._g_dialog_url = url self._fetch_inst.galleries = [dummy_gallery] self._disconnect() self._fetch_inst.GALLERY_PICKER.connect(gallery_picker) self._fetch_inst.GALLERY_EMITTER.connect(self.set_web_metadata) self._fetch_inst.FINISHED.connect(status) self._fetch_thread.start() def set_web_metadata(self, metadata): assert isinstance(metadata, gallerydb.Gallery) self.link_lbl.setText(metadata.link) self.title_edit.setText(metadata.title) self.author_edit.setText(metadata.artist) tags = "" lang = ['English', 'Japanese'] self._find_combobox_match(self.lang_box, metadata.language, 2) self.tags_edit.setText(utils.tag_to_string(metadata.tags)) pub_string = "{}".format(metadata.pub_date) pub_date = QDate.fromString(pub_string.split()[0], "yyyy-MM-dd") self.pub_edit.setDate(pub_date) self._find_combobox_match(self.type_box, metadata.type, 0) def make_gallery(self, new_gallery, add_to_model=True, new=False): def is_checked(widget): return widget.g_check.isChecked() if self.check(): if is_checked(self.title_edit): new_gallery.title = self.title_edit.text() log_d('Adding gallery title') if is_checked(self.author_edit): new_gallery.artist = self.author_edit.text() log_d('Adding gallery artist') if not self._multiple_galleries: new_gallery.path = self.path_lbl.text() log_d('Adding gallery path') if is_checked(self.descr_edit): new_gallery.info = self.descr_edit.toPlainText() log_d('Adding gallery descr') if is_checked(self.type_box): new_gallery.type = self.type_box.currentText() log_d('Adding gallery type') if is_checked(self.lang_box): new_gallery.language = self.lang_box.currentText() log_d('Adding gallery lang') if is_checked(self.rating_box): new_gallery.rating = self.rating_box.value() log_d('Adding gallery rating') if is_checked(self.status_box): new_gallery.status = self.status_box.currentText() log_d('Adding gallery status') if is_checked(self.tags_edit): if self.tags_append.isChecked(): new_gallery.tags = utils.tag_to_dict( utils.tag_to_string(new_gallery.tags) + "," + self.tags_edit.toPlainText()) else: new_gallery.tags = utils.tag_to_dict( self.tags_edit.toPlainText()) log_d('Adding gallery: tagging to dict') if is_checked(self.pub_edit): qpub_d = self.pub_edit.date().toString("ddMMyyyy") dpub_d = datetime.strptime(qpub_d, "%d%m%Y").date() try: d_t = self.gallery_time except AttributeError: d_t = datetime.now().time().replace(microsecond=0) dpub_d = datetime.combine(dpub_d, d_t) new_gallery.pub_date = dpub_d log_d('Adding gallery pub date') if is_checked(self.link_lbl): new_gallery.link = self.link_lbl.text() log_d('Adding gallery link') if new: if not new_gallery.chapters: log_d('Starting chapters') thread = threading.Thread(target=utils.make_chapters, args=(new_gallery, )) thread.start() thread.join() log_d('Finished chapters') if new and app_constants.MOVE_IMPORTED_GALLERIES: app_constants.OVERRIDE_MONITOR = True new_gallery.move_gallery() if add_to_model: self.parent_widget.default_manga_view.add_gallery( new_gallery, True) log_i('Sent gallery to model') else: if add_to_model: self.parent_widget.default_manga_view.replace_gallery( [new_gallery], False) return new_gallery def link_set(self): t = self.link_edit.text() self.link_edit.hide() self.link_lbl.show() self.link_lbl.setText(t) self.link_btn2.hide() self.link_btn.show() def link_modify(self): t = self.link_lbl.text() self.link_lbl.hide() self.link_edit.show() self.link_edit.setText(t) self.link_btn.hide() self.link_btn2.show() def _disconnect(self): try: self._fetch_inst.GALLERY_PICKER.disconnect() self._fetch_inst.GALLERY_EMITTER.disconnect() self._fetch_inst.FINISHED.disconnect() except TypeError: pass def delayed_close(self): if self._fetch_thread.isRunning(): self._fetch_thread.finished.connect(self.close) self.hide() else: self.close() def accept(self): self.make_gallery(gallerydb.Gallery(), new=True) self.delayed_close() def accept_edit(self): gallerydb.execute(database.db.DBBase.begin, True) app_constants.APPEND_TAGS_GALLERIES = self.tags_append.isChecked() settings.set(app_constants.APPEND_TAGS_GALLERIES, 'Application', 'append tags to gallery') for g in self._edit_galleries: self.make_gallery(g) self.delayed_close() gallerydb.execute(database.db.DBBase.end, True) def reject_edit(self): self.delayed_close()
class MainWindow(QMainWindow, Ui_MainWindow): qtcb_enumerate = pyqtSignal(str, str, str, type((0, )), type((0, )), int, int) qtcb_connected = pyqtSignal(int) qtcb_disconnected = pyqtSignal(int) def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.setupUi(self) # Setting the minimum width of the setup tab ensures, that other tabs can grow # the window if more space is required, but we have a sane default for status # messages. Setting the minimum width of the main window itself would enfoce # it, even if children (i.e. tabs) need more space. self.tab_setup.setMinimumWidth(550) signal.signal(signal.SIGINT, self.exit_brickv) signal.signal(signal.SIGTERM, self.exit_brickv) self.async_thread = async_start_thread(self) title = 'Brick Viewer ' + config.BRICKV_FULL_VERSION self.setWindowTitle(title) self.delayed_update_tree_view_timer = QTimer(self) self.delayed_update_tree_view_timer.timeout.connect( self.update_tree_view) self.delayed_update_tree_view_timer.setInterval(100) self.tree_view_model_labels = ['Name', 'UID', 'Position', 'FW Version'] self.tree_view_model = QStandardItemModel(self) self.tree_view_proxy_model = DevicesProxyModel(self) self.tree_view_proxy_model.setSourceModel(self.tree_view_model) self.tree_view.setModel(self.tree_view_proxy_model) self.tree_view.activated.connect(self.item_activated) self.set_tree_view_defaults() inventory.info_changed.connect( lambda: self.delayed_update_tree_view_timer.start()) self.tab_widget.removeTab(1) # remove dummy tab self.tab_widget.setUsesScrollButtons(True) # force scroll buttons self.update_tab_button = IconButton( QIcon(load_pixmap('update-icon-normal.png')), QIcon(load_pixmap('update-icon-hover.png')), parent=self.tab_setup) self.update_tab_button.setToolTip('Updates available') self.update_tab_button.clicked.connect(self.flashing_clicked) self.update_tab_button.hide() self.name = '<unknown>' self.uid = '<unknown>' self.version = (0, 0, 0) self.disconnect_times = [] self.qtcb_enumerate.connect(self.cb_enumerate) self.qtcb_connected.connect(self.cb_connected) self.qtcb_disconnected.connect(self.cb_disconnected) self.ipcon = IPConnection() self.ipcon.register_callback(IPConnection.CALLBACK_ENUMERATE, self.qtcb_enumerate.emit) self.ipcon.register_callback(IPConnection.CALLBACK_CONNECTED, self.qtcb_connected.emit) self.ipcon.register_callback(IPConnection.CALLBACK_DISCONNECTED, self.qtcb_disconnected.emit) self.current_device_info = None self.flashing_window = None self.advanced_window = None self.data_logger_window = None self.delayed_refresh_updates_timer = QTimer(self) self.delayed_refresh_updates_timer.timeout.connect( self.delayed_refresh_updates) self.delayed_refresh_updates_timer.setInterval(100) self.reset_view() self.button_advanced.setDisabled(True) self.fw_version_fetcher = LatestFWVersionFetcher() self.fw_version_fetcher.fw_versions_avail.connect( self.fw_versions_fetched) self.fw_version_fetcher_thread = QThread(self) self.fw_version_fetcher_thread.setObjectName( "fw_version_fetcher_thread") if config.get_auto_search_for_updates(): self.enable_auto_search_for_updates() else: self.disable_auto_search_for_updates() self.tab_widget.currentChanged.connect(self.tab_changed) self.tab_widget.setMovable(True) self.tab_widget.tabBar().installEventFilter(self) self.button_connect.clicked.connect(self.connect_clicked) self.button_flashing.clicked.connect(self.flashing_clicked) self.button_advanced.clicked.connect(self.advanced_clicked) self.button_data_logger.clicked.connect(self.data_logger_clicked) self.plugin_manager = PluginManager() # host info self.host_infos = config.get_host_infos(config.HOST_INFO_COUNT) self.host_index_changing = True for host_info in self.host_infos: self.combo_host.addItem(host_info.host) self.last_host = None self.combo_host.installEventFilter(self) self.combo_host.currentIndexChanged.connect(self.host_index_changed) self.spinbox_port.setValue(self.host_infos[0].port) self.spinbox_port.valueChanged.connect(self.port_changed) self.spinbox_port.installEventFilter(self) self.checkbox_authentication.stateChanged.connect( self.authentication_state_changed) self.label_secret.hide() self.edit_secret.hide() self.edit_secret.setEchoMode(QLineEdit.Password) self.edit_secret.textEdited.connect(self.secret_changed) self.edit_secret.installEventFilter(self) self.checkbox_secret_show.hide() self.checkbox_secret_show.stateChanged.connect( self.secret_show_state_changed) self.checkbox_remember_secret.hide() self.checkbox_remember_secret.stateChanged.connect( self.remember_secret_state_changed) self.checkbox_authentication.setChecked( self.host_infos[0].use_authentication) self.edit_secret.setText(self.host_infos[0].secret) self.checkbox_remember_secret.setChecked( self.host_infos[0].remember_secret) self.host_index_changing = False # auto-reconnect self.label_auto_reconnects.hide() self.auto_reconnects = 0 # RED Session losts self.label_red_session_losts.hide() self.red_session_losts = 0 # fusion style self.check_fusion_gui_style.setChecked( config.get_use_fusion_gui_style()) self.check_fusion_gui_style.stateChanged.connect( self.gui_style_changed) self.checkbox_auto_search_for_updates.setChecked( config.get_auto_search_for_updates()) self.checkbox_auto_search_for_updates.stateChanged.connect( self.auto_search_for_updates_changed) self.button_update_pixmap_normal = load_pixmap( 'update-icon-normal.png') self.button_update_pixmap_hover = load_pixmap('update-icon-hover.png') self.last_status_message_id = '' def disable_auto_search_for_updates(self): self.fw_version_fetcher.abort() def enable_auto_search_for_updates(self): self.fw_version_fetcher.reset() self.fw_version_fetcher.moveToThread(self.fw_version_fetcher_thread) self.fw_version_fetcher_thread.started.connect( self.fw_version_fetcher.run) self.fw_version_fetcher.finished.connect( self.fw_version_fetcher_thread.quit) self.fw_version_fetcher_thread.start() # override QMainWindow.closeEvent def closeEvent(self, event): if not self.exit_logger(): event.ignore() return self.exit_brickv() event.accept() async_stop_thread() # Without this, the quit event seems to not reach the main loop under OSX. QApplication.quit() def exit_brickv(self, signl=None, frme=None): self.update_current_host_info() config.set_host_infos(self.host_infos) self.do_disconnect() if signl != None and frme != None: print("Received SIGINT or SIGTERM, shutting down.") sys.exit() def exit_logger(self): exitBrickv = True if (self.data_logger_window is not None) and \ (self.data_logger_window.data_logger_thread is not None) and \ (not self.data_logger_window.data_logger_thread.stopped): quit_msg = "The Data Logger is running. Are you sure you want to exit the program?" reply = QMessageBox.question(self, 'Message', quit_msg, QMessageBox.Yes, QMessageBox.No) if reply == QMessageBox.Yes: self.data_logger_window.data_logger_thread.stop() else: exitBrickv = False return exitBrickv def host_index_changed(self, i): if i < 0: return self.host_index_changing = True self.spinbox_port.setValue(self.host_infos[i].port) self.checkbox_authentication.setChecked( self.host_infos[i].use_authentication) self.edit_secret.setText(self.host_infos[i].secret) self.checkbox_remember_secret.setChecked( self.host_infos[i].remember_secret) self.host_index_changing = False def port_changed(self, _value): self.update_current_host_info() def authentication_state_changed(self, state): visible = state == Qt.Checked self.label_secret.setVisible(visible) self.edit_secret.setVisible(visible) self.checkbox_secret_show.setVisible(visible) self.checkbox_remember_secret.setVisible(visible) self.update_current_host_info() def secret_changed(self): self.update_current_host_info() def secret_show_state_changed(self, state): if state == Qt.Checked: self.edit_secret.setEchoMode(QLineEdit.Normal) else: self.edit_secret.setEchoMode(QLineEdit.Password) self.update_current_host_info() def remember_secret_state_changed(self, _state): self.update_current_host_info() def tab_changed(self, i): if not hasattr(self.tab_widget.widget(i), '_info'): new_current_device_info = None else: new_current_device_info = self.tab_widget.widget(i)._info new_current_device_info.plugin.start_plugin() # stop the now deselected plugin, if there is one that's running if self.current_device_info is not None: self.current_device_info.plugin.stop_plugin() self.current_device_info = new_current_device_info def update_current_host_info(self): if self.host_index_changing: return i = self.combo_host.currentIndex() if i < 0: return self.host_infos[i].port = self.spinbox_port.value() self.host_infos[ i].use_authentication = self.checkbox_authentication.isChecked() self.host_infos[i].secret = self.edit_secret.text() self.host_infos[ i].remember_secret = self.checkbox_remember_secret.isChecked() def gui_style_changed(self): config.set_use_fusion_gui_style( self.check_fusion_gui_style.isChecked()) QMessageBox.information( self, 'GUI Style', 'GUI style change will be applied on next Brick Viewer start.', QMessageBox.Ok) def auto_search_for_updates_changed(self): config.set_auto_search_for_updates( self.checkbox_auto_search_for_updates.isChecked()) if self.checkbox_auto_search_for_updates.isChecked(): self.enable_auto_search_for_updates() else: self.disable_auto_search_for_updates() def remove_all_device_infos(self): for device_info in inventory.get_device_infos(): self.remove_device_info(device_info.uid) def remove_device_info(self, uid): tab_id = self.tab_for_uid(uid) device_info = inventory.get_info(uid) device_info.plugin.stop_plugin() device_info.plugin.destroy_plugin() if tab_id >= 0: self.tab_widget.removeTab(tab_id) # ensure that the widget gets correctly destroyed. otherwise QWidgets # tend to leak as Python is not able to collect their PyQt object tab_window = device_info.tab_window device_info.tab_window = None # If we reboot the RED Brick, the tab_window sometimes is # already None here if tab_window != None: tab_window.hide() tab_window.setParent(None) plugin = device_info.plugin device_info.plugin = None if plugin != None: plugin.hide() plugin.setParent(None) inventory.remove_info(uid) def reset_view(self): self.tab_widget.setCurrentIndex(0) self.remove_all_device_infos() self.update_tree_view() def do_disconnect(self): self.auto_reconnects = 0 self.label_auto_reconnects.hide() self.red_session_losts = 0 self.label_red_session_losts.hide() self.reset_view() async_next_session() # force garbage collection, to ensure that all plugin related objects # got destroyed before disconnect is called. this is especially # important for the RED Brick plugin because its relies on releasing # the the RED Brick API objects in the __del__ method as a last resort # to avoid leaking object references. but this only works if garbage # collection is done before disconnect is called gc.collect() try: self.ipcon.disconnect() except: pass def do_authenticate(self, is_auto_reconnect): if not self.checkbox_authentication.isChecked(): return True try: secret = self.edit_secret.text() # Try to encode the secret, as only ASCII chars are allowed. Don't save the result, as the IP Connection does the same. secret.encode('ascii') except: self.do_disconnect() QMessageBox.critical( self, 'Connection', 'Authentication secret cannot contain non-ASCII characters.', QMessageBox.Ok) return False self.ipcon.set_auto_reconnect( False) # don't auto-reconnect on authentication error try: self.ipcon.authenticate(secret) except: self.do_disconnect() if is_auto_reconnect: extra = ' after auto-reconnect' else: extra = '' QMessageBox.critical( self, 'Connection', 'Could not authenticate' + extra + '. Check secret and ensure ' + 'authentication for Brick Daemon is enabled.', QMessageBox.Ok) return False self.ipcon.set_auto_reconnect(True) return True def flashing_clicked(self): if self.flashing_window is None: self.flashing_window = FlashingWindow(self) else: self.flashing_window.refresh_update_tree_view() self.flashing_window.show() self.flashing_window.tab_widget.setCurrentWidget( self.flashing_window.tab_updates) def advanced_clicked(self): if self.advanced_window is None: self.advanced_window = AdvancedWindow(self) self.advanced_window.show() def data_logger_clicked(self): if self.data_logger_window is None: self.data_logger_window = DataLoggerWindow(self, self.host_infos) self.data_logger_window.show() def connect_error(self): self.setDisabled(False) self.button_connect.setText("Connect") QMessageBox.critical( self, 'Connection', 'Could not connect. Please check host, check ' + 'port and ensure that Brick Daemon is running.') def connect_clicked(self): if self.ipcon.get_connection_state( ) == IPConnection.CONNECTION_STATE_DISCONNECTED: self.last_host = self.combo_host.currentText() self.setDisabled(True) self.button_connect.setText("Connecting...") async_call(self.ipcon.connect, (self.last_host, self.spinbox_port.value()), None, self.connect_error) else: self.do_disconnect() def item_activated(self, index): index = self.tree_view_proxy_model.mapToSource(index) position_index = index.sibling(index.row(), 2) extension_clicked = position_index.isValid() and position_index.data( ).startswith('Ext') if extension_clicked: extension_index = int(position_index.data().replace('Ext', '')) index = index.parent() uid_index = index.sibling(index.row(), 1) if uid_index.isValid(): plugin = self.show_plugin(uid_index.data()) if extension_clicked: plugin.show_extension(extension_index) def show_brick_update(self, url_part): if self.flashing_window is None: self.flashing_window = FlashingWindow(self) self.flashing_window.show() self.flashing_window.refresh_update_tree_view() self.flashing_window.show_brick_update(url_part) def show_bricklet_update(self, parent_uid, port): if self.flashing_window is None: self.flashing_window = FlashingWindow(self) self.flashing_window.show() self.flashing_window.refresh_update_tree_view() self.flashing_window.show_bricklet_update(parent_uid, port) def show_extension_update(self, master_uid): if self.flashing_window is None: self.flashing_window = FlashingWindow(self) self.flashing_window.show() self.flashing_window.refresh_update_tree_view() self.flashing_window.show_extension_update(master_uid) def show_red_brick_update(self): text = "To update the RED Brick Image, please follow the instructions " + \ "<a href=https://www.tinkerforge.com/en/doc/Hardware/Bricks/RED_Brick.html#red-brick-copy-image>here</a>." QMessageBox.information(self, "RED Brick Update", text) def create_tab_window(self, device_info, ipcon): tab_window = TabWindow(self.tab_widget, device_info.name, self.untab) tab_window._info = device_info tab_window.add_callback_on_tab(lambda index: self.ipcon.get_connection_state() == IPConnection.CONNECTION_STATE_PENDING and \ self.tab_widget.setTabEnabled(index, False), 'main_window_disable_tab_if_connection_pending') layout = QVBoxLayout(tab_window) info_bars = [QHBoxLayout(), QHBoxLayout()] # uid info_bars[0].addWidget(QLabel('UID:')) label = QLabel('{0}'.format(device_info.uid)) label.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard) info_bars[0].addWidget(label) info_bars[0].addSpacerItem(QSpacerItem(20, 1, QSizePolicy.Preferred)) # firmware version label_version_name = QLabel('Version:') label_version = QLabel('Querying...') button_update = QPushButton(QIcon(self.button_update_pixmap_normal), 'Update') button_update.installEventFilter(self) if isinstance(device_info, BrickREDInfo): button_update.clicked.connect(self.show_red_brick_update) elif device_info.flashable_like_bricklet: button_update.clicked.connect(lambda: self.show_bricklet_update( device_info.connected_uid, device_info.position)) elif device_info.kind == 'brick': button_update.clicked.connect( lambda: self.show_brick_update(device_info.url_part)) if not device_info.plugin.has_custom_version(label_version_name, label_version): label_version_name.setText('FW Version:') label_version.setText( get_version_string(device_info.plugin.firmware_version)) info_bars[0].addWidget(label_version_name) info_bars[0].addWidget(label_version) info_bars[0].addWidget(button_update) button_update.hide() tab_window.button_update = button_update info_bars[0].addSpacerItem(QSpacerItem(20, 1, QSizePolicy.Preferred)) # timeouts info_bars[0].addWidget(QLabel('Timeouts:')) label_timeouts = QLabel('0') info_bars[0].addWidget(label_timeouts) info_bars[0].addSpacerItem(QSpacerItem(1, 1, QSizePolicy.Expanding)) # connected uid if device_info.connected_uid != '0': info_bars[1].addWidget(QLabel('Connected to:')) button = QToolButton() button.setText(device_info.connected_uid) button.clicked.connect( lambda: self.show_plugin(device_info.connected_uid)) device_info.plugin.button_parent = button info_bars[1].addWidget(button) info_bars[1].addSpacerItem( QSpacerItem(20, 1, QSizePolicy.Preferred)) # position info_bars[1].addWidget(QLabel('Position:')) label_position = QLabel('{0}'.format(device_info.position.title())) device_info.plugin.label_position = label_position info_bars[1].addWidget(label_position) info_bars[1].addSpacerItem(QSpacerItem(1, 1, QSizePolicy.Expanding)) # configs configs = device_info.plugin.get_configs() def config_changed(combobox): i = combobox.currentIndex() if i < 0: return combobox.itemData(i).trigger() if len(configs) > 0: for cfg in configs: if cfg[1] != None: combobox = QComboBox() for i, item in enumerate(cfg[2]): combobox.addItem(item.text(), item) item.triggered.connect( functools.partial(combobox.setCurrentIndex, i)) combobox.currentIndexChanged.connect( functools.partial(config_changed, combobox)) info_bars[cfg[0]].addWidget(QLabel(cfg[1])) info_bars[cfg[0]].addWidget(combobox) elif len(cfg[2]) > 0: checkbox = QCheckBox(cfg[2][0].text()) cfg[2][0].toggled.connect(checkbox.setChecked) checkbox.toggled.connect(cfg[2][0].setChecked) info_bars[cfg[0]].addWidget(checkbox) # actions actions = device_info.plugin.get_actions() if len(actions) > 0: for action in actions: if action[1] != None: button = QPushButton(action[1]) menu = QMenu() for item in action[2]: menu.addAction(item) button.setMenu(menu) elif len(action[2]) > 0: button = QPushButton(action[2][0].text()) button.clicked.connect(action[2][0].trigger) info_bars[action[0]].addWidget(button) def more_clicked(button, info_bar): visible = button.text().replace( '&', '') == 'More' # remove &s, they mark the buttons hotkey if visible: button.setText('Less') else: button.setText('More') for i in range(info_bar.count()): widget = info_bar.itemAt(i).widget() if widget != None: widget.setVisible(visible) more_button = QPushButton('More') more_button.clicked.connect( lambda: more_clicked(more_button, info_bars[1])) info_bars[0].addWidget(more_button) for i in range(info_bars[1].count()): widget = info_bars[1].itemAt(i).widget() if widget != None: widget.hide() layout.addLayout(info_bars[0]) layout.addLayout(info_bars[1]) line = QFrame() line.setObjectName("MainWindow_line") line.setFrameShape(QFrame.HLine) line.setFrameShadow(QFrame.Sunken) device_info.plugin.label_timeouts = label_timeouts device_info.plugin.label_version = label_version device_info.plugin.layout().setContentsMargins(0, 0, 0, 0) layout.addWidget(line) if device_info.plugin.has_comcu: device_info.plugin.widget_bootloader = COMCUBootloader( ipcon, device_info) device_info.plugin.widget_bootloader.hide() layout.addWidget(device_info.plugin.widget_bootloader) layout.addWidget(device_info.plugin, 1) return tab_window def tab_move(self, event): # visualize rearranging of tabs (if allowed by tab_widget) if self.tab_widget.isMovable(): if event.type( ) == QEvent.MouseButtonPress and event.button() & Qt.LeftButton: QApplication.setOverrideCursor(QCursor(Qt.SizeHorCursor)) elif event.type( ) == QEvent.MouseButtonRelease and event.button() & Qt.LeftButton: QApplication.restoreOverrideCursor() return False def untab(self, tab_index): tab = self.tab_widget.widget(tab_index) tab.untab() tab._info.plugin.start_plugin() self.tab_widget.setCurrentIndex(0) def connect_on_return(self, event): if event.type() == QEvent.KeyPress and (event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter): self.connect_clicked() return True return False def eventFilter(self, source, event): if source is self.tab_widget.tabBar(): return self.tab_move(event) if source is self.combo_host or source is self.spinbox_port or source is self.edit_secret: return self.connect_on_return(event) if isinstance(source, QPushButton) and event.type() == QEvent.Enter: source.setIcon(QIcon(self.button_update_pixmap_hover)) elif isinstance(source, QPushButton) and event.type() == QEvent.Leave: source.setIcon(QIcon(self.button_update_pixmap_normal)) return False def tab_for_uid(self, uid): for index in range(1, self.tab_widget.count()): try: if self.tab_widget.widget(index)._info.uid == uid: return index except: pass return -1 def show_plugin(self, uid): device_info = inventory.get_info(uid) if device_info == None: return index = self.tab_for_uid(uid) tab_window = device_info.tab_window if index > 0 and self.tab_widget.isTabEnabled(index): self.tab_widget.setCurrentIndex(index) QApplication.setActiveWindow(tab_window) tab_window.show() tab_window.activateWindow() tab_window.raise_() return device_info.plugin def cb_enumerate(self, uid, connected_uid, position, hardware_version, firmware_version, device_identifier, enumeration_type): if self.ipcon.get_connection_state( ) != IPConnection.CONNECTION_STATE_CONNECTED: # ignore enumerate callbacks that arrived after the connection got closed return if enumeration_type in [ IPConnection.ENUMERATION_TYPE_AVAILABLE, IPConnection.ENUMERATION_TYPE_CONNECTED ]: device_info = inventory.get_info(uid) # If the enum_type is CONNECTED, the bricklet was restarted externally. # The plugin could now be in an inconsistent state. if enumeration_type == IPConnection.ENUMERATION_TYPE_CONNECTED and device_info is not None: if device_info.connected_uid != connected_uid: # Fix connections if bricklet was connected to another brick. parent_info = inventory.get_info(device_info.connected_uid) if parent_info is not None: parent_info.connections_remove_item( (device_info.position, device_info)) self.show_status( "Hot plugging is not supported! Please reset Brick with UID {} and reconnect Brick Viewer." .format(device_info.connected_uid), message_id='mainwindow_hotplug') device_info.reverse_connection = connected_uid elif device_info.position != position: # Bricklet was connected to the same brick, but to another port self.show_status( "Hot plugging is not supported! Please reset Brick with UID {} and reconnect Brick Viewer." .format(device_info.connected_uid), message_id='mainwindow_hotplug') # If the plugin is not running, pause will do nothing, so it is always save to call it. # The plugin will be (unconditionally) resumed later, as resume also only does something # if it was paused before (e.g. here). if device_info.plugin is not None: device_info.plugin.pause_plugin() if device_info == None: if device_identifier == BrickMaster.DEVICE_IDENTIFIER: device_info = BrickMasterInfo() elif device_identifier == BrickRED.DEVICE_IDENTIFIER: device_info = BrickREDInfo() elif hat_brick_supported and device_identifier == BrickHAT.DEVICE_IDENTIFIER: device_info = BrickHATInfo() elif hat_zero_brick_supported and device_identifier == BrickHATZero.DEVICE_IDENTIFIER: device_info = BrickHATZeroInfo() elif device_identifier == BrickletIsolator.DEVICE_IDENTIFIER: device_info = BrickletIsolatorInfo() elif str(device_identifier).startswith('20'): device_info = TNGInfo() elif '0' <= position <= '9': device_info = BrickInfo() else: device_info = BrickletInfo() position = position.lower() device_info.uid = uid device_info.connected_uid = connected_uid device_info.position = position device_info.hardware_version = hardware_version if device_identifier != BrickRED.DEVICE_IDENTIFIER: device_info.firmware_version_installed = firmware_version device_info.device_identifier = device_identifier device_info.enumeration_type = enumeration_type # Update connections and reverse_connection with new device for info in inventory.get_device_infos(): if info == device_info: continue def add_to_connections(info_to_add, connected_info): hotplug = connected_info.connections_add_item( (info_to_add.position, info_to_add)) info_to_add.reverse_connection = connected_info # '0' is the port where other stacks connected by RS485 extensions are connected. Multiple connections are allowed here. if hotplug and info_to_add.position != '0': self.show_status( "Hot plugging is not supported! Please reset Brick with UID {} and reconnect Brick Viewer." .format(connected_info.uid), message_id='mainwindow_hotplug') if info.uid != '' and info.uid == device_info.connected_uid: if device_info in info.connections_values( ): # device was already connected, but to another port info.connections_remove_value(device_info) if device_info not in info.connections_get( device_info.position): add_to_connections(device_info, info) if info.connected_uid != '' and info.connected_uid == device_info.uid: if info in device_info.connections_values( ): # device was already connected, but to another port device_info.connections_remove_value(info) if info not in device_info.connections_get(info.position): add_to_connections(info, device_info) if device_info.plugin == None: self.plugin_manager.create_plugin_instance( device_identifier, self.ipcon, device_info) device_info.tab_window = self.create_tab_window( device_info, self.ipcon) device_info.tab_window.setWindowFlags(Qt.Widget) device_info.tab_window.tab() inventory.add_info(device_info) device_info.update_firmware_version_latest() inventory.sync() # The plugin was paused before if it was reconnected. device_info.plugin.resume_plugin() elif enumeration_type == IPConnection.ENUMERATION_TYPE_DISCONNECTED: self.remove_device_tab(uid) def remove_device_tab(self, uid): device_info = inventory.get_info(uid) if device_info == None: return assert isinstance(device_info, DeviceInfo) self.tab_widget.setCurrentIndex(0) self.remove_device_info(device_info.uid) for other_info in inventory.get_device_infos(): other_info.connections_remove_value(device_info) self.update_tree_view() def hack_to_remove_red_brick_tab(self, uid): device_info = inventory.get_info(uid) if device_info == None: return assert isinstance(device_info, DeviceInfo) self.tab_widget.setCurrentIndex(0) self.remove_device_info(device_info.uid) self.red_session_losts += 1 self.label_red_session_losts.setText( 'RED Brick Session Loss Count: {0}'.format(self.red_session_losts)) self.label_red_session_losts.show() self.update_tree_view() def cb_connected(self, connect_reason): self.disconnect_times = [] self.update_ui_state() if connect_reason == IPConnection.CONNECT_REASON_REQUEST: self.setDisabled(False) self.auto_reconnects = 0 self.label_auto_reconnects.hide() self.red_session_losts = 0 self.label_red_session_losts.hide() self.ipcon.set_auto_reconnect(True) index = self.combo_host.findText(self.last_host) if index >= 0: self.combo_host.removeItem(index) host_info = self.host_infos[index] del self.host_infos[index] self.host_infos.insert(0, host_info) else: index = self.combo_host.currentIndex() host_info = self.host_infos[index].duplicate() host_info.host = self.last_host self.host_infos.insert(0, host_info) self.combo_host.insertItem(-1, self.last_host) self.combo_host.setCurrentIndex(0) while self.combo_host.count() > config.HOST_INFO_COUNT: self.combo_host.removeItem(self.combo_host.count() - 1) if not self.do_authenticate(False): return try: self.ipcon.enumerate() except: self.update_ui_state() elif connect_reason == IPConnection.CONNECT_REASON_AUTO_RECONNECT: self.auto_reconnects += 1 self.label_auto_reconnects.setText( 'Auto-Reconnect Count: {0}'.format(self.auto_reconnects)) self.label_auto_reconnects.show() if not self.do_authenticate(True): return try: self.ipcon.enumerate() except: self.update_ui_state() else: try: self.ipcon.enumerate() except: self.update_ui_state() def cb_disconnected(self, disconnect_reason): self.hide_status('mainwindow_hotplug') if disconnect_reason == IPConnection.DISCONNECT_REASON_REQUEST: self.auto_reconnects = 0 self.label_auto_reconnects.hide() self.red_session_losts = 0 self.label_red_session_losts.hide() if disconnect_reason == IPConnection.DISCONNECT_REASON_REQUEST or not self.ipcon.get_auto_reconnect( ): self.update_ui_state() elif len(self.disconnect_times ) >= 3 and self.disconnect_times[-3] < time.time() + 1: self.disconnect_times = [] self.ipcon.set_auto_reconnect(False) self.update_ui_state() self.reset_view() QMessageBox.critical( self, 'Connection', 'Stopped automatic reconnecting due to multiple connection errors in a row.' ) else: self.disconnect_times.append(time.time()) self.update_ui_state(IPConnection.CONNECTION_STATE_PENDING) def set_tree_view_defaults(self): self.tree_view_model.setHorizontalHeaderLabels( self.tree_view_model_labels) self.tree_view.expandAll() self.tree_view.setColumnWidth(0, 280) self.tree_view.setColumnWidth(1, 70) self.tree_view.setColumnWidth(2, 90) self.tree_view.setColumnWidth(3, 105) self.tree_view.setColumnWidth(4, 105) self.tree_view.setExpandsOnDoubleClick(False) self.tree_view.setSortingEnabled(True) self.tree_view.header().setSortIndicator(2, Qt.AscendingOrder) def update_ui_state(self, connection_state=None): # FIXME: need to call processEvents() otherwise get_connection_state() # might return the wrong value QApplication.processEvents() if connection_state is None: connection_state = self.ipcon.get_connection_state() self.button_flashing.setDisabled(False) if connection_state == IPConnection.CONNECTION_STATE_DISCONNECTED: self.button_connect.setText('Connect') self.combo_host.setDisabled(False) self.spinbox_port.setDisabled(False) self.checkbox_authentication.setDisabled(False) self.edit_secret.setDisabled(False) self.button_advanced.setDisabled(True) elif connection_state == IPConnection.CONNECTION_STATE_CONNECTED: self.button_connect.setText("Disconnect") self.combo_host.setDisabled(True) self.spinbox_port.setDisabled(True) self.checkbox_authentication.setDisabled(True) self.edit_secret.setDisabled(True) self.update_advanced_window() # restart all pause plugins for info in inventory.get_device_infos(): info.plugin.resume_plugin() elif connection_state == IPConnection.CONNECTION_STATE_PENDING: self.button_connect.setText('Abort Pending Automatic Reconnect') self.combo_host.setDisabled(True) self.spinbox_port.setDisabled(True) self.checkbox_authentication.setDisabled(True) self.edit_secret.setDisabled(True) self.button_advanced.setDisabled(True) self.button_flashing.setDisabled(True) # pause all running plugins for info in inventory.get_device_infos(): info.plugin.pause_plugin() enable = connection_state == IPConnection.CONNECTION_STATE_CONNECTED for i in range(1, self.tab_widget.count()): self.tab_widget.setTabEnabled(i, enable) for device_info in inventory.get_device_infos(): device_info.tab_window.setEnabled(enable) QApplication.processEvents() def update_tree_view(self): self.delayed_update_tree_view_timer.stop() self.tree_view_model.setHorizontalHeaderLabels( self.tree_view_model_labels) self.tab_widget.tabBar().setTabButton(0, QTabBar.RightSide, None) sis = self.tree_view.header().sortIndicatorSection() sio = self.tree_view.header().sortIndicatorOrder() self.tree_view_model.clear() def get_row(info): replacement = '0.0.0' is_red_brick = isinstance(info, BrickREDInfo) if is_red_brick or info.url_part == 'wifi_v2': replacement = "Querying..." elif info.kind == "extension": replacement = "" fw_version = get_version_string(info.firmware_version_installed, replace_unknown=replacement, is_red_brick=is_red_brick) uid = info.uid if info.kind != "extension" else '' row = [ QStandardItem(info.name), QStandardItem(uid), QStandardItem(info.position.title()), QStandardItem(fw_version) ] updateable = info.firmware_version_installed != ( 0, 0, 0 ) and info.firmware_version_installed < info.firmware_version_latest if is_red_brick: old_updateable = updateable for binding in info.bindings_infos: updateable |= binding.firmware_version_installed != (0, 0, 0) \ and binding.firmware_version_installed < binding.firmware_version_latest updateable |= info.brickv_info.firmware_version_installed != (0, 0, 0) \ and info.brickv_info.firmware_version_installed < info.brickv_info.firmware_version_latest \ and not info.firmware_version_installed < (1, 14, 0) # Hide Brickv update if image is too old. # There are bindings/brickv updates but there is no image update red_brick_binding_update_only = not old_updateable and updateable else: red_brick_binding_update_only = False if updateable: self.tree_view_model.setHorizontalHeaderLabels( self.tree_view_model_labels + ['Update']) row.append( QStandardItem( get_version_string(info.firmware_version_latest, is_red_brick=is_red_brick) + ("+" if red_brick_binding_update_only else ""))) self.tab_widget.tabBar().setTabButton(0, QTabBar.RightSide, self.update_tab_button) self.update_tab_button.show() for item in row: item.setFlags(item.flags() & ~Qt.ItemIsEditable) if updateable: item.setData(QBrush(QColor(255, 160, 55)), Qt.BackgroundRole) return row def recurse_on_device(info, insertion_point): row = get_row(info) insertion_point.appendRow(row) for child in info.connections_values(): recurse_on_device(child, row[0]) if info.can_have_extension: for extension in info.extensions.values(): if extension is None: continue ext_row = get_row(extension) row[0].appendRow(ext_row) for info in inventory.get_device_infos(): # If a device has a reverse connection, it will be handled as a child of another top-level brick. if info.reverse_connection is not None: continue recurse_on_device(info, self.tree_view_model) self.set_tree_view_defaults() self.tree_view.header().setSortIndicator(sis, sio) self.update_advanced_window() self.delayed_refresh_updates_timer.start() def update_advanced_window(self): self.button_advanced.setEnabled(len(inventory.get_brick_infos()) > 0) def delayed_refresh_updates(self): self.delayed_refresh_updates_timer.stop() if self.flashing_window is not None and self.flashing_window.isVisible( ): self.flashing_window.refresh_update_tree_view() def show_status(self, message, icon='warning', message_id=''): self.setStatusBar(None) if icon != 'none': icon_dict = { 'warning': 'warning-icon-16.png', } icon_label = QLabel() icon_label.setPixmap(load_pixmap(icon_dict[icon])) self.statusBar().addWidget(icon_label) message_label = QLabel(message) message_label.setOpenExternalLinks(True) self.statusBar().addWidget(message_label, 1) self.last_status_message_id = message_id def hide_status(self, message_id): if self.last_status_message_id == message_id: self.setStatusBar(None) def fw_versions_fetched(self, firmware_info): if isinstance(firmware_info, int): if firmware_info > 0: if firmware_info == 1: message = 'Update information could not be downloaded from tinkerforge.com.<br/>' + \ 'Is your computer connected to the Internet?' else: message = ( "Update information on tinkerforge.com is malformed " + "(error code {0}).<br/>Please report this error to " + "<a href='mailto:[email protected]'>[email protected]</a>." ).format(firmware_info) self.show_status(message, message_id='fw_versions_fetched_error') inventory.reset_latest_fws() else: self.hide_status('fw_versions_fetched_error') inventory.update_latest_fws(firmware_info)
class MainWindow(QMainWindow, Ui_MainWindow): qtcb_enumerate = pyqtSignal(str, str, str, type((0,)), type((0,)), int, int) qtcb_connected = pyqtSignal(int) qtcb_disconnected = pyqtSignal(int) def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.setupUi(self) # Setting the minimum width of the setup tab ensures, that other tabs can grow # the window if more space is required, but we have a sane default for status # messages. Setting the minimum width of the main window itself would enfoce # it, even if children (i.e. tabs) need more space. self.tab_setup.setMinimumWidth(550) signal.signal(signal.SIGINT, self.exit_brickv) signal.signal(signal.SIGTERM, self.exit_brickv) self.async_thread = async_start_thread(self) title = 'Brick Viewer ' + config.BRICKV_VERSION if config.INTERNAL != None: title += '~{}'.format(config.INTERNAL) self.setWindowTitle(title) self.tree_view_model_labels = ['Name', 'UID', 'Position', 'FW Version'] self.tree_view_model = QStandardItemModel(self) self.tree_view_proxy_model = DevicesProxyModel(self) self.tree_view_proxy_model.setSourceModel(self.tree_view_model) self.tree_view.setModel(self.tree_view_proxy_model) self.tree_view.activated.connect(self.item_activated) self.set_tree_view_defaults() self.tab_widget.removeTab(1) # remove dummy tab self.tab_widget.setUsesScrollButtons(True) # force scroll buttons self.update_tab_button = IconButton(QIcon(load_pixmap('update-icon-normal.png')), QIcon(load_pixmap('update-icon-hover.png')), parent=self.tab_setup) self.update_tab_button.setToolTip('Updates available') self.update_tab_button.clicked.connect(self.flashing_clicked) self.update_tab_button.hide() self.name = '<unknown>' self.uid = '<unknown>' self.version = (0, 0, 0) self.disconnect_times = [] self.qtcb_enumerate.connect(self.cb_enumerate) self.qtcb_connected.connect(self.cb_connected) self.qtcb_disconnected.connect(self.cb_disconnected) self.ipcon = IPConnection() self.ipcon.register_callback(IPConnection.CALLBACK_ENUMERATE, self.qtcb_enumerate.emit) self.ipcon.register_callback(IPConnection.CALLBACK_CONNECTED, self.qtcb_connected.emit) self.ipcon.register_callback(IPConnection.CALLBACK_DISCONNECTED, self.qtcb_disconnected.emit) self.current_device_info = None self.flashing_window = None self.advanced_window = None self.data_logger_window = None self.delayed_refresh_updates_timer = QTimer(self) self.delayed_refresh_updates_timer.timeout.connect(self.delayed_refresh_updates) self.delayed_refresh_updates_timer.setInterval(100) self.reset_view() self.button_advanced.setDisabled(True) self.fw_version_fetcher = LatestFWVersionFetcher() self.fw_version_fetcher.fw_versions_avail.connect(self.fw_versions_fetched) self.fw_version_fetcher_thread = QThread() self.fw_version_fetcher_thread.setObjectName("fw_version_fetcher_thread") if config.get_auto_search_for_updates(): self.enable_auto_search_for_updates() else: self.disable_auto_search_for_updates() self.tab_widget.currentChanged.connect(self.tab_changed) self.tab_widget.setMovable(True) self.tab_widget.tabBar().installEventFilter(self) self.button_connect.clicked.connect(self.connect_clicked) self.button_flashing.clicked.connect(self.flashing_clicked) self.button_advanced.clicked.connect(self.advanced_clicked) self.button_data_logger.clicked.connect(self.data_logger_clicked) self.plugin_manager = PluginManager() # host info self.host_infos = config.get_host_infos(config.HOST_INFO_COUNT) self.host_index_changing = True for host_info in self.host_infos: self.combo_host.addItem(host_info.host) self.last_host = None self.combo_host.installEventFilter(self) self.combo_host.currentIndexChanged.connect(self.host_index_changed) self.spinbox_port.setValue(self.host_infos[0].port) self.spinbox_port.valueChanged.connect(self.port_changed) self.spinbox_port.installEventFilter(self) self.checkbox_authentication.stateChanged.connect(self.authentication_state_changed) self.label_secret.hide() self.edit_secret.hide() self.edit_secret.setEchoMode(QLineEdit.Password) self.edit_secret.textEdited.connect(self.secret_changed) self.edit_secret.installEventFilter(self) self.checkbox_secret_show.hide() self.checkbox_secret_show.stateChanged.connect(self.secret_show_state_changed) self.checkbox_remember_secret.hide() self.checkbox_remember_secret.stateChanged.connect(self.remember_secret_state_changed) self.checkbox_authentication.setChecked(self.host_infos[0].use_authentication) self.edit_secret.setText(self.host_infos[0].secret) self.checkbox_remember_secret.setChecked(self.host_infos[0].remember_secret) self.host_index_changing = False # auto-reconnect self.label_auto_reconnects.hide() self.auto_reconnects = 0 # RED Session losts self.label_red_session_losts.hide() self.red_session_losts = 0 # fusion style self.check_fusion_gui_style.setChecked(config.get_use_fusion_gui_style()) self.check_fusion_gui_style.stateChanged.connect(self.gui_style_changed) self.checkbox_auto_search_for_updates.setChecked(config.get_auto_search_for_updates()) self.checkbox_auto_search_for_updates.stateChanged.connect(self.auto_search_for_updates_changed) self.button_update_pixmap_normal = load_pixmap('update-icon-normal.png') self.button_update_pixmap_hover = load_pixmap('update-icon-hover.png') self.last_status_message_id = '' infos.get_infos_changed_signal().connect(self.update_red_brick_version) def update_red_brick_version(self, uid): if not isinstance(infos.get_info(uid), infos.BrickREDInfo): return self.update_tree_view() def disable_auto_search_for_updates(self): self.fw_version_fetcher.abort() def enable_auto_search_for_updates(self): self.fw_version_fetcher.reset() self.fw_version_fetcher.moveToThread(self.fw_version_fetcher_thread) self.fw_version_fetcher_thread.started.connect(self.fw_version_fetcher.run) self.fw_version_fetcher.finished.connect(self.fw_version_fetcher_thread.quit) self.fw_version_fetcher_thread.start() # override QMainWindow.closeEvent def closeEvent(self, event): if not self.exit_logger(): event.ignore() return self.exit_brickv() event.accept() async_stop_thread() # Without this, the quit event seems to not reach the main loop under OSX. QApplication.quit() def exit_brickv(self, signl=None, frme=None): self.update_current_host_info() config.set_host_infos(self.host_infos) self.do_disconnect() if signl != None and frme != None: print("Received SIGINT or SIGTERM, shutting down.") sys.exit() def exit_logger(self): exitBrickv = True if (self.data_logger_window is not None) and \ (self.data_logger_window.data_logger_thread is not None) and \ (not self.data_logger_window.data_logger_thread.stopped): quit_msg = "The Data Logger is running. Are you sure you want to exit the program?" reply = QMessageBox.question(self, 'Message', quit_msg, QMessageBox.Yes, QMessageBox.No) if reply == QMessageBox.Yes: self.data_logger_window.data_logger_thread.stop() else: exitBrickv = False return exitBrickv def host_index_changed(self, i): if i < 0: return self.host_index_changing = True self.spinbox_port.setValue(self.host_infos[i].port) self.checkbox_authentication.setChecked(self.host_infos[i].use_authentication) self.edit_secret.setText(self.host_infos[i].secret) self.checkbox_remember_secret.setChecked(self.host_infos[i].remember_secret) self.host_index_changing = False def port_changed(self, _value): self.update_current_host_info() def authentication_state_changed(self, state): visible = state == Qt.Checked self.label_secret.setVisible(visible) self.edit_secret.setVisible(visible) self.checkbox_secret_show.setVisible(visible) self.checkbox_remember_secret.setVisible(visible) self.update_current_host_info() def secret_changed(self): self.update_current_host_info() def secret_show_state_changed(self, state): if state == Qt.Checked: self.edit_secret.setEchoMode(QLineEdit.Normal) else: self.edit_secret.setEchoMode(QLineEdit.Password) self.update_current_host_info() def remember_secret_state_changed(self, _state): self.update_current_host_info() def tab_changed(self, i): if not hasattr(self.tab_widget.widget(i), '_info'): new_current_device_info = None else: new_current_device_info = self.tab_widget.widget(i)._info new_current_device_info.plugin.start_plugin() # stop the now deselected plugin, if there is one that's running if self.current_device_info is not None: self.current_device_info.plugin.stop_plugin() self.current_device_info = new_current_device_info def update_current_host_info(self): if self.host_index_changing: return i = self.combo_host.currentIndex() if i < 0: return self.host_infos[i].port = self.spinbox_port.value() self.host_infos[i].use_authentication = self.checkbox_authentication.isChecked() self.host_infos[i].secret = self.edit_secret.text() self.host_infos[i].remember_secret = self.checkbox_remember_secret.isChecked() def gui_style_changed(self): config.set_use_fusion_gui_style(self.check_fusion_gui_style.isChecked()) QMessageBox.information(self, 'GUI Style', 'GUI style change will be applied on next Brick Viewer start.', QMessageBox.Ok) def auto_search_for_updates_changed(self): config.set_auto_search_for_updates(self.checkbox_auto_search_for_updates.isChecked()) if self.checkbox_auto_search_for_updates.isChecked(): self.enable_auto_search_for_updates() else: self.disable_auto_search_for_updates() def remove_all_device_infos(self): for device_info in infos.get_device_infos(): self.remove_device_info(device_info.uid) def remove_device_info(self, uid): tab_id = self.tab_for_uid(uid) device_info = infos.get_info(uid) device_info.plugin.stop_plugin() device_info.plugin.destroy_plugin() if tab_id >= 0: self.tab_widget.removeTab(tab_id) # ensure that the widget gets correctly destroyed. otherwise QWidgets # tend to leak as Python is not able to collect their PyQt object tab_window = device_info.tab_window device_info.tab_window = None # If we reboot the RED Brick, the tab_window sometimes is # already None here if tab_window != None: tab_window.hide() tab_window.setParent(None) plugin = device_info.plugin device_info.plugin = None if plugin != None: plugin.hide() plugin.setParent(None) infos.remove_info(uid) def reset_view(self): self.tab_widget.setCurrentIndex(0) self.remove_all_device_infos() self.update_tree_view() def do_disconnect(self): self.auto_reconnects = 0 self.label_auto_reconnects.hide() self.red_session_losts = 0 self.label_red_session_losts.hide() self.reset_view() async_next_session() # force garbage collection, to ensure that all plugin related objects # got destroyed before disconnect is called. this is especially # important for the RED Brick plugin because its relies on releasing # the the RED Brick API objects in the __del__ method as a last resort # to avoid leaking object references. but this only works if garbage # collection is done before disconnect is called gc.collect() try: self.ipcon.disconnect() except: pass def do_authenticate(self, is_auto_reconnect): if not self.checkbox_authentication.isChecked(): return True try: secret = self.edit_secret.text() # Try to encode the secret, as only ASCII chars are allowed. Don't save the result, as the IP Connection does the same. secret.encode('ascii') except: self.do_disconnect() QMessageBox.critical(self, 'Connection', 'Authentication secret cannot contain non-ASCII characters.', QMessageBox.Ok) return False self.ipcon.set_auto_reconnect(False) # don't auto-reconnect on authentication error try: self.ipcon.authenticate(secret) except: self.do_disconnect() if is_auto_reconnect: extra = ' after auto-reconnect' else: extra = '' QMessageBox.critical(self, 'Connection', 'Could not authenticate' + extra + '. Check secret and ensure ' + 'authentication for Brick Daemon is enabled.', QMessageBox.Ok) return False self.ipcon.set_auto_reconnect(True) return True def flashing_clicked(self): if self.flashing_window is None: self.flashing_window = FlashingWindow(self) self.flashing_window.show() self.flashing_window.tab_widget.setCurrentWidget(self.flashing_window.tab_updates) self.flashing_window.update_version_info() def advanced_clicked(self): if self.advanced_window is None: self.advanced_window = AdvancedWindow(self) self.advanced_window.show() def data_logger_clicked(self): if self.data_logger_window is None: self.data_logger_window = DataLoggerWindow(self, self.host_infos) self.data_logger_window.show() def connect_error(self): self.setDisabled(False) self.button_connect.setText("Connect") QMessageBox.critical(self, 'Connection', 'Could not connect. Please check host, check ' + 'port and ensure that Brick Daemon is running.') def connect_clicked(self): if self.ipcon.get_connection_state() == IPConnection.CONNECTION_STATE_DISCONNECTED: self.last_host = self.combo_host.currentText() self.setDisabled(True) self.button_connect.setText("Connecting...") async_call(self.ipcon.connect, (self.last_host, self.spinbox_port.value()), None, self.connect_error) else: self.do_disconnect() def item_activated(self, index): index = self.tree_view_proxy_model.mapToSource(index) position_index = index.sibling(index.row(), 2) if position_index.isValid() and position_index.data().startswith('Ext'): index = index.parent() uid_index = index.sibling(index.row(), 1) if uid_index.isValid(): self.show_plugin(uid_index.data()) def show_brick_update(self, url_part): if self.flashing_window is None: self.flashing_window = FlashingWindow(self) self.flashing_window.show() self.flashing_window.update_version_info() self.flashing_window.show_brick_update(url_part) def show_bricklet_update(self, parent_uid, port): if self.flashing_window is None: self.flashing_window = FlashingWindow(self) self.flashing_window.show() self.flashing_window.update_version_info() self.flashing_window.show_bricklet_update(parent_uid, port) def show_extension_update(self, master_uid): if self.flashing_window is None: self.flashing_window = FlashingWindow(self) self.flashing_window.show() self.flashing_window.update_version_info() self.flashing_window.show_extension_update(master_uid) def show_red_brick_update(self): text = "To update the RED Brick Image, please follow the instructions " + \ "<a href=https://www.tinkerforge.com/en/doc/Hardware/Bricks/RED_Brick.html#red-brick-copy-image>here</a>." QMessageBox.information(self, "RED Brick Update", text) def create_tab_window(self, device_info, ipcon): tab_window = TabWindow(self.tab_widget, device_info.name, self.untab) tab_window._info = device_info tab_window.add_callback_on_tab(lambda index: self.ipcon.get_connection_state() == IPConnection.CONNECTION_STATE_PENDING and \ self.tab_widget.setTabEnabled(index, False), 'main_window_disable_tab_if_connection_pending') layout = QVBoxLayout(tab_window) info_bars = [QHBoxLayout(), QHBoxLayout()] # uid info_bars[0].addWidget(QLabel('UID:')) label = QLabel('{0}'.format(device_info.uid)) label.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard) info_bars[0].addWidget(label) info_bars[0].addSpacerItem(QSpacerItem(20, 1, QSizePolicy.Preferred)) # firmware version label_version_name = QLabel('Version:') label_version = QLabel('Querying...') button_update = QPushButton(QIcon(self.button_update_pixmap_normal), 'Update') button_update.installEventFilter(self) if isinstance(device_info, infos.BrickREDInfo): button_update.clicked.connect(self.show_red_brick_update) elif device_info.type == 'brick': button_update.clicked.connect(lambda: self.show_brick_update(device_info.url_part)) elif device_info.type == 'bricklet': button_update.clicked.connect(lambda: self.show_bricklet_update(device_info.connected_uid, device_info.position)) if not device_info.plugin.has_custom_version(label_version_name, label_version): label_version_name.setText('FW Version:') label_version.setText(infos.get_version_string(device_info.plugin.firmware_version)) info_bars[0].addWidget(label_version_name) info_bars[0].addWidget(label_version) info_bars[0].addWidget(button_update) button_update.hide() tab_window.button_update = button_update info_bars[0].addSpacerItem(QSpacerItem(20, 1, QSizePolicy.Preferred)) # timeouts info_bars[0].addWidget(QLabel('Timeouts:')) label_timeouts = QLabel('0') info_bars[0].addWidget(label_timeouts) info_bars[0].addSpacerItem(QSpacerItem(1, 1, QSizePolicy.Expanding)) # connected uid if device_info.connected_uid != '0': info_bars[1].addWidget(QLabel('Connected to:')) button = QToolButton() button.setText(device_info.connected_uid) button.clicked.connect(lambda: self.show_plugin(device_info.connected_uid)) device_info.plugin.button_parent = button info_bars[1].addWidget(button) info_bars[1].addSpacerItem(QSpacerItem(20, 1, QSizePolicy.Preferred)) # position info_bars[1].addWidget(QLabel('Position:')) label_position = QLabel('{0}'.format(device_info.position.title())) device_info.plugin.label_position = label_position info_bars[1].addWidget(label_position) info_bars[1].addSpacerItem(QSpacerItem(1, 1, QSizePolicy.Expanding)) # configs configs = device_info.plugin.get_configs() def config_changed(combobox): i = combobox.currentIndex() if i < 0: return combobox.itemData(i).trigger() if len(configs) > 0: for cfg in configs: if cfg[1] != None: combobox = QComboBox() for i, item in enumerate(cfg[2]): combobox.addItem(item.text(), item) item.triggered.connect(functools.partial(combobox.setCurrentIndex, i)) combobox.currentIndexChanged.connect(functools.partial(config_changed, combobox)) info_bars[cfg[0]].addWidget(QLabel(cfg[1])) info_bars[cfg[0]].addWidget(combobox) elif len(cfg[2]) > 0: checkbox = QCheckBox(cfg[2][0].text()) cfg[2][0].toggled.connect(checkbox.setChecked) checkbox.toggled.connect(cfg[2][0].setChecked) info_bars[cfg[0]].addWidget(checkbox) # actions actions = device_info.plugin.get_actions() if len(actions) > 0: for action in actions: if action[1] != None: button = QPushButton(action[1]) menu = QMenu() for item in action[2]: menu.addAction(item) button.setMenu(menu) elif len(action[2]) > 0: button = QPushButton(action[2][0].text()) button.clicked.connect(action[2][0].trigger) info_bars[action[0]].addWidget(button) def more_clicked(button, info_bar): visible = button.text().replace('&', '') == 'More' # remove &s, they mark the buttons hotkey if visible: button.setText('Less') else: button.setText('More') for i in range(info_bar.count()): widget = info_bar.itemAt(i).widget() if widget != None: widget.setVisible(visible) more_button = QPushButton('More') more_button.clicked.connect(lambda: more_clicked(more_button, info_bars[1])) info_bars[0].addWidget(more_button) for i in range(info_bars[1].count()): widget = info_bars[1].itemAt(i).widget() if widget != None: widget.hide() layout.addLayout(info_bars[0]) layout.addLayout(info_bars[1]) line = QFrame() line.setObjectName("MainWindow_line") line.setFrameShape(QFrame.HLine) line.setFrameShadow(QFrame.Sunken) device_info.plugin.label_timeouts = label_timeouts device_info.plugin.label_version = label_version device_info.plugin.layout().setContentsMargins(0, 0, 0, 0) layout.addWidget(line) if device_info.plugin.has_comcu: device_info.plugin.widget_bootloader = COMCUBootloader(ipcon, device_info) device_info.plugin.widget_bootloader.hide() layout.addWidget(device_info.plugin.widget_bootloader) layout.addWidget(device_info.plugin, 1) return tab_window def tab_move(self, event): # visualize rearranging of tabs (if allowed by tab_widget) if self.tab_widget.isMovable(): if event.type() == QEvent.MouseButtonPress and event.button() & Qt.LeftButton: QApplication.setOverrideCursor(QCursor(Qt.SizeHorCursor)) elif event.type() == QEvent.MouseButtonRelease and event.button() & Qt.LeftButton: QApplication.restoreOverrideCursor() return False def untab(self, tab_index): tab = self.tab_widget.widget(tab_index) tab.untab() tab._info.plugin.start_plugin() self.tab_widget.setCurrentIndex(0) def connect_on_return(self, event): if event.type() == QEvent.KeyPress and (event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter): self.connect_clicked() return True return False def eventFilter(self, source, event): if source is self.tab_widget.tabBar(): return self.tab_move(event) if source is self.combo_host or source is self.spinbox_port or source is self.edit_secret: return self.connect_on_return(event) if isinstance(source, QPushButton) and event.type() == QEvent.Enter: source.setIcon(QIcon(self.button_update_pixmap_hover)) elif isinstance(source, QPushButton) and event.type() == QEvent.Leave: source.setIcon(QIcon(self.button_update_pixmap_normal)) return False def tab_for_uid(self, uid): for index in range(1, self.tab_widget.count()): try: if self.tab_widget.widget(index)._info.uid == uid: return index except: pass return -1 def show_plugin(self, uid): device_info = infos.get_info(uid) if device_info == None: return index = self.tab_for_uid(uid) tab_window = device_info.tab_window if index > 0 and self.tab_widget.isTabEnabled(index): self.tab_widget.setCurrentIndex(index) QApplication.setActiveWindow(tab_window) tab_window.show() tab_window.activateWindow() tab_window.raise_() def cb_enumerate(self, uid, connected_uid, position, hardware_version, firmware_version, device_identifier, enumeration_type): if self.ipcon.get_connection_state() != IPConnection.CONNECTION_STATE_CONNECTED: # ignore enumerate callbacks that arrived after the connection got closed return if enumeration_type in [IPConnection.ENUMERATION_TYPE_AVAILABLE, IPConnection.ENUMERATION_TYPE_CONNECTED]: device_info = infos.get_info(uid) something_changed_ref = [False] # If the enum_type is CONNECTED, the bricklet was restarted externally. # The plugin could now be in an inconsistent state. if enumeration_type == IPConnection.ENUMERATION_TYPE_CONNECTED and device_info is not None: if device_info.connected_uid != connected_uid: # Fix connections if bricklet was connected to another brick. parent_info = infos.get_info(device_info.connected_uid) if parent_info is not None: parent_info.connections.remove((device_info.position, device_info)) self.show_status("Hot plugging is not supported! Please reset the brick {} and restart brick viewer.".format(device_info.connected_uid)) device_info.reverse_connection = connected_uid elif device_info.position != position: # Bricklet was connected to the same brick, but to another port self.show_status("Hot plugging is not supported! Please reset the brick {} and restart brick viewer.".format(device_info.connected_uid)) # If the plugin is not running, pause will do nothing, so it is always save to call it. # The plugin will be (unconditionally) resumed later, as resume also only does something # if it was paused before (e.g. here). if device_info.plugin is not None: device_info.plugin.pause_plugin() if device_info == None: if device_identifier == BrickMaster.DEVICE_IDENTIFIER: device_info = infos.BrickMasterInfo() elif device_identifier == BrickRED.DEVICE_IDENTIFIER: device_info = infos.BrickREDInfo() elif hat_brick_supported and device_identifier == BrickHAT.DEVICE_IDENTIFIER: device_info = infos.BrickHATInfo() elif hat_zero_brick_supported and device_identifier == BrickHATZero.DEVICE_IDENTIFIER: device_info = infos.BrickHATZeroInfo() elif device_identifier == BrickletIsolator.DEVICE_IDENTIFIER: device_info = infos.BrickletIsolatorInfo() elif '0' <= position <= '9': device_info = infos.BrickInfo() something_changed_ref[0] = True else: device_info = infos.BrickletInfo() position = position.lower() def set_device_info_value(name, value): if getattr(device_info, name) != value: setattr(device_info, name, value) something_changed_ref[0] = True infos.get_infos_changed_signal().emit(device_info.uid) set_device_info_value('uid', uid) set_device_info_value('connected_uid', connected_uid) set_device_info_value('position', position) set_device_info_value('hardware_version', hardware_version) if device_identifier != BrickRED.DEVICE_IDENTIFIER: set_device_info_value('firmware_version_installed', firmware_version) set_device_info_value('device_identifier', device_identifier) set_device_info_value('enumeration_type', enumeration_type) # Update connections and reverse_connection with new device for info in infos.get_device_infos(): if info == device_info: continue def add_to_connections(info_to_add, connected_info): connected_info.connections.append((info_to_add.position, info_to_add)) info_to_add.reverse_connection = connected_info something_changed_ref[0] = True infos.get_infos_changed_signal().emit(connected_info.uid) if info.uid != '' and info.uid == device_info.connected_uid: if device_info in info.connections_values(): #Device was already connected, but to another port info.connections = [(pos, i) for pos, i in info.connections if i.uid != device_info.uid] if device_info not in info.connections_get(device_info.position): add_to_connections(device_info, info) if info.connected_uid != '' and info.connected_uid == device_info.uid: if info in device_info.connections_values(): #Device was already connected, but to another port device_info.connections = [(pos, i) for pos, i in device_info.connections if i.uid != info.uid] if info not in device_info.connections_get(info.position): add_to_connections(info, device_info) if device_info.plugin == None: plugin = self.plugin_manager.create_plugin_instance(device_identifier, self.ipcon, device_info) device_info.tab_window = self.create_tab_window(device_info, self.ipcon) device_info.tab_window.setWindowFlags(Qt.Widget) device_info.tab_window.tab() infos.add_info(device_info) something_changed_ref[0] = True if device_identifier == BrickRED.DEVICE_IDENTIFIER and isinstance(plugin, RED): plugin.get_image_version_async() plugin.get_bindings_versions_async() # The plugin was paused before if it was reconnected. device_info.plugin.resume_plugin() if something_changed_ref[0]: self.update_tree_view() elif enumeration_type == IPConnection.ENUMERATION_TYPE_DISCONNECTED: self.remove_device_tab(uid) def remove_device_tab(self, uid): for device_info in infos.get_device_infos(): if device_info.uid == uid: self.tab_widget.setCurrentIndex(0) self.remove_device_info(device_info.uid) if isinstance(device_info, infos.DeviceInfo): to_delete = [] for idx, tup in enumerate(device_info.connections): port, info = tup if info.uid == uid: to_delete.append(idx) for idx in to_delete: del device_info.connections[idx] self.update_tree_view() def hack_to_remove_red_brick_tab(self, red_brick_uid): for device_info in infos.get_device_infos(): if device_info.uid == red_brick_uid: self.tab_widget.setCurrentIndex(0) self.remove_device_info(device_info.uid) self.red_session_losts += 1 self.label_red_session_losts.setText('RED Brick Session Loss Count: {0}'.format(self.red_session_losts)) self.label_red_session_losts.show() break self.update_tree_view() def cb_connected(self, connect_reason): self.disconnect_times = [] self.update_ui_state() if connect_reason == IPConnection.CONNECT_REASON_REQUEST: self.setDisabled(False) self.auto_reconnects = 0 self.label_auto_reconnects.hide() self.red_session_losts = 0 self.label_red_session_losts.hide() self.ipcon.set_auto_reconnect(True) index = self.combo_host.findText(self.last_host) if index >= 0: self.combo_host.removeItem(index) host_info = self.host_infos[index] del self.host_infos[index] self.host_infos.insert(0, host_info) else: index = self.combo_host.currentIndex() host_info = self.host_infos[index].duplicate() host_info.host = self.last_host self.host_infos.insert(0, host_info) self.combo_host.insertItem(-1, self.last_host) self.combo_host.setCurrentIndex(0) while self.combo_host.count() > config.HOST_INFO_COUNT: self.combo_host.removeItem(self.combo_host.count() - 1) if not self.do_authenticate(False): return try: self.ipcon.enumerate() except: self.update_ui_state() elif connect_reason == IPConnection.CONNECT_REASON_AUTO_RECONNECT: self.auto_reconnects += 1 self.label_auto_reconnects.setText('Auto-Reconnect Count: {0}'.format(self.auto_reconnects)) self.label_auto_reconnects.show() if not self.do_authenticate(True): return try: self.ipcon.enumerate() except: self.update_ui_state() else: try: self.ipcon.enumerate() except: self.update_ui_state() def cb_disconnected(self, disconnect_reason): if disconnect_reason == IPConnection.DISCONNECT_REASON_REQUEST: self.auto_reconnects = 0 self.label_auto_reconnects.hide() self.red_session_losts = 0 self.label_red_session_losts.hide() if disconnect_reason == IPConnection.DISCONNECT_REASON_REQUEST or not self.ipcon.get_auto_reconnect(): self.update_ui_state() elif len(self.disconnect_times) >= 3 and self.disconnect_times[-3] < time.time() + 1: self.disconnect_times = [] self.ipcon.set_auto_reconnect(False) self.update_ui_state() self.reset_view() QMessageBox.critical(self, 'Connection', 'Stopped automatic reconnecting due to multiple connection errors in a row.') else: self.disconnect_times.append(time.time()) self.update_ui_state(IPConnection.CONNECTION_STATE_PENDING) def set_tree_view_defaults(self): self.tree_view_model.setHorizontalHeaderLabels(self.tree_view_model_labels) self.tree_view.expandAll() self.tree_view.setColumnWidth(0, 280) self.tree_view.setColumnWidth(1, 70) self.tree_view.setColumnWidth(2, 90) self.tree_view.setColumnWidth(3, 105) self.tree_view.setColumnWidth(4, 105) self.tree_view.setExpandsOnDoubleClick(False) self.tree_view.setSortingEnabled(True) self.tree_view.header().setSortIndicator(2, Qt.AscendingOrder) def update_ui_state(self, connection_state=None): # FIXME: need to call processEvents() otherwise get_connection_state() # might return the wrong value QApplication.processEvents() if connection_state is None: connection_state = self.ipcon.get_connection_state() self.button_flashing.setDisabled(False) if connection_state == IPConnection.CONNECTION_STATE_DISCONNECTED: self.button_connect.setText('Connect') self.combo_host.setDisabled(False) self.spinbox_port.setDisabled(False) self.checkbox_authentication.setDisabled(False) self.edit_secret.setDisabled(False) self.button_advanced.setDisabled(True) elif connection_state == IPConnection.CONNECTION_STATE_CONNECTED: self.button_connect.setText("Disconnect") self.combo_host.setDisabled(True) self.spinbox_port.setDisabled(True) self.checkbox_authentication.setDisabled(True) self.edit_secret.setDisabled(True) self.update_advanced_window() # restart all pause plugins for info in infos.get_device_infos(): info.plugin.resume_plugin() elif connection_state == IPConnection.CONNECTION_STATE_PENDING: self.button_connect.setText('Abort Pending Automatic Reconnect') self.combo_host.setDisabled(True) self.spinbox_port.setDisabled(True) self.checkbox_authentication.setDisabled(True) self.edit_secret.setDisabled(True) self.button_advanced.setDisabled(True) self.button_flashing.setDisabled(True) # pause all running plugins for info in infos.get_device_infos(): info.plugin.pause_plugin() enable = connection_state == IPConnection.CONNECTION_STATE_CONNECTED for i in range(1, self.tab_widget.count()): self.tab_widget.setTabEnabled(i, enable) for device_info in infos.get_device_infos(): device_info.tab_window.setEnabled(enable) QApplication.processEvents() def update_tree_view(self): self.tree_view_model.setHorizontalHeaderLabels(self.tree_view_model_labels) self.tab_widget.tabBar().setTabButton(0, QTabBar.RightSide, None) sis = self.tree_view.header().sortIndicatorSection() sio = self.tree_view.header().sortIndicatorOrder() self.tree_view_model.clear() def get_row(info): replacement = '0.0.0' is_red_brick = isinstance(info, infos.BrickREDInfo) if is_red_brick or info.url_part == 'wifi_v2': replacement = "Querying..." elif info.type == "extension": replacement = "" fw_version = infos.get_version_string(info.firmware_version_installed, replace_unknown=replacement, is_red_brick=is_red_brick) uid = info.uid if info.type != "extension" else '' row = [QStandardItem(info.name), QStandardItem(uid), QStandardItem(info.position.title()), QStandardItem(fw_version)] updateable = info.firmware_version_installed != (0, 0, 0) and info.firmware_version_installed < info.firmware_version_latest if is_red_brick: old_updateable = updateable for binding in info.bindings_infos: updateable |= binding.firmware_version_installed != (0, 0, 0) \ and binding.firmware_version_installed < binding.firmware_version_latest updateable |= info.brickv_info.firmware_version_installed != (0, 0, 0) \ and info.brickv_info.firmware_version_installed < info.brickv_info.firmware_version_latest \ and not info.firmware_version_installed < (1, 14, 0) # Hide Brickv update if image is too old. # There are bindings/brickv updates but there is no image update red_brick_binding_update_only = not old_updateable and updateable else: red_brick_binding_update_only = False if updateable: self.tree_view_model.setHorizontalHeaderLabels(self.tree_view_model_labels + ['Update']) row.append(QStandardItem( infos.get_version_string(info.firmware_version_latest, is_red_brick=is_red_brick) + ("+" if red_brick_binding_update_only else ""))) self.tab_widget.tabBar().setTabButton(0, QTabBar.RightSide, self.update_tab_button) self.update_tab_button.show() for item in row: item.setFlags(item.flags() & ~Qt.ItemIsEditable) if updateable: item.setData(QBrush(QColor(255, 160, 55)), Qt.BackgroundRole) return row def recurse_on_device(info, insertion_point): row = get_row(info) insertion_point.appendRow(row) for child in info.connections_values(): recurse_on_device(child, row[0]) if info.can_have_extension: for extension in info.extensions.values(): if extension is None: continue ext_row = get_row(extension) row[0].appendRow(ext_row) for info in infos.get_device_infos(): # If a device has a reverse connection, it will be handled as a child of another top-level brick. if info.reverse_connection is not None: continue recurse_on_device(info, self.tree_view_model) self.set_tree_view_defaults() self.tree_view.header().setSortIndicator(sis, sio) self.update_advanced_window() self.delayed_refresh_updates_timer.start() def update_advanced_window(self): self.button_advanced.setEnabled(len(infos.get_brick_infos()) > 0) def delayed_refresh_updates(self): self.delayed_refresh_updates_timer.stop() if self.flashing_window is not None and self.flashing_window.isVisible(): self.flashing_window.refresh_update_tree_view() def show_status(self, message, icon='warning', message_id=''): self.setStatusBar(None) if icon != 'none': icon_dict = { 'warning': 'warning-icon-16.png', } icon_label = QLabel() icon_label.setPixmap(load_pixmap(icon_dict[icon])) self.statusBar().addWidget(icon_label) message_label = QLabel(message) message_label.setOpenExternalLinks(True) self.statusBar().addWidget(message_label, 1) self.last_status_message_id = message_id def hide_status(self, message_id): if self.last_status_message_id == message_id: self.setStatusBar(None) def fw_versions_fetched(self, firmware_info): if isinstance(firmware_info, int): if firmware_info > 0: if firmware_info == 1: message = 'Update information could not be downloaded from tinkerforge.com.<br/>' + \ 'Is your computer connected to the Internet?' else: message = ("Update information on tinkerforge.com is malformed " + "(error code {0}).<br/>Please report this error to " + "<a href='mailto:[email protected]'>[email protected]</a>.").format(firmware_info) self.show_status(message, message_id='fw_versions_fetched_error') infos.reset_latest_fws() else: self.hide_status('fw_versions_fetched_error') infos.update_latest_fws(firmware_info) self.update_tree_view()
class ProtocolDesignWidget(QWidget, LoadUIFileMixin): # todo - temp to let other parts of program know when protocol running. Program object itself should reside # inside main window. programStarted = pyqtSignal() def __init__(self, image_stack, intensity_mask, labjack, stimulus_points, selected_stimulus_points, stimulus_widget, workspace, parent=None): super().__init__(parent) # self.fio_mappings = fio_mappings self.image_stack = image_stack self._intensity_mask = intensity_mask self.labjack = labjack self.program = None self.protocol_sequence = selected_stimulus_points self.stimulus_points = stimulus_points self.stimulus_points_dialog = None self.stimulus_sequence_thread = None self.stimulus_sequence_worker = None self.stimulus_widget = stimulus_widget self.workspace = workspace uic.loadUi(views.get('ProtocolDesignView.ui'), self) self.stimulus_points_dialog = StimulusPointsDialog( model=self.stimulus_points, parent=self) self.execute_loop_widget = ExecuteLoopWidget(parent=self) self.inputLayout.addWidget(self.execute_loop_widget) self.program_scroll_area_widget = ProgramScrollAreaWidget(self) self.program_scroll_area_widget.set_model(self.protocol_sequence) self.programScrollArea.setWidget(self.program_scroll_area_widget) self.chooseFromInput.setRange(0, 0) self.patterns = [IncrementByOnePattern, RandomPattern] self.pattern_icons = [] for pattern in self.patterns: self.patternComboBox.insertItem(self.patternComboBox.count(), pattern.icon(), pattern.name, pattern) self.fio4Mapping.setText(self.labjack.fio4.label) self.fio5Mapping.setText(self.labjack.fio5.label) self.fio6Mapping.setText(self.labjack.fio6.label) self.fio7Mapping.setText(self.labjack.fio7.label) self.protocol_sequence.random_seed = self.randomSeedSpinBox.value() self.connect() def connect(self): self.execute_loop_widget.loop.valueChanged.connect( self.on_ExecuteLoopValue_valueChanged) self.execute_loop_widget.execute.pressed.connect(self.on_execute) self.laserInput.toggled.connect(lambda checked: self.waitInput. setChecked(False) if checked else None) self.pmtInput.toggled.connect(lambda checked: self.waitInput. setChecked(False) if checked else None) self.syncInput.toggled.connect(lambda checked: self.waitInput. setChecked(False) if checked else None) self.labjack.fio4.labelChanged.connect( lambda text: self.fio4Mapping.setText(text)) self.labjack.fio5.labelChanged.connect( lambda text: self.fio5Mapping.setText(text)) self.labjack.fio6.labelChanged.connect( lambda text: self.fio6Mapping.setText(text)) self.labjack.fio7.labelChanged.connect( lambda text: self.fio7Mapping.setText(text)) self.protocol_sequence.randomSeedChanged.connect( lambda new_seed: self.randomSeedSpinBox.setValue(new_seed)) self.stimulus_points_dialog.stimulus_points_pattern_selected.connect( self.add_stimulus_point_list) self.stimulus_points.dataChanged.connect( lambda: self.chooseFromInput.setRange( 1, self.stimulus_points.rowCount())) self.stimulus_points.rowsRemoved.connect( lambda: self.on_clearListButton_pressed()) self.workspace.dataLoaded.connect(self.on_workspace_dataLoaded) def add_stimulus_point_list(self, stimulus_points_indices): if not stimulus_points_indices: return last_row = self.protocol_sequence.rowCount() selected_stimulus_points = [ self.stimulus_points.points[i] for i in stimulus_points_indices ] if self.protocol_sequence.insertRow(self.protocol_sequence.rowCount()): model_index = self.protocol_sequence.index(last_row, 0) self.protocol_sequence.setData(model_index, value=selected_stimulus_points, role=Qt.EditRole) def clear_inputs(self): self.laserInput.setChecked(False) self.pmtInput.setChecked(False) self.syncInput.setChecked(False) self.waitInput.setChecked(False) self.durationInput.setValue(0.0) @pyqtSlot() def on_addProtocolElementButton_pressed(self): laser = self.get_input('laser') pmt = self.get_input('pmt') sync = self.get_input('sync') wait = self.get_input('wait') duration = self.get_input('duration') self.protocol_sequence.add_element(self.select_stimulus_points(), laser, pmt, sync, wait, duration) loop_count = self.execute_loop_widget.loop.value() self.update_generated_sequence_views(loop_count=loop_count) self.clear_inputs() @pyqtSlot() def on_execute(self): if len(self.protocol_sequence) == 0: message_boxes.warning( self, title="Empty Protocol!", text="No elements have been added to the protocol!") return if self.stimulus_sequence_thread and self.stimulus_sequence_thread.isRunning( ): if self.stimulus_sequence_thread.isInterruptionRequested(): return if message_boxes.question( self, title="Busy", text="Program is still executing, would you like to abort?" ): self.stimulus_sequence_thread.requestInterruption() self.execute_loop_widget.execute.setText('Aborting') return problems = [] if not self.stimulus_widget.isVisible(): problems.append("Stimulus window is not visible.") if not self.labjack.is_connected: problems.append("LabJack not found.") if problems: text = "The following problems were found:\n\n" for problem in problems: text += problem + "\n" text += "\nWould you like to continue?" if not message_boxes.question(self, "Problems Found", text): return if self.labjack.is_connected: self.labjack.clear() self.stimulus_sequence_thread = QThread() self.stimulus_sequence_thread.setObjectName('Program Thread') # if self.stimulus_widget.isVisible(): # self.program.generate_image_set() self.stimulus_sequence_worker = ExecuteProtocolSequenceWorker( program=self.program, labjack=self.labjack) self.stimulus_sequence_worker.moveToThread( self.stimulus_sequence_thread) self.stimulus_sequence_worker. \ elementChanged.connect(lambda points: self.stimulus_widget.scene().display_points(points)) self.stimulus_sequence_worker.loop_progress.connect( self.execute_loop_widget.update_progress_bar) self.stimulus_sequence_worker.interrupted.connect( self.stimulus_sequence_thread.quit) self.stimulus_sequence_worker.finished.connect( self.on_stimulus_sequence_finished) self.stimulus_sequence_worker.finished.connect( self.stimulus_sequence_thread.quit) if self.labjack.is_connected: self.stimulus_sequence_worker.finished.connect( lambda: self.labjack.clear()) self.stimulus_sequence_thread.started.connect(self.on_thread_start) self.stimulus_sequence_thread.started.connect( self.stimulus_sequence_worker.run) self.stimulus_sequence_thread.finished.connect(self.on_thread_stop) self.stimulus_sequence_thread.finished.connect( self.stimulus_sequence_worker.deleteLater) self.stimulus_sequence_thread.start() self.programStarted.emit() # @pyqtSlot() # def on_abort_complete(self): # self.on_t @pyqtSlot() def on_thread_start(self): self.labjack.check_device = False self.execute_loop_widget.execute.setText('Abort') @pyqtSlot() def on_thread_stop(self): self.labjack.check_device = True try: self.labjack.clear() except DeviceException: pass self.stimulus_widget.image = None self.execute_loop_widget.execute.setText('Execute') @pyqtSlot(int) def on_patternComboBox_currentIndexChanged(self, index): self.stimulus_points_dialog.pattern = self.pattern() self.update_generated_sequence_views( pattern=self.pattern(), loop_count=self.execute_loop_widget.loop.value()) @pyqtSlot(str) def on_workspace_dataLoaded(self, loaded_from): for e in self.protocol_sequence.loaded_in_protocol['protocol']: stimulus_points = [] for point in e['stimulus_points']: stimulus_point = self.stimulus_points[ point['stimulus_point_index']] stimulus_points.append( SelectedStimulusPoint(stimulus_point=stimulus_point, pattern=point['pattern'])) self.protocol_sequence.add_element(stimulus_points=stimulus_points, laser=e['laser'], pmt=e['pmt'], sync=e['sync'], wait=e['wait'], duration=e['duration']) self.protocol_sequence.pattern = self.protocol_sequence.loaded_in_protocol[ 'pattern'] self.patternComboBox.setCurrentIndex( self.patternComboBox.findText(self.protocol_sequence.pattern.name)) def keyPressEvent(self, event): if event.key() == Qt.Key_Delete: selection = self.stimulus_points.selectionModel() model_indices = selection.selectedRows() [ self.protocol_sequence.removeRow(model_index.row() - i) for i, model_index in enumerate(model_indices) ] else: super().keyPressEvent(event) @pyqtSlot(int) def on_ExecuteLoopValue_valueChanged(self, value): self.update_generated_sequence_views(loop_count=value) @pyqtSlot(int) def on_patternComboBox_currentIndexChanged(self, index): pattern = self.patternComboBox.currentData() self.stimulus_points_dialog.pattern = pattern self.protocol_sequence.pattern = pattern self.update_generated_sequence_views() @pyqtSlot(int, int) def on_protocol_elementChanged(self, loop, iteration): self.stimulus_widget.image = self.program.get_image(loop, iteration) @pyqtSlot() def on_selectStimulusPointsButton_pressed(self): self.stimulus_points_dialog.show() @pyqtSlot(int) def on_randomInput_stateChanged(self, state): self.chooseFromInput.setEnabled(state) @pyqtSlot(int) def on_randomSeedSpinBox_valueChanged(self, value): self.protocol_sequence.random_seed = value @pyqtSlot() def on_stimulus_sequence_finished(self): self.stimulus_widget.image = None @pyqtSlot(bool) def on_waitInput_toggled(self, checked): if checked: self.laserInput.setChecked(False) self.pmtInput.setChecked(False) self.syncInput.setChecked(False) self.durationInput.setValue(0.00) @pyqtSlot() def on_clearListButton_pressed(self): self.protocol_sequence.removeRows(0, self.protocol_sequence.rowCount()) self.update_generated_sequence_views(loop_count=0) self.stimulus_widget.scene().display_points([]) def pattern(self): return self.patternComboBox.currentData() @pyqtSlot() def on_sendPreviewButton_pressed(self): iteration = self.iterationPreviewSpinBox.value() loop = self.loopNumberPreviewSpinBox.value() self.stimulus_widget.open() self.stimulus_widget.scene().display_points([ p.stimulus_point for p in self.program[loop][iteration].stimulus_points ]) def select_stimulus_points(self): selected_stimulus_points = [] if self.randomInput.isChecked(): chosen_points = self.stimulus_points.sample( choose=self.chooseFromInput.value()) selected_stimulus_points = [ SelectedStimulusPoint(stimulus_point=point, pattern=NormalPattern) for point in chosen_points ] elif self.stimulus_points_dialog: selected_stimulus_points = self.stimulus_points_dialog.selected_stimulus_points( ) return selected_stimulus_points def update_generated_sequence_views(self, loop_count=None): if not loop_count: loop_count = self.execute_loop_widget.loop.value() # todo - don't remake this every time, fix and move to main window and generate sequence in place if self.protocol_sequence.protocol: self.program = Program( image_stack=self.image_stack, initial_sequence=self.protocol_sequence.protocol, inter_loop_delay=self.execute_loop_widget.loop_delay.value(), random_seed=self.protocol_sequence.random_seed, stimulus_points=self.stimulus_points, stimulus_widget=self.stimulus_widget, pattern=self.protocol_sequence.pattern, loop_count=loop_count) self.program.generate() models = [ ProtocolSequence(sequence=sequence) for sequence in self.program.program ] self.program_scroll_area_widget.set_number_of_widgets( models=models, number=loop_count)
class main_principal(QWidget): fecha_actual = datetime.datetime.now( ) # datetime es la libreria que nos ayuda con las fechas horas minutos y hsta segundos #en este caso nos referimos a que nos de la fecha actual contador_hilo = 1 cursor = None lista_hilos = [] def banner_anonymous(self): #banner un bannner es solo un mensaje de texto #el \32 es para indicarle que queremos un espacio en blanco #puedes ver que hay muchos \32 en este paratado eso es debido a que #se necesitan espacios para imprimir bien el banner self.textarea_proceso.append( "$$$$$\32$\32\32\32\32$\32\32$$$$$\32$\32\32\32\32\32$$$$$$$\32\32\32\32\32\32######\32#####\32######\32##\32\32\32##" ) self.textarea_proceso.append( "$\32\32\32\32\32\32\32\32$\32\32\32\32$\32\32\32\32\32$\32\32\32\32$\32\32\32\32\32$\32\32\32\32\32$\32\32\32\32\32$\32\32\32\32\32\32#\32\32\32\32\32\32#" ) self.textarea_proceso.append( "$$$$$\32\32\32$\32\32\32\32\32$$$$$\32$\32\32\32\32\32$\32\32\32\32\32$||||||\32\32\32\32\32######\32#####\32######\32#\32\32\32\32\32#" ) self.textarea_proceso.append( "$\32\32\32\32\32\32$\32\32$\32\32\32$\32\32\32\32\32$\32\32\32\32\32$\32\32\32\32\32\32$\32\32\32\32\32\32\32\32\32\32\32#|#\32\32\32\32\32#\32\32\32\32#\32#\32\32\32\32\32#" ) self.textarea_proceso.append( "$$$$$\32$\32\32\32\32$\32\32$\32\32\32\32\32$$$$$\32$$$$$$$\32\32\32\32\32\32######\32#\32\32\32\32\32#\32\32\32\32#\32#\32\32\32\32\32#" ) self.textarea_proceso.append( "Autor: Aldair Martinez Alias Hans Krammler Junior" ) # Aqui declaramos mi nombre jajaj XD def spam_uno_a_uno(self): self.banner_anonymous() print(self.fecha_actual) def spam_uno_a_varios(self): self.banner_anonymous() print(self.fecha_actual) print(Fore.GREEN + "HAZ ELEGIDO ENVIAR DESDE TU COREO A OTROS CORREO") def mensaje(self): print("SE TERMINO EL HILO") contador_hilo_aux = 0 lista_hilos = [] workers = [] threads = [] def incializa_hilos(self): if self.contador_hilo == 1: self.thread = QThread() self.thread.setObjectName("Hilo_1") self.worker = Worker(self.contador_hilo, self.textarea_proceso, self.label_proceso_verificacion, None) self.worker.moveToThread(self.thread) self.thread.setTerminationEnabled(True) self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.terminado) self.thread.finished.connect( lambda: self.verificar_resultado_hilo(self.thread)) self.worker.progressed.connect( lambda: self.reportProgress(self.worker)) self.worker.siguiente.connect( lambda: self.siguiente_hilo(self.thread)) self.thread.finished.connect(self.thread.quit) self.lista_hilos.append(self.thread) return self.lista_hilos else: self.lista_hilos = [] self.threads.append(QThread()) self.threads[self.contador_hilo_aux].setObjectName("Hilo_2") if self.contador_hilo == 2: print("Aqui tambien funciona") self.workers.append( Worker(self.contador_hilo, self.textarea_proceso, None, self.label_proceso_verificacion2)) elif self.contador_hilo == 3: self.workers.append( Worker(self.contador_hilo, self.textarea_proceso, None, self.label_proceso_verificacion3)) self.workers[self.contador_hilo_aux].moveToThread( self.threads[self.contador_hilo_aux]) self.threads[self.contador_hilo_aux].started.connect( self.workers[self.contador_hilo_aux].run) self.workers[self.contador_hilo_aux].finished.connect( self.terminado) print("Pasando terminado") self.threads[self.contador_hilo_aux].finished.connect( lambda: self.verificar_resultado_hilo(self.threads[ self.contador_hilo_aux])) print("Pasando verificar resultado") self.workers[self.contador_hilo_aux].progressed.connect( lambda: self.reportProgress(self.workers[self.contador_hilo_aux ])) print("Pasando reportprogress") self.workers[self.contador_hilo_aux].siguiente.connect( lambda: self.siguiente_hilo(self.threads[self.contador_hilo_aux ])) self.threads[self.contador_hilo_aux].finished.connect( self.threads[self.contador_hilo_aux].quit) print("Pasando quit") self.lista_hilos.append(self.threads[self.contador_hilo_aux]) return self.lista_hilos return None def siguiente_hilo(self, hilo): if self.contador_hilo == 1: if hilo.isRunning() == False: if hilo.isFinished() == True: print("si funciono") hilo.finished.connect(hilo.deleteLater) hilo.finished.connect(hilo.terminate) self.contador_hilo = self.contador_hilo + 1 self.incializa_hilos()[0].start() elif self.contador_hilo > 1: if self.get_verificado() == -1: return -1 elif self.get_verificado() == -2: return -2 else: return -3 if hilo.isRunning() == False: if hilo.isFinished() == True: self.contador_hilo = self.contador_hilo + 1 self.contador_hilo_aux = self.contador_hilo_aux + 1 self.incializa_hilos()[0].start() return 0 cursor = None estado_hilo_x = 0 lista_verificado = [] def set_verificado(self, estado): self.estado_hilo_x = estado def get_verificado(self): self.lista_verificado.append(self.estado_hilo_x) print("get.lista.verificado" + str(self.lista_verificado[0])) return int(self.lista_verificado[0]) def terminado(self): if self.contador_hilo == 1: print("TRABAJO TERMINADO") self.thread.finished.emit() elif self.contador_hilo > 1: self.threads[self.contador_hilo_aux].finished.emit() def verificar_resultado_hilo(self, thread): if self.contador_hilo == 1: if self.thread.isRunning() == False: print("EL HILO 1 HA TERMINADO SU TAREA") if self.thread.isFinished() == True: print("EL HILI 1 TERMINO CON EXITO") self.worker.progressed.emit() elif self.contador_hilo > 1: print(str(self.contador_hilo_aux)) if thread.isRunning() == False: print("EL HILO" + str(self.contador_hilo_aux) + "ESTA CORRIENDO") if thread.isFinished() == True: print("EL HILO" + str(self.contador_hilo_aux) + "HA TERMINADO") self.workers[self.contador_hilo_aux].progressed.emit() porcentaje = 10 _i = 0 lista_progreso = ["INICIANDO HILO DE EJECUCION......"] def reportProgress(self, worker): print(self.contador_hilo) if self.contador_hilo == 1: print("REPORTANDO HILO") #print("POSICION ACTUAL DEL CURSOR"+str(int(self.textarea_proceso.textCursor().position()))) worker.siguiente.emit() else: if self.contador_hilo == 2: email = self.line_edit_user_email.text() password = self.line_edit_user_password.text() longitud_email = len(email) longitud_password = len(password) if longitud_email == 0 or longitud_password == 0: self.set_verificado(-1) elif self.contador_hilo == 3: email = self.line_edit_user_email.text() email_to_list = list(email) contador = 1 for _x in len(email): if email_to_list[_x] == "@": contador = contador + 1 if contador > 1: self.set_verificado(-2) elif self.contador_hilo == 4: contador = 1 email_dominio = email.split(".") textarea_proceso.append("DIVIDIENDO TU EMAIL--->" + email_dominio) longitud_dominio_email = len(email_dominio) textarea_proceso.append( "VERIFICANDO COINCIDENCIA DE DOMINIO DE EMAIL....") dominios = [ "gmail.com", "outlook.com", "outlook.es", "hotmail.com", "hotmail.mx", "hotmail.es" ] longitud_dominio_gmail = len(dominios[0]) longitud_dominio_outlook_com = len(dominios[1]) longitud_dominio_outlook_es = len(dominios[2]) longitud_dominio_hotmail_com = len(dominios[3]) longitud_dominio_hotmail_mx = len(dominios[4]) longitud_dominio_hotmail_es = len(dominios[5]) for _i in range(longitud_dominio_email): for _j in (range(longitud_dominio_gmail) or range(longitud_dominio_outlook_com) or range(longitud_dominio_outlook_es) or range(longitud_dominio_hotmail_com) or range(longitud_dominio_hotmail_mx) or range(longitud_dominio_hotmail_es)): if _j < longitud_dominio_gmail: if dominios[0][_j] == email_dominio[1][_j]: print(dominios[0][j]) contador = contador + 1 elif _j < longitud_dominio_outlook_com: if dominios[1][_j] == email_dominio[1][_j]: contador = contador + 1 elif _j < longitud_dominio_outlook_es: if dominios[2][_j] == email_dominio[1][_j]: contador = contador + 1 elif _j < longitud_dominio_hotmail_com: if dominios[3][_j] == email_dominio[1][_j]: contador = contador + 1 elif _j < longitud_dominio_hotmail_mx: if dominios[4][_j] == email_dominio[1][_j]: contador = contador + 1 elif _j < longitud_dominio_hotmail_es: if dominios[5][_j] == email_dominio[1][_j]: contador = contador + 1 if contador > longitud_dominio_gmail: return -3 else: contador = 1 print("AQUI MEN") posicion_textarea = len(self.lista_progreso[self._i]) self.cursor = self.textarea_proceso.textCursor() print("CURSOR ACTUAL" + str(self.cursor)) print("POSICION CURSOR ACTUAL++" + str(self.cursor.position())) if self.cursor.atEnd() == True: print("FIN DEL CURSOR True") print("TEXTO SELECCIONADO ANTES" + self.cursor.selectedText()) posicion_inicial = (self.cursor.position() - 1) - (posicion_textarea - 1) + 1 print("posicion inicial" + str(posicion_inicial)) time.sleep(0.4) self.cursor.setPosition(posicion_inicial, QTextCursor.KeepAnchor) print("TEXTO SELECCIONADO AHORA" + self.cursor.selectedText()) print("POSICION CURSOR ACTUAL--" + str(self.cursor.position())) self.progreso = self.cursor.selectedText() + str( self.porcentaje) self.porcentaje = self.porcentaje + 10 self.textarea_proceso.append(self.progreso) print("POSICION CURSOR ACTUAL++" + str(self.cursor.position())) self._i = self._i + 1 worker.siguiente.emit() def verificar_opcion(self): pass def validar_oprciones(self): if self.checkbox_opcion_spam_uno_a_uno.isChecked( ) == True and self.checkbox_opcion_spam_uno_a_varios.isChecked(): self.label_estado_proceso.setText( Fore.RED + "LO SIENTO NO PUEDES SELECCIONAR DOS OPCIONES A LA VEZ") return False else: return True def escannear_red(self): scan = nmap.PortScanner() informacion_sistema = os.uname() def presionado(self): resultado_validar_opciones = self.validar_oprciones() if resultado_validar_opciones == True: self.inciar_hilo_principal() self.resultado_verificacion_email = 0 if self.resultado_verificacion_email == 0: print("PASANDO AL SIGUIENTE HILO DE EJECUCION") if self.resultado_verificacion_email == 1: if self.checkbox_opcion_spam_uno_a_uno.isChecked() == True: print( "LAS OCPIONES SELECCIONADAAS EN LOS CASILLA SON VALIDAS" ) else: if self.resultado_verificacion_email == -1 or self.resultado_verificacion_email == -2 or self.resultado_verificacion_email == -3: if self.resultado_verificacion_email == -1: self.label_estado_proceso.setText( "NO HAS INGRESADO NADA EN EL PRIMER CAMPO") self.textarea_proceso.append( "ERROR NO HAS INGRESADO NADA EN EL PRIMER CAMPO") elif self.resultado_verificacion_email == -2: self.label_estado_proceso.setText( "HAS MAS DE UN ARROBA EN TU CORREO") self.textarea_proceso.append( "ERROR HAY MAS DE UN ARROBA EN TU CORREO") elif self.resultado_verificacion_email == -3: self.label_estado_proceso.setText( "TERMINACION DE CORREO NO VALIDA") else: self.textarea_proceso.append( "INTENTE DE NUEVO RELLENAR LOS CAMPOS ADECUADAMENTE") def __init__(self, parent=None): #Parte de intefaz grafica super().__init__(parent) self.color = QColor(0, 0, 0, 0.5) self.setGeometry(550, 100, 1500, 500) self.pallette = QPalette(self.color) self.pallette.setColor(QPalette.Text, Qt.cyan) self.titulo_ventana = "ENVIO DE CORREO PARA HACER SPAM" self.setWindowTitle(self.titulo_ventana) self.setStyleSheet( "border-color: cyan; border-style: dashed; border-width: 2px; color:white" ) self.setPalette(self.pallette) self.checkbox_opcion_spam_uno_a_uno = QCheckBox( "REALIZAR SPAM UNO A UN CORREO") self.checkbox_opcion_spam_uno_a_uno.clicked.connect( self.spam_uno_a_uno) self.checkbox_opcion_spam_uno_a_varios = QCheckBox( "REALIZAR SPAM UNO A VARIOS COOREOS") self.checkbox_opcion_spam_uno_a_varios.clicked.connect( self.spam_uno_a_varios) self.label_user_email = QLabel("Ingresa tu direccion de correo: ") self.label_user_email.setFont(QFont("Times", 14, QFont.Bold, True)) self.label_user_email.setStyleSheet("color: white") self.line_edit_user_email = QLineEdit(self) self.line_edit_user_email.setPlaceholderText( "Ingresa tu direccion de cooreo:") self.line_edit_user_email.setFont(QFont("Times", 14, QFont.Bold, True)) self.line_edit_user_email.setStyleSheet("color : black") self.label_user_password = QLabel("Ingresa tu password de correo") self.label_user_password.setFont(QFont("Times", 14, QFont.Bold, True)) self.label_user_password.setStyleSheet("color: white") self.line_edit_user_password = QLineEdit(self) self.line_edit_user_password.setPlaceholderText( "Ingresa tu password de tu correo:") self.line_edit_user_password.setFont( QFont("Times", 14, QFont.Bold, True)) self.line_edit_user_password.setStyleSheet("color : black") self.label_victima_email = QLabel( "Ingresa direccion de correo destinatario: ") self.label_victima_email.setFont(QFont("Times", 14, QFont.Bold, True)) self.label_victima_email.setStyleSheet("color: white") self.line_edit_victima_email = QLineEdit(self) self.line_edit_victima_email.setPlaceholderText( "Ingresa tu direccion de cooreo:") self.line_edit_victima_email.setFont( QFont("Times", 14, QFont.Bold, True)) self.line_edit_victima_email.setStyleSheet("color : black") self.label_user_asunto = QLabel("Ingresa el asunto ") self.label_user_asunto.setFont(QFont("Times", 14, QFont.Bold, True)) self.label_user_asunto.setStyleSheet("color: white") self.line_edit_user_asunto = QLineEdit(self) self.line_edit_user_asunto.setPlaceholderText( "Ingresa tu direccion de cooreo:") self.line_edit_user_asunto.setFont(QFont("Times", 14, QFont.Bold, True)) self.line_edit_user_asunto.setStyleSheet("color : black") self.pushbutton_enviar = QPushButton("ENVIAR SPAM") self.pushbutton_enviar.setFont(QFont("Times", 14, QFont.Bold, True)) self.pushbutton_enviar.setPalette(self.pallette) self.label_estado_proceso = QLabel("") self.label_estado_proceso.setText("ESPERANDO DATOS....") self.label_proceso = QLabel("Estado verificacion email y password") self.label_proceso_verificacion = QLabel("") self.label_proceso2 = QLabel( "Estado verificacion arroba de email origen y destino") self.label_proceso_verificacion2 = QLabel("") self.label_proceso3 = QLabel( "Estado verificacion terminacion correo origen y destino") self.label_proceso_verificacion3 = QLabel("") self.combobox_dominios_from = QComboBox() self.combobox_dominios_from.addItem("Gmail.com") self.combobox_dominios_from.insertSeparator(1) self.combobox_dominios_from.addItem("Outlook.com") self.combobox_dominios_from.addItem("Outlook.es") self.combobox_dominios_from.insertSeparator(4) self.combobox_dominios_from.addItem("Hotmail.com") self.combobox_dominios_from.addItem("Hotmail.es") self.combobox_dominios_from.addItem("Hotmail.mx") self.combobox_dominios_from.setStyleSheet( "background-color:black; color: cyan") self.combobox_dominios_to = QComboBox() self.combobox_dominios_to.addItem("Gmail.com") self.combobox_dominios_to.insertSeparator(1) self.combobox_dominios_to.addItem("Outlook.com") self.combobox_dominios_to.addItem("Outlook.es") self.combobox_dominios_to.insertSeparator(4) self.combobox_dominios_to.addItem("Hotmail.com") self.combobox_dominios_to.addItem("Hotmail.es") self.combobox_dominios_to.addItem("Hotmail.mx") self.combobox_dominios_to.setStyleSheet( "background-color:black; color: cyan") self.combobox_dominios_from.currentTextChanged.connect( self.verificar_opcion) self.combobox_dominios_to.currentTextChanged.connect( self.verificar_opcion) self.pushbutton_enviar.clicked.connect(self.presionado) self.layout_main = QHBoxLayout(self) self.layout_principal = QVBoxLayout() self.layout_principal.addWidget(self.label_user_email) self.layout_principal.addWidget(self.line_edit_user_email) self.layout_principal.addWidget(self.label_user_password) self.layout_principal.addWidget(self.line_edit_user_password) self.layout_principal.addWidget(self.label_victima_email) self.layout_principal.addWidget(self.line_edit_victima_email) self.layout_principal.addWidget(self.label_user_asunto) self.layout_principal.addWidget(self.line_edit_user_asunto) self.layout_principal.addWidget(self.checkbox_opcion_spam_uno_a_uno) self.layout_principal.addWidget(self.checkbox_opcion_spam_uno_a_varios) self.layout_principal.addWidget(self.pushbutton_enviar) self.layout_principal.addWidget(self.label_estado_proceso) self.layout_principal.addWidget(self.combobox_dominios_from) self.layout_principal.addWidget(self.combobox_dominios_to) self.layout_principal.addWidget(self.label_proceso) self.layout_principal.addWidget(self.label_proceso_verificacion) self.layout_principal.addWidget(self.label_proceso2) self.layout_principal.addWidget(self.label_proceso_verificacion2) self.layout_principal.addWidget(self.label_proceso3) self.layout_principal.addWidget(self.label_proceso_verificacion3) self.layout_secundario = QVBoxLayout() self.textarea_proceso = QTextEdit() self.textarea_proceso.setPlaceholderText("ESPERANDO PARA PROCESAR") self.textarea_proceso.setFont(QFont("Times", 14, QFont.Bold, True)) self.textarea_proceso.setGeometry(0, 0, 500, 400) self.textarea_proceso.setStyleSheet( "color: yellow; background-color:black; border-style:solid") self.textarea_proceso.setReadOnly(False) self.textarea_proceso.setOverwriteMode(QTextEdit.WidgetWidth) self.layout_secundario.addWidget(self.textarea_proceso) self.layout_main.addLayout(self.layout_principal, 4) self.layout_main.addLayout(self.layout_secundario, 4) def inciar_hilo_principal(self): self.incializa_hilos()[0].start() return 0
def btn_event(self): if (self.btn_flag == 0): self.btn_flag = 1 self.startBtn.setIcon(QtGui.QIcon('stop.png')) self.startBtn.setIconSize(QtCore.QSize(100, 100)) self.__threads = [] # create a recorder object record = Recorder() record_thread = QThread() record_thread.setObjectName('record thread') self.__threads.append( (record_thread, record)) # need to store worker too otherwise will be gc'd record.moveToThread(record_thread) # get progress messages from worker: record.sig_step.connect(self.on_recorder_worker_step) # control worker: self.sig_recorder_abort_workers.connect(record.abort) # get read to start worker:record record_thread.started.connect(record.work) record_thread.start( ) # this will emit 'started' and start thread's event loop else: self.btn_flag = 0 self.startBtn.setIcon(QtGui.QIcon('record.png')) self.startBtn.setIconSize(QtCore.QSize(100, 100)) self.sig_recorder_abort_workers.emit() print('Asking each worker to abort') for record_thread, record in self.__threads: # note nice unpacking by Python, avoids indexing record_thread.quit( ) # this will quit **as soon as thread event loop unblocks** record_thread.wait( ) # <- so you need to wait for it to *actually* quit self.startBtn.setDisabled(True) self.statusLabel.show() inference = Inference() inference_thread = QThread() inference_thread.setObjectName('Inference Thread') self.__threads.append( (inference_thread, inference)) # need to store worker too otherwise will be gc'd inference.moveToThread(inference_thread) inference.sig_result.connect(self.on_inference_worker_end) self.sig_inference_abort_workers.connect(inference.abort) inference_thread.started.connect(inference.work) inference_thread.start( ) # this will emit 'started' and start thread's event loop
def get_metadata(self, gal=None): metadata_spinner = misc.Spinner(self) def move_md_spinner(): metadata_spinner.update_move( QPoint( self.pos().x()+self.width()-65, self.pos().y()+self.toolbar.height()+55)) metadata_spinner.set_text("Metadata") metadata_spinner.set_size(55) metadata_spinner.move(QPoint(self.pos().x()+self.width()-65, self.pos().y()+self.toolbar.height()+55)) self.move_listener.connect(move_md_spinner) thread = QThread(self) thread.setObjectName('App.get_metadata') fetch_instance = fetch.Fetch() if gal: if not isinstance(gal, list): galleries = [gal] else: galleries = gal else: if app_constants.CONTINUE_AUTO_METADATA_FETCHER: galleries = [g for g in self.manga_list_view.gallery_model._data if not g.exed] else: galleries = self.manga_list_view.gallery_model._data if not galleries: self.notification_bar.add_text('Looks like we\'ve already gone through all galleries!') return None fetch_instance.galleries = galleries self.notification_bar.begin_show() fetch_instance.moveToThread(thread) def done(status): self.notification_bar.end_show() fetch_instance.deleteLater() if not isinstance(status, bool): galleries = [] for tup in status: galleries.append(tup[0]) class GalleryContextMenu(QMenu): app_instance = self def __init__(self, parent=None): super().__init__(parent) show_in_library_act = self.addAction('Show in library') show_in_library_act.triggered.connect(self.show_in_library) def show_in_library(self): viewer = self.app_instance.manga_list_view index = viewer.find_index(self.gallery_widget.gallery.id, True) if index: self.app_instance.manga_table_view.scroll_to_index(index) self.app_instance.manga_list_view.scroll_to_index(index) g_popup = io_misc.GalleryPopup(('Fecthing metadata for these galleries failed.'+ ' Check happypanda.log for details.', galleries), self, menu=GalleryContextMenu()) #errors = {g[0].id: g[1] for g in status} #for g_item in g_popup.get_all_items(): # g_item.setToolTip(errors[g_item.gallery.id]) g_popup.graphics_blur.setEnabled(False) close_button = g_popup.add_buttons('Close')[0] close_button.clicked.connect(g_popup.close) fetch_instance.GALLERY_PICKER.connect(self._web_metadata_picker) fetch_instance.GALLERY_EMITTER.connect(self.manga_list_view.replace_edit_gallery) fetch_instance.AUTO_METADATA_PROGRESS.connect(self.notification_bar.add_text) thread.started.connect(fetch_instance.auto_web_metadata) fetch_instance.FINISHED.connect(done) fetch_instance.FINISHED.connect(metadata_spinner.close) fetch_instance.FINISHED.connect(lambda: self.move_listener.disconnect(move_md_spinner)) thread.finished.connect(thread.deleteLater) thread.start() metadata_spinner.show()
class MainModel: status_bar_progress = None status_bar_thread = None status_bar_listener = None def __init__(self, videos, videos_limit): super().__init__() self.logger = create_logger(__name__) self.videos_limit = videos_limit self.playview_videos_limit = videos_limit self.videos = videos self.subfeed_videos = [] self.subfeed_videos_removed = {} self.playview_videos = [] self.playview_videos_removed = {} self.download_progress_signals = [] self.logger.info("Creating listeners and threads") self.playback_grid_view_listener = PlaybackGridViewListener(self) self.playback_grid_thread = QThread() self.playback_grid_thread.setObjectName('playback_grid_thread') self.playback_grid_view_listener.moveToThread( self.playback_grid_thread) self.playback_grid_thread.start() self.subfeed_grid_view_listener = SubfeedGridViewListener(self) self.subfeed_grid_thread = QThread() self.subfeed_grid_thread.setObjectName('subfeed_grid_thread') self.subfeed_grid_view_listener.moveToThread(self.subfeed_grid_thread) self.subfeed_grid_thread.start() self.database_listener = DatabaseListener(self) self.db_thread = QThread() self.db_thread.setObjectName('db_thread') self.database_listener.moveToThread(self.db_thread) self.db_thread.start() self.main_window_listener = MainWindowListener(self) self.main_w_thread = QThread() self.main_w_thread.setObjectName('main_w_thread') self.main_window_listener.moveToThread(self.main_w_thread) self.main_w_thread.start() self.download_handler = DownloadViewListener(self) self.download_thread = QThread() self.download_thread.setObjectName('download_thread') self.download_handler.moveToThread(self.download_thread) self.download_thread.start() if read_config("Play", "yt_file_path", literal_eval=False): self.yt_dir_listener = YoutubeDirListener(self) self.yt_dir_thread = QThread() self.yt_dir_thread.setObjectName('yt_dir_thread') self.yt_dir_listener.moveToThread(self.yt_dir_thread) self.yt_dir_thread.start() else: self.logger.warning( "No youtube file path provided, directory listener is disabled" ) self.yt_dir_listener = None def hide_video_item(self, video, widget_id): """ Hides the video from a View. :param widget_id: Identifier for which View called this function. :param video: :return: """ if widget_id: self.logger.debug("Hiding video item: {}".format(video)) if widget_id == SUBFEED_VIEW_ID: self.subfeed_videos_removed.update( {video: remove_video(self.subfeed_videos, video)}) elif widget_id == PLAYBACK_VIEW_ID: self.playview_videos_removed.update( {video: remove_video(self.playview_videos, video)}) else: self.logger.error("Unable to hide video item: widget_id was None!") def unhide_video_item(self, video, widget_id): """ Shows a video previously hidden from view. :param widget_id: Identifier for which View called this function. :param video: :return: """ if widget_id: self.logger.debug("Un-hiding video item: {}".format(video.title)) if widget_id == SUBFEED_VIEW_ID: add_video(self.subfeed_videos, video, self.subfeed_videos_removed[video]) self.subfeed_videos_removed.pop(video) elif widget_id == PLAYBACK_VIEW_ID: add_video(self.playview_videos, video, self.playview_videos_removed[video]) self.playview_videos_removed.pop(video) else: self.logger.error("Unable to hide video item: widget_id was None!") def update_subfeed_videos_from_db(self, filtered=True): """ Updates Subscription feed video list from DB. Updates the filter with values in model and calls static database (read operation) function which doesn't have direct access to the model object. :param filtered: :return: """ self.logger.info("Getting newest stored videos from DB") # FIXME: only does filtered videos if filtered: show_downloaded = read_config('SubFeed', 'show_downloaded') show_dismissed = read_config('GridView', 'show_dismissed') update_filter = () if not show_downloaded: update_filter += (~Video.downloaded, ) if not show_dismissed: update_filter += (~Video.discarded, ) self.subfeed_videos = get_db_videos_subfeed(self.videos_limit, filters=update_filter) self.subfeed_grid_view_listener.videosChanged.emit() else: self.videos = get_db_videos_subfeed(self.videos_limit, filtered) def update_subfeed_videos_from_remote( self, filtered=True, refresh_type=LISTENER_SIGNAL_NORMAL_REFRESH): """ Updates Subscription feed video list from a remote source (likely YouTube API). :param filtered: Whether to filter out certain videos based on set boolean attributes. :param refresh_type: A signal determining whether it is a Normal (int(0)) or Deep (int(1)) refresh. This kwarg is not used here, but passed on to the refresh function. :return: """ self.logger.info("Reloading and getting newest videos from YouTube") try: if filtered: show_downloaded = not read_config('SubFeed', 'show_downloaded') show_dismissed = not read_config('GridView', 'show_dismissed') self.subfeed_videos = refresh_and_get_newest_videos( self.videos_limit, progress_listener=self.status_bar_listener, refresh_type=refresh_type, filter_discarded=show_dismissed, filter_downloaded=show_downloaded) self.subfeed_grid_view_listener.videosChanged.emit() else: self.videos = refresh_and_get_newest_videos( self.videos_limit, filtered, self.status_bar_listener, refresh_type=refresh_type) except SaneAbortedOperation as exc_sao: # FIXME: Send aborted operation signal back up to GUI self.logger.critical( "A SaneAbortedOperation exc occurred while updating subfeed from remote! Exceptions:" ) for exc in exc_sao.exceptions: self.logger.exception(str(exc), exc_info=exc_sao) def update_playback_videos_from_db(self): """ Update the PlaybackView video list from DB. Note: There's no remote update for PlaybackView like there is for SubfeedView. :return: """ update_filter = self.filter_playback_view_videos() update_sort = self.sort_playback_view_videos() self.playview_videos = get_db_videos_playback( self.playview_videos_limit, filters=update_filter, sort_method=update_sort) self.playback_grid_view_listener.videosChanged.emit() def create_progressbar_on_statusbar(self, parent): """ Creates a QProgressBar and attaches it to the status bar. :param parent: :return: """ self.status_bar_progress = QProgressBar(parent=parent) palette = QPalette(self.status_bar_progress.palette()) palette.setColor(QPalette.Highlight, QColor(24, 68, 91).lighter(200)) self.status_bar_progress.setPalette(palette) self.status_bar_listener = ProgressBarListener( self, self.status_bar_progress) self.status_bar_thread = QThread() self.status_bar_thread.setObjectName('status_bar_thread') self.status_bar_listener.moveToThread(self.status_bar_thread) self.status_bar_thread.start() return self.status_bar_progress def update_thumbnails(self): """ Updates thumbnails for downloaded and filtered videos. :return: """ videos = [] videos.extend(self.playview_videos) videos.extend(self.subfeed_videos) self.logger.info( "Updating thumbnails for downloaded and filtered videos") download_thumbnails_threaded(videos) UpdateVideosThread(videos, update_existing=True).start() @staticmethod def filter_playback_view_videos(): """ Applies filters to the PlaybackGridView Videos list based on config. :return: """ show_watched = read_config('GridView', 'show_watched') show_dismissed = read_config('GridView', 'show_dismissed') update_filter = (Video.downloaded, ) if not show_watched: update_filter += (or_(Video.watched == false(), Video.watched == None), ) if not show_dismissed: update_filter += (~Video.discarded, ) return update_filter def sort_playback_view_videos(self): """ Applies a sort-by rule to the PlaybackGridView videos list. update_sort is a tuple of priority sort categories, first element is highest, last is lowest. update_sort += operations requires at least two items on rhs. :return: """ sort_by_ascending_date = read_config('PlaySort', 'ascending_date') sort_by_channel = read_config('PlaySort', 'by_channel') self.logger.info( "Sorting PlaybackGridView Videos: date = {} | channel = {}".format( sort_by_ascending_date, sort_by_channel)) update_sort = (asc(Video.watch_prio), ) # Sort-by ascending date if sort_by_ascending_date: update_sort += (asc(Video.date_downloaded), asc(Video.date_published)) # Sort-by channel name (implied by default: then descending date) if sort_by_channel: update_sort += (desc(Video.channel_title), ) # Sort-by channel name then ascending date # FIXME: Implement handling both sorts toggled if sort_by_channel and sort_by_ascending_date: # update_sort += (asc(Video.channel_title),) self.logger.debug5("By-Channel|By-date update_sort: {}".format( str(update_sort))) for t in update_sort: self.logger.debug5(t.compile(dialect=postgresql.dialect())) # FIXME: workaround for not handling both: disable channel sort if both toggled, and run date sort set_config('PlaySort', 'by_channel', format(not read_config('PlaySort', 'by_channel'))) sort_by_channel = read_config('PlaySort', 'by_channel') update_sort += (asc(Video.date_downloaded), asc(Video.date_published)) # DEFAULT: Sort-by descending date else: update_sort += (desc(Video.date_downloaded), desc(Video.date_published)) self.logger.info( "Sorted PlaybackGridView Videos: date = {} | channel = {}".format( sort_by_ascending_date, sort_by_channel)) return update_sort
class MainModel: status_bar_progress = None status_bar_thread = None status_bar_listener = None def __init__(self, videos, videos_limit): super().__init__() self.logger = create_logger(__name__) self.videos_limit = videos_limit self.downloaded_videos_limit = videos_limit self.videos = videos self.filtered_videos = [] self.downloaded_videos = [] self.download_progress_signals = [] self.logger.info("Creating listeners and threads") self.grid_view_listener = GridViewListener(self) self.grid_thread = QThread() self.grid_thread.setObjectName('grid_thread') self.grid_view_listener.moveToThread(self.grid_thread) self.grid_thread.start() self.database_listener = DatabaseListener(self) self.db_thread = QThread() self.db_thread.setObjectName('db_thread') self.database_listener.moveToThread(self.db_thread) self.db_thread.start() self.main_window_listener = MainWindowListener(self) self.main_w_thread = QThread() self.main_w_thread.setObjectName('main_w_thread') self.main_window_listener.moveToThread(self.main_w_thread) self.main_w_thread.start() self.download_handler = DownloadHandler(self) self.download_thread = QThread() self.download_thread.setObjectName('download_thread') self.download_handler.moveToThread(self.download_thread) self.download_thread.start() if read_config("Play", "yt_file_path", literal_eval=False): self.yt_dir_listener = YtDirListener(self) self.yt_dir_thread = QThread() self.yt_dir_thread.setObjectName('yt_dir_thread') self.yt_dir_listener.moveToThread(self.yt_dir_thread) self.yt_dir_thread.start() else: self.logger.warning( "No youtube file path provided, directory listener is disabled" ) def hide_video_item(self, video): self.logger.debug("Hiding video item: {}".format(video)) remove_video(self.filtered_videos, video) remove_video(self.downloaded_videos, video) def hide_downloaded_video_item(self, video): remove_video(self.downloaded_videos, video) def db_update_videos(self, filtered=True): self.logger.info("Getting newest stored videos from DB") # FIXME: only does filtered videos if filtered: show_downloaded = read_config('SubFeed', 'show_downloaded') show_dismissed = read_config('GridView', 'show_dismissed') update_filter = () if not show_downloaded: update_filter += (~Video.downloaded, ) if not show_dismissed: update_filter += (~Video.discarded, ) self.filtered_videos = get_newest_stored_videos( self.videos_limit, filters=update_filter) self.grid_view_listener.hiddenVideosChanged.emit() else: self.videos = get_newest_stored_videos(self.videos_limit, filtered) def remote_update_videos(self, filtered=True, refresh_type=LISTENER_SIGNAL_NORMAL_REFRESH): self.logger.info("Reloading and getting newest videos from YouTube") if filtered: show_downloaded = not read_config('SubFeed', 'show_downloaded') show_dismissed = not read_config('GridView', 'show_dismissed') self.filtered_videos = refresh_and_get_newest_videos( self.videos_limit, progress_listener=self.status_bar_listener, refresh_type=refresh_type, filter_discarded=show_dismissed, filter_downloaded=show_downloaded) self.grid_view_listener.hiddenVideosChanged.emit() else: self.videos = refresh_and_get_newest_videos( self.videos_limit, filtered, self.status_bar_listener, refresh_type=refresh_type) def new_status_bar_progress(self, parent): self.status_bar_progress = QProgressBar(parent=parent) self.status_bar_listener = ProgressBar(self, self.status_bar_progress) self.status_bar_thread = QThread() self.status_bar_thread.setObjectName('status_bar_thread') self.status_bar_listener.moveToThread(self.status_bar_thread) self.status_bar_thread.start() return self.status_bar_progress def db_update_downloaded_videos(self): update_filter = self.config_get_filter_downloaded() update_sort = self.config_get_sort_downloaded() self.downloaded_videos = get_best_downloaded_videos( self.downloaded_videos_limit, filters=update_filter, sort_method=update_sort) self.grid_view_listener.downloadedVideosChanged.emit() def update_thumbnails(self): videos = [] videos.extend(self.downloaded_videos) videos.extend(self.filtered_videos) self.logger.info( "Updating thumbnails for downloaded and filtered videos") download_thumbnails_threaded(videos) UpdateVideosThread(videos, update_existing=True).start() def config_get_filter_downloaded(self): show_watched = read_config('GridView', 'show_watched') show_dismissed = read_config('GridView', 'show_dismissed') update_filter = (Video.downloaded, ) if not show_watched: update_filter += (or_(Video.watched == false(), Video.watched == None), ) if not show_dismissed: update_filter += (~Video.discarded, ) return update_filter def config_get_sort_downloaded(self): ascending_date = read_config('PlaySort', 'ascending_date') update_sort = (asc(Video.watch_prio), ) if ascending_date: update_sort += (asc(Video.date_downloaded), asc(Video.date_published)) else: update_sort += (desc(Video.date_downloaded), desc(Video.date_published)) return update_sort
def button_import_action(self): """ Slot for buttonImport clicked signal """ self.buttonClose.setEnabled(False) headers = True if not self.checkHeaders.isChecked(): headers = False self.progressBar.setRange(0, 0) # set spinning progressbar if self.selectedFile: # import selected file to contact table if self.selectedTable == "contacts": worker = Worker(1, self.__app) thread = QThread(self) thread.setObjectName("contacts_csv") self.__threads.append((thread, worker)) worker.moveToThread(thread) worker.sig_status.connect(self.on_status) worker.sig_done.connect(self.on_done) try: thread.started.connect( worker.import_contacts_csv(self._contacts, self.selectedFile, header=headers)) thread.start() except TypeError: pass # import selected file to customer table if self.selectedTable == "customers": worker = Worker(2, self.__app) thread = QThread(self) thread.setObjectName("customers_csv") self.__threads.append((thread, worker)) worker.moveToThread(thread) worker.sig_status.connect(self.on_status) worker.sig_done.connect(self.on_done) try: thread.started.connect( worker.import_customers_csv(self._customers, self.selectedFile, header=headers)) thread.start() except TypeError: pass # import selected file to lines table if self.selectedTable == "lines": worker = Worker(4, self.__app) thread = QThread(self) thread.setObjectName("orderlines_csv") self.__threads.append((thread, worker)) worker.moveToThread(thread) worker.sig_status.connect(self.on_status) worker.sig_done.connect(self.on_done) try: thread.started.connect( worker.import_orderlines_csv(self._orderlines, self.selectedFile, header=headers)) thread.start() except TypeError: pass # import selected file to report table if self.selectedTable == "reports": worker = Worker(5, self.__app) thread = QThread(self) thread.setObjectName("reports_csv") self.__threads.append((thread, worker)) worker.moveToThread(thread) worker.sig_status.connect(self.on_status) worker.sig_done.connect(self.on_done) try: thread.started.connect( worker.import_reports_csv( self._employees.employee["employee_id"], self._reports, self.selectedFile, header=headers)) thread.start() except TypeError: pass # import selected file to visit table if self.selectedTable == "visits": worker = Worker(3, self.__app) thread = QThread(self) thread.setObjectName("visits_csv") self.__threads.append((thread, worker)) worker.moveToThread(thread) worker.sig_status.connect(self.on_status) worker.sig_done.connect(self.on_done) try: thread.started.connect( worker.import_visits_csv(self._visits, self.selectedFile, header=headers)) thread.start() except TypeError: pass self.selectedFile = "" self.txtSelectedFile.clear() self.comboImport.removeItem(self.comboImport.currentIndex()) self.comboImport.setCurrentIndex(0) self.buttonImport.setEnabled( False) # disable the button until next file is selected self.buttonBrowse.setEnabled(True) # enable browse button self.buttonClose.setEnabled(True) # enable close button
class GalleryDialog(QWidget): """ A window for adding/modifying gallery. Pass a list of QModelIndexes to edit their data or pass a path to preset path """ SERIES = pyqtSignal(list) SERIES_EDIT = pyqtSignal(list, int) #gallery_list = [] # might want to extend this to allow mass gallery adding def __init__(self, parent=None, arg=None): super().__init__(parent, Qt.Dialog) self.setAttribute(Qt.WA_DeleteOnClose) self.parent_widget = parent log_d('Triggered Gallery Edit/Add Dialog') m_l = QVBoxLayout() self.main_layout = QVBoxLayout() dummy = QWidget(self) scroll_area = QScrollArea(self) scroll_area.setWidgetResizable(True) scroll_area.setFrameStyle(scroll_area.StyledPanel) dummy.setLayout(self.main_layout) scroll_area.setWidget(dummy) m_l.addWidget(scroll_area, 3) final_buttons = QHBoxLayout() final_buttons.setAlignment(Qt.AlignRight) m_l.addLayout(final_buttons) self.done = QPushButton("Done") self.done.setDefault(True) cancel = QPushButton("Cancel") final_buttons.addWidget(cancel) final_buttons.addWidget(self.done) def new_gallery(): self.setWindowTitle('Add a new gallery') self.newUI() self.commonUI() self.done.clicked.connect(self.accept) cancel.clicked.connect(self.reject) if arg: if isinstance(arg, list): self.setWindowTitle('Edit gallery') self.position = arg[0].row() for index in arg: gallery = index.data(Qt.UserRole+1) self.commonUI() self.setGallery(gallery) self.done.clicked.connect(self.accept_edit) cancel.clicked.connect(self.reject_edit) elif isinstance(arg, str): new_gallery() self.choose_dir(arg) else: new_gallery() log_d('GalleryDialog: Create UI: successful') #TODO: Implement a way to mass add galleries #IDEA: Extend dialog in a ScrollArea with more forms... self.setLayout(m_l) self.resize(500,560) frect = self.frameGeometry() frect.moveCenter(QDesktopWidget().availableGeometry().center()) self.move(frect.topLeft()) #self.setAttribute(Qt.WA_DeleteOnClose) self._fetch_inst = fetch.Fetch() self._fetch_thread = QThread(self) self._fetch_thread.setObjectName("GalleryDialog metadata thread") self._fetch_inst.moveToThread(self._fetch_thread) self._fetch_thread.started.connect(self._fetch_inst.auto_web_metadata) def commonUI(self): f_web = QGroupBox("Metadata from the Web") f_web.setCheckable(False) self.main_layout.addWidget(f_web) web_main_layout = QVBoxLayout() web_info = misc.ClickedLabel("Which gallery URLs are supported? (hover)", parent=self) web_info.setToolTip(app_constants.SUPPORTED_METADATA_URLS) web_info.setToolTipDuration(999999999) web_main_layout.addWidget(web_info) web_layout = QHBoxLayout() web_main_layout.addLayout(web_layout) f_web.setLayout(web_main_layout) f_gallery = QGroupBox("Gallery Info") f_gallery.setCheckable(False) self.main_layout.addWidget(f_gallery) gallery_layout = QFormLayout() f_gallery.setLayout(gallery_layout) def basic_web(name): return QLabel(name), QLineEdit(), QPushButton("Get metadata"), QProgressBar() url_lbl, self.url_edit, url_btn, url_prog = basic_web("URL:") url_btn.clicked.connect(lambda: self.web_metadata(self.url_edit.text(), url_btn, url_prog)) url_prog.setTextVisible(False) url_prog.setMinimum(0) url_prog.setMaximum(0) web_layout.addWidget(url_lbl, 0, Qt.AlignLeft) web_layout.addWidget(self.url_edit, 0) web_layout.addWidget(url_btn, 0, Qt.AlignRight) web_layout.addWidget(url_prog, 0, Qt.AlignRight) self.url_edit.setPlaceholderText("Insert supported gallery URLs or just press the button!") url_prog.hide() self.title_edit = QLineEdit() self.author_edit = QLineEdit() author_completer = misc.GCompleter(self, False, True, False) author_completer.setCaseSensitivity(Qt.CaseInsensitive) self.author_edit.setCompleter(author_completer) self.descr_edit = QTextEdit() self.descr_edit.setAcceptRichText(True) self.lang_box = QComboBox() self.lang_box.addItems(app_constants.G_LANGUAGES) self.lang_box.addItems(app_constants.G_CUSTOM_LANGUAGES) self._find_combobox_match(self.lang_box, app_constants.G_DEF_LANGUAGE, 0) tags_l = QVBoxLayout() tag_info = misc.ClickedLabel("How do i write namespace & tags? (hover)", parent=self) tag_info.setToolTip("Ways to write tags:\n\nNormal tags:\ntag1, tag2, tag3\n\n"+ "Namespaced tags:\nns1:tag1, ns1:tag2\n\nNamespaced tags with one or more"+ " tags under same namespace:\nns1:[tag1, tag2, tag3], ns2:[tag1, tag2]\n\n"+ "Those three ways of writing namespace & tags can be combined freely.\n"+ "Tags are seperated by a comma, NOT whitespace.\nNamespaces will be capitalized while tags"+ " will be lowercased.") tag_info.setToolTipDuration(99999999) tags_l.addWidget(tag_info) self.tags_edit = misc.CompleterTextEdit() self.tags_edit.setCompleter(misc.GCompleter(self, False, False)) tags_l.addWidget(self.tags_edit, 3) self.tags_edit.setPlaceholderText("Press Tab to autocomplete (Ctrl + E to show popup)") self.type_box = QComboBox() self.type_box.addItems(app_constants.G_TYPES) self._find_combobox_match(self.type_box, app_constants.G_DEF_TYPE, 0) #self.type_box.currentIndexChanged[int].connect(self.doujin_show) #self.doujin_parent = QLineEdit() #self.doujin_parent.setVisible(False) self.status_box = QComboBox() self.status_box.addItems(app_constants.G_STATUS) self._find_combobox_match(self.status_box, app_constants.G_DEF_STATUS, 0) self.pub_edit = QDateEdit() self.pub_edit.setCalendarPopup(True) self.pub_edit.setDate(QDate.currentDate()) self.path_lbl = misc.ClickedLabel("") self.path_lbl.setWordWrap(True) self.path_lbl.clicked.connect(lambda a: utils.open_path(a, a) if a else None) link_layout = QHBoxLayout() self.link_lbl = QLabel("") self.link_lbl.setWordWrap(True) self.link_edit = QLineEdit() link_layout.addWidget(self.link_edit) link_layout.addWidget(self.link_lbl) self.link_edit.hide() self.link_btn = QPushButton("Modify") self.link_btn.setFixedWidth(50) self.link_btn2 = QPushButton("Set") self.link_btn2.setFixedWidth(40) self.link_btn.clicked.connect(self.link_modify) self.link_btn2.clicked.connect(self.link_set) link_layout.addWidget(self.link_btn) link_layout.addWidget(self.link_btn2) self.link_btn2.hide() gallery_layout.addRow("Title:", self.title_edit) gallery_layout.addRow("Author:", self.author_edit) gallery_layout.addRow("Description:", self.descr_edit) gallery_layout.addRow("Language:", self.lang_box) gallery_layout.addRow("Tags:", tags_l) gallery_layout.addRow("Type:", self.type_box) gallery_layout.addRow("Status:", self.status_box) gallery_layout.addRow("Publication Date:", self.pub_edit) gallery_layout.addRow("Path:", self.path_lbl) gallery_layout.addRow("Link:", link_layout) self.title_edit.setFocus() def resizeEvent(self, event): self.tags_edit.setFixedHeight(event.size().height()//8) self.descr_edit.setFixedHeight(event.size().height()//12.5) return super().resizeEvent(event) def _find_combobox_match(self, combobox, key, default): f_index = combobox.findText(key, Qt.MatchFixedString) if f_index != -1: combobox.setCurrentIndex(f_index) else: combobox.setCurrentIndex(default) def setGallery(self, gallery): "To be used for when editing a gallery" self.gallery = gallery self.url_edit.setText(gallery.link) self.title_edit.setText(gallery.title) self.author_edit.setText(gallery.artist) self.descr_edit.setText(gallery.info) self.tags_edit.setText(utils.tag_to_string(gallery.tags)) self._find_combobox_match(self.lang_box, gallery.language, 2) self._find_combobox_match(self.type_box, gallery.type, 0) self._find_combobox_match(self.status_box, gallery.status, 0) gallery_pub_date = "{}".format(gallery.pub_date).split(' ') try: self.gallery_time = datetime.strptime(gallery_pub_date[1], '%H:%M:%S').time() except IndexError: pass qdate_pub_date = QDate.fromString(gallery_pub_date[0], "yyyy-MM-dd") self.pub_edit.setDate(qdate_pub_date) self.link_lbl.setText(gallery.link) self.path_lbl.setText(gallery.path) def newUI(self): f_local = QGroupBox("Directory/Archive") f_local.setCheckable(False) self.main_layout.addWidget(f_local) local_layout = QHBoxLayout() f_local.setLayout(local_layout) choose_folder = QPushButton("From Directory") choose_folder.clicked.connect(lambda: self.choose_dir('f')) local_layout.addWidget(choose_folder) choose_archive = QPushButton("From Archive") choose_archive.clicked.connect(lambda: self.choose_dir('a')) local_layout.addWidget(choose_archive) self.file_exists_lbl = QLabel() local_layout.addWidget(self.file_exists_lbl) self.file_exists_lbl.hide() def choose_dir(self, mode): """ Pass which mode to open the folder explorer in: 'f': directory 'a': files Or pass a predefined path """ self.done.show() self.file_exists_lbl.hide() if mode == 'a': name = QFileDialog.getOpenFileName(self, 'Choose archive', filter=utils.FILE_FILTER) name = name[0] elif mode == 'f': name = QFileDialog.getExistingDirectory(self, 'Choose folder') elif mode: if os.path.exists(mode): name = mode else: return None if not name: return head, tail = os.path.split(name) name = os.path.join(head, tail) parsed = utils.title_parser(tail) self.title_edit.setText(parsed['title']) self.author_edit.setText(parsed['artist']) self.path_lbl.setText(name) if not parsed['language']: parsed['language'] = app_constants.G_DEF_LANGUAGE l_i = self.lang_box.findText(parsed['language']) if l_i != -1: self.lang_box.setCurrentIndex(l_i) if gallerydb.GalleryDB.check_exists(name): self.file_exists_lbl.setText('<font color="red">Gallery already exists.</font>') self.file_exists_lbl.show() # check galleries gs = 1 if name.endswith(utils.ARCHIVE_FILES): gs = len(utils.check_archive(name)) elif os.path.isdir(name): g_dirs, g_archs = utils.recursive_gallery_check(name) gs = len(g_dirs) + len(g_archs) if gs == 0: self.file_exists_lbl.setText('<font color="red">Invalid gallery source.</font>') self.file_exists_lbl.show() self.done.hide() if app_constants.SUBFOLDER_AS_GALLERY: if gs > 1: self.file_exists_lbl.setText('<font color="red">More than one galleries detected in source! Use other methods to add.</font>') self.file_exists_lbl.show() self.done.hide() def check(self): if len(self.title_edit.text()) is 0: self.title_edit.setFocus() self.title_edit.setStyleSheet("border-style:outset;border-width:2px;border-color:red;") return False elif len(self.author_edit.text()) is 0: self.author_edit.setText("Unknown") if len(self.path_lbl.text()) == 0 or self.path_lbl.text() == 'No path specified': self.path_lbl.setStyleSheet("color:red") self.path_lbl.setText('No path specified') return False return True def set_chapters(self, gallery_object, add_to_model=True): path = gallery_object.path chap_container = gallerydb.ChaptersContainer(gallery_object) metafile = utils.GMetafile() try: log_d('Listing dir...') con = scandir.scandir(path) # list all folders in gallery dir log_i('Gallery source is a directory') log_d('Sorting') chapters = sorted([sub.path for sub in con if sub.is_dir() or sub.name.endswith(utils.ARCHIVE_FILES)]) #subfolders # if gallery has chapters divided into sub folders if len(chapters) != 0: log_d('Chapters divided in folders..') for ch in chapters: chap = chap_container.create_chapter() chap.title = utils.title_parser(ch)['title'] chap.path = os.path.join(path, ch) metafile.update(utils.GMetafile(chap.path)) chap.pages = len(list(scandir.scandir(chap.path))) else: #else assume that all images are in gallery folder chap = chap_container.create_chapter() chap.title = utils.title_parser(os.path.split(path)[1])['title'] chap.path = path metafile.update(utils.GMetafile(path)) chap.pages = len(list(scandir.scandir(path))) except NotADirectoryError: if path.endswith(utils.ARCHIVE_FILES): gallery_object.is_archive = 1 log_i("Gallery source is an archive") archive_g = sorted(utils.check_archive(path)) for g in archive_g: chap = chap_container.create_chapter() chap.path = g chap.in_archive = 1 metafile.update(utils.GMetafile(g, path)) arch = utils.ArchiveFile(path) chap.pages = len(arch.dir_contents(g)) arch.close() metafile.apply_gallery(gallery_object) if add_to_model: self.SERIES.emit([gallery_object]) log_d('Sent gallery to model') def reject(self): if self.check(): msgbox = QMessageBox() msgbox.setText("<font color='red'><b>Noo oniichan! You were about to add a new gallery.</b></font>") msgbox.setInformativeText("Do you really want to discard?") msgbox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) msgbox.setDefaultButton(QMessageBox.No) if msgbox.exec() == QMessageBox.Yes: self.close() else: self.close() def web_metadata(self, url, btn_widget, pgr_widget): self.link_lbl.setText(url) btn_widget.hide() pgr_widget.show() def status(stat): def do_hide(): try: pgr_widget.hide() btn_widget.show() except RuntimeError: pass if stat: do_hide() else: danger = """QProgressBar::chunk { background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0,stop: 0 #FF0350,stop: 0.4999 #FF0020,stop: 0.5 #FF0019,stop: 1 #FF0000 ); border-bottom-right-radius: 5px; border-bottom-left-radius: 5px; border: .px solid black;}""" pgr_widget.setStyleSheet(danger) QTimer.singleShot(3000, do_hide) def gallery_picker(gallery, title_url_list, q): self.parent_widget._web_metadata_picker(gallery, title_url_list, q, self) try: dummy_gallery = self.make_gallery(self.gallery) except AttributeError: dummy_gallery = self.make_gallery(gallerydb.Gallery(), False) if not dummy_gallery: status(False) return None dummy_gallery._g_dialog_url = url self._fetch_inst.galleries = [dummy_gallery] self._disconnect() self._fetch_inst.GALLERY_PICKER.connect(gallery_picker) self._fetch_inst.GALLERY_EMITTER.connect(self.set_web_metadata) self._fetch_inst.FINISHED.connect(status) self._fetch_thread.start() def set_web_metadata(self, metadata): assert isinstance(metadata, gallerydb.Gallery) self.link_lbl.setText(metadata.link) self.title_edit.setText(metadata.title) self.author_edit.setText(metadata.artist) tags = "" lang = ['English', 'Japanese'] self._find_combobox_match(self.lang_box, metadata.language, 2) self.tags_edit.setText(utils.tag_to_string(metadata.tags)) pub_string = "{}".format(metadata.pub_date) pub_date = QDate.fromString(pub_string.split()[0], "yyyy-MM-dd") self.pub_edit.setDate(pub_date) self._find_combobox_match(self.type_box, metadata.type, 0) def make_gallery(self, new_gallery, add_to_model=True, new=False): if self.check(): new_gallery.title = self.title_edit.text() log_d('Adding gallery title') new_gallery.artist = self.author_edit.text() log_d('Adding gallery artist') log_d('Adding gallery path') if new and app_constants.MOVE_IMPORTED_GALLERIES: app_constants.OVERRIDE_MONITOR = True new_gallery.path = utils.move_files(self.path_lbl.text()) else: new_gallery.path = self.path_lbl.text() new_gallery.info = self.descr_edit.toPlainText() log_d('Adding gallery descr') new_gallery.type = self.type_box.currentText() log_d('Adding gallery type') new_gallery.language = self.lang_box.currentText() log_d('Adding gallery lang') new_gallery.status = self.status_box.currentText() log_d('Adding gallery status') new_gallery.tags = utils.tag_to_dict(self.tags_edit.toPlainText()) log_d('Adding gallery: tagging to dict') qpub_d = self.pub_edit.date().toString("ddMMyyyy") dpub_d = datetime.strptime(qpub_d, "%d%m%Y").date() try: d_t = self.gallery_time except AttributeError: d_t = datetime.now().time().replace(microsecond=0) dpub_d = datetime.combine(dpub_d, d_t) new_gallery.pub_date = dpub_d log_d('Adding gallery pub date') new_gallery.link = self.link_lbl.text() log_d('Adding gallery link') if not new_gallery.chapters: log_d('Starting chapters') thread = threading.Thread(target=self.set_chapters, args=(new_gallery,add_to_model), daemon=True) thread.start() thread.join() log_d('Finished chapters') return new_gallery def link_set(self): t = self.link_edit.text() self.link_edit.hide() self.link_lbl.show() self.link_lbl.setText(t) self.link_btn2.hide() self.link_btn.show() def link_modify(self): t = self.link_lbl.text() self.link_lbl.hide() self.link_edit.show() self.link_edit.setText(t) self.link_btn.hide() self.link_btn2.show() def _disconnect(self): try: self._fetch_inst.GALLERY_PICKER.disconnect() self._fetch_inst.GALLERY_EMITTER.disconnect() self._fetch_inst.FINISHED.disconnect() except TypeError: pass def delayed_close(self): if self._fetch_thread.isRunning(): self._fetch_thread.finished.connect(self.close) self.hide() else: self.close() def accept(self): new_gallery = self.make_gallery(gallerydb.Gallery(), new=True) if new_gallery: self.delayed_close() def accept_edit(self): new_gallery = self.make_gallery(self.gallery) #for ser in self.gallery: if new_gallery: self.SERIES_EDIT.emit([new_gallery], self.position) self.delayed_close() def reject_edit(self): self.delayed_close()
def gallery_populate(self, path, validate=False): "Scans the given path for gallery to add into the DB" if len(path) is not 0: data_thread = QThread(self) data_thread.setObjectName("General gallery populate") loading = misc.Loading(self) if not loading.ON: misc.Loading.ON = True fetch_instance = fetch.Fetch() fetch_instance.series_path = path loading.show() def finished(status): def hide_loading(): loading.hide() if status: if len(status) != 0: def add_gallery(gallery_list): def append_to_model(x): self.manga_list_view.gallery_model.insertRows(x, None, len(x)) class A(QObject): done = pyqtSignal() prog = pyqtSignal(int) def __init__(self, obj, parent=None): super().__init__(parent) self.obj = obj self.galleries = [] def add_to_db(self): gui_constants.NOTIF_BAR.begin_show() gui_constants.NOTIF_BAR.add_text("Populating database...") for y, x in enumerate(self.obj): gui_constants.NOTIF_BAR.add_text( "Populating database {}/{}".format(y + 1, len(self.obj)) ) gallerydb.add_method_queue(gallerydb.GalleryDB.add_gallery_return, False, x) self.galleries.append(x) y += 1 self.prog.emit(y) append_to_model(self.galleries) gui_constants.NOTIF_BAR.end_show() self.done.emit() loading.progress.setMaximum(len(gallery_list)) a_instance = A(gallery_list) thread = QThread(self) thread.setObjectName("Database populate") def loading_show(): loading.setText("Populating database.\nPlease wait...") loading.show() def loading_hide(): loading.close() self.manga_list_view.gallery_model.ROWCOUNT_CHANGE.emit() def del_later(): try: a_instance.deleteLater() except NameError: pass a_instance.moveToThread(thread) a_instance.prog.connect(loading.progress.setValue) thread.started.connect(loading_show) thread.started.connect(a_instance.add_to_db) a_instance.done.connect(loading_hide) a_instance.done.connect(del_later) thread.finished.connect(thread.deleteLater) thread.start() data_thread.quit hide_loading() log_i("Populating DB from gallery folder: OK") if validate: gallery_list = misc.GalleryListView(self) gallery_list.SERIES.connect(add_gallery) for ser in status: gallery_list.add_gallery(ser, os.path.split(ser.path)[1]) # self.manga_list_view.gallery_model.populate_data() gallery_list.show() else: add_gallery(status) misc.Loading.ON = False else: log_d("No new gallery was found") loading.setText("No new gallery found") data_thread.quit misc.Loading.ON = False else: log_e("Populating DB from gallery folder: Nothing was added!") loading.setText("<font color=red>Nothing was added. Check happypanda_log for details..</font>") loading.progress.setStyleSheet("background-color:red;") data_thread.quit QTimer.singleShot(10000, loading.close) def fetch_deleteLater(): try: fetch_instance.deleteLater except NameError: pass def a_progress(prog): loading.progress.setValue(prog) loading.setText("Searching for galleries...") fetch_instance.moveToThread(data_thread) fetch_instance.DATA_COUNT.connect(loading.progress.setMaximum) fetch_instance.PROGRESS.connect(a_progress) data_thread.started.connect(fetch_instance.local) fetch_instance.FINISHED.connect(finished) fetch_instance.FINISHED.connect(fetch_deleteLater) data_thread.finished.connect(data_thread.deleteLater) data_thread.start() log_i("Populating DB from gallery folder")
def gallery_populate(self, path, validate=False): "Scans the given path for gallery to add into the DB" if len(path) is not 0: data_thread = QThread(self) data_thread.setObjectName('General gallery populate') loading = misc.Loading(self) self.g_populate_inst = fetch.Fetch() self.g_populate_inst.series_path = path loading.show() def finished(status): def hide_loading(): loading.hide() if status: if len(status) != 0: def add_gallery(gallery_list): def append_to_model(x): self.manga_list_view.sort_model.insertRows(x, None, len(x)) self.manga_list_view.sort_model.init_search( self.manga_list_view.sort_model.current_term) class A(QObject): done = pyqtSignal() prog = pyqtSignal(int) def __init__(self, obj, parent=None): super().__init__(parent) self.obj = obj self.galleries = [] def add_to_db(self): for y, x in enumerate(self.obj): gallerydb.add_method_queue( gallerydb.GalleryDB.add_gallery_return, False, x) self.galleries.append(x) y += 1 self.prog.emit(y) append_to_model(self.galleries) self.done.emit() loading.progress.setMaximum(len(gallery_list)) self.a_instance = A(gallery_list) thread = QThread(self) thread.setObjectName('Database populate') def loading_show(numb): if loading.isHidden(): loading.show() loading.setText('Populating database ({}/{})\nPlease wait...'.format( numb, loading.progress.maximum())) loading.progress.setValue(numb) loading.show() def loading_hide(): loading.close() self.manga_list_view.gallery_model.ROWCOUNT_CHANGE.emit() self.a_instance.moveToThread(thread) self.a_instance.prog.connect(loading_show) thread.started.connect(self.a_instance.add_to_db) self.a_instance.done.connect(loading_hide) self.a_instance.done.connect(self.a_instance.deleteLater) #a_instance.add_to_db() thread.finished.connect(thread.deleteLater) thread.start() #data_thread.quit hide_loading() log_i('Populating DB from gallery folder: OK') if validate: gallery_list = misc.GalleryListView(self) gallery_list.SERIES.connect(add_gallery) for ser in status: if ser.is_archive and app_constants.SUBFOLDER_AS_GALLERY: p = os.path.split(ser.path)[1] if ser.chapters[0].path: pt_in_arch = os.path.split(ser.path_in_archive) pt_in_arch = pt_in_arch[1] or pt_in_arch[0] text = '{}: {}'.format(p, pt_in_arch) else: text = p gallery_list.add_gallery(ser, text) else: gallery_list.add_gallery(ser, os.path.split(ser.path)[1]) #self.manga_list_view.gallery_model.populate_data() gallery_list.update_count() gallery_list.show() else: add_gallery(status) else: log_d('No new gallery was found') loading.setText("No new gallery found") #data_thread.quit else: log_e('Populating DB from gallery folder: Nothing was added!') loading.setText("<font color=red>Nothing was added. Check happypanda_log for details..</font>") loading.progress.setStyleSheet("background-color:red;") data_thread.quit QTimer.singleShot(8000, loading.close) def skipped_gs(s_list): "Skipped galleries" msg_box = QMessageBox(self) msg_box.setIcon(QMessageBox.Question) msg_box.setText('Do you want to view skipped paths?') msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) msg_box.setDefaultButton(QMessageBox.No) if msg_box.exec() == QMessageBox.Yes: list_wid = QTableWidget(self) list_wid.setAttribute(Qt.WA_DeleteOnClose) list_wid.setRowCount(len(s_list)) list_wid.setColumnCount(2) list_wid.setAlternatingRowColors(True) list_wid.setEditTriggers(list_wid.NoEditTriggers) list_wid.setHorizontalHeaderLabels(['Reason', 'Path']) list_wid.setSelectionBehavior(list_wid.SelectRows) list_wid.setSelectionMode(list_wid.SingleSelection) list_wid.setSortingEnabled(True) list_wid.verticalHeader().hide() list_wid.setAutoScroll(False) for x, g in enumerate(s_list): list_wid.setItem(x, 0, QTableWidgetItem(g[1])) list_wid.setItem(x, 1, QTableWidgetItem(g[0])) list_wid.resizeColumnsToContents() list_wid.setWindowTitle('{} skipped paths'.format(len(s_list))) list_wid.setWindowFlags(Qt.Window) list_wid.resize(900,400) list_wid.doubleClicked.connect(lambda i: utils.open_path( list_wid.item(i.row(), 1).text(), list_wid.item(i.row(), 1).text())) list_wid.show() def a_progress(prog): loading.progress.setValue(prog) loading.setText("Preparing galleries...") self.g_populate_inst.moveToThread(data_thread) self.g_populate_inst.DATA_COUNT.connect(loading.progress.setMaximum) self.g_populate_inst.PROGRESS.connect(a_progress) self.g_populate_inst.FINISHED.connect(finished) self.g_populate_inst.FINISHED.connect(self.g_populate_inst.deleteLater) self.g_populate_inst.SKIPPED.connect(skipped_gs) data_thread.finished.connect(data_thread.deleteLater) data_thread.started.connect(self.g_populate_inst.local) data_thread.start() #.g_populate_inst.local() log_i('Populating DB from directory/archive')
class ProgressWidget(QWidget): ''' The ProgressWidget initializes the process and displays information about it. Args: progress_icon (str): the file path of the loading icon gif Attributes: thread (QThread): the thread used by the widget to delegate the processor to file_name_label (QLabel): displays the file name that was selected averaging_label (QLabel): displays the averaging duration number and unit selected start_label (QLabel): displays the start time selected (or N/A) end_label (QLabel): displays the end time selected (or N/A) movie (QMovie): runs the loading icon gif All labels are defined as attributes of the class so they can be modified at the start of each process. ''' def __init__(self, progress_icon): super().__init__() # delegation thread used to run the process off of the main event loop self.thread = QThread() self.thread.setObjectName("Data Processor Thread") # define information labels title_label = QLabel("Analysis in progress...") title_label.setObjectName("title") self.file_name_label = QLabel("") self.file_name_label.setObjectName("details") self.averaging_label = QLabel("") self.averaging_label.setObjectName("details") self.start_label = QLabel("") self.start_label.setObjectName("details") self.end_label = QLabel("") self.end_label.setObjectName("details") # define movie and container label self.movie = QMovie(progress_icon) loading_label = QLabel() loading_label.setAlignment(Qt.AlignCenter) loading_label.setMovie(self.movie) # define layout layout = QVBoxLayout() layout.addWidget(title_label) layout.addWidget(self.file_name_label) layout.addWidget(self.averaging_label) layout.addWidget(self.start_label) layout.addWidget(self.end_label) layout.addWidget(loading_label) self.setLayout(layout) # takes in all parameters related to the process and spawns a worker to run it def begin_progress(self, file_name, file_path, output_path, ad_num, ad_unit, start, end, pdf_header): # populate labels with inputted parameters self.file_name_label.setText("File Name: " + file_name) self.averaging_label.setText("Averaging Duration: " + str(ad_num) + " " + ad_unit) if start != None and end != None: self.start_label.setText("Start Time: " + start.toString()) self.end_label.setText("End Time: " + end.toString()) else: self.start_label.setText("Start Time: N/A") self.end_label.setText("End Time: N/A") # start icon gif and allow main event loop to process it self.movie.start() qApp.processEvents() # define a worker object, move it to a new thread, and begin the processor work self.processor = Processor(file_path, output_path, ad_num, ad_unit, start, end, pdf_header) self.processor.moveToThread(self.thread) self.thread.started.connect(self.processor.work) self.processor.result_signal.connect(self.finish) self.thread.start() # ends the process and makes a callback to the main window with the result @pyqtSlot(str, bool) def finish(self, output, error): # stop and disconnect the thread from the processor object self.thread.quit() self.thread.wait() self.thread.disconnect() self.movie.stop() self.parentWidget().parentWidget().complete_analysis(output, error)
class BlenderListView(QListView): """ A ListView QWidget used on the animated title window """ # Our signals start_render = pyqtSignal(str, str, int) cancel_render = pyqtSignal() def currentChanged(self, selected, deselected): # Get selected item self.selected = selected self.deselected = deselected # Get translation object _ = self.app._tr self.win.clear_effect_controls() animation = self.get_animation_details() self.selected_template = animation.get("service") # In newer versions of Qt, setting the model invokes the currentChanged signal, # but the selection is -1. So, just do nothing here. if not self.selected_template: return # Assign a new unique id for each template selected self.generateUniqueFolder() # Loop through params for param in animation.get("params", []): log.debug('Using parameter %s: %s' % (param["name"], param["title"])) # Is Hidden Param? if param["name"] in ["start_frame", "end_frame"]: # add value to dictionary self.params[param["name"]] = int(param["default"]) # skip to next param without rendering a control continue widget = None label = QLabel() label.setText(_(param["title"])) label.setToolTip(_(param["title"])) if param["type"] == "spinner": # add value to dictionary self.params[param["name"]] = float(param["default"]) # create spinner widget = QDoubleSpinBox() widget.setMinimum(float(param["min"])) widget.setMaximum(float(param["max"])) widget.setValue(float(param["default"])) widget.setSingleStep(0.01) widget.setToolTip(param["title"]) widget.valueChanged.connect( functools.partial(self.spinner_value_changed, param)) elif param["type"] == "text": # add value to dictionary self.params[param["name"]] = _(param["default"]) # create spinner widget = QLineEdit() widget.setText(_(param["default"])) widget.textChanged.connect( functools.partial(self.text_value_changed, widget, param)) elif param["type"] == "multiline": # add value to dictionary self.params[param["name"]] = _(param["default"]) # create spinner widget = QPlainTextEdit() widget.setPlainText(_(param["default"]).replace("\\n", "\n")) widget.textChanged.connect( functools.partial(self.text_value_changed, widget, param)) elif param["type"] == "dropdown": # add value to dictionary self.params[param["name"]] = param["default"] # create spinner widget = QComboBox() widget.currentIndexChanged.connect( functools.partial(self.dropdown_index_changed, widget, param)) # Add values to dropdown if "project_files" in param["name"]: # override files dropdown param["values"] = {} for file in File.filter(): if file.data["media_type"] not in ("image", "video"): continue fileName = os.path.basename(file.data["path"]) fileExtension = os.path.splitext(fileName)[1] if fileExtension.lower() in (".svg"): continue param["values"][fileName] = "|".join( (file.data["path"], str(file.data["height"]), str(file.data["width"]), file.data["media_type"], str(file.data["fps"]["num"] / file.data["fps"]["den"]))) # Add normal values for i, (k, v) in enumerate(sorted(param["values"].items())): # add dropdown item widget.addItem(_(k), v) # select dropdown (if default) if v == param["default"]: widget.setCurrentIndex(i) if not param["values"]: widget.addItem(_("No Files Found"), "") widget.setEnabled(False) elif param["type"] == "color": # add value to dictionary color = QColor(param["default"]) self.params[param["name"]] = [ color.redF(), color.greenF(), color.blueF() ] if "diffuse_color" in param.get("name"): self.params[param["name"]].append(color.alphaF()) widget = QPushButton() widget.setText("") widget.setStyleSheet("background-color: {}".format( param["default"])) widget.clicked.connect( functools.partial(self.color_button_clicked, widget, param)) # Add Label and Widget to the form if (widget and label): self.win.settingsContainer.layout().addRow(label, widget) elif (label): self.win.settingsContainer.layout().addRow(label) self.end_processing() self.init_slider_values() def spinner_value_changed(self, param, value): self.params[param["name"]] = value log.info('Animation param %s set to %s' % (param["name"], value)) def text_value_changed(self, widget, param, value=None): try: # Attempt to load value from QPlainTextEdit (i.e. multi-line) if not value: value = widget.toPlainText() except Exception: log.debug('Failed to read plain text value from widget') return self.params[param["name"]] = value # XXX: This will log every individual KEYPRESS in the text field. # log.info('Animation param %s set to %s' % (param["name"], value)) def dropdown_index_changed(self, widget, param, index): value = widget.itemData(index) self.params[param["name"]] = value log.info('Animation param %s set to %s' % (param["name"], value)) if param["name"] == "length_multiplier": self.init_slider_values() def color_button_clicked(self, widget, param, index): # Get translation object _ = get_app()._tr color_value = self.params[param["name"]] currentColor = QColor("#FFFFFF") if len(color_value) >= 3: currentColor.setRgbF(color_value[0], color_value[1], color_value[2]) # Store our arguments for the callback to pick up again self._color_scratchpad = (widget, param) ColorPicker(currentColor, callback=self.color_selected, parent=self.win) @pyqtSlot(QColor) def color_selected(self, newColor): """Callback when the user chooses a color in the dialog""" if not self._color_scratchpad: log.warning("ColorPicker callback called without parameter to set") return (widget, param) = self._color_scratchpad if not newColor or not newColor.isValid(): return widget.setStyleSheet("background-color: {}".format(newColor.name())) self.params[param["name"]] = [ newColor.redF(), newColor.greenF(), newColor.blueF() ] if "diffuse_color" in param.get("name"): self.params[param["name"]].append(newColor.alphaF()) log.info('Animation param %s set to %s', param["name"], newColor.name()) def generateUniqueFolder(self): """ Generate a new, unique folder name to contain Blender frames """ # Assign a new unique id for each template selected self.unique_folder_name = str(self.app.project.generate_id()) # Create a folder (if it does not exist) if not os.path.exists( os.path.join(info.BLENDER_PATH, self.unique_folder_name)): os.mkdir(os.path.join(info.BLENDER_PATH, self.unique_folder_name)) def processing_mode(self, cursor=True): """ Disable all controls on interface """ # Store keyboard-focused widget self.focus_owner = self.win.focusWidget() self.win.btnRefresh.setEnabled(False) self.win.sliderPreview.setEnabled(False) self.win.btnRender.setEnabled(False) # Show 'Wait' cursor if cursor: QApplication.setOverrideCursor(Qt.WaitCursor) @pyqtSlot() def end_processing(self): """ Enable all controls on interface """ self.win.btnRefresh.setEnabled(True) self.win.sliderPreview.setEnabled(True) self.win.btnRender.setEnabled(True) self.win.statusContainer.hide() # Restore normal cursor and keyboard focus QApplication.restoreOverrideCursor() if self.focus_owner: self.focus_owner.setFocus() def init_slider_values(self): """ Init the slider and preview frame label to the currently selected animation """ # Get current preview slider frame length = int(self.params.get("end_frame", 1)) * int( self.params.get("length_multiplier", 1)) # Update the preview slider middle_frame = int(length / 2) self.win.sliderPreview.setMinimum(self.params.get("start_frame", 1)) self.win.sliderPreview.setMaximum(length) self.win.sliderPreview.setValue(middle_frame) # Trigger a refresh of the preview self.preview_timer.start() @pyqtSlot() def render_finished(self): # Don't try to capture image sequences for preview frames if not self.final_render: return # Compose image sequence data seq_params = { "folder_path": os.path.join(info.BLENDER_PATH, self.unique_folder_name), "base_name": self.params["file_name"], "fixlen": True, "digits": 4, "extension": "png" } filename = "{}%04d.png".format(seq_params["base_name"]) final_path = os.path.join(seq_params["folder_path"], filename) log.info( 'RENDER FINISHED! Adding to project files: {}'.format(filename)) # Add to project files get_app().window.files_model.add_files(final_path, seq_params) # We're done here self.win.close() @pyqtSlot(str) def render_stage(self, stage=None): _ = get_app()._tr self.win.frameProgress.setRange(0, 0) self.win.frameStatus.setText(_("Generating")) log.debug("Set Blender progress to Generating step") @pyqtSlot(int, int) def render_progress(self, step_value, step_max): _ = get_app()._tr self.win.frameProgress.setRange(0, step_max) self.win.frameProgress.setValue(step_value) self.win.frameStatus.setText(_("Rendering")) log.debug("set Blender progress to Rendering step, %d of %d complete", step_value, step_max) @pyqtSlot(int) def render_saved(self, frame=None): _ = get_app()._tr self.win.frameProgress.setValue(self.win.frameProgress.maximum() + 1) self.win.frameStatus.setText(_("Saved")) log.debug("Set Blender progress to Saved step") @pyqtSlot() def render_initialize(self): _ = get_app()._tr self.win.frameProgress.setRange(0, 0) self.win.frameStatus.setText(_("Initializing")) self.win.statusContainer.show() log.debug("Set Blender progress to Initializing step") @pyqtSlot(int) def update_progress_bar(self, current_frame): # update label and preview slider self.win.sliderPreview.setValue(current_frame) length = int(self.params.get("end_frame", 1)) * int( self.params.get("length_multiplier", 1)) self.win.lblFrame.setText("{}/{}".format(current_frame, length)) @pyqtSlot(int) def sliderPreview_valueChanged(self, new_value): """Get new value of preview slider, and start timer to Render frame""" if self.win.sliderPreview.isEnabled(): self.preview_timer.start() # Update preview label length = int(self.params.get("end_frame", 1)) * int( self.params.get("length_multiplier", 1)) self.win.lblFrame.setText("{}/{}".format(new_value, length)) def preview_timer_onTimeout(self): """Timer is ready to Render frame""" # Update preview label preview_frame_number = self.win.sliderPreview.value() log.info('Previewing frame %s' % preview_frame_number) # Render current frame self.Render(preview_frame_number) def get_animation_details(self): """ Build a dictionary of all animation settings and properties from XML """ if not self.selected: return {} elif self.selected and self.selected.row() == -1: return {} # Get all selected rows items ItemRow = self.blender_model.model.itemFromIndex(self.selected).row() animation_title = self.blender_model.model.item(ItemRow, 1).text() xml_path = self.blender_model.model.item(ItemRow, 2).text() service = self.blender_model.model.item(ItemRow, 3).text() # load xml effect file xmldoc = xml.parse(xml_path) # Get list of params animation = { "title": animation_title, "path": xml_path, "service": service, "params": [] } # Loop through params for param in xmldoc.getElementsByTagName("param"): # Set up item dict, "default" key is required param_item = {"default": ""} # Get details of param for att in ["title", "description", "name", "type"]: if param.attributes[att]: param_item[att] = param.attributes[att].value for tag in ["min", "max", "step", "digits", "default"]: for p in param.getElementsByTagName(tag): if p.childNodes: param_item[tag] = p.firstChild.data try: # Build values dict from list of (name, num) tuples param_item["values"] = dict([ (p.attributes["name"].value, p.attributes["num"].value) for p in param.getElementsByTagName("value") if ("name" in p.attributes and "num" in p.attributes) ]) except (TypeError, AttributeError) as ex: log.warn("XML parser: %s", ex) pass # Append param object to list animation["params"].append(param_item) # Free up XML document memory xmldoc.unlink() # Return animation dictionary return animation def mousePressEvent(self, event): # Ignore event, propagate to parent event.ignore() super().mousePressEvent(event) def refresh_view(self): self.blender_model.update_model() def get_project_params(self, is_preview=True): """ Return a dictionary of project related settings, needed by the Blender python script. """ project = self.app.project project_params = {} # Append some project settings fps = project.get("fps") project_params["fps"] = fps["num"] if fps["den"] != 1: project_params["fps_base"] = fps["den"] project_params["resolution_x"] = project.get("width") project_params["resolution_y"] = project.get("height") if is_preview: project_params["resolution_percentage"] = 50 else: project_params["resolution_percentage"] = 100 project_params["quality"] = 100 project_params["file_format"] = "PNG" project_params["color_mode"] = "RGBA" project_params["alpha_mode"] = 1 project_params["horizon_color"] = (0.57, 0.57, 0.57) project_params["animation"] = True project_params["output_path"] = os.path.join(info.BLENDER_PATH, self.unique_folder_name, self.params["file_name"]) # return the dictionary return project_params # Error from blender (with version number) @pyqtSlot(str) def onBlenderVersionError(self, version): self.error_with_blender(version, None) # Signal error from blender (with custom message) @pyqtSlot() @pyqtSlot(str) def onBlenderError(self, error=None): self.error_with_blender(None, error) def error_with_blender(self, version=None, worker_message=None): """ Show a friendly error message regarding the blender executable or version. """ _ = self.app._tr s = self.app.get_settings() error_message = "" if version: error_message = _("Version Detected: {}").format(version) log.info("Blender version detected: {}".format(version)) if worker_message: error_message = _("Error Output:\n{}").format(worker_message) log.error("Blender error: {}".format(worker_message)) QMessageBox.critical( self, error_message, _(""" Blender, the free open source 3D content creation suite, is required for this action. (http://www.blender.org) Please check the preferences in OpenShot and be sure the Blender executable is correct. This setting should be the path of the 'blender' executable on your computer. Also, please be sure that it is pointing to Blender version {} or greater. Blender Path: {} {}""").format(info.BLENDER_MIN_VERSION, s.get("blender_command"), error_message)) # Close the blender interface self.win.close() def inject_params(self, source_path, out_path, frame=None): # determine if this is 'preview' mode? is_preview = False if frame: # if a frame is passed in, we are in preview mode. # This is used to turn the background color to off-white... instead of transparent is_preview = True # prepare string to inject user_params = "\n#BEGIN INJECTING PARAMS\n" param_data = copy.deepcopy(self.params) param_data.update(self.get_project_params(is_preview)) param_serialization = json.dumps(param_data) user_params += 'params_json = r' + '"""{}"""'.format( param_serialization) user_params += "\n#END INJECTING PARAMS\n" # If GPU rendering is selected, see if GPU enable code is available s = self.app.get_settings() gpu_code_body = None if s.get("blender_gpu_enabled"): gpu_enable_py = os.path.join(info.PATH, "blender", "scripts", "gpu_enable.py.in") try: with open(gpu_enable_py, 'r') as f: gpu_code_body = f.read() if gpu_code_body: log.info("Injecting GPU enable code from {}".format( gpu_enable_py)) user_params += "\n#ENABLE GPU RENDERING\n" user_params += gpu_code_body user_params += "\n#END ENABLE GPU RENDERING\n" except IOError as e: log.error("Could not load GPU enable code! %s", e) # Read Python source from script file with open(source_path, 'r') as f: script_body = f.read() # insert our modifications to script source script_body = script_body.replace("# INJECT_PARAMS_HERE", user_params) # Write final script to output dir try: with open(out_path, "w", encoding="UTF-8", errors="strict") as f: f.write(script_body) except Exception: log.error("Could not write blender script to %s", out_path, exc_info=1) @pyqtSlot(str) def update_image(self, image_path): # get the pixbuf image = QImage(image_path) scaled_image = image.scaled(self.win.imgPreview.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) pixmap = QPixmap.fromImage(scaled_image) self.win.imgPreview.setPixmap(pixmap) def Cancel(self): """Cancel the current render, if any""" #QMetaObject.invokeMethod(self.worker, 'Cancel', Qt.DirectConnection) self.cancel_render.emit() def Render(self, frame=None): """ Render an images sequence of the current template using Blender 2.62+ and the Blender Python API. """ self.processing_mode() # Init blender paths blend_file_path = os.path.join(info.PATH, "blender", "blend", self.selected_template) source_script = os.path.join( info.PATH, "blender", "scripts", self.selected_template.replace(".blend", ".py.in")) target_script = os.path.join( info.BLENDER_PATH, self.unique_folder_name, self.selected_template.replace(".blend", ".py")) # Background Worker Thread (for Blender process) self.background = QThread(self) self.background.setObjectName("openshot_renderer") self.worker = Worker(blend_file_path, target_script, int(frame or 0)) # no parent! self.worker.setObjectName("render_worker") # Move Worker to new thread self.worker.moveToThread(self.background) # Hook up signals to/from Background Worker self.background.started.connect(self.worker.Render) self.worker.render_complete.connect(self.render_finished) self.cancel_render.connect(self.worker.Cancel) # State changes self.worker.end_processing.connect(self.end_processing) self.worker.start_processing.connect(self.render_initialize) # Actual communication between the worker and front-end self.worker.blender_version_error.connect(self.onBlenderVersionError) self.worker.blender_error_nodata.connect(self.onBlenderError) self.worker.blender_error_with_data.connect(self.onBlenderError) self.worker.progress.connect(self.update_progress_bar) self.worker.image_updated.connect(self.update_image) self.worker.frame_saved.connect(self.render_saved) self.worker.frame_stage.connect(self.render_stage) self.worker.frame_render.connect(self.render_progress) # Cleanup signals all 'round self.worker.finished.connect(self.worker.deleteLater) self.worker.finished.connect(self.background.quit, Qt.DirectConnection) self.background.finished.connect(self.background.deleteLater) self.background.finished.connect(self.worker.deleteLater) # Read .py file, inject user parameters, and write to output path self.inject_params(source_script, target_script, frame) # Note whether we're rendering a preview or an animation self.final_render = bool(frame is None) # Run worker in background thread self.background.start() def __init__(self, parent, *args): # Invoke base class init super().__init__(*args) self.win = parent self.app = get_app() # Get Model data self.blender_model = BlenderModel() self.selected = None self.deselected = None self._color_scratchpad = None self.selected_template = "" self.final_render = False # Preview render timer self.preview_timer = QTimer(self) self.preview_timer.setInterval(300) self.preview_timer.setSingleShot(True) self.preview_timer.timeout.connect(self.preview_timer_onTimeout) # Init dictionary which holds the values to the template parameters self.params = {} # Assign a new unique id for each template selected self.unique_folder_name = None # Disable interface self.processing_mode(cursor=False) # Setup header columns self.setModel(self.blender_model.model) self.setIconSize(QSize(131, 108)) self.setGridSize(QSize(102, 92)) self.setViewMode(QListView.IconMode) self.setResizeMode(QListView.Adjust) self.setUniformItemSizes(True) self.setWordWrap(True) self.setTextElideMode(Qt.ElideRight) # Hook up controls self.win.btnRefresh.clicked.connect(self.preview_timer.start) self.win.sliderPreview.valueChanged.connect( functools.partial(self.sliderPreview_valueChanged)) # Refresh view self.refresh_view()