def test_simulator_graphics_view(self): self.__setup_project() self.add_all_signals_to_simulator() stc = self.form.simulator_tab_controller # type: SimulatorTabController self.assertGreater(len(stc.simulator_config.get_all_items()), 0) self.assertEqual(len(stc.simulator_scene.selectedItems()), 0) # select first message messages = stc.simulator_scene.get_all_message_items() pos = stc.ui.gvSimulator.mapFromScene(messages[0].scenePos()) QTest.mouseClick(stc.ui.gvSimulator.viewport(), Qt.LeftButton, Qt.NoModifier, pos) self.assertEqual(len(stc.simulator_scene.selectedItems()), 1) self.assertIsInstance(stc.simulator_scene.selectedItems()[0], MessageItem) rules = [item for item in stc.simulator_scene.items() if isinstance(item, RuleItem)] self.assertEqual(len(rules), 0) self.menus_to_ignore = [w for w in QApplication.topLevelWidgets() if isinstance(w, QMenu)] timer = QTimer(self.form) timer.setInterval(1) timer.setSingleShot(True) timer.timeout.connect(self.__on_context_menu_simulator_graphics_view_timer_timeout) timer.start() stc.ui.gvSimulator.contextMenuEvent(QContextMenuEvent(QContextMenuEvent.Mouse, pos)) rules = [item for item in stc.simulator_scene.items() if isinstance(item, RuleItem)] self.assertEqual(len(rules), 1)
class myScrollBar(QScrollBar): def __init__(self, color=Qt.white, parent=None): QScrollBar.__init__(self, parent) self._color = color # self.setAttribute(Qt.WA_TranslucentBackground) self.timer = QTimer() self.timer.setInterval(500) self.timer.setSingleShot(True) self.timer.timeout.connect(lambda: self.parent().hideWidget(self)) self.valueChanged.connect(lambda v: self.timer.start()) self.valueChanged.connect(lambda: self.parent().showWidget(self)) def setColor(self, color): self._color = color def paintEvent(self, event): opt = QStyleOptionSlider() self.initStyleOption(opt) style = qApp.style() painter = QPainter(self) # Background (Necessary with Qt 5.2 it seems, not with 5.4) # painter.save() # painter.setPen(Qt.NoPen) # painter.setBrush(self.palette().brush(QPalette.Base)) # painter.drawRect(event.rect()) # painter.restore() # slider r = style.subControlRect(style.CC_ScrollBar, opt, style.SC_ScrollBarSlider) painter.fillRect(r, self._color) painter.end()
def __init_log_writer(self): timer = QTimer() timer.setSingleShot(False) timer.setInterval(self.__app_log_write_interval) timer.timeout.connect(self.__write_app_log) timer.start() return timer
def _process_entry_point(channel, iface_name): logger.info('Bus monitor process started with PID %r', os.getpid()) app = QApplication(sys.argv) # Inheriting args from the parent process def exit_if_should(): if RUNNING_ON_WINDOWS: return False else: return os.getppid() != PARENT_PID # Parent is dead exit_check_timer = QTimer() exit_check_timer.setSingleShot(False) exit_check_timer.timeout.connect(exit_if_should) exit_check_timer.start(2000) def get_frame(): received, obj = channel.receive_nonblocking() if received: if obj == IPC_COMMAND_STOP: logger.info('Bus monitor process has received a stop request, goodbye') app.exit(0) else: return obj win = BusMonitorWindow(get_frame, iface_name) win.show() logger.info('Bus monitor process %r initialized successfully, now starting the event loop', os.getpid()) sys.exit(app.exec_())
class Runner(object): """Useful class for running jobs with a delay""" def __init__(self, delay=2000): self._timer = QTimer() self._timer.setSingleShot(True) self._timer.timeout.connect(self._execute_job) self._delay = delay self._job = None self._args = [] self._kw = {} def cancel(self): """Cancel the current job""" self._timer.stop() self._job = None self._args = [] self._kw = {} def run(self, job, *args, **kw): """Request a job run. If there is a job, cancel and run the new job with a delay specified in __init__""" self.cancel() self._job = job self._args = args self._kw = kw self._timer.start(self._delay) def _execute_job(self): """Execute job after the timer has timeout""" self._timer.stop() self._job(*self._args, **self._kw)
class MemoryInfo(QObject): def __init__(self, parent=None): super(MemoryInfo, self).__init__(parent) self.__usage = 0 self.load_usage() self.__timer = QTimer(self) self.__timer.setInterval(1000) self.__timer.setSingleShot(False) self.__timer.timeout.connect(self.load_usage) self.__timer.start() def load_usage(self): with open("/proc/self/status") as f: data = f.read() index = data.index("VmRSS:") split = data[index:].split(None, 3) self.__usage = int(split[1]) self.usage_changed.emit() usage_changed = pyqtSignal(name="usageChanged") @pyqtProperty(int, notify=usage_changed) def usage(self): return self.__usage @usage.setter def usage(self, usage): if self.__usage != usage: self.__usage = usage self.usage_changed.emit()
class Queued: """ A queued trigger. Calling the trigger will invoke the handler function in another mainloop iteration. Calling the trigger multiple times before the handler was invoked (e.g. within the same mainloop iteration) will result in only a *single* handler invocation! This can only be used with at least a ``QCoreApplication`` instanciated. """ def __init__(self, func): self.timer = QTimer() self.timer.setSingleShot(True) self.timer.timeout.connect(func) def __call__(self): """Schedule the handler invocation for another mainloop iteration.""" self.timer.start() @classmethod def method(cls, func): """Decorator for a queued method, i.e. a method that when called, actually runs at a later time.""" return property(memoize(functools.wraps(func)( lambda self: cls(func.__get__(self)))))
class _StatusBar(QStatusBar): """Extended status bar. Supports HTML messages """ def __init__(self, *args): QStatusBar.__init__(self, *args) self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Expanding) self.setSizeGripEnabled(False) self.setStyleSheet("QStatusBar {border: 0} QStatusBar::item {border: 0}") self._label = QLabel(self) self._label.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self._label.setStyleSheet("color: red") self.addWidget(self._label) self._timer = QTimer() self._timer.setSingleShot(True) self._timer.timeout.connect(self.clearMessage) def term(self): self._timer.stop() def showMessage(self, text, timeout=0): """QStatusBar.showMessage() """ self._label.setText(text) self._timer.stop() if timeout > 0: self._timer.start(timeout) def clearMessage(self): """QStatusBar.clearMessage() """ self._label.clear() def currentMessage(self): return self._label.text()
def register_timer(self, time_delta, callback): """Registers a callback function to be run after time_delta ms.""" timer = QTimer(self.window) timer.setSingleShot(True) timer.timeout.connect(callback) timer.setInterval(time_delta) timer.start()
class _GlobalUpdateWordSetTimer: """Timer updates word set, when editor is idle. (5 sec. after last change) Timer is global, for avoid situation, when all instances update set simultaneously """ _IDLE_TIMEOUT_MS = 1000 def __init__(self): self._timer = QTimer() self._timer.setSingleShot(True) self._timer.timeout.connect(self._onTimer) self._scheduledMethods = [] def schedule(self, method): if not method in self._scheduledMethods: self._scheduledMethods.append(method) self._timer.start(self._IDLE_TIMEOUT_MS) def cancel(self, method): """Cancel scheduled method Safe method, may be called with not-scheduled method""" if method in self._scheduledMethods: self._scheduledMethods.remove(method) if not self._scheduledMethods: self._timer.stop() def _onTimer(self): method = self._scheduledMethods.pop() method() if self._scheduledMethods: self._timer.start(self._IDLE_TIMEOUT_MS)
def _processPendingEvents(): """Process pending application events.""" # Create an event loop to run in. Otherwise, we need to use the # QApplication main loop, which may already be running and therefore # unusable. qe = QEventLoop() # Create a single-shot timer. Could use QTimer.singleShot(), # but can't cancel this / disconnect it. timer = QTimer() timer.setSingleShot(True) timer.timeout.connect(qe.quit) timer.start(1) # Wait for an emitted signal. qe.exec_() # Clean up: don't allow the timer to call qe.quit after this # function exits, which would produce "interesting" behavior. timer.stop() # Stopping the timer may not cancel timeout signals in the # event queue. Disconnect the signal to be sure that loop # will never receive a timeout after the function exits. timer.timeout.disconnect(qe.quit)
def test_save_remove_decoding(self): def set_save_name(): timer.stop() input_dialog = next(w for w in qApp.topLevelWidgets() if isinstance(w, QInputDialog)) input_dialog.setTextValue("Test decoding") input_dialog.accept() def accept_delete(): timer.stop() message_box = next(w for w in qApp.topLevelWidgets() if isinstance(w, QMessageBox)) message_box.button(QMessageBox.Yes).click() self.dialog.ui.decoderchain.addItem(constants.DECODING_CUT) self.dialog.decoderchainUpdate() self.assertEqual(self.dialog.ui.decoderchain.count(), 1) timer = QTimer(self.dialog) timer.setSingleShot(True) timer.timeout.connect(set_save_name) timer.start(10) self.dialog.ui.saveas.click() self.assertEqual(self.dialog.ui.combobox_decodings.currentText(), "Test decoding") timer.timeout.disconnect(set_save_name) timer.timeout.connect(accept_delete) timer.start(10) self.dialog.ui.delete_decoding.click() self.assertNotEqual(self.dialog.ui.combobox_decodings.currentText(), "Test decoding")
def wait(self, time_ms, callback, onredirect=None, onerror=None): """ Wait for time_ms, then run callback. If onredirect is True then the timer is cancelled if redirect happens. If onredirect is callable then in case of redirect the timer is cancelled and this callable is called. If onerror is True then the timer is cancelled if a render error happens. If onerror is callable then in case of a render error the timer is cancelled and this callable is called. """ timer = QTimer() timer.setSingleShot(True) timer_callback = functools.partial(self._on_wait_timeout, timer=timer, callback=callback, ) timer.timeout.connect(timer_callback) self.logger.log("waiting %sms; timer %s" % (time_ms, id(timer)), min_level=2) timer.start(time_ms) self._active_timers.add(timer) if onredirect: self._timers_to_cancel_on_redirect[timer] = onredirect if onerror: self._timers_to_cancel_on_error[timer] = onerror
class Clipboard(QObject): changed = pyqtSignal(dict) def __init__(self): QObject.__init__(self) clipboard = QGuiApplication.clipboard() clipboard.changed.connect(self.onClipboardChanged) self.clipboardTimer = QTimer() self.clipboardTimer.setInterval(500) self.clipboardTimer.setSingleShot(True) self.clipboardTimer.timeout.connect(self.onClipboardChangedAfterDelay) self.selectionTimer = QTimer() self.selectionTimer.setInterval(1000) self.selectionTimer.setSingleShot(True) self.selectionTimer.timeout.connect(self.onSelectionChangedAfterDelay) self.formats = set([mimeText]) def setFormats(self, formats): self.formats = set(formats) def onClipboardChanged(self, mode): if mode == QClipboard.Clipboard: if not QGuiApplication.clipboard().ownsClipboard(): self.clipboardTimer.start() else: self.clipboardTimer.stop() elif mode == QClipboard.Selection: if not QGuiApplication.clipboard().ownsSelection(): self.selectionTimer.start() else: self.selectionTimer.stop() def onClipboardChangedAfterDelay(self): self.emitChanged(QClipboard.Selection) def onSelectionChangedAfterDelay(self): self.emitChanged(QClipboard.Selection) def emitChanged(self, mode): clipboard = QGuiApplication.clipboard() mimeData = clipboard.mimeData() data = {} for format in self.formats: if mimeData.hasFormat(format): data[format] = mimeData.data(format) self.changed.emit(data) @pyqtProperty(str) def text(self): clipboard = QGuiApplication.clipboard() return clipboard.text() @text.setter def text(self, text): clipboard = QGuiApplication.clipboard() return clipboard.setText(text)
def repeat(self, interval, function, *args): timer = QTimer() timer.setSingleShot(False) self.params[timer] = (interval, function, args) timer.start(0) timer.timeout.connect(self.timeout, Qt.QueuedConnection)
def schedule(self, interval, function, *args): timer = QTimer() timer.setInterval(interval * 1000) timer.setSingleShot(True) self.params[timer] = (None, function, args) timer.timeout.connect(self.timeout) timer.start()
class MultiBuildPlateModel(ListModel): maxBuildPlateChanged = pyqtSignal() activeBuildPlateChanged = pyqtSignal() selectionChanged = pyqtSignal() def __init__(self, parent = None): super().__init__(parent) self._update_timer = QTimer() self._update_timer.setInterval(100) self._update_timer.setSingleShot(True) self._update_timer.timeout.connect(self._updateSelectedObjectBuildPlateNumbers) self._application = Application.getInstance() self._application.getController().getScene().sceneChanged.connect(self._updateSelectedObjectBuildPlateNumbersDelayed) Selection.selectionChanged.connect(self._updateSelectedObjectBuildPlateNumbers) self._max_build_plate = 1 # default self._active_build_plate = -1 def setMaxBuildPlate(self, max_build_plate): if self._max_build_plate != max_build_plate: self._max_build_plate = max_build_plate self.maxBuildPlateChanged.emit() ## Return the highest build plate number @pyqtProperty(int, notify = maxBuildPlateChanged) def maxBuildPlate(self): return self._max_build_plate def setActiveBuildPlate(self, nr): if self._active_build_plate != nr: self._active_build_plate = nr self.activeBuildPlateChanged.emit() @pyqtProperty(int, notify = activeBuildPlateChanged) def activeBuildPlate(self): return self._active_build_plate def _updateSelectedObjectBuildPlateNumbersDelayed(self, *args): if not isinstance(args[0], Camera): self._update_timer.start() def _updateSelectedObjectBuildPlateNumbers(self, *args): result = set() for node in Selection.getAllSelectedObjects(): result.add(node.callDecoration("getBuildPlateNumber")) self._selection_build_plates = list(result) self.selectionChanged.emit() @pyqtProperty("QVariantList", notify = selectionChanged) def selectionBuildPlates(self): return self._selection_build_plates
def collapse(callback): timer = QTimer() timer.setSingleShot(True) timer.setInterval(200) def out(*pargs, **kwargs): out.pargs = pargs out.kwargs = kwargs timer.start() out.stop = lambda: timer.stop() timer.timeout.connect(lambda: callback(*out.pargs, **out.kwargs)) return out
def connect(self): config.UID_LED_STRIP_BRICKLET = None self.setup.label_led_strip_found.setText('No') self.setup.label_led_strip_uid.setText('None') config.UID_MULTI_TOUCH_BRICKLET = None self.setup.label_multi_touch_found.setText('No') self.setup.label_multi_touch_uid.setText('None') config.UID_DUAL_BUTTON_BRICKLET = (None, None) self.setup.label_dual_button1_found.setText('No') self.setup.label_dual_button1_uid.setText('None') self.setup.label_dual_button2_found.setText('No') self.setup.label_dual_button2_uid.setText('None') config.UID_PIEZO_SPEAKER_BRICKLET = None self.setup.label_piezo_speaker_found.setText('No') self.setup.label_piezo_speaker_uid.setText('None') config.UID_SEGMENT_DISPLAY_4X7_BRICKLET = None self.setup.label_segment_display_found.setText('No') self.setup.label_segment_display_uid.setText('None') if self.ipcon != None: try: self.ipcon.disconnect() except: pass self.ipcon = IPConnection() host = self.setup.edit_host.text() port = self.setup.spinbox_port.value() try: self.ipcon.connect(host, port) except Error as e: self.error_msg.showMessage('Connection Error: ' + str(e.description) + "<br><br>Brickd installed and running?") return except socket.error as e: self.error_msg.showMessage('Socket error: ' + str(e) + "<br><br>Brickd installed and running?") return self.ipcon.register_callback(IPConnection.CALLBACK_ENUMERATE, self.cb_enumerate) self.ipcon.register_callback(IPConnection.CALLBACK_CONNECTED, self.cb_connected) # Wait for a second to give user visual feedback timer = QTimer(self) timer.setSingleShot(True) timer.timeout.connect(self.ipcon.enumerate) timer.start(250)
def __init__(self, stickMan, keyReceiver): self.m_stickMan = stickMan self.m_keyReceiver = keyReceiver # Create animation group to be used for all transitions. self.m_animationGroup = QParallelAnimationGroup() stickManNodeCount = self.m_stickMan.nodeCount() self._pas = [] for i in range(stickManNodeCount): pa = QPropertyAnimation(self.m_stickMan.node(i), b'pos') self._pas.append(pa) self.m_animationGroup.addAnimation(pa) # Set up intial state graph. self.m_machine = QStateMachine() self.m_machine.addDefaultAnimation(self.m_animationGroup) self.m_alive = QState(self.m_machine) self.m_alive.setObjectName('alive') # Make it blink when lightning strikes before entering dead animation. lightningBlink = QState(self.m_machine) lightningBlink.assignProperty(self.m_stickMan.scene(), 'backgroundBrush', Qt.white) lightningBlink.assignProperty(self.m_stickMan, 'penColor', Qt.black) lightningBlink.assignProperty(self.m_stickMan, 'fillColor', Qt.white) lightningBlink.assignProperty(self.m_stickMan, 'isDead', True) timer = QTimer(lightningBlink) timer.setSingleShot(True) timer.setInterval(100) lightningBlink.entered.connect(timer.start) lightningBlink.exited.connect(timer.stop) self.m_dead = QState(self.m_machine) self.m_dead.assignProperty(self.m_stickMan.scene(), 'backgroundBrush', Qt.black) self.m_dead.assignProperty(self.m_stickMan, 'penColor', Qt.white) self.m_dead.assignProperty(self.m_stickMan, 'fillColor', Qt.black) self.m_dead.setObjectName('dead') # Idle state (sets no properties). self.m_idle = QState(self.m_alive) self.m_idle.setObjectName('idle') self.m_alive.setInitialState(self.m_idle) # Lightning strikes at random. self.m_alive.addTransition(LightningStrikesTransition(lightningBlink)) lightningBlink.addTransition(timer.timeout, self.m_dead) self.m_machine.setInitialState(self.m_alive)
class AutoSave: def __init__(self, application): self._application = application self._application.getPreferences().preferenceChanged.connect(self._triggerTimer) self._global_stack = None self._application.getPreferences().addPreference("cura/autosave_delay", 1000 * 10) self._change_timer = QTimer() self._change_timer.setInterval(int(self._application.getPreferences().getValue("cura/autosave_delay"))) self._change_timer.setSingleShot(True) self._enabled = True self._saving = False def initialize(self): # only initialise if the application is created and has started self._change_timer.timeout.connect(self._onTimeout) self._application.globalContainerStackChanged.connect(self._onGlobalStackChanged) self._onGlobalStackChanged() self._triggerTimer() def _triggerTimer(self, *args): if not self._saving: self._change_timer.start() def setEnabled(self, enabled: bool) -> None: self._enabled = enabled if self._enabled: self._change_timer.start() else: self._change_timer.stop() def _onGlobalStackChanged(self): if self._global_stack: self._global_stack.propertyChanged.disconnect(self._triggerTimer) self._global_stack.containersChanged.disconnect(self._triggerTimer) self._global_stack = self._application.getGlobalContainerStack() if self._global_stack: self._global_stack.propertyChanged.connect(self._triggerTimer) self._global_stack.containersChanged.connect(self._triggerTimer) def _onTimeout(self): self._saving = True # To prevent the save process from triggering another autosave. Logger.log("d", "Autosaving preferences, instances and profiles") self._application.saveSettings() self._saving = False
def _set_reply_timeout(self, reply, timeout_ms): request_id = self._get_request_id(reply.request()) # reply is used as a parent for the timer in order to destroy # the timer when reply is destroyed. It segfaults otherwise. timer = QTimer(reply) timer.setSingleShot(True) timer_callback = functools.partial(self._on_reply_timeout, reply=reply, timer=timer, request_id=request_id) timer.timeout.connect(timer_callback) self._reply_timeout_timers[request_id] = timer timer.start(timeout_ms)
def main(): app = QApplication(sys.argv) socket = QUdpSocket(app) def lambdatimeout(): socket.writeDatagram(QByteArray(b""), QHostAddress("127.0.0.1"), 14555) timer = QTimer(app) timer.timeout.connect(lambdatimeout) timer.setSingleShot(False) timer.start(1000) # socket.connectToHost(QHostAddress('127.0.0.1'), 14550) sys.exit(app.exec())
def __init__(self, args): super().__init__(args) self.error_msg = QErrorMessage() self.ipcon = IPConnection() signal.signal(signal.SIGINT, self.exit_demo) signal.signal(signal.SIGTERM, self.exit_demo) timer = QTimer(self) timer.setSingleShot(True) timer.timeout.connect(self.connect) timer.start(1)
def enable_asyncore_loop(self): """Hooks our asyncore loop into Qt's event queue.""" def beat(): asyncore.loop(count=1, timeout=0) # Yep, this isn't especially real-time IO, but it's fine for what we do. timer = QTimer() timer.timeout.connect(beat) timer.setSingleShot(False) timer.setInterval(15) timer.start() self._timer = timer
class MyGlWidget(QGLWidget): "PySideApp uses Qt library to create an opengl context, listen to keyboard events, and clean up" def __init__(self, renderer, glformat, app): "Creates an OpenGL context and a window, and acquires OpenGL resources" super(MyGlWidget, self).__init__(glformat) self.renderer = renderer self.app = app # Use a timer to rerender as fast as possible self.timer = QTimer(self) self.timer.setSingleShot(True) self.timer.setInterval(0) self.timer.timeout.connect(self.render_vr) # Accept keyboard events self.setFocusPolicy(Qt.StrongFocus) def __enter__(self): "setup for RAII using 'with' keyword" return self def __exit__(self, type_arg, value, traceback): "cleanup for RAII using 'with' keyword" self.dispose_gl() def initializeGL(self): if self.renderer is not None: self.renderer.init_gl() self.timer.start() def paintGL(self): "render scene one time" self.renderer.render_scene() self.swapBuffers() # Seems OK even in single-buffer mode def render_vr(self): self.makeCurrent() self.paintGL() self.doneCurrent() self.timer.start() # render again real soon now def disposeGL(self): if self.renderer is not None: self.makeCurrent() self.renderer.dispose_gl() self.doneCurrent() def keyPressEvent(self, event): "press ESCAPE to quit the application" key = event.key() if key == Qt.Key_Escape: self.app.quit()
class gameWidget(QWidget): updateScore = pyqtSignal(int, name='updateScore') updateCurrentScore = pyqtSignal(int, name='updateCurrentScore') started = pyqtSignal() finished = pyqtSignal() def __init__(self, cfg): super(gameWidget, self).__init__() self.cfg = cfg self.totalScore = 0 self.curScore = 0 self.questionsAsked = 0 self.runState = 0 self.correct = 0 self.wrong = 0 self.timer = QTimer() self.timer.setInterval(500) self.timer.setSingleShot(False) self.startTime = time.time() self.timer.timeout.connect(self.updateTimer) def updateTimer(self): dt = time.time() - self.startTime if self.cfg['level'] < 4: dt = 0 if dt > self.cfg['maxTime']: self.timer.stop() curScore = max(self.cfg['minPoints'], int((self.cfg['maxTime']-dt)*self.cfg['pointsPerSec'])) self.updateCurrentScore.emit(curScore) def startGame(self): raise RuntimeError('Game objects must implement start') def start(self): startScore = self.cfg['pointsPerSec']*self.cfg['maxTime'] self.updateCurrentScore.emit(startScore) self.started.emit() self.startGame() def finishGame(self): raise RuntimeError('Game objects must implement finishGame') def finish(self): self.timer.stop() self.finished.emit() self.finishGame()
class AutoSave(Extension): def __init__(self): super().__init__() #Preferences.getInstance().preferenceChanged.connect(self._onPreferenceChanged) Preferences.getInstance().preferenceChanged.connect(self._triggerTimer) machine_manager = Application.getInstance().getMachineManager() self._profile = None machine_manager.activeProfileChanged.connect(self._onActiveProfileChanged) machine_manager.profileNameChanged.connect(self._triggerTimer) machine_manager.profilesChanged.connect(self._triggerTimer) machine_manager.machineInstanceNameChanged.connect(self._triggerTimer) machine_manager.machineInstancesChanged.connect(self._triggerTimer) self._onActiveProfileChanged() Preferences.getInstance().addPreference("cura/autosave_delay", 1000 * 10) self._change_timer = QTimer() self._change_timer.setInterval(Preferences.getInstance().getValue("cura/autosave_delay")) self._change_timer.setSingleShot(True) self._change_timer.timeout.connect(self._onTimeout) self._saving = False def _triggerTimer(self, *args): if not self._saving: self._change_timer.start() def _onActiveProfileChanged(self): if self._profile: self._profile.settingValueChanged.disconnect(self._triggerTimer) self._profile = Application.getInstance().getMachineManager().getWorkingProfile() if self._profile: self._profile.settingValueChanged.connect(self._triggerTimer) def _onTimeout(self): self._saving = True # To prevent the save process from triggering another autosave. Logger.log("d", "Autosaving preferences, instances and profiles") Preferences.getInstance().writeToFile(Resources.getStoragePath(Resources.Preferences, Application.getInstance().getApplicationName() + ".cfg")) Application.getInstance().getMachineManager().saveMachineInstances() Application.getInstance().getMachineManager().saveProfiles() self._saving = False
class MainWindow(QMainWindow): def __init__(self, icon, node, iface_name): # Parent super(MainWindow, self).__init__() self.setWindowTitle('UAVCAN GUI Tool') self.setWindowIcon(icon) self._icon = icon self._node = node self._node_spin_timer = QTimer(self) self._node_spin_timer.timeout.connect(self._spin_node) self._node_spin_timer.setSingleShot(False) self._node_spin_timer.start(1) self._node_monitor_widget = NodeMonitorWidget(self, node) self._local_node_widget = LocalNodeWidget(self, node) self._log_message_widget = LogMessageDisplayWidget(self, node) self._bus_monitor_widget = BusMonitorWidget(self, node, iface_name) self._dynamic_node_id_allocation_widget = DynamicNodeIDAllocatorWidget(self, node, self._node_monitor_widget.monitor) outer_hbox = QHBoxLayout(self) left_vbox = QVBoxLayout(self) left_vbox.addWidget(self._local_node_widget) left_vbox.addWidget(self._node_monitor_widget) left_vbox.addWidget(self._log_message_widget) right_vbox = QVBoxLayout(self) right_vbox.addWidget(self._bus_monitor_widget, 1) right_vbox.addWidget(self._dynamic_node_id_allocation_widget) outer_hbox.addLayout(left_vbox, 1) outer_hbox.addLayout(right_vbox, 1) outer_widget = QWidget(self) outer_widget.setLayout(outer_hbox) self.setCentralWidget(outer_widget) def _spin_node(self): # We're running the node in the GUI thread. # This is not great, but at the moment seems like other options are even worse. try: self._node.spin(0) except Exception as ex: logger.error('Node spin error: %r', ex, exc_info=True)
def run(): """ Creates all the top-level assets for the application, sets things up and then runs the application. Specific tasks include: - set up logging - create an application object - create an editor window and status bar - display a splash screen while starting - close the splash screen after startup timer ends """ setup_logging() logging.info('\n\n-----------------\n\nStarting Mu {}'.format(__version__)) logging.info(platform.uname()) logging.info('Python path: {}'.format(sys.path)) # The app object is the application running on your computer. app = QApplication(sys.argv) app.setAttribute(Qt.AA_DontShowIconsInMenus) # Create the "window" we'll be looking at. editor_window = Window() # Create the "editor" that'll control the "window". editor = Editor(view=editor_window) editor.setup(setup_modes(editor, editor_window)) # Setup the window. editor_window.closeEvent = editor.quit editor_window.setup(editor.debug_toggle_breakpoint, editor.theme) # Restore the previous session along with files passed by the os editor.restore_session(sys.argv[1:]) # Connect the various UI elements in the window to the editor. editor_window.connect_tab_rename(editor.rename_tab, 'Ctrl+Shift+S') status_bar = editor_window.status_bar status_bar.connect_logs(editor.show_admin, 'Ctrl+Shift+D') # Display a friendly "splash" icon. splash = QSplashScreen(load_pixmap('splash-screen')) splash.show() # Finished starting up the application, so hide the splash icon. splash_be_gone = QTimer() splash_be_gone.timeout.connect(lambda: splash.finish(editor_window)) splash_be_gone.setSingleShot(True) splash_be_gone.start(5000) # Stop the program after the application finishes executing. sys.exit(app.exec_())
class RingWindow(QMainWindow): log = logging.getLogger('gui.RingWindow') image: RingImageQLabel statusbar: QStatusBar def __init__(self, dna_ch=None, sig_ch=None): super(RingWindow, self).__init__() path = os.path.join(sys.path[0], *__package__.split('.')) uic.loadUi(os.path.join(path, 'gui_ring.ui'), self) self.ctrl = QWidget() uic.loadUi(os.path.join(path, 'gui_ring_controls.ui'), self.ctrl) self.ctrl.zSpin.valueChanged.connect(self.onZValueChange) self.ctrl.openButton.pressed.connect(self.onOpenButton) self.ctrl.addButton.pressed.connect(self.onAddButton) self.ctrl.plotButton.pressed.connect(self.onPlotButton) self.ctrl.measureButton.pressed.connect(self.onMeasureButton) self.ctrl.dnaSpin.valueChanged.connect(self.onDnaValChange) self.ctrl.actSpin.valueChanged.connect(self.onActValChange) self.ctrl.dnaChk.toggled.connect(self.onImgToggle) self.ctrl.actChk.toggled.connect(self.onImgToggle) self.ctrl.renderChk.stateChanged.connect(self.onRenderChk) self.image.clicked.connect(self.onImgUpdate) self.image.lineUpdated.connect(self.onImgUpdate) self.image.linePicked.connect(self.onLinePickedFromImage) self.image.nucleusPicked.connect(self.onNucleusPickedFromImage) self.grph = GraphWidget() self.grphtimer = QTimer() self.grphtimer.setSingleShot(True) # self.stk = StkRingWidget(self.image, linePicked=self.onLinePickedFromStackGraph) self.grph.linePicked.connect(self.onLinePickedFromGraph) # self.stk.linePicked.connect(self.onLinePickedFromStackGraph) self.grphtimer.timeout.connect(self._graph) if dna_ch is not None: self.ctrl.dnaSpin.setValue(int(dna_ch)) if sig_ch is not None: self.ctrl.actSpin.setValue(int(sig_ch)) self.measure_n = 0 self.selectedLine = None self.line_length = 4 self.currMeasurement = None self.currN = None self.currZ = None self.df = pd.DataFrame() self.file = "/Users/Fabio/data/lab/airyscan/nil.czi" self.show() self.grph.show() # self.stk.show() self.ctrl.show() self.move(0, 0) self.resizeEvent(None) self.moveEvent(None) def resizeEvent(self, event): # this is a hack to resize everything when the user resizes the main window self.grph.setFixedWidth(self.width()) self.image.setFixedWidth(self.width()) self.image.setFixedHeight(self.height()) self.image.resizeEvent(None) self.moveEvent(None) def moveEvent(self, QMoveEvent): self.ctrl.move(self.frameGeometry().topRight()) self.grph.move(self.geometry().bottomLeft()) # self.stk.move(self.ctrl.frameGeometry().topRight()) def closeEvent(self, event): self._saveCurrentFileMeasurements() # if not self.df.empty: # self.df.loc[:, "condition"] = self.ctrl.experimentLineEdit.text() # self.df.loc[:, "l"] = self.df.loc[:, "l"].apply(lambda v: np.array2string(v, separator=',')) # self.df.to_csv(os.path.join(os.path.dirname(self.image.file), "ringlines.csv")) self.grph.close() self.ctrl.close() # self.stk.close() def focusInEvent(self, QFocusEvent): self.log.debug('focusInEvent') self.ctrl.activateWindow() self.grph.activateWindow() # self.stk.focusInEvent(None) def showEvent(self, event): self.setFocus() def _saveCurrentFileMeasurements(self): if not self.df.empty: self.log.info("Saving measurements.") fname = os.path.basename(self.image.file) df = self.df[self.df["file"] == fname] df.loc[:, "condition"] = self.ctrl.experimentLineEdit.text() df.loc[:, "l"] = self.df.loc[:, "l"].apply( lambda v: np.array2string(v, separator=',')) df.to_csv( os.path.join(os.path.dirname(self.image.file), f"{fname}.csv")) def _graphTendency(self): df = pd.DataFrame(self.image.measurements).drop( ['x', 'y', 'c', 'ls0', 'ls1', 'd', 'sum'], axis=1) df.loc[:, "xx"] = df.loc[:, "l"].apply(lambda v: np.arange( start=0, stop=len(v) * self.image.dl, step=self.image.dl)) df = m.vector_column_to_long_fmt(df, val_col="l", ix_col="xx") sns.lineplot(x="xx", y="l", data=df, ax=self.grph.ax, color='k', ci="sd", zorder=20) self.grph.ax.set_ylabel('') self.grph.ax.set_xlabel('') self.grph.canvas.draw() def _graph(self, alpha=1.0): self.grph.clear() lines = self.image.lines(self.image.currNucleusId) if lines.empty: return for ix, me in lines.iterrows(): x = np.arange(start=0, stop=len(me['value']) * self.image.dl, step=self.image.dl) lw = 0.1 if me['li'] != self.image.selectedLine else 0.5 self.grph.ax.plot(x, me['value'], linewidth=lw, linestyle='-', color=me['c'], alpha=alpha, zorder=10, picker=5, label=int( me['li'])) # , marker='o', markersize=1) self.grph.format_ax() # self.statusbar.showMessage("ptp: %s" % ["%d " % me['d'] for me in self.image.lines().iterrows()]) self.grph.canvas.draw() @QtCore.pyqtSlot() def onImgToggle(self): self.log.debug('onImgToggle') if self.ctrl.dnaChk.isChecked(): self.image.activeCh = "dna" if self.ctrl.actChk.isChecked(): self.image.activeCh = "act" @QtCore.pyqtSlot() def onRenderChk(self): self.log.debug('onRenderChk') self.image.renderMeasurements = self.ctrl.renderChk.isChecked() # self.stk.renderMeasurements = self.ctrl.renderChk.isChecked() @QtCore.pyqtSlot() def onOpenButton(self): self.log.debug('onOpenButton') # save current file measurements as a backup self._saveCurrentFileMeasurements() qfd = QFileDialog() path = os.path.dirname(self.file) if self.image.file is not None: self.statusbar.showMessage("current file: %s" % os.path.basename(self.image.file)) flt = "zeiss(*.czi)" f = QFileDialog.getOpenFileName(qfd, "Open File", path, flt) if len(f) > 0: self._open(f[0]) def _open(self, fname): assert type(fname) is str and len(fname) > 0, "No filename given!" self.file = fname self.image.file = fname self.image.zstack = self.ctrl.zSpin.value() self.image.dnaChannel = self.ctrl.dnaSpin.value() self.ctrl.nchLbl.setText("%d channels" % self.image.nChannels) self.ctrl.nzsLbl.setText("%d z-stacks" % self.image.nZstack) self.ctrl.nfrLbl.setText( "%d %s" % (self.image.nFrames, "frames" if self.image.nFrames > 1 else "frame")) self.currMeasurement = None self.currN = None self.currZ = None # self.stk.close() # self.stk = StkRingWidget(self.image, # nucleus_id=self.image.currNucleusId, # linePicked=self.onLinePickedFromStackGraph, # line_length=self.line_length, # dl=self.image.dl, # lines_to_measure=self.image._nlin # ) # # self.stk.linePicked.connect(self.onLinePickedFromStackGraph) # self.stk.loadImages(self.image.images, xy=(100, 100), wh=(200, 200)) # self.stk.hide() # self.stk.show() self.moveEvent(None) @QtCore.pyqtSlot() def onImgUpdate(self): # self.log.debug(f"onImgUpdate") self.ctrl.renderChk.setChecked(True) # self.stk.selectedLineId = self.image.selectedLine if self.image.selectedLine is not None else 0 # self.log.debug(f"onImgUpdate. Selected line is {self.stk.selectedLineId}") # self.stk.drawMeasurements(erase_bkg=True) self.grphtimer.start(1000) @QtCore.pyqtSlot() def onNucleusPickedFromImage(self): self.log.debug('onNucleusPickedFromImage\r\n' f' current image={self.image.file}\r\n' f' current nucleus={self.image.currNucleusId}') # self.stk.dnaChannel = self.image.dnaChannel # self.stk.rngChannel = self.image.rngChannel # self.stk.selectedLineId = self.image.selectedLine if self.image.selectedLine is not None else 0 # self.stk.selectedNucId = self.image.currNucleusId if self.image.currNucleusId is not None else 0 # # test rectification code # tsplaprx = PlotSplineApproximation(self.image.currNucleus, self.image) # tsplaprx.fit_xy(ax=plt.figure(10).gca()) # tsplaprx.fit_polygon(ax=plt.figure(11).gca()) # tsplaprx.grid(ax=plt.figure(12).gca()) # # trct = TestPiecewiseLinearRectification(tsplaprx, dl=4, n_dl=10, n_theta=100, pix_per_dl=1, pix_per_theta=1) # trct.plot_rectification() # # tfnrect = TestFunctionRectification(tsplaprx, pix_per_dl=1, pix_per_arclen=1) # tfnrect.plot_rectification() # plt.show(block=False) minx, miny, maxx, maxy = self.image.currNucleus.bounds r = int(max(maxx - minx, maxy - miny) / 2) # self.stk.loadImages(self.image.images, xy=[n[0] for n in self.image.currNucleus.centroid.xy], # wh=(r * self.image.pix_per_um, r * self.image.pix_per_um)) # self.stk.measure() # self.stk.drawMeasurements(erase_bkg=True) @QtCore.pyqtSlot() def onMeasureButton(self): self.log.debug('onMeasureButton') self.image.paint_measures() self._graph(alpha=0.2) # self._graphTendency() @QtCore.pyqtSlot() def onZValueChange(self): self.log.debug('onZValueChange') self.image.zstack = self.ctrl.zSpin.value() % self.image.nZstack self.ctrl.zSpin.setValue(self.image.zstack) self._graph() @QtCore.pyqtSlot() def onDnaValChange(self): self.log.debug('onDnaValChange') if not self.image.nChannels > 0: self.log.warning( 'Not a valid number of channels (image not loaded?).') return val = self.ctrl.dnaSpin.value() % self.image.nChannels self.ctrl.dnaSpin.setValue(val) self.image.dnaChannel = val if self.ctrl.dnaChk.isChecked(): self.image.activeCh = "dna" self.ctrl.dnaChk.setChecked(True) @QtCore.pyqtSlot() def onActValChange(self): self.log.debug('onActValChange') if not self.image.nChannels > 0: self.log.warning( 'Not a valid number of channels (image not loaded?).') return val = self.ctrl.actSpin.value() % self.image.nChannels self.ctrl.actSpin.setValue(val) self.image.rngChannel = val if self.ctrl.actChk.isChecked(): self.image.activeCh = "act" self.ctrl.actChk.setChecked(True) @QtCore.pyqtSlot() def onAddButton(self): self.log.debug('onAddButton') if self.currMeasurement is not None and self.currN is not None and self.currZ is not None: new = pd.DataFrame(self.currMeasurement) new = new.loc[(new["n"] == self.currN) & (new["z"] == self.currZ)] new.loc[:, "m"] = self.measure_n new.loc[:, "file"] = os.path.basename(self.image.file) # new.loc[:, "x"] = new.loc[:, "l"].apply(lambda v: np.arange(start=0, stop=len(v), step=self.image.dl)) self.df = self.df.append(new, ignore_index=True, sort=False) self.measure_n += 1 self.currMeasurement = None self.currN = None self.currZ = None print(self.df) @QtCore.pyqtSlot() def onPlotButton(self): self.log.debug('onPlotButton') if self.image.measurements is None: return import matplotlib.pyplot as plt import seaborn as sns from matplotlib.gridspec import GridSpec plt.style.use('bmh') pal = sns.color_palette("Blues", n_colors=len(self.image.measurements)) fig = plt.figure(figsize=(2, 2 * 4), dpi=300) gs = GridSpec(nrows=2, ncols=1, height_ratios=[4, 0.5]) ax1 = plt.subplot(gs[0, 0]) ax2 = plt.subplot(gs[1, 0]) self.image.drawMeasurements(ax1, pal) lw = 1 for me, c in zip(self.image.measurements, pal): x = np.arange(start=0, stop=len(me['l']) * self.image.dl, step=self.image.dl) ax2.plot(x, me['l'], linewidth=lw, linestyle='-', color=c, alpha=1, zorder=10) ax1.xaxis.set_major_locator(ticker.MultipleLocator(20)) ax1.xaxis.set_minor_locator(ticker.MultipleLocator(10)) ax1.yaxis.set_major_locator(ticker.MultipleLocator(20)) ax1.yaxis.set_minor_locator(ticker.MultipleLocator(10)) ax2.xaxis.set_major_locator(ticker.MultipleLocator(1)) ax2.xaxis.set_minor_locator(ticker.MultipleLocator(0.5)) ax2.yaxis.set_major_locator(ticker.MultipleLocator(1e4)) ax2.yaxis.set_minor_locator(ticker.MultipleLocator(5e3)) ax2.yaxis.set_major_formatter(EngFormatter(unit='')) fig.savefig(os.path.basename(self.image.file) + ".pdf") @QtCore.pyqtSlot() def onLinePickedFromGraph(self): self.log.debug('onLinePickedFromGraph') self.selectedLine = self.grph.selectedLine if self.grph.selectedLine is not None else None if self.selectedLine is not None: self.image.selectedLine = self.selectedLine self.currMeasurement = self.image.measurements self.currN = self.selectedLine self.currZ = self.image.zstack # self.stk.selectedLineId = self.image.selectedLine if self.image.selectedLine is not None else 0 # self.stk.selectedNucId = self.image.currNucleusId if self.image.currNucleusId is not None else 0 # self.stk.selectedZ = self.currZ # try: # self.stk.drawMeasurements(erase_bkg=True) # except Exception as e: # self.log.error(e) self.statusbar.showMessage("line %d selected" % self.selectedLine) @QtCore.pyqtSlot() def onLinePickedFromStackGraph(self): self.log.debug('onLinePickedFromStackGraph') self.selectedLine = self.stk.selectedLineId if self.stk.selectedLineId is not None else None if self.selectedLine is not None: self.currN = self.stk.selectedLineId self.stk.selectedLineId = self.image.selectedLine if self.image.selectedLine is not None else 0 self.stk.selectedNucId = self.image.currNucleusId if self.image.currNucleusId is not None else 0 self.currZ = self.stk.selectedZ self.statusbar.showMessage( f"Line {self.currN} of z-stack {self.currZ} selected.") self.log.info( f"Line {self.currN} of z-stack {self.currZ} selected.") @QtCore.pyqtSlot() def onLinePickedFromImage(self): self.log.debug('onLinePickedFromImage') self.selectedLine = self.image.selectedLine if self.image.selectedLine is not None else None if self.selectedLine is not None: self.currN = self.selectedLine self.currZ = self.image.zstack # self.stk.selectedLineId = self.image.selectedLine if self.image.selectedLine is not None else 0 # self.stk.selectedNucId = self.image.currNucleusId if self.image.currNucleusId is not None else 0 # self.stk.selectedZ = self.currZ self.statusbar.showMessage("Line %d selected" % self.selectedLine)
class ElectrumGui: def __init__(self, config, daemon, plugins): set_language(config.get('language')) # Uncomment this call to verify objects are being properly # GC-ed when windows are closed #network.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer, # ElectrumWindow], interval=5)]) QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"): QtCore.QCoreApplication.setAttribute( QtCore.Qt.AA_ShareOpenGLContexts) if hasattr(QGuiApplication, 'setDesktopFileName'): QGuiApplication.setDesktopFileName('electrum-sv.desktop') self.config = config self.daemon = daemon self.plugins = plugins self.windows = [] self.efilter = OpenFileEventFilter(self.windows) self.app = QElectrumApplication(sys.argv) self.app.installEventFilter(self.efilter) # timer self.timer = QTimer(self.app) self.timer.setSingleShot(False) self.timer.setInterval(500) # msec self.nd = None self.exception_hook = None # init tray self.dark_icon = self.config.get("dark_icon", False) self.tray = QSystemTrayIcon(self.tray_icon(), None) self.tray.setToolTip('ElectrumSV') self.tray.activated.connect(self.tray_activated) self.build_tray_menu() self.tray.show() self.app.new_window_signal.connect(self.start_new_window) run_hook('init_qt', self) ColorScheme.update_from_widget(QWidget()) def build_tray_menu(self): # Avoid immediate GC of old menu when window closed via its action if self.tray.contextMenu() is None: m = QMenu() self.tray.setContextMenu(m) else: m = self.tray.contextMenu() m.clear() for window in self.windows: submenu = m.addMenu(window.wallet.basename()) submenu.addAction(_("Show/Hide"), window.show_or_hide) submenu.addAction(_("Close"), window.close) m.addAction(_("Dark/Light"), self.toggle_tray_icon) m.addSeparator() m.addAction(_("Exit ElectrumSV"), self.close) self.tray.setContextMenu(m) def tray_icon(self): if self.dark_icon: return QIcon(':icons/electrumsv_dark_icon.png') else: return QIcon(':icons/electrumsv_light_icon.png') def toggle_tray_icon(self): self.dark_icon = not self.dark_icon self.config.set_key("dark_icon", self.dark_icon, True) self.tray.setIcon(self.tray_icon()) def tray_activated(self, reason): if reason == QSystemTrayIcon.DoubleClick: if all([w.is_hidden() for w in self.windows]): for w in self.windows: w.bring_to_top() else: for w in self.windows: w.hide() def close(self): for window in self.windows: window.close() def new_window(self, path, uri=None): # Use a signal as can be called from daemon thread self.app.new_window_signal.emit(path, uri) def show_network_dialog(self, parent): if not self.daemon.network: parent.show_warning(_( 'You are using ElectrumSV in offline mode; restart ' 'ElectrumSV if you want to get connected'), title=_('Offline')) return if self.nd: self.nd.on_update() self.nd.show() self.nd.raise_() return self.nd = NetworkDialog(self.daemon.network, self.config) self.nd.show() def create_window_for_wallet(self, wallet): w = ElectrumWindow(self, wallet) self.windows.append(w) self.build_tray_menu() # FIXME: Remove in favour of the load_wallet hook run_hook('on_new_window', w) return w def start_new_window(self, path, uri): '''Raises the window for the wallet if it is open. Otherwise opens the wallet and creates a new window for it.''' for w in self.windows: if w.wallet.storage.path == path: w.bring_to_top() break else: try: wallet = self.daemon.load_wallet(path, None) if not wallet: storage = WalletStorage(path, manual_upgrades=True) wizard = InstallWizard(self.config, self.app, self.plugins, storage) try: wallet = wizard.run_and_get_wallet() except UserCancelled: pass except GoBack as e: logging.error( '[start_new_window] Exception caught (GoBack) %s', e) finally: wizard.terminate() if not wallet: return wallet.start_threads(self.daemon.network) self.daemon.add_wallet(wallet) except BaseException as e: logging.exception("") if '2fa' in str(e): d = QMessageBox(QMessageBox.Warning, _('Error'), '2FA wallets are not unsupported.') d.exec_() else: d = QMessageBox(QMessageBox.Warning, _('Error'), 'Cannot load wallet:\n' + str(e)) d.exec_() return w = self.create_window_for_wallet(wallet) if uri: w.pay_to_URI(uri) w.bring_to_top() w.setWindowState(w.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) # this will activate the window w.activateWindow() return w def close_window(self, window): self.windows.remove(window) self.build_tray_menu() # save wallet path of last open window if not self.windows: self.config.save_last_wallet(window.wallet) run_hook('on_close_window', window) def maybe_choose_server(self): # Show network dialog if config does not exist if self.daemon.network and self.config.get('auto_connect') is None: try: wizard = InstallWizard(self.config, self.app, self.plugins, None) wizard.init_network(self.daemon.network) wizard.terminate() except Exception as e: if not isinstance(e, (UserCancelled, GoBack)): logging.exception("") self.app.quit() def event_loop_started(self): if self.config.get("show_crash_reporter", default=True): self.exception_hook = Exception_Hook(self.app) self.timer.start() signal.signal(signal.SIGINT, lambda *args: self.app.quit()) self.maybe_choose_server() self.config.open_last_wallet() path = self.config.get_wallet_path() if not self.start_new_window(path, self.config.get('url')): self.app.quit() def main(self): QTimer.singleShot(0, self.event_loop_started) self.app.exec_() # Shut down the timer cleanly self.timer.stop() # clipboard persistence # see http://www.mail-archive.com/[email protected]/msg17328.html event = QtCore.QEvent(QtCore.QEvent.Clipboard) self.app.sendEvent(self.app.clipboard(), event) self.tray.hide()
class Demo(QWidget, Ui_Form): start_timer_signal = pyqtSignal() def __init__(self): super(Demo, self).__init__() self.setupUi(self) self.save_ui = Save_demo() self.code_box = CodeBox() self.myrecognition = Face_recognition() self.mylock = MyLock() self.activation_timer = QTimer() self.slot_init() self.current_photo = '' self.save_photo_flag = False self.stop_camera_flag = False self.save_button_init() self.unlock_button_init() self.delete_button_init() self.activation_timer_init() self.recognition_thread = Thread(target=self.run_face_recognition) self.recognition_thread.start() def slot_init(self): # 信号和槽连接 self.save_button.clicked.connect(self.savePhoto) self.activation_timer.timeout.connect(self.deactivate_button) self.start_timer_signal.connect(self.start_timer) self.delete_button.clicked.connect(self.go_delete_photo) self.unlock_button.clicked.connect(self.unlock_the_door) self.code_box_button.clicked.connect(self.run_code_box) self.code_box.login_successfully.connect(self.login_successfully) def save_button_init(self): # 如果没有保存人,则保存按键可以点击,否则不能点击 if self.myrecognition.is_dir_empty(): self.save_button.setEnabled(True) else: self.save_button.setEnabled(False) def unlock_button_init(self): self.unlock_button.setEnabled(False) def delete_button_init(self): self.delete_button.setEnabled(False) def activation_timer_init(self): self.activation_timer.setSingleShot(True) def run_face_recognition(self): self.myrecognition.turn_on_camera() self.myrecognition.get_known_face_from_dir() while True: if not self.stop_camera_flag: self.myrecognition.face_recognition() # 检测是否有识别出的任务,有则运行按键按下 self.count = self.myrecognition.found_known_people_number() if self.count != 0: self.activate_button() self.start_timer_signal.emit() # 检测是否按下保存 if self.save_photo_flag: self.photo_to_save = self.myrecognition.frame.copy() self.save_photo_flag = False self.stop_camera_flag = True self.myrecognition.add_mark() self.show = cv2.resize(self.myrecognition.frame, None, fx=1, fy=1, interpolation=cv2.INTER_CUBIC) self.show = cv2.cvtColor(self.show, cv2.COLOR_BGR2RGB) self.showImage = QImage(self.show.data, self.show.shape[1], self.show.shape[0], QImage.Format_RGB888) self.label.setPixmap(QPixmap.fromImage(self.showImage)) # 保存图片 def savePhoto(self): self.save_photo_flag = True while self.save_photo_flag: pass self.save_ui.image = self.photo_to_save self.save_ui.show_picture() self.save_ui.exec_() # 刷新已经记录的人 self.reload_people() self.stop_camera_flag = False def reload_people(self): # 刷新存的人的资料 self.myrecognition.get_known_face_from_dir() def start_timer(self): self.activation_timer.start(4000) def activate_button(self): if not self.activation_timer.isActive(): self.save_button.setEnabled(True) self.delete_button.setEnabled(True) self.unlock_button.setEnabled(True) self.message.setText("认证成功") def deactivate_button(self): if self.myrecognition.is_dir_empty(): self.save_button.setEnabled(True) else: self.save_button.setEnabled(False) self.delete_button.setEnabled(False) self.unlock_button.setEnabled(False) self.message.setText("欢迎^_^") def go_delete_photo(self): self.stop_camera_flag = True delete_page = DeletePage() delete_page.exec_() # 刷新已经记录的人 self.reload_people() # 检测人是否空了,如果空了,要允许保存 if self.myrecognition.is_dir_empty(): self.save_button.setEnabled(True) self.stop_camera_flag = False def unlock_the_door(self): self.mylock.unlock() def run_code_box(self): self.code_box.exec_() def login_successfully(self): self.activate_button() self.start_timer_signal.emit()
class PrinterOutputDevice(QObject, OutputDevice): printersChanged = pyqtSignal() connectionStateChanged = pyqtSignal(str) acceptsCommandsChanged = pyqtSignal() # Signal to indicate that the material of the active printer on the remote changed. materialIdChanged = pyqtSignal() # # Signal to indicate that the hotend of the active printer on the remote changed. hotendIdChanged = pyqtSignal() # Signal to indicate that the info text about the connection has changed. connectionTextChanged = pyqtSignal() # Signal to indicate that the configuration of one of the printers has changed. uniqueConfigurationsChanged = pyqtSignal() def __init__( self, device_id: str, connection_type: "ConnectionType" = ConnectionType.NotConnected, parent: QObject = None) -> None: super().__init__( device_id=device_id, parent=parent ) # type: ignore # MyPy complains with the multiple inheritance self._printers = [] # type: List[PrinterOutputModel] self._unique_configurations = [ ] # type: List[PrinterConfigurationModel] self._monitor_view_qml_path = "" # type: str self._monitor_component = None # type: Optional[QObject] self._monitor_item = None # type: Optional[QObject] self._control_view_qml_path = "" # type: str self._control_component = None # type: Optional[QObject] self._control_item = None # type: Optional[QObject] self._accepts_commands = False # type: bool self._update_timer = QTimer() # type: QTimer self._update_timer.setInterval( 2000) # TODO; Add preference for update interval self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._update) self._connection_state = ConnectionState.Closed # type: ConnectionState self._connection_type = connection_type # type: ConnectionType self._firmware_updater = None # type: Optional[FirmwareUpdater] self._firmware_name = None # type: Optional[str] self._address = "" # type: str self._connection_text = "" # type: str self.printersChanged.connect(self._onPrintersChanged) QtApplication.getInstance().getOutputDeviceManager( ).outputDevicesChanged.connect(self._updateUniqueConfigurations) @pyqtProperty(str, notify=connectionTextChanged) def address(self) -> str: return self._address def setConnectionText(self, connection_text): if self._connection_text != connection_text: self._connection_text = connection_text self.connectionTextChanged.emit() @pyqtProperty(str, constant=True) def connectionText(self) -> str: return self._connection_text def materialHotendChangedMessage(self, callback: Callable[[int], None]) -> None: Logger.log( "w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'" ) callback(QMessageBox.Yes) def isConnected(self) -> bool: return self._connection_state != ConnectionState.Closed and self._connection_state != ConnectionState.Error def setConnectionState(self, connection_state: "ConnectionState") -> None: if self._connection_state != connection_state: self._connection_state = connection_state self.connectionStateChanged.emit(self._id) @pyqtProperty(int, constant=True) def connectionType(self) -> "ConnectionType": return self._connection_type @pyqtProperty(int, notify=connectionStateChanged) def connectionState(self) -> "ConnectionState": return self._connection_state def _update(self) -> None: pass def _getPrinterByKey(self, key: str) -> Optional["PrinterOutputModel"]: for printer in self._printers: if printer.key == key: return printer return None def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None: raise NotImplementedError("requestWrite needs to be implemented") @pyqtProperty(QObject, notify=printersChanged) def activePrinter(self) -> Optional["PrinterOutputModel"]: if len(self._printers): return self._printers[0] return None @pyqtProperty("QVariantList", notify=printersChanged) def printers(self) -> List["PrinterOutputModel"]: return self._printers @pyqtProperty(QObject, constant=True) def monitorItem(self) -> QObject: # Note that we specifically only check if the monitor component is created. # It could be that it failed to actually create the qml item! If we check if the item was created, it will try to # create the item (and fail) every time. if not self._monitor_component: self._createMonitorViewFromQML() return self._monitor_item @pyqtProperty(QObject, constant=True) def controlItem(self) -> QObject: if not self._control_component: self._createControlViewFromQML() return self._control_item def _createControlViewFromQML(self) -> None: if not self._control_view_qml_path: return if self._control_item is None: self._control_item = QtApplication.getInstance( ).createQmlComponent(self._control_view_qml_path, {"OutputDevice": self}) def _createMonitorViewFromQML(self) -> None: if not self._monitor_view_qml_path: return if self._monitor_item is None: self._monitor_item = QtApplication.getInstance( ).createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self}) ## Attempt to establish connection def connect(self) -> None: self.setConnectionState(ConnectionState.Connecting) self._update_timer.start() ## Attempt to close the connection def close(self) -> None: self._update_timer.stop() self.setConnectionState(ConnectionState.Closed) ## Ensure that close gets called when object is destroyed def __del__(self) -> None: self.close() @pyqtProperty(bool, notify=acceptsCommandsChanged) def acceptsCommands(self) -> bool: return self._accepts_commands ## Set a flag to signal the UI that the printer is not (yet) ready to receive commands def _setAcceptsCommands(self, accepts_commands: bool) -> None: if self._accepts_commands != accepts_commands: self._accepts_commands = accepts_commands self.acceptsCommandsChanged.emit() # Returns the unique configurations of the printers within this output device @pyqtProperty("QVariantList", notify=uniqueConfigurationsChanged) def uniqueConfigurations(self) -> List["PrinterConfigurationModel"]: return self._unique_configurations def _updateUniqueConfigurations(self) -> None: all_configurations = set() for printer in self._printers: if printer.printerConfiguration is not None and printer.printerConfiguration.hasAnyMaterialLoaded( ): all_configurations.add(printer.printerConfiguration) all_configurations.update(printer.availableConfigurations) new_configurations = sorted( all_configurations, key=lambda config: config.printerType or "") if new_configurations != self._unique_configurations: self._unique_configurations = new_configurations self.uniqueConfigurationsChanged.emit() # Returns the unique configurations of the printers within this output device @pyqtProperty("QStringList", notify=uniqueConfigurationsChanged) def uniquePrinterTypes(self) -> List[str]: return list( sorted( set([ configuration.printerType or "" for configuration in self._unique_configurations ]))) def _onPrintersChanged(self) -> None: for printer in self._printers: printer.configurationChanged.connect( self._updateUniqueConfigurations) printer.availableConfigurationsChanged.connect( self._updateUniqueConfigurations) # At this point there may be non-updated configurations self._updateUniqueConfigurations() ## Set the device firmware name # # \param name The name of the firmware. def _setFirmwareName(self, name: str) -> None: self._firmware_name = name ## Get the name of device firmware # # This name can be used to define device type def getFirmwareName(self) -> Optional[str]: return self._firmware_name def getFirmwareUpdater(self) -> Optional["FirmwareUpdater"]: return self._firmware_updater @pyqtSlot(str) def updateFirmware(self, firmware_file: Union[str, QUrl]) -> None: if not self._firmware_updater: return self._firmware_updater.updateFirmware(firmware_file)
class NetworkClusterPrinterOutputDevice( NetworkPrinterOutputDevice.NetworkPrinterOutputDevice): printJobsChanged = pyqtSignal() printersChanged = pyqtSignal() selectedPrinterChanged = pyqtSignal() def __init__(self, key, address, properties, api_prefix, plugin_path): super().__init__(key, address, properties, api_prefix) # Store the address of the master. self._master_address = address name_property = properties.get(b"name", b"") if name_property: name = name_property.decode("utf-8") else: name = key self._plugin_path = plugin_path self.setName(name) description = i18n_catalog.i18nc( "@action:button Preceded by 'Ready to'.", "Print over network") self.setShortDescription(description) self.setDescription(description) self._stage = OutputStage.ready host_override = os.environ.get("CLUSTER_OVERRIDE_HOST", "") if host_override: Logger.log( "w", "Environment variable CLUSTER_OVERRIDE_HOST is set to [%s], cluster hosts are now set to this host", host_override) self._host = "http://" + host_override else: self._host = "http://" + address # is the same as in NetworkPrinterOutputDevicePlugin self._cluster_api_version = "1" self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/" self._api_base_uri = self._host + self._cluster_api_prefix self._file_name = None self._progress_message = None self._request = None self._reply = None # The main reason to keep the 'multipart' form data on the object # is to prevent the Python GC from claiming it too early. self._multipart = None self._print_view = None self._request_job = [] self._monitor_view_qml_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "ClusterMonitorItem.qml") self._control_view_qml_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "ClusterControlItem.qml") self._print_jobs = [] self._print_job_by_printer_uuid = {} self._print_job_by_uuid = {} # Print jobs by their own uuid self._printers = [] self._printers_dict = {} # by unique_name self._connected_printers_type_count = [] self._automatic_printer = { "unique_name": "", "friendly_name": "Automatic" } # empty unique_name IS automatic selection self._selected_printer = self._automatic_printer self._cluster_status_update_timer = QTimer() self._cluster_status_update_timer.setInterval(5000) self._cluster_status_update_timer.setSingleShot(False) self._cluster_status_update_timer.timeout.connect( self._requestClusterStatus) self._can_pause = True self._can_abort = True self._can_pre_heat_bed = False self._cluster_size = int(properties.get(b"cluster_size", 0)) self._cleanupRequest() #These are texts that are to be translated for future features. temporary_translation = i18n_catalog.i18n( "This printer is not set up to host a group of connected Ultimaker 3 printers." ) temporary_translation2 = i18n_catalog.i18nc( "Count is number of printers.", "This printer is the host for a group of {count} connected Ultimaker 3 printers." ).format(count=3) temporary_translation3 = i18n_catalog.i18n( "{printer_name} has finished printing '{job_name}'. Please collect the print and confirm clearing the build plate." ) #When finished. temporary_translation4 = i18n_catalog.i18n( "{printer_name} is reserved to print '{job_name}'. Please change the printer's configuration to match the job, for it to start printing." ) #When configuration changed. @pyqtProperty(QObject, notify=selectedPrinterChanged) def controlItem(self): # TODO: Probably not the nicest way to do this. This needs to be done better at some point in time. if not self._control_component: self._createControlViewFromQML() name = self._selected_printer.get("friendly_name") if name == self._automatic_printer.get("friendly_name") or name == "": return self._control_item # Let cura use the default. return None @pyqtSlot(int, result=str) def getTimeCompleted(self, time_remaining): current_time = time.time() datetime_completed = datetime.datetime.fromtimestamp(current_time + time_remaining) return "{hour:02d}:{minute:02d}".format( hour=datetime_completed.hour, minute=datetime_completed.minute) @pyqtSlot(int, result=str) def getDateCompleted(self, time_remaining): current_time = time.time() datetime_completed = datetime.datetime.fromtimestamp(current_time + time_remaining) return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper() @pyqtProperty(int, constant=True) def clusterSize(self): return self._cluster_size @pyqtProperty(str, notify=selectedPrinterChanged) def name(self): # Show the name of the selected printer. # This is not the nicest way to do this, but changes to the Cura UI are required otherwise. name = self._selected_printer.get("friendly_name") if name != self._automatic_printer.get("friendly_name"): return name # Return name of cluster master. return self._properties.get(b"name", b"").decode("utf-8") def connect(self): super().connect() self._cluster_status_update_timer.start() def close(self): super().close() self._cluster_status_update_timer.stop() def _setJobState(self, job_state): if not self._selected_printer: return selected_printer_uuid = self._printers_dict[ self._selected_printer["unique_name"]]["uuid"] if selected_printer_uuid not in self._print_job_by_printer_uuid: return print_job_uuid = self._print_job_by_printer_uuid[ selected_printer_uuid]["uuid"] url = QUrl(self._api_base_uri + "print_jobs/" + print_job_uuid + "/action") put_request = QNetworkRequest(url) put_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") data = '{"action": "' + job_state + '"}' self._manager.put(put_request, data.encode()) def _requestClusterStatus(self): # TODO: Handle timeout. We probably want to know if the cluster is still reachable or not. url = QUrl(self._api_base_uri + "print_jobs/") print_jobs_request = QNetworkRequest(url) self._addUserAgentHeader(print_jobs_request) self._manager.get(print_jobs_request) # See _finishedPrintJobsRequest() url = QUrl(self._api_base_uri + "printers/") printers_request = QNetworkRequest(url) self._addUserAgentHeader(printers_request) self._manager.get(printers_request) # See _finishedPrintersRequest() def _finishedPrintJobsRequest(self, reply): try: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received an invalid print job state message: Not valid JSON.") return self.setPrintJobs(json_data) def _finishedPrintersRequest(self, reply): try: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received an invalid print job state message: Not valid JSON.") return self.setPrinters(json_data) def materialHotendChangedMessage(self, callback): pass # Do nothing. def _startCameraStream(self): ## Request new image url = QUrl("http://" + self._printers_dict[ self._selected_printer["unique_name"]]["ip_address"] + ":8080/?action=stream") self._image_request = QNetworkRequest(url) self._addUserAgentHeader(self._image_request) self._image_reply = self._manager.get(self._image_request) self._image_reply.downloadProgress.connect( self._onStreamDownloadProgress) def spawnPrintView(self): if self._print_view is None: path = QUrl.fromLocalFile( os.path.join(self._plugin_path, "PrintWindow.qml")) component = QQmlComponent(Application.getInstance()._engine, path) self._print_context = QQmlContext( Application.getInstance()._engine.rootContext()) self._print_context.setContextProperty("OutputDevice", self) self._print_view = component.create(self._print_context) if component.isError(): Logger.log( "e", " Errors creating component: \n%s", "\n".join([e.toString() for e in component.errors()])) if self._print_view is not None: self._print_view.show() ## Store job info, show Print view for settings def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): self._selected_printer = self._automatic_printer # reset to default option self._request_job = [ nodes, file_name, filter_by_machine, file_handler, kwargs ] if self._stage != OutputStage.ready: if self._error_message: self._error_message.hide() self._error_message = Message( i18n_catalog.i18nc( "@info:status", "Sending new jobs (temporarily) blocked, still sending the previous print job." )) self._error_message.show() return if len(self._printers) > 1: self.spawnPrintView() # Ask user how to print it. elif len(self._printers) == 1: # If there is only one printer, don't bother asking. self.selectAutomaticPrinter() self.sendPrintJob() else: # Cluster has no printers, warn the user of this. if self._error_message: self._error_message.hide() self._error_message = Message( i18n_catalog.i18nc( "@info:status", "Unable to send new print job: this 3D printer is not (yet) set up to host a group of connected Ultimaker 3 printers." )) self._error_message.show() ## Actually send the print job, called from the dialog # :param: require_printer_name: name of printer, or "" @pyqtSlot() def sendPrintJob(self): nodes, file_name, filter_by_machine, file_handler, kwargs = self._request_job require_printer_name = self._selected_printer["unique_name"] self._send_gcode_start = time.time() Logger.log("d", "Sending print job [%s] to host..." % file_name) if self._stage != OutputStage.ready: Logger.log("d", "Unable to send print job as the state is %s", self._stage) raise OutputDeviceError.DeviceBusyError() self._stage = OutputStage.uploading self._file_name = "%s.gcode.gz" % file_name self._showProgressMessage() self._request = self._buildSendPrintJobHttpRequest( require_printer_name) self._reply = self._manager.post(self._request, self._multipart) self._reply.uploadProgress.connect(self._onUploadProgress) # See _finishedPostPrintJobRequest() def _buildSendPrintJobHttpRequest(self, require_printer_name): api_url = QUrl(self._api_base_uri + "print_jobs/") request = QNetworkRequest(api_url) # Create multipart request and add the g-code. self._multipart = QHttpMultiPart(QHttpMultiPart.FormDataType) # Add gcode part = QHttpPart() part.setHeader( QNetworkRequest.ContentDispositionHeader, 'form-data; name="file"; filename="%s"' % self._file_name) gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") compressed_gcode = self._compressGcode(gcode) if compressed_gcode is None: return # User aborted print, so stop trying. part.setBody(compressed_gcode) self._multipart.append(part) # require_printer_name "" means automatic if require_printer_name: self._multipart.append( self.__createKeyValueHttpPart("require_printer_name", require_printer_name)) user_name = self.__get_username() if user_name is None: user_name = "unknown" self._multipart.append( self.__createKeyValueHttpPart("owner", user_name)) self._addUserAgentHeader(request) return request def _compressGcode(self, gcode): self._compressing_print = True batched_line = "" max_chars_per_line = int(1024 * 1024 / 4) # 1 / 4 MB byte_array_file_data = b"" def _compressDataAndNotifyQt(data_to_append): compressed_data = gzip.compress(data_to_append.encode("utf-8")) QCoreApplication.processEvents( ) # Ensure that the GUI does not freeze. # Pretend that this is a response, as zipping might take a bit of time. self._last_response_time = time.time() return compressed_data if gcode is None: Logger.log("e", "Unable to find sliced gcode, returning empty.") return byte_array_file_data for line in gcode: if not self._compressing_print: self._progress_message.hide() return # Stop trying to zip, abort was called. batched_line += line # if the gcode was read from a gcode file, self._gcode will be a list of all lines in that file. # Compressing line by line in this case is extremely slow, so we need to batch them. if len(batched_line) < max_chars_per_line: continue byte_array_file_data += _compressDataAndNotifyQt(batched_line) batched_line = "" # Also compress the leftovers. if batched_line: byte_array_file_data += _compressDataAndNotifyQt(batched_line) return byte_array_file_data def __createKeyValueHttpPart(self, key, value): metadata_part = QHttpPart() metadata_part.setHeader(QNetworkRequest.ContentTypeHeader, 'text/plain') metadata_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="%s"' % (key)) metadata_part.setBody(bytearray(value, "utf8")) return metadata_part def __get_username(self): try: return getpass.getuser() except: Logger.log( "d", "Could not get the system user name, returning 'unknown' instead." ) return None def _finishedPrintJobPostRequest(self, reply): self._stage = OutputStage.ready if self._progress_message: self._progress_message.hide() self._progress_message = None self.writeFinished.emit(self) if reply.error(): self._showRequestFailedMessage(reply) self.writeError.emit(self) else: self._showRequestSucceededMessage() self.writeSuccess.emit(self) self._cleanupRequest() def _showRequestFailedMessage(self, reply): if reply is not None: Logger.log( "w", "Unable to send print job to group {cluster_name}: {error_string} ({error})" .format(cluster_name=self.getName(), error_string=str(reply.errorString()), error=str(reply.error()))) error_message_template = i18n_catalog.i18nc( "@info:status", "Unable to send print job to group {cluster_name}.") message = Message(text=error_message_template.format( cluster_name=self.getName())) message.show() def _showRequestSucceededMessage(self): confirmation_message_template = i18n_catalog.i18nc( "@info:status", "Sent {file_name} to group {cluster_name}.") file_name = os.path.basename(self._file_name).split(".")[0] message_text = confirmation_message_template.format( cluster_name=self.getName(), file_name=file_name) message = Message(text=message_text) button_text = i18n_catalog.i18nc("@action:button", "Show print jobs") button_tooltip = i18n_catalog.i18nc( "@info:tooltip", "Opens the print jobs interface in your browser.") message.addAction("open_browser", button_text, "globe", button_tooltip) message.actionTriggered.connect(self._onMessageActionTriggered) message.show() def setPrintJobs(self, print_jobs): #TODO: hack, last seen messes up the check, so drop it. for job in print_jobs: del job["last_seen"] # Strip any extensions job["name"] = self._removeGcodeExtension(job["name"]) if self._print_jobs != print_jobs: old_print_jobs = self._print_jobs self._print_jobs = print_jobs self._notifyFinishedPrintJobs(old_print_jobs, print_jobs) # Yes, this is a hacky way of doing it, but it's quick and the API doesn't give the print job per printer # for some reason. ugh. self._print_job_by_printer_uuid = {} self._print_job_by_uuid = {} for print_job in print_jobs: if "printer_uuid" in print_job and print_job[ "printer_uuid"] is not None: self._print_job_by_printer_uuid[ print_job["printer_uuid"]] = print_job self._print_job_by_uuid[print_job["uuid"]] = print_job self.printJobsChanged.emit() def _removeGcodeExtension(self, name): parts = name.split(".") if parts[-1].upper() == "GZ": parts = parts[:-1] if parts[-1].upper() == "GCODE": parts = parts[:-1] return ".".join(parts) def _notifyFinishedPrintJobs(self, old_print_jobs, new_print_jobs): """Notify the user when any of their print jobs have just completed. Arguments: old_print_jobs -- the previous list of print job status information as returned by the cluster REST API. new_print_jobs -- the current list of print job status information as returned by the cluster REST API. """ if old_print_jobs is None: return username = self.__get_username() if username is None: return our_old_print_jobs = self.__filterOurPrintJobs(old_print_jobs) our_old_not_finished_print_jobs = [ pj for pj in our_old_print_jobs if pj["status"] != "wait_cleanup" ] our_new_print_jobs = self.__filterOurPrintJobs(new_print_jobs) our_new_finished_print_jobs = [ pj for pj in our_new_print_jobs if pj["status"] == "wait_cleanup" ] old_not_finished_print_job_uuids = set( [pj["uuid"] for pj in our_old_not_finished_print_jobs]) for print_job in our_new_finished_print_jobs: if print_job["uuid"] in old_not_finished_print_job_uuids: printer_name = self.__getPrinterNameFromUuid( print_job["printer_uuid"]) if printer_name is None: printer_name = i18n_catalog.i18nc("@info:status", "Unknown printer") message_text = (i18n_catalog.i18nc( "@info:status", "Printer '{printer_name}' has finished printing '{job_name}'." ).format(printer_name=printer_name, job_name=print_job["name"])) message = Message(text=message_text, title=i18n_catalog.i18nc( "@info:status", "Print finished")) Application.getInstance().showMessage(message) Application.getInstance().showToastMessage( i18n_catalog.i18nc("@info:status", "Print finished"), message_text) def __filterOurPrintJobs(self, print_jobs): username = self.__get_username() return [ print_job for print_job in print_jobs if print_job["owner"] == username ] def __getPrinterNameFromUuid(self, printer_uuid): for printer in self._printers: if printer["uuid"] == printer_uuid: return printer["friendly_name"] return None def setPrinters(self, printers): if self._printers != printers: self._connected_printers_type_count = [] printers_count = {} self._printers = printers self._printers_dict = dict( (p["unique_name"], p) for p in printers) # for easy lookup by unique_name for printer in printers: variant = printer["machine_variant"] if variant in printers_count: printers_count[variant] += 1 else: printers_count[variant] = 1 for type in printers_count: self._connected_printers_type_count.append({ "machine_type": type, "count": printers_count[type] }) self.printersChanged.emit() @pyqtProperty("QVariantList", notify=printersChanged) def connectedPrintersTypeCount(self): return self._connected_printers_type_count @pyqtProperty("QVariantList", notify=printersChanged) def connectedPrinters(self): return self._printers @pyqtProperty(int, notify=printJobsChanged) def numJobsPrinting(self): num_jobs_printing = 0 for job in self._print_jobs: if job["status"] in [ "printing", "wait_cleanup", "sent_to_printer", "pre_print", "post_print" ]: num_jobs_printing += 1 return num_jobs_printing @pyqtProperty(int, notify=printJobsChanged) def numJobsQueued(self): num_jobs_queued = 0 for job in self._print_jobs: if job["status"] == "queued": num_jobs_queued += 1 return num_jobs_queued @pyqtProperty("QVariantMap", notify=printJobsChanged) def printJobsByUUID(self): return self._print_job_by_uuid @pyqtProperty("QVariantMap", notify=printJobsChanged) def printJobsByPrinterUUID(self): return self._print_job_by_printer_uuid @pyqtProperty("QVariantList", notify=printJobsChanged) def printJobs(self): return self._print_jobs @pyqtProperty("QVariantList", notify=printersChanged) def printers(self): return [ self._automatic_printer, ] + self._printers @pyqtSlot(str, str) def selectPrinter(self, unique_name, friendly_name): self.stopCamera() self._selected_printer = { "unique_name": unique_name, "friendly_name": friendly_name } Logger.log("d", "Selected printer: %s %s", friendly_name, unique_name) # TODO: Probably not the nicest way to do this. This needs to be done better at some point in time. if unique_name == "": self._address = self._master_address else: self._address = self._printers_dict[ self._selected_printer["unique_name"]]["ip_address"] self.selectedPrinterChanged.emit() def _updateJobState(self, job_state): name = self._selected_printer.get("friendly_name") if name == "" or name == "Automatic": # TODO: This is now a bit hacked; If no printer is selected, don't show job state. if self._job_state != "": self._job_state = "" self.jobStateChanged.emit() else: if self._job_state != job_state: self._job_state = job_state self.jobStateChanged.emit() @pyqtSlot() def selectAutomaticPrinter(self): self.stopCamera() self._selected_printer = self._automatic_printer self.selectedPrinterChanged.emit() @pyqtProperty("QVariant", notify=selectedPrinterChanged) def selectedPrinterName(self): return self._selected_printer.get("unique_name", "") def getPrintJobsUrl(self): return self._host + "/print_jobs" def getPrintersUrl(self): return self._host + "/printers" def _showProgressMessage(self): progress_message_template = i18n_catalog.i18nc( "@info:progress", "Sending <filename>{file_name}</filename> to group {cluster_name}") file_name = os.path.basename(self._file_name).split(".")[0] self._progress_message = Message( progress_message_template.format(file_name=file_name, cluster_name=self.getName()), 0, False, -1) self._progress_message.addAction( "Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect( self._onMessageActionTriggered) self._progress_message.show() def _addUserAgentHeader(self, request): request.setRawHeader(b"User-agent", b"CuraPrintClusterOutputDevice Plugin") def _cleanupRequest(self): self._reply = None self._request = None self._multipart = None self._stage = OutputStage.ready self._file_name = None def _onFinished(self, reply): super()._onFinished(reply) reply_url = reply.url().toString() status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if status_code == 500: Logger.log( "w", "Request to {url} returned a 500.".format(url=reply_url)) return if reply.error() == QNetworkReply.ContentOperationNotPermittedError: # It was probably "/api/v1/materials" for legacy UM3 return if reply.error() == QNetworkReply.ContentNotFoundError: # It was probably "/api/v1/print_job" for legacy UM3 return if reply.operation() == QNetworkAccessManager.PostOperation: if self._cluster_api_prefix + "print_jobs" in reply_url: self._finishedPrintJobPostRequest(reply) return # We need to do this check *after* we process the post operation! # If the sending of g-code is cancelled by the user it will result in an error, but we do need to handle this. if reply.error() != QNetworkReply.NoError: Logger.log( "e", "After requesting [%s] we got a network error [%s]. Not processing anything...", reply_url, reply.error()) return elif reply.operation() == QNetworkAccessManager.GetOperation: if self._cluster_api_prefix + "print_jobs" in reply_url: self._finishedPrintJobsRequest(reply) elif self._cluster_api_prefix + "printers" in reply_url: self._finishedPrintersRequest(reply) @pyqtSlot() def openPrintJobControlPanel(self): Logger.log("d", "Opening print job control panel...") QDesktopServices.openUrl(QUrl(self.getPrintJobsUrl())) @pyqtSlot() def openPrinterControlPanel(self): Logger.log("d", "Opening printer control panel...") QDesktopServices.openUrl(QUrl(self.getPrintersUrl())) def _onMessageActionTriggered(self, message, action): if action == "open_browser": QDesktopServices.openUrl(QUrl(self.getPrintJobsUrl())) if action == "Abort": Logger.log("d", "User aborted sending print to remote.") self._progress_message.hide() self._compressing_print = False self._stage = OutputStage.ready if self._reply: self._reply.abort() self._reply = None Application.getInstance().showPrintMonitor.emit(False)
class StartDownloadDialog(DialogContainer): button_clicked = pyqtSignal(int) received_metainfo = pyqtSignal(dict) def __init__(self, parent, download_uri): DialogContainer.__init__(self, parent) torrent_name = download_uri if torrent_name.startswith('file:'): torrent_name = torrent_name[5:] elif torrent_name.startswith('magnet:'): torrent_name = unquote_plus(torrent_name) self.download_uri = download_uri self.has_metainfo = False self.metainfo_fetch_timer = None self.metainfo_retries = 0 uic.loadUi(get_ui_file_path('startdownloaddialog.ui'), self.dialog_widget) self.dialog_widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) connect(self.dialog_widget.browse_dir_button.clicked, self.on_browse_dir_clicked) connect(self.dialog_widget.cancel_button.clicked, lambda _: self.button_clicked.emit(0)) connect(self.dialog_widget.download_button.clicked, self.on_download_clicked) connect(self.dialog_widget.select_all_files_button.clicked, self.on_all_files_selected_clicked) connect(self.dialog_widget.deselect_all_files_button.clicked, self.on_all_files_deselected_clicked) connect(self.dialog_widget.loading_files_label.clicked, self.on_reload_torrent_info) connect(self.dialog_widget.anon_download_checkbox.clicked, self.on_reload_torrent_info) self.dialog_widget.destination_input.setStyleSheet(""" QComboBox { background-color: #444; border: none; color: #C0C0C0; padding: 4px; } QComboBox::drop-down { width: 20px; border: 1px solid #999; border-radius: 2px; } QComboBox QAbstractItemView { selection-background-color: #707070; color: #C0C0C0; } QComboBox::down-arrow { width: 12px; height: 12px; image: url('%s'); } """ % get_image_path('down_arrow_input.png')) # self.dialog_widget.add_to_channel_checkbox.setStyleSheet(get_checkbox_style()) checkbox_style = get_checkbox_style() for checkbox in [ self.dialog_widget.add_to_channel_checkbox, self.dialog_widget.safe_seed_checkbox, self.dialog_widget.anon_download_checkbox, ]: checkbox.setStyleSheet(checkbox_style) if self.window().tribler_settings: # Set the most recent download locations in the QComboBox current_settings = get_gui_setting(self.window().gui_settings, tr("recent_download_locations"), "") if len(current_settings) > 0: recent_locations = [ unhexlify(url).decode('utf-8') for url in current_settings.split(",") ] self.dialog_widget.destination_input.addItems(recent_locations) else: self.dialog_widget.destination_input.setCurrentText( self.window().tribler_settings['download_defaults'] ['saveas']) self.dialog_widget.torrent_name_label.setText(torrent_name) connect(self.dialog_widget.anon_download_checkbox.stateChanged, self.on_anon_download_state_changed) self.dialog_widget.anon_download_checkbox.setChecked( self.window().tribler_settings['download_defaults'] ['anonymity_enabled']) self.dialog_widget.safe_seed_checkbox.setChecked( self.window().tribler_settings['download_defaults'] ['safeseeding_enabled']) self.dialog_widget.add_to_channel_checkbox.setChecked( self.window().tribler_settings['download_defaults'] ['add_download_to_channel']) self.dialog_widget.safe_seed_checkbox.setEnabled( self.dialog_widget.anon_download_checkbox.isChecked()) self.perform_files_request() self.dialog_widget.files_list_view.setHidden(True) self.dialog_widget.download_files_container.setHidden(True) self.dialog_widget.adjustSize() self.on_anon_download_state_changed(None) self.on_main_window_resize() self.rest_request = None def close_dialog(self, checked=False): if self.rest_request: self.rest_request.cancel_request() if self.metainfo_fetch_timer: self.metainfo_fetch_timer.stop() # Loading files label is a clickable label with pyqtsignal which could leak, # so delete the widget while closing the dialog. if self.dialog_widget and self.dialog_widget.loading_files_label: try: self.dialog_widget.loading_files_label.deleteLater() except RuntimeError: logging.debug( "Deleting loading files widget in the dialog widget failed." ) super().close_dialog() def get_selected_files(self): included_files = [] for ind in range( self.dialog_widget.files_list_view.topLevelItemCount()): item = self.dialog_widget.files_list_view.topLevelItem(ind) if item.checkState(2) == Qt.Checked: included_files.append(ind) return included_files def perform_files_request(self): if self.closed: return direct = not self.dialog_widget.anon_download_checkbox.isChecked() request = f"torrentinfo?uri={quote_plus_unicode(self.download_uri)}" if direct is True: request = request + f"&hops=0" self.rest_request = TriblerNetworkRequest(request, self.on_received_metainfo, capture_core_errors=False) if self.metainfo_retries <= METAINFO_MAX_RETRIES: fetch_mode = tr("directly") if direct else tr("anonymously") loading_message = tr("Loading torrent files %s...") % fetch_mode timeout_message = tr( "Timeout in fetching files %s. Retrying %i/%i") % ( fetch_mode, self.metainfo_retries, METAINFO_MAX_RETRIES, ) self.dialog_widget.loading_files_label.setText( loading_message if not self.metainfo_retries else timeout_message) self.metainfo_fetch_timer = QTimer() connect(self.metainfo_fetch_timer.timeout, self.perform_files_request) self.metainfo_fetch_timer.setSingleShot(True) self.metainfo_fetch_timer.start(METAINFO_TIMEOUT) self.metainfo_retries += 1 def on_received_metainfo(self, response): if not response or not self or self.closed: return if 'error' in response: if response['error'] == 'metainfo error': # If it failed to load metainfo for max number of times, show an error message in red. if self.metainfo_retries > METAINFO_MAX_RETRIES: self.dialog_widget.loading_files_label.setStyleSheet( "color:#ff0000;") self.dialog_widget.loading_files_label.setText( tr("Failed to load files. Click to retry again.")) return self.perform_files_request() elif 'code' in response['error'] and response['error'][ 'code'] == 'IOError': self.dialog_widget.loading_files_label.setText( tr("Unable to read torrent file data")) else: self.dialog_widget.loading_files_label.setText( tr("Error: %s") % response['error']) return metainfo = json.loads(unhexlify(response['metainfo'])) if 'files' in metainfo['info']: # Multi-file torrent files = metainfo['info']['files'] else: files = [{ 'path': [metainfo['info']['name']], 'length': metainfo['info']['length'] }] # Show if the torrent already exists in the downloads if response.get('download_exists'): self.dialog_widget.existing_download_info_label.setText( tr("Note: this torrent already exists in the Downloads")) else: self.dialog_widget.existing_download_info_label.setText("") self.dialog_widget.files_list_view.clear() for filename in files: item = DownloadFileTreeWidgetItem( self.dialog_widget.files_list_view) item.setText(0, '/'.join(filename['path'])) item.setText(1, format_size(float(filename['length']))) item.setData(0, Qt.UserRole, filename) item.setCheckState(2, Qt.Checked) self.dialog_widget.files_list_view.addTopLevelItem(item) self.has_metainfo = True self.dialog_widget.loading_files_label.setHidden(True) self.dialog_widget.download_files_container.setHidden(False) self.dialog_widget.files_list_view.setHidden(False) self.dialog_widget.adjustSize() self.on_main_window_resize() self.received_metainfo.emit(metainfo) def on_reload_torrent_info(self, *args): """ This method is called when user clicks the QLabel text showing loading or error message. Here, we reset the number of retries to fetch the metainfo. Note color of QLabel is also reset to white. """ self.dialog_widget.loading_files_label.setStyleSheet("color:#ffffff;") self.metainfo_retries = 0 self.perform_files_request() def on_browse_dir_clicked(self, checked): chosen_dir = QFileDialog.getExistingDirectory( self.window(), tr("Please select the destination directory of your download"), "", QFileDialog.ShowDirsOnly) if len(chosen_dir) != 0: self.dialog_widget.destination_input.setCurrentText(chosen_dir) is_writable, error = is_dir_writable(chosen_dir) if not is_writable: gui_error_message = tr( "Tribler cannot download to <i>%s</i> directory. Please add proper write " "permissions to the directory or choose another download directory. [%s]" ) % (chosen_dir, error) ConfirmationDialog.show_message(self.dialog_widget, tr("Insufficient Permissions"), gui_error_message, "OK") def on_anon_download_state_changed(self, _): if self.dialog_widget.anon_download_checkbox.isChecked(): self.dialog_widget.safe_seed_checkbox.setChecked(True) self.dialog_widget.safe_seed_checkbox.setEnabled( not self.dialog_widget.anon_download_checkbox.isChecked()) def on_download_clicked(self, checked): if self.has_metainfo and len(self.get_selected_files() ) == 0: # User deselected all torrents ConfirmationDialog.show_error( self.window(), tr("No files selected"), tr("Please select at least one file to download.")) else: download_dir = self.dialog_widget.destination_input.currentText() is_writable, error = is_dir_writable(download_dir) if not is_writable: gui_error_message = tr( "Tribler cannot download to <i>%s</i> directory. Please add proper write " "permissions to the directory or choose another download directory and try " "to download again. [%s]") % (download_dir, error) ConfirmationDialog.show_message(self.dialog_widget, tr("Insufficient Permissions"), gui_error_message, "OK") else: self.button_clicked.emit(1) def on_all_files_selected_clicked(self, checked): for ind in range( self.dialog_widget.files_list_view.topLevelItemCount()): item = self.dialog_widget.files_list_view.topLevelItem(ind) item.setCheckState(2, Qt.Checked) def on_all_files_deselected_clicked(self, checked): for ind in range( self.dialog_widget.files_list_view.topLevelItemCount()): item = self.dialog_widget.files_list_view.topLevelItem(ind) item.setCheckState(2, Qt.Unchecked)
class SnapWidget(QWidget, Ui_SnapWidget): """ Class implementing the snapshot widget. """ ModeFullscreen = 0 ModeScreen = 1 ModeRectangle = 2 ModeFreehand = 3 ModeEllipse = 4 def __init__(self, parent=None): """ Constructor @param parent reference to the parent widget (QWidget) """ super(SnapWidget, self).__init__(parent) self.setupUi(self) self.saveButton.setIcon(UI.PixmapCache.getIcon("fileSaveAs.png")) self.takeButton.setIcon(UI.PixmapCache.getIcon("cameraPhoto.png")) self.copyButton.setIcon(UI.PixmapCache.getIcon("editCopy.png")) self.copyPreviewButton.setIcon(UI.PixmapCache.getIcon("editCopy.png")) self.setWindowIcon(UI.PixmapCache.getIcon("ericSnap.png")) self.modeCombo.addItem(self.tr("Fullscreen"), SnapWidget.ModeFullscreen) self.modeCombo.addItem(self.tr("Rectangular Selection"), SnapWidget.ModeRectangle) self.modeCombo.addItem(self.tr("Ellipical Selection"), SnapWidget.ModeEllipse) self.modeCombo.addItem(self.tr("Freehand Selection"), SnapWidget.ModeFreehand) if QApplication.desktop().screenCount() > 1: self.modeCombo.addItem(self.tr("Current Screen"), SnapWidget.ModeScreen) self.__mode = int(Preferences.Prefs.settings.value("Snapshot/Mode", 0)) index = self.modeCombo.findData(self.__mode) if index == -1: index = 0 self.modeCombo.setCurrentIndex(index) self.__delay = int( Preferences.Prefs.settings.value("Snapshot/Delay", 0)) self.delaySpin.setValue(self.__delay) if PYQT_VERSION_STR >= "5.0.0": from PyQt5.QtCore import QStandardPaths picturesLocation = QStandardPaths.writableLocation( QStandardPaths.PicturesLocation) else: from PyQt5.QtGui import QDesktopServices picturesLocation = QDesktopServices.storageLocation( QDesktopServices.PicturesLocation) self.__filename = Preferences.Prefs.settings.value( "Snapshot/Filename", os.path.join(picturesLocation, self.tr("snapshot") + "1.png")) self.__grabber = None self.__snapshot = QPixmap() self.__savedPosition = QPoint() self.__modified = False self.__grabberWidget = QWidget(None, Qt.X11BypassWindowManagerHint) self.__grabberWidget.move(-10000, -10000) self.__grabberWidget.installEventFilter(self) self.__initFileFilters() self.__initShortcuts() self.preview.startDrag.connect(self.__dragSnapshot) from .SnapshotTimer import SnapshotTimer self.__grabTimer = SnapshotTimer() self.__grabTimer.timeout.connect(self.__grabTimerTimeout) self.__updateTimer = QTimer() self.__updateTimer.setSingleShot(True) self.__updateTimer.timeout.connect(self.__updatePreview) self.__updateCaption() self.takeButton.setFocus() def __initFileFilters(self): """ Private method to define the supported image file filters. """ filters = { 'bmp': self.tr("Windows Bitmap File (*.bmp)"), 'gif': self.tr("Graphic Interchange Format File (*.gif)"), 'ico': self.tr("Windows Icon File (*.ico)"), 'jpg': self.tr("JPEG File (*.jpg)"), 'mng': self.tr("Multiple-Image Network Graphics File (*.mng)"), 'pbm': self.tr("Portable Bitmap File (*.pbm)"), 'pcx': self.tr("Paintbrush Bitmap File (*.pcx)"), 'pgm': self.tr("Portable Graymap File (*.pgm)"), 'png': self.tr("Portable Network Graphics File (*.png)"), 'ppm': self.tr("Portable Pixmap File (*.ppm)"), 'sgi': self.tr("Silicon Graphics Image File (*.sgi)"), 'svg': self.tr("Scalable Vector Graphics File (*.svg)"), 'tga': self.tr("Targa Graphic File (*.tga)"), 'tif': self.tr("TIFF File (*.tif)"), 'xbm': self.tr("X11 Bitmap File (*.xbm)"), 'xpm': self.tr("X11 Pixmap File (*.xpm)"), } outputFormats = [] writeFormats = QImageWriter.supportedImageFormats() for writeFormat in writeFormats: try: outputFormats.append(filters[bytes(writeFormat).decode()]) except KeyError: pass outputFormats.sort() self.__outputFilter = ';;'.join(outputFormats) self.__defaultFilter = filters['png'] def __initShortcuts(self): """ Private method to initialize the keyboard shortcuts. """ self.__quitShortcut = QShortcut(QKeySequence(QKeySequence.Quit), self, self.close) self.__copyShortcut = QShortcut(QKeySequence(QKeySequence.Copy), self, self.copyButton.animateClick) self.__quickSaveShortcut = QShortcut(QKeySequence(Qt.Key_Q), self, self.__quickSave) self.__save1Shortcut = QShortcut(QKeySequence(QKeySequence.Save), self, self.saveButton.animateClick) self.__save2Shortcut = QShortcut(QKeySequence(Qt.Key_S), self, self.saveButton.animateClick) self.__grab1Shortcut = QShortcut(QKeySequence(QKeySequence.New), self, self.takeButton.animateClick) self.__grab2Shortcut = QShortcut(QKeySequence(Qt.Key_N), self, self.takeButton.animateClick) self.__grab3Shortcut = QShortcut(QKeySequence(Qt.Key_Space), self, self.takeButton.animateClick) def __quickSave(self): """ Private slot to save the snapshot bypassing the file selection dialog. """ if not self.__snapshot.isNull(): while os.path.exists(self.__filename): self.__autoIncFilename() if self.__saveImage(self.__filename): self.__modified = False self.__autoIncFilename() self.__updateCaption() @pyqtSlot() def on_saveButton_clicked(self): """ Private slot to save the snapshot. """ if not self.__snapshot.isNull(): while os.path.exists(self.__filename): self.__autoIncFilename() fileName, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( self, self.tr("Save Snapshot"), self.__filename, self.__outputFilter, self.__defaultFilter, E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) if not fileName: return ext = QFileInfo(fileName).suffix() if not ext: ex = selectedFilter.split("(*")[1].split(")")[0] if ex: fileName += ex if self.__saveImage(fileName): self.__modified = False self.__filename = fileName self.__autoIncFilename() self.__updateCaption() def __saveImage(self, fileName): """ Private method to save the snapshot. @param fileName name of the file to save to (string) @return flag indicating success (boolean) """ if QFileInfo(fileName).exists(): res = E5MessageBox.yesNo( self, self.tr("Save Snapshot"), self.tr("<p>The file <b>{0}</b> already exists." " Overwrite it?</p>").format(fileName), icon=E5MessageBox.Warning) if not res: return False file = QFile(fileName) if not file.open(QFile.WriteOnly): E5MessageBox.warning( self, self.tr("Save Snapshot"), self.tr("Cannot write file '{0}:\n{1}.").format( fileName, file.errorString())) return False ok = self.__snapshot.save(file) file.close() if not ok: E5MessageBox.warning( self, self.tr("Save Snapshot"), self.tr("Cannot write file '{0}:\n{1}.").format( fileName, file.errorString())) return ok def __autoIncFilename(self): """ Private method to auto-increment the file name. """ # Extract the file name name = os.path.basename(self.__filename) # If the name contains a number, then increment it. numSearch = QRegExp("(^|[^\\d])(\\d+)") # We want to match as far left as possible, and when the number is # at the start of the name. # Does it have a number? start = numSearch.lastIndexIn(name) if start != -1: # It has a number, increment it. start = numSearch.pos(2) # Only the second group is of interest. numAsStr = numSearch.capturedTexts()[2] number = "{0:0{width}d}".format(int(numAsStr) + 1, width=len(numAsStr)) name = name[:start] + number + name[start + len(numAsStr):] else: # no number start = name.rfind('.') if start != -1: # has a '.' somewhere, e.g. it has an extension name = name[:start] + '1' + name[start:] else: # no extension, just tack it on to the end name += '1' self.__filename = os.path.join(os.path.dirname(self.__filename), name) self.__updateCaption() @pyqtSlot() def on_takeButton_clicked(self): """ Private slot to take a snapshot. """ self.__mode = self.modeCombo.itemData(self.modeCombo.currentIndex()) self.__delay = self.delaySpin.value() self.__savedPosition = self.pos() self.hide() if self.__delay: self.__grabTimer.start(self.__delay) else: QTimer.singleShot(200, self.__startUndelayedGrab) def __grabTimerTimeout(self): """ Private slot to perform a delayed grab operation. """ if self.__mode == SnapWidget.ModeRectangle: self.__grabRectangle() elif self.__mode == SnapWidget.ModeEllipse: self.__grabEllipse() elif self.__mode == SnapWidget.ModeFreehand: self.__grabFreehand() else: self.__performGrab() def __startUndelayedGrab(self): """ Private slot to perform an undelayed grab operation. """ if self.__mode == SnapWidget.ModeRectangle: self.__grabRectangle() elif self.__mode == SnapWidget.ModeEllipse: self.__grabEllipse() elif self.__mode == SnapWidget.ModeFreehand: self.__grabFreehand() else: if Globals.isMacPlatform(): self.__performGrab() else: self.__grabberWidget.show() self.__grabberWidget.grabMouse(Qt.CrossCursor) def __grabRectangle(self): """ Private method to grab a rectangular screen region. """ from .SnapshotRegionGrabber import SnapshotRegionGrabber self.__grabber = SnapshotRegionGrabber( mode=SnapshotRegionGrabber.Rectangle) self.__grabber.grabbed.connect(self.__captured) def __grabEllipse(self): """ Private method to grab an elliptical screen region. """ from .SnapshotRegionGrabber import SnapshotRegionGrabber self.__grabber = SnapshotRegionGrabber( mode=SnapshotRegionGrabber.Ellipse) self.__grabber.grabbed.connect(self.__captured) def __grabFreehand(self): """ Private method to grab a non-rectangular screen region. """ from .SnapshotFreehandGrabber import SnapshotFreehandGrabber self.__grabber = SnapshotFreehandGrabber() self.__grabber.grabbed.connect(self.__captured) def __performGrab(self): """ Private method to perform a screen grab other than a selected region. """ self.__grabberWidget.releaseMouse() self.__grabberWidget.hide() self.__grabTimer.stop() if self.__mode == SnapWidget.ModeFullscreen: desktop = QApplication.desktop() if qVersion() >= "5.0.0": self.__snapshot = QApplication.screens()[0].grabWindow( desktop.winId(), desktop.x(), desktop.y(), desktop.width(), desktop.height()) else: self.__snapshot = QPixmap.grabWindow(desktop.winId(), desktop.x(), desktop.y(), desktop.width(), desktop.height()) elif self.__mode == SnapWidget.ModeScreen: desktop = QApplication.desktop() screenId = desktop.screenNumber(QCursor.pos()) geom = desktop.screenGeometry(screenId) x = geom.x() y = geom.y() if qVersion() >= "5.0.0": self.__snapshot = QApplication.screens()[0].grabWindow( desktop.winId(), x, y, geom.width(), geom.height()) else: self.__snapshot = QPixmap.grabWindow(desktop.winId(), x, y, geom.width(), geom.height()) else: self.__snapshot = QPixmap() self.__redisplay() self.__modified = True self.__updateCaption() def __redisplay(self): """ Private method to redisplay the window. """ self.__updatePreview() QApplication.restoreOverrideCursor() if not self.__savedPosition.isNull(): self.move(self.__savedPosition) self.show() self.raise_() self.saveButton.setEnabled(not self.__snapshot.isNull()) self.copyButton.setEnabled(not self.__snapshot.isNull()) self.copyPreviewButton.setEnabled(not self.__snapshot.isNull()) @pyqtSlot() def on_copyButton_clicked(self): """ Private slot to copy the snapshot to the clipboard. """ if not self.__snapshot.isNull(): QApplication.clipboard().setPixmap(QPixmap(self.__snapshot)) @pyqtSlot() def on_copyPreviewButton_clicked(self): """ Private slot to copy the snapshot preview to the clipboard. """ QApplication.clipboard().setPixmap(self.preview.pixmap()) def __captured(self, pixmap): """ Private slot to show a preview of the snapshot. @param pixmap pixmap of the snapshot (QPixmap) """ self.__grabber.close() self.__snapshot = QPixmap(pixmap) self.__grabber.grabbed.disconnect(self.__captured) self.__grabber = None self.__redisplay() self.__modified = True self.__updateCaption() def __updatePreview(self): """ Private slot to update the preview picture. """ self.preview.setToolTip( self.tr("Preview of the snapshot image ({0:n} x {1:n})").format( self.__snapshot.width(), self.__snapshot.height())) self.preview.setPreview(self.__snapshot) self.preview.adjustSize() def resizeEvent(self, evt): """ Protected method handling a resizing of the window. @param evt resize event (QResizeEvent) """ self.__updateTimer.start(200) def __dragSnapshot(self): """ Private slot handling the dragging of the preview picture. """ drag = QDrag(self) mimeData = QMimeData() mimeData.setImageData(self.__snapshot) drag.setMimeData(mimeData) drag.setPixmap(self.preview.pixmap()) drag.exec_(Qt.CopyAction) def eventFilter(self, obj, evt): """ Public method to handle event for other objects. @param obj reference to the object (QObject) @param evt reference to the event (QEvent) @return flag indicating that the event should be filtered out (boolean) """ if obj == self.__grabberWidget and \ evt.type() == QEvent.MouseButtonPress: if QWidget.mouseGrabber() != self.__grabberWidget: return False if evt.button() == Qt.LeftButton: self.__performGrab() return False def closeEvent(self, evt): """ Protected method handling the close event. @param evt close event (QCloseEvent) """ if self.__modified: res = E5MessageBox.question( self, self.tr("eric6 Snapshot"), self.tr("""The application contains an unsaved snapshot."""), E5MessageBox.StandardButtons(E5MessageBox.Abort | E5MessageBox.Discard | E5MessageBox.Save)) if res == E5MessageBox.Abort: evt.ignore() return elif res == E5MessageBox.Save: self.on_saveButton_clicked() Preferences.Prefs.settings.setValue("Snapshot/Delay", self.delaySpin.value()) Preferences.Prefs.settings.setValue( "Snapshot/Mode", self.modeCombo.itemData(self.modeCombo.currentIndex())) Preferences.Prefs.settings.setValue("Snapshot/Filename", self.__filename) Preferences.Prefs.settings.sync() def __updateCaption(self): """ Private method to update the window caption. """ self.setWindowTitle("{0}[*] - {1}".format( os.path.basename(self.__filename), self.tr("eric6 Snapshot"))) self.setWindowModified(self.__modified) self.pathNameEdit.setText(os.path.dirname(self.__filename))
class LayerView(View): def __init__(self): super().__init__() self._shader = None self._selection_shader = None self._num_layers = 0 self._layer_percentage = 0 # what percentage of layers need to be shown (SLider gives value between 0 - 100) self._proxy = LayerViewProxy.LayerViewProxy() self._controller.getScene().getRoot().childrenChanged.connect( self._onSceneChanged) self._max_layers = 0 self._current_layer_num = 0 self._current_layer_mesh = None self._current_layer_jumps = None self._top_layers_job = None self._activity = False self._solid_layers = 5 self._top_layer_timer = QTimer() self._top_layer_timer.setInterval(50) self._top_layer_timer.setSingleShot(True) self._top_layer_timer.timeout.connect(self._startUpdateTopLayers) self._busy = False def getActivity(self): return self._activity def getCurrentLayer(self): return self._current_layer_num def _onSceneChanged(self, node): self.calculateMaxLayers() def getMaxLayers(self): return self._max_layers busyChanged = Signal() def isBusy(self): return self._busy def setBusy(self, busy): if busy != self._busy: self._busy = busy self.busyChanged.emit() def resetLayerData(self): self._current_layer_mesh = None self._current_layer_jumps = None def beginRendering(self): scene = self.getController().getScene() renderer = self.getRenderer() if not self._selection_shader: self._selection_shader = OpenGL.getInstance().createShaderProgram( Resources.getPath(Resources.Shaders, "color.shader")) self._selection_shader.setUniformValue("u_color", Color(32, 32, 32, 128)) for node in DepthFirstIterator(scene.getRoot()): # We do not want to render ConvexHullNode as it conflicts with the bottom layers. # However, it is somewhat relevant when the node is selected, so do render it then. if type(node) is ConvexHullNode and not Selection.isSelected( node.getWatchedNode()): continue if not node.render(renderer): if node.getMeshData() and node.isVisible(): if Selection.isSelected(node): renderer.queueNode(node, transparent=True, shader=self._selection_shader) layer_data = node.callDecoration("getLayerData") if not layer_data: continue # Render all layers below a certain number as line mesh instead of vertices. if self._current_layer_num - self._solid_layers > -1: start = 0 end = 0 element_counts = layer_data.getElementCounts() for layer, counts in element_counts.items(): if layer + self._solid_layers > self._current_layer_num: break end += counts # This uses glDrawRangeElements internally to only draw a certain range of lines. renderer.queueNode(node, mesh=layer_data, mode=RenderBatch.RenderMode.Lines, range=(start, end)) if self._current_layer_mesh: renderer.queueNode(node, mesh=self._current_layer_mesh) if self._current_layer_jumps: renderer.queueNode(node, mesh=self._current_layer_jumps) def setLayer(self, value): if self._current_layer_num != value: self._current_layer_num = value if self._current_layer_num < 0: self._current_layer_num = 0 if self._current_layer_num > self._max_layers: self._current_layer_num = self._max_layers self._current_layer_mesh = None self._current_layer_jumps = None self._top_layer_timer.start() self.currentLayerNumChanged.emit() currentLayerNumChanged = Signal() def calculateMaxLayers(self): scene = self.getController().getScene() renderer = self.getRenderer() # TODO: @UnusedVariable self._activity = True self._old_max_layers = self._max_layers ## Recalculate num max layers new_max_layers = 0 for node in DepthFirstIterator(scene.getRoot()): layer_data = node.callDecoration("getLayerData") if not layer_data: continue if new_max_layers < len(layer_data.getLayers()): new_max_layers = len(layer_data.getLayers()) - 1 if new_max_layers > 0 and new_max_layers != self._old_max_layers: self._max_layers = new_max_layers # The qt slider has a bit of weird behavior that if the maxvalue needs to be changed first # if it's the largest value. If we don't do this, we can have a slider block outside of the # slider. if new_max_layers > self._current_layer_num: self.maxLayersChanged.emit() self.setLayer(int(self._max_layers)) else: self.setLayer(int(self._max_layers)) self.maxLayersChanged.emit() self._top_layer_timer.start() maxLayersChanged = Signal() currentLayerNumChanged = Signal() ## Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created # as this caused some issues. def getProxy(self, engine, script_engine): return self._proxy def endRendering(self): pass def event(self, event): modifiers = QApplication.keyboardModifiers() ctrl_is_active = modifiers == Qt.ControlModifier if event.type == Event.KeyPressEvent and ctrl_is_active: if event.key == KeyEvent.UpKey: self.setLayer(self._current_layer_num + 1) return True if event.key == KeyEvent.DownKey: self.setLayer(self._current_layer_num - 1) return True def _startUpdateTopLayers(self): if self._top_layers_job: self._top_layers_job.finished.disconnect( self._updateCurrentLayerMesh) self._top_layers_job.cancel() self.setBusy(True) self._top_layers_job = _CreateTopLayersJob(self._controller.getScene(), self._current_layer_num, self._solid_layers) self._top_layers_job.finished.connect(self._updateCurrentLayerMesh) self._top_layers_job.start() def _updateCurrentLayerMesh(self, job): self.setBusy(False) if not job.getResult(): return self._current_layer_mesh = job.getResult().get("layers") self._current_layer_jumps = job.getResult().get("jumps") self._controller.getScene().sceneChanged.emit( self._controller.getScene().getRoot()) self._top_layers_job = None
class Editor(CodeEditor,ComponentMixin): name = 'Code Editor' # This signal is emitted whenever the currently-open file changes and # autoreload is enabled. triggerRerender = pyqtSignal(bool) sigFilenameChanged = pyqtSignal(str) preferences = Parameter.create(name='Preferences',children=[ {'name': 'Font size', 'type': 'int', 'value': 12}, {'name': 'Autoreload', 'type': 'bool', 'value': False}, {'name': 'Autoreload delay', 'type': 'int', 'value': 50}, {'name': 'Autoreload: watch imported modules', 'type': 'bool', 'value': False}, {'name': 'Line wrap', 'type': 'bool', 'value': False}, {'name': 'Color scheme', 'type': 'list', 'values': ['Spyder','Monokai','Zenburn'], 'value': 'Spyder'}]) EXTENSIONS = 'py' def __init__(self,parent=None): self._watched_file = None super(Editor,self).__init__(parent) ComponentMixin.__init__(self) self.setup_editor(linenumbers=True, markers=True, edge_line=False, tab_mode=False, show_blanks=True, font=QFontDatabase.systemFont(QFontDatabase.FixedFont), language='Python', filename='') self._actions = \ {'File' : [QAction(icon('new'), 'New', self, shortcut='ctrl+N', triggered=self.new), QAction(icon('open'), 'Open', self, shortcut='ctrl+O', triggered=self.open), QAction(icon('save'), 'Save', self, shortcut='ctrl+S', triggered=self.save), QAction(icon('save_as'), 'Save as', self, shortcut='ctrl+shift+S', triggered=self.save_as), QAction(icon('autoreload'), 'Automatic reload and preview', self,triggered=self.autoreload, checkable=True, checked=False, objectName='autoreload'), ]} for a in self._actions.values(): self.addActions(a) self._fixContextMenu() # autoreload support self._file_watcher = QFileSystemWatcher(self) # we wait for 50ms after a file change for the file to be written completely self._file_watch_timer = QTimer(self) self._file_watch_timer.setInterval(self.preferences['Autoreload delay']) self._file_watch_timer.setSingleShot(True) self._file_watcher.fileChanged.connect( lambda val: self._file_watch_timer.start()) self._file_watch_timer.timeout.connect(self._file_changed) self.updatePreferences() def _fixContextMenu(self): menu = self.menu menu.removeAction(self.run_cell_action) menu.removeAction(self.run_cell_and_advance_action) menu.removeAction(self.run_selection_action) menu.removeAction(self.re_run_last_cell_action) def updatePreferences(self,*args): self.set_color_scheme(self.preferences['Color scheme']) font = self.font() font.setPointSize(self.preferences['Font size']) self.set_font(font) self.findChild(QAction, 'autoreload') \ .setChecked(self.preferences['Autoreload']) self._file_watch_timer.setInterval(self.preferences['Autoreload delay']) self.toggle_wrap_mode(self.preferences['Line wrap']) self._clear_watched_paths() self._watch_paths() def confirm_discard(self): if self.modified: rv = confirm(self,'Please confirm','Current document is not saved - do you want to continue?') else: rv = True return rv def new(self): if not self.confirm_discard(): return self.set_text('') self.filename = '' self.reset_modified() def open(self): if not self.confirm_discard(): return curr_dir = Path(self.filename).abspath().dirname() fname = get_open_filename(self.EXTENSIONS, curr_dir) if fname != '': self.load_from_file(fname) def load_from_file(self,fname): self.set_text_from_file(fname) self.filename = fname self.reset_modified() def save(self): if self._filename != '': if self.preferences['Autoreload']: self._file_watcher.blockSignals(True) self._file_watch_timer.stop() with open(self._filename, 'w') as f: f.write(self.toPlainText()) if self.preferences['Autoreload']: self._file_watcher.blockSignals(False) self.triggerRerender.emit(True) self.reset_modified() else: self.save_as() def save_as(self): fname = get_save_filename(self.EXTENSIONS) if fname != '': with open(fname,'w') as f: f.write(self.toPlainText()) self.filename = fname self.reset_modified() def _update_filewatcher(self): if self._watched_file and (self._watched_file != self.filename or not self.preferences['Autoreload']): self._clear_watched_paths() self._watched_file = None if self.preferences['Autoreload'] and self.filename and self.filename != self._watched_file: self._watched_file = self._filename self._watch_paths() @property def filename(self): return self._filename @filename.setter def filename(self, fname): self._filename = fname self._update_filewatcher() self.sigFilenameChanged.emit(fname) def _clear_watched_paths(self): paths = self._file_watcher.files() if paths: self._file_watcher.removePaths(paths) def _watch_paths(self): if self._filename: self._file_watcher.addPath(self._filename) if self.preferences['Autoreload: watch imported modules']: self._file_watcher.addPaths(get_imported_module_paths(self._filename)) # callback triggered by QFileSystemWatcher def _file_changed(self): # neovim writes a file by removing it first so must re-add each time self._watch_paths() self.set_text_from_file(self._filename) self.triggerRerender.emit(True) # Turn autoreload on/off. def autoreload(self, enabled): self.preferences['Autoreload'] = enabled self._update_filewatcher() def reset_modified(self): self.document().setModified(False) @property def modified(self): return self.document().isModified() def saveComponentState(self,store): if self.filename != '': store.setValue(self.name+'/state',self.filename) def restoreComponentState(self,store): filename = store.value(self.name+'/state',self.filename) if filename and filename != '': try: self.load_from_file(filename) except IOError: self._logger.warning(f'could not open {filename}')
class revisions(QWidget, Ui_revisions): def __init__(self, parent=None): QWidget.__init__(self, parent) self.setupUi(self) self.splitter.setStretchFactor(0, 5) self.splitter.setStretchFactor(1, 70) self.listDelegate = listCompleterDelegate(self) self.list.setItemDelegate(self.listDelegate) self.list.itemClicked.connect(self.showDiff) self.list.setContextMenuPolicy(Qt.CustomContextMenu) self.list.customContextMenuRequested.connect(self.popupMenu) self.btnDelete.setEnabled(False) self.btnDelete.clicked.connect(self.delete) self.btnRestore.clicked.connect(self.restore) self.btnRestore.setEnabled(False) # self.list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.updateTimer = QTimer() self.updateTimer.setSingleShot(True) self.updateTimer.setInterval(500) self.updateTimer.timeout.connect(self.update) self.updateTimer.stop() self.menu = QMenu(self) self.actGroup = QActionGroup(self) self.actShowDiff = QAction(self.tr("Show modifications"), self.menu) self.actShowDiff.setCheckable(True) self.actShowDiff.setChecked(True) self.actShowDiff.triggered.connect(self.showDiff) self.menu.addAction(self.actShowDiff) self.actGroup.addAction(self.actShowDiff) self.actShowVersion = QAction(self.tr("Show ancient version"), self.menu) self.actShowVersion.setCheckable(True) self.actShowVersion.setChecked(False) self.actShowVersion.triggered.connect(self.showDiff) self.menu.addAction(self.actShowVersion) self.actGroup.addAction(self.actShowVersion) self.menu.addSeparator() self.actShowSpaces = QAction(self.tr("Show spaces"), self.menu) self.actShowSpaces.setCheckable(True) self.actShowSpaces.setChecked(False) self.actShowSpaces.triggered.connect(self.showDiff) self.menu.addAction(self.actShowSpaces) self.actDiffOnly = QAction(self.tr("Show modifications only"), self.menu) self.actDiffOnly.setCheckable(True) self.actDiffOnly.setChecked(True) self.actDiffOnly.triggered.connect(self.showDiff) self.menu.addAction(self.actDiffOnly) self.btnOptions.setMenu(self.menu) self._model = None self._index = None def setModel(self, model): self._model = model self._model.dataChanged.connect(self.updateMaybe) def setCurrentModelIndex(self, index): self._index = index self.view.setText("") self.update() def updateMaybe(self, topLeft, bottomRight): if self._index and \ topLeft.column() <= Outline.revisions <= bottomRight.column() and \ topLeft.row() <= self._index.row() <= bottomRight.row(): # self.update() self.updateTimer.start() def update(self): self.list.clear() item = self._index.internalPointer() rev = item.revisions() # Sort revisions rev = sorted(rev, key=lambda x: x[0], reverse=True) for r in rev: timestamp = datetime.datetime.fromtimestamp( r[0]).strftime('%Y-%m-%d %H:%M:%S') readable = self.readableDelta(r[0]) i = QListWidgetItem(readable) i.setData(Qt.UserRole, r[0]) i.setData(Qt.UserRole + 1, timestamp) self.list.addItem(i) def readableDelta(self, timestamp): now = datetime.datetime.now() delta = now - datetime.datetime.fromtimestamp(timestamp) if delta.days > 365: return self.tr("{} years ago").format(str(int(delta.days / 365))) elif delta.days > 30: return self.tr("{} months ago").format(str(int(delta.days / 30.5))) elif delta.days > 0: return self.tr("{} days ago").format(str(delta.days)) if delta.days == 1: return self.tr("1 day ago") elif delta.seconds > 60 * 60: return self.tr("{} hours ago").format( str(int(delta.seconds / 60 / 60))) elif delta.seconds > 60: return self.tr("{} minutes ago").format( str(int(delta.seconds / 60))) else: return self.tr("{} seconds ago").format(str(delta.seconds)) def showDiff(self): # UI stuff self.actShowSpaces.setEnabled(self.actShowDiff.isChecked()) self.actDiffOnly.setEnabled(self.actShowDiff.isChecked()) # FIXME: Errors in line number i = self.list.currentItem() if not i: self.btnDelete.setEnabled(False) self.btnRestore.setEnabled(False) return self.btnDelete.setEnabled(True) self.btnRestore.setEnabled(True) ts = i.data(Qt.UserRole) item = self._index.internalPointer() textNow = item.text() textBefore = [r[1] for r in item.revisions() if r[0] == ts][0] if self.actShowVersion.isChecked(): self.view.setText(textBefore) return textNow = textNow.splitlines() textBefore = textBefore.splitlines() d = difflib.Differ() diff = list(d.compare(textBefore, textNow)) if self.actShowSpaces.isChecked(): _format = lambda x: x.replace(" ", "␣ ") else: _format = lambda x: x extra = "<br>" diff = [d for d in diff if d and not d[:2] == "? "] mydiff = "" skip = False for n, l in enumerate(diff): l = diff[n] op = l[:2] txt = l[2:] op2 = diff[n + 1][:2] if n + 1 < len(diff) else None txt2 = diff[n + 1][2:] if n + 1 < len(diff) else None if skip: skip = False continue # Same line if op == " " and not self.actDiffOnly.isChecked(): mydiff += "{}{}".format(txt, extra) elif op == "- " and op2 == "+ ": if self.actDiffOnly.isChecked(): mydiff += "<br><span style='color: blue;'>{}</span><br>".format( self.tr("Line {}:").format(str(n))) s = difflib.SequenceMatcher(None, txt, txt2, autojunk=True) newline = "" for tag, i1, i2, j1, j2 in s.get_opcodes(): if tag == "equal": newline += txt[i1:i2] elif tag == "delete": newline += "<span style='color:red; background:yellow;'>{}</span>".format( _format(txt[i1:i2])) elif tag == "insert": newline += "<span style='color:green; background:yellow;'>{}</span>".format( _format(txt2[j1:j2])) elif tag == "replace": newline += "<span style='color:red; background:yellow;'>{}</span>".format( _format(txt[i1:i2])) newline += "<span style='color:green; background:yellow;'>{}</span>".format( _format(txt2[j1:j2])) # Few ugly tweaks for html diffs newline = re.sub( r"(<span style='color.*?><span.*?>)</span>(.*)<span style='color:.*?>(</span></span>)", "\\1\\2\\3", newline) newline = re.sub( r"<p align=\"<span style='color:red; background:yellow;'>cen</span><span style='color:green; background:yellow;'>righ</span>t<span style='color:red; background:yellow;'>er</span>\" style=\" -qt-block-indent:0; -qt-user-state:0; \">(.*?)</p>", "<p align=\"right\"><span style='color:green; background:yellow;'>\\1</span></p>", newline) newline = re.sub( r"<p align=\"<span style='color:green; background:yellow;'>cente</span>r<span style='color:red; background:yellow;'>ight</span>\" style=\" -qt-block-indent:0; -qt-user-state:0; \">(.*)</p>", "<p align=\"center\"><span style='color:green; background:yellow;'>\\1</span></p>", newline) newline = re.sub(r"<p(<span.*?>)(.*?)(</span>)(.*?)>(.*?)</p>", "<p\\2\\4>\\1\\5\\3</p>", newline) mydiff += newline + extra skip = True elif op == "- ": if self.actDiffOnly.isChecked(): mydiff += "<br>{}:<br>".format(str(n)) mydiff += "<span style='color:red;'>{}</span>{}".format( txt, extra) elif op == "+ ": if self.actDiffOnly.isChecked(): mydiff += "<br>{}:<br>".format(str(n)) mydiff += "<span style='color:green;'>{}</span>{}".format( txt, extra) self.view.setText(mydiff) def restore(self): i = self.list.currentItem() if not i: return ts = i.data(Qt.UserRole) item = self._index.internalPointer() textBefore = [r[1] for r in item.revisions() if r[0] == ts][0] index = self._index.sibling(self._index.row(), Outline.text) self._index.model().setData(index, textBefore) # item.setData(Outline.text, textBefore) def delete(self): i = self.list.currentItem() if not i: return ts = i.data(Qt.UserRole) self._index.internalPointer().deleteRevision(ts) def clearAll(self): self._index.internalPointer().clearAllRevisions() def saveState(self): return [ self.actShowDiff.isChecked(), self.actShowVersion.isChecked(), self.actShowSpaces.isChecked(), self.actDiffOnly.isChecked(), ] def popupMenu(self, pos): i = self.list.itemAt(pos) m = QMenu(self) if i: m.addAction(self.tr("Restore")).triggered.connect(self.restore) m.addAction(self.tr("Delete")).triggered.connect(self.delete) m.addSeparator() if self.list.count(): m.addAction(self.tr("Clear all")).triggered.connect(self.clearAll) m.popup(self.list.mapToGlobal(pos)) def restoreState(self, state): self.actShowDiff.setChecked(state[0]) self.actShowVersion.setChecked(state[1]) self.actShowSpaces.setChecked(state[2]) self.actDiffOnly.setChecked(state[3]) self.actShowSpaces.setEnabled(self.actShowDiff.isChecked()) self.actDiffOnly.setEnabled(self.actShowDiff.isChecked())
class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): def __init__(self, device_id, address: str, properties, parent=None) -> None: super().__init__(device_id=device_id, address=address, properties=properties, parent=parent) self._api_prefix = "/api/v1/" self._number_of_extruders = 2 self._authentication_id = None self._authentication_key = None self._authentication_counter = 0 self._max_authentication_counter = 5 * 60 # Number of attempts before authentication timed out (5 min) self._authentication_timer = QTimer() self._authentication_timer.setInterval( 1000) # TODO; Add preference for update interval self._authentication_timer.setSingleShot(False) self._authentication_timer.timeout.connect(self._onAuthenticationTimer) # The messages are created when connect is called the first time. # This ensures that the messages are only created for devices that actually want to connect. self._authentication_requested_message = None self._authentication_failed_message = None self._authentication_succeeded_message = None self._not_authenticated_message = None self.authenticationStateChanged.connect( self._onAuthenticationStateChanged) self.setPriority( 3 ) # Make sure the output device gets selected above local file output self.setName(self._id) self.setShortDescription( i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network")) self.setDescription( i18n_catalog.i18nc("@properties:tooltip", "Print over network")) self.setIconName("print") self._monitor_view_qml_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml") self._output_controller = LegacyUM3PrinterOutputController(self) def _onAuthenticationStateChanged(self): # We only accept commands if we are authenticated. self._setAcceptsCommands( self._authentication_state == AuthState.Authenticated) if self._authentication_state == AuthState.Authenticated: self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connected over the network.")) elif self._authentication_state == AuthState.AuthenticationRequested: self.setConnectionText( i18n_catalog.i18nc( "@info:status", "Connected over the network. Please approve the access request on the printer." )) elif self._authentication_state == AuthState.AuthenticationDenied: self.setConnectionText( i18n_catalog.i18nc( "@info:status", "Connected over the network. No access to control the printer." )) def _setupMessages(self): self._authentication_requested_message = Message( i18n_catalog.i18nc( "@info:status", "Access to the printer requested. Please approve the request on the printer" ), lifetime=0, dismissable=False, progress=0, title=i18n_catalog.i18nc("@info:title", "Authentication status")) self._authentication_failed_message = Message( i18n_catalog.i18nc("@info:status", ""), title=i18n_catalog.i18nc("@info:title", "Authentication Status")) self._authentication_failed_message.addAction( "Retry", i18n_catalog.i18nc("@action:button", "Retry"), None, i18n_catalog.i18nc("@info:tooltip", "Re-send the access request")) self._authentication_failed_message.actionTriggered.connect( self._messageCallback) self._authentication_succeeded_message = Message( i18n_catalog.i18nc("@info:status", "Access to the printer accepted"), title=i18n_catalog.i18nc("@info:title", "Authentication Status")) self._not_authenticated_message = Message(i18n_catalog.i18nc( "@info:status", "No access to print with this printer. Unable to send print job."), title=i18n_catalog.i18nc( "@info:title", "Authentication Status")) self._not_authenticated_message.addAction( "Request", i18n_catalog.i18nc("@action:button", "Request Access"), None, i18n_catalog.i18nc("@info:tooltip", "Send access request to the printer")) self._not_authenticated_message.actionTriggered.connect( self._messageCallback) def _messageCallback(self, message_id=None, action_id="Retry"): if action_id == "Request" or action_id == "Retry": if self._authentication_failed_message: self._authentication_failed_message.hide() if self._not_authenticated_message: self._not_authenticated_message.hide() self._requestAuthentication() def connect(self): super().connect() self._setupMessages() global_container = CuraApplication.getInstance( ).getGlobalContainerStack() if global_container: self._authentication_id = global_container.getMetaDataEntry( "network_authentication_id", None) self._authentication_key = global_container.getMetaDataEntry( "network_authentication_key", None) def close(self): super().close() if self._authentication_requested_message: self._authentication_requested_message.hide() if self._authentication_failed_message: self._authentication_failed_message.hide() if self._authentication_succeeded_message: self._authentication_succeeded_message.hide() self._sending_gcode = False self._compressing_gcode = False self._authentication_timer.stop() ## Send all material profiles to the printer. def _sendMaterialProfiles(self): Logger.log("i", "Sending material profiles to printer") # TODO: Might want to move this to a job... for container in ContainerRegistry.getInstance( ).findInstanceContainers(type="material"): try: xml_data = container.serialize() if xml_data == "" or xml_data is None: continue names = ContainerManager.getInstance().getLinkedMaterials( container.getId()) if names: # There are other materials that share this GUID. if not container.isReadOnly(): continue # If it's not readonly, it's created by user, so skip it. file_name = "none.xml" self.postForm("materials", "form-data; name=\"file\";filename=\"%s\"" % file_name, xml_data.encode(), onFinished=None) except NotImplementedError: # If the material container is not the most "generic" one it can't be serialized an will raise a # NotImplementedError. We can simply ignore these. pass def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: if not self.activePrinter: # No active printer. Unable to write return if self.activePrinter.state not in ["idle", ""]: # Printer is not able to accept commands. return if self._authentication_state != AuthState.Authenticated: # Not authenticated, so unable to send job. return self.writeStarted.emit(self) gcode_dict = getattr( CuraApplication.getInstance().getController().getScene(), "gcode_dict", []) active_build_plate_id = CuraApplication.getInstance( ).getMultiBuildPlateModel().activeBuildPlate gcode_list = gcode_dict[active_build_plate_id] if not gcode_list: # Unable to find g-code. Nothing to send return self._gcode = gcode_list errors = self._checkForErrors() if errors: text = i18n_catalog.i18nc("@label", "Unable to start a new print job.") informative_text = i18n_catalog.i18nc( "@label", "There is an issue with the configuration of your Ultimaker, which makes it impossible to start the print. " "Please resolve this issues before continuing.") detailed_text = "" for error in errors: detailed_text += error + "\n" CuraApplication.getInstance().messageBox( i18n_catalog.i18nc("@window:title", "Mismatched configuration"), text, informative_text, detailed_text, buttons=QMessageBox.Ok, icon=QMessageBox.Critical, callback=self._messageBoxCallback) return # Don't continue; Errors must block sending the job to the printer. # There might be multiple things wrong with the configuration. Check these before starting. warnings = self._checkForWarnings() if warnings: text = i18n_catalog.i18nc( "@label", "Are you sure you wish to print with the selected configuration?" ) informative_text = i18n_catalog.i18nc( "@label", "There is a mismatch between the configuration or calibration of the printer and Cura. " "For the best result, always slice for the PrintCores and materials that are inserted in your printer." ) detailed_text = "" for warning in warnings: detailed_text += warning + "\n" CuraApplication.getInstance().messageBox( i18n_catalog.i18nc("@window:title", "Mismatched configuration"), text, informative_text, detailed_text, buttons=QMessageBox.Yes + QMessageBox.No, icon=QMessageBox.Question, callback=self._messageBoxCallback) return # No warnings or errors, so we're good to go. self._startPrint() # Notify the UI that a switch to the print monitor should happen CuraApplication.getInstance().getController().setActiveStage( "MonitorStage") def _startPrint(self): Logger.log("i", "Sending print job to printer.") if self._sending_gcode: self._error_message = Message( i18n_catalog.i18nc( "@info:status", "Sending new jobs (temporarily) blocked, still sending the previous print job." )) self._error_message.show() return self._sending_gcode = True self._send_gcode_start = time() self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1, i18n_catalog.i18nc("@info:title", "Sending Data")) self._progress_message.addAction( "Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect( self._progressMessageActionTriggered) self._progress_message.show() compressed_gcode = self._compressGCode() if compressed_gcode is None: # Abort was called. return file_name = "%s.gcode.gz" % CuraApplication.getInstance( ).getPrintInformation().jobName self.postForm("print_job", "form-data; name=\"file\";filename=\"%s\"" % file_name, compressed_gcode, onFinished=self._onPostPrintJobFinished) return def _progressMessageActionTriggered(self, message_id=None, action_id=None): if action_id == "Abort": Logger.log("d", "User aborted sending print to remote.") self._progress_message.hide() self._compressing_gcode = False self._sending_gcode = False CuraApplication.getInstance().getController().setActiveStage( "PrepareStage") def _onPostPrintJobFinished(self, reply): self._progress_message.hide() self._sending_gcode = False def _onUploadPrintJobProgress(self, bytes_sent, bytes_total): if bytes_total > 0: new_progress = bytes_sent / bytes_total * 100 # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get # timeout responses if this happens. self._last_response_time = time() if new_progress > self._progress_message.getProgress(): self._progress_message.show( ) # Ensure that the message is visible. self._progress_message.setProgress(bytes_sent / bytes_total * 100) else: self._progress_message.setProgress(0) self._progress_message.hide() def _messageBoxCallback(self, button): def delayedCallback(): if button == QMessageBox.Yes: self._startPrint() else: CuraApplication.getInstance().getController().setActiveStage( "PrepareStage") # For some unknown reason Cura on OSX will hang if we do the call back code # immediately without first returning and leaving QML's event system. QTimer.singleShot(100, delayedCallback) def _checkForErrors(self): errors = [] print_information = CuraApplication.getInstance().getPrintInformation() if not print_information.materialLengths: Logger.log( "w", "There is no material length information. Unable to check for errors." ) return errors for index, extruder in enumerate(self.activePrinter.extruders): # Due to airflow issues, both slots must be loaded, regardless if they are actually used or not. if extruder.hotendID == "": # No Printcore loaded. errors.append( i18n_catalog.i18nc( "@info:status", "No Printcore loaded in slot {slot_number}".format( slot_number=index + 1))) if index < len(print_information.materialLengths ) and print_information.materialLengths[index] != 0: # The extruder is by this print. if extruder.activeMaterial is None: # No active material errors.append( i18n_catalog.i18nc( "@info:status", "No material loaded in slot {slot_number}".format( slot_number=index + 1))) return errors def _checkForWarnings(self): warnings = [] print_information = CuraApplication.getInstance().getPrintInformation() if not print_information.materialLengths: Logger.log( "w", "There is no material length information. Unable to check for warnings." ) return warnings extruder_manager = ExtruderManager.getInstance() for index, extruder in enumerate(self.activePrinter.extruders): if index < len(print_information.materialLengths ) and print_information.materialLengths[index] != 0: # The extruder is by this print. # TODO: material length check # Check if the right Printcore is active. variant = extruder_manager.getExtruderStack( index).findContainer({"type": "variant"}) if variant: if variant.getName() != extruder.hotendID: warnings.append( i18n_catalog.i18nc( "@label", "Different PrintCore (Cura: {cura_printcore_name}, Printer: {remote_printcore_name}) selected for extruder {extruder_id}" .format( cura_printcore_name=variant.getName(), remote_printcore_name=extruder.hotendID, extruder_id=index + 1))) else: Logger.log("w", "Unable to find variant.") # Check if the right material is loaded. local_material = extruder_manager.getExtruderStack( index).findContainer({"type": "material"}) if local_material: if extruder.activeMaterial.guid != local_material.getMetaDataEntry( "GUID"): Logger.log( "w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1, extruder.activeMaterial.guid, local_material.getMetaDataEntry("GUID")) warnings.append( i18n_catalog.i18nc( "@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}" ).format(local_material.getName(), extruder.activeMaterial.name, index + 1)) else: Logger.log("w", "Unable to find material.") return warnings def _update(self): if not super()._update(): return if self._authentication_state == AuthState.NotAuthenticated: if self._authentication_id is None and self._authentication_key is None: # This machine doesn't have any authentication, so request it. self._requestAuthentication() elif self._authentication_id is not None and self._authentication_key is not None: # We have authentication info, but we haven't checked it out yet. Do so now. self._verifyAuthentication() elif self._authentication_state == AuthState.AuthenticationReceived: # We have an authentication, but it's not confirmed yet. self._checkAuthentication() # We don't need authentication for requesting info, so we can go right ahead with requesting this. self.get("printer", onFinished=self._onGetPrinterDataFinished) self.get("print_job", onFinished=self._onGetPrintJobFinished) def _resetAuthenticationRequestedMessage(self): if self._authentication_requested_message: self._authentication_requested_message.hide() self._authentication_timer.stop() self._authentication_counter = 0 def _onAuthenticationTimer(self): self._authentication_counter += 1 self._authentication_requested_message.setProgress( self._authentication_counter / self._max_authentication_counter * 100) if self._authentication_counter > self._max_authentication_counter: self._authentication_timer.stop() Logger.log( "i", "Authentication timer ended. Setting authentication to denied for printer: %s" % self._id) self.setAuthenticationState(AuthState.AuthenticationDenied) self._resetAuthenticationRequestedMessage() self._authentication_failed_message.show() def _verifyAuthentication(self): Logger.log("d", "Attempting to verify authentication") # This will ensure that the "_onAuthenticationRequired" is triggered, which will setup the authenticator. self.get("auth/verify", onFinished=self._onVerifyAuthenticationCompleted) def _onVerifyAuthenticationCompleted(self, reply): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if status_code == 401: # Something went wrong; We somehow tried to verify authentication without having one. Logger.log("d", "Attempted to verify auth without having one.") self._authentication_id = None self._authentication_key = None self.setAuthenticationState(AuthState.NotAuthenticated) elif status_code == 403 and self._authentication_state != AuthState.Authenticated: # If we were already authenticated, we probably got an older message back all of the sudden. Drop that. Logger.log( "d", "While trying to verify the authentication state, we got a forbidden response. Our own auth state was %s. ", self._authentication_state) self.setAuthenticationState(AuthState.AuthenticationDenied) self._authentication_failed_message.show() elif status_code == 200: self.setAuthenticationState(AuthState.Authenticated) def _checkAuthentication(self): Logger.log( "d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) self.get("auth/check/" + str(self._authentication_id), onFinished=self._onCheckAuthenticationFinished) def _onCheckAuthenticationFinished(self, reply): if str(self._authentication_id) not in reply.url().toString(): Logger.log("w", "Got an old id response.") # Got response for old authentication ID. return try: data = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received an invalid authentication check from printer: Not valid JSON." ) return if data.get("message", "") == "authorized": Logger.log("i", "Authentication was approved") self.setAuthenticationState(AuthState.Authenticated) self._saveAuthentication() # Double check that everything went well. self._verifyAuthentication() # Notify the user. self._resetAuthenticationRequestedMessage() self._authentication_succeeded_message.show() elif data.get("message", "") == "unauthorized": Logger.log("i", "Authentication was denied.") self.setAuthenticationState(AuthState.AuthenticationDenied) self._authentication_failed_message.show() def _saveAuthentication(self): global_container_stack = CuraApplication.getInstance( ).getGlobalContainerStack() if global_container_stack: if "network_authentication_key" in global_container_stack.getMetaData( ): global_container_stack.setMetaDataEntry( "network_authentication_key", self._authentication_key) else: global_container_stack.addMetaDataEntry( "network_authentication_key", self._authentication_key) if "network_authentication_id" in global_container_stack.getMetaData( ): global_container_stack.setMetaDataEntry( "network_authentication_id", self._authentication_id) else: global_container_stack.addMetaDataEntry( "network_authentication_id", self._authentication_id) # Force save so we are sure the data is not lost. CuraApplication.getInstance().saveStack(global_container_stack) Logger.log("i", "Authentication succeeded for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) else: Logger.log("e", "Unable to save authentication for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) def _onRequestAuthenticationFinished(self, reply): try: data = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received an invalid authentication request reply from printer: Not valid JSON." ) self.setAuthenticationState(AuthState.NotAuthenticated) return self.setAuthenticationState(AuthState.AuthenticationReceived) self._authentication_id = data["id"] self._authentication_key = data["key"] Logger.log( "i", "Got a new authentication ID (%s) and KEY (%s). Waiting for authorization.", self._authentication_id, self._getSafeAuthKey()) def _requestAuthentication(self): self._authentication_requested_message.show() self._authentication_timer.start() # Reset any previous authentication info. If this isn't done, the "Retry" action on the failed message might # give issues. self._authentication_key = None self._authentication_id = None self.post("auth/request", json.dumps({ "application": "Cura-" + CuraApplication.getInstance().getVersion(), "user": self._getUserName() }).encode(), onFinished=self._onRequestAuthenticationFinished) self.setAuthenticationState(AuthState.AuthenticationRequested) def _onAuthenticationRequired(self, reply, authenticator): if self._authentication_id is not None and self._authentication_key is not None: Logger.log( "d", "Authentication was required for printer: %s. Setting up authenticator with ID %s and key %s", self._id, self._authentication_id, self._getSafeAuthKey()) authenticator.setUser(self._authentication_id) authenticator.setPassword(self._authentication_key) else: Logger.log( "d", "No authentication is available to use for %s, but we did got a request for it.", self._id) def _onGetPrintJobFinished(self, reply): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if not self._printers: return # Ignore the data for now, we don't have info about a printer yet. printer = self._printers[0] if status_code == 200: try: result = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received an invalid print job state message: Not valid JSON." ) return if printer.activePrintJob is None: print_job = PrintJobOutputModel( output_controller=self._output_controller) printer.updateActivePrintJob(print_job) else: print_job = printer.activePrintJob print_job.updateState(result["state"]) print_job.updateTimeElapsed(result["time_elapsed"]) print_job.updateTimeTotal(result["time_total"]) print_job.updateName(result["name"]) elif status_code == 404: # No job found, so delete the active print job (if any!) printer.updateActivePrintJob(None) else: Logger.log( "w", "Got status code {status_code} while trying to get printer data" .format(status_code=status_code)) def materialHotendChangedMessage(self, callback): CuraApplication.getInstance().messageBox( i18n_catalog.i18nc("@window:title", "Sync with your printer"), i18n_catalog.i18nc( "@label", "Would you like to use your current printer configuration in Cura?" ), i18n_catalog.i18nc( "@label", "The PrintCores and/or materials on your printer differ from those within your current project. For the best result, always slice for the PrintCores and materials that are inserted in your printer." ), buttons=QMessageBox.Yes + QMessageBox.No, icon=QMessageBox.Question, callback=callback) def _onGetPrinterDataFinished(self, reply): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if status_code == 200: try: result = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received an invalid printer state message: Not valid JSON." ) return if not self._printers: # Quickest way to get the firmware version is to grab it from the zeroconf. firmware_version = self._properties.get( b"firmware_version", b"").decode("utf-8") self._printers = [ PrinterOutputModel( output_controller=self._output_controller, number_of_extruders=self._number_of_extruders, firmware_version=firmware_version) ] self._printers[0].setCamera( NetworkCamera("http://" + self._address + ":8080/?action=stream")) for extruder in self._printers[0].extruders: extruder.activeMaterialChanged.connect( self.materialIdChanged) extruder.hotendIDChanged.connect(self.hotendIdChanged) self.printersChanged.emit() # LegacyUM3 always has a single printer. printer = self._printers[0] printer.updateBedTemperature( result["bed"]["temperature"]["current"]) printer.updateTargetBedTemperature( result["bed"]["temperature"]["target"]) printer.updateState(result["status"]) try: # If we're still handling the request, we should ignore remote for a bit. if not printer.getController().isPreheatRequestInProgress(): printer.updateIsPreheating( result["bed"]["pre_heat"]["active"]) except KeyError: # Older firmwares don't support preheating, so we need to fake it. pass head_position = result["heads"][0]["position"] printer.updateHeadPosition(head_position["x"], head_position["y"], head_position["z"]) for index in range(0, self._number_of_extruders): temperatures = result["heads"][0]["extruders"][index][ "hotend"]["temperature"] extruder = printer.extruders[index] extruder.updateTargetHotendTemperature(temperatures["target"]) extruder.updateHotendTemperature(temperatures["current"]) material_guid = result["heads"][0]["extruders"][index][ "active_material"]["guid"] if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_guid: # Find matching material (as we need to set brand, type & color) containers = ContainerRegistry.getInstance( ).findInstanceContainers(type="material", GUID=material_guid) if containers: color = containers[0].getMetaDataEntry("color_code") brand = containers[0].getMetaDataEntry("brand") material_type = containers[0].getMetaDataEntry( "material") name = containers[0].getName() else: # Unknown material. color = "#00000000" brand = "Unknown" material_type = "Unknown" name = "Unknown" material = MaterialOutputModel(guid=material_guid, type=material_type, brand=brand, color=color, name=name) extruder.updateActiveMaterial(material) try: hotend_id = result["heads"][0]["extruders"][index][ "hotend"]["id"] except KeyError: hotend_id = "" printer.extruders[index].updateHotendID(hotend_id) else: Logger.log( "w", "Got status code {status_code} while trying to get printer data" .format(status_code=status_code)) ## Convenience function to "blur" out all but the last 5 characters of the auth key. # This can be used to debug print the key, without it compromising the security. def _getSafeAuthKey(self): if self._authentication_key is not None: result = self._authentication_key[-5:] result = "********" + result return result return self._authentication_key
class OctoPrintOutputDevice(NetworkedPrinterOutputDevice): def __init__(self, key, address: str, port, properties, parent=None): super().__init__(device_id=key, address=address, properties=properties, parent=parent) self._address = address self._port = port self._path = properties.get(b"path", b"/").decode("utf-8") if self._path[-1:] != "/": self._path += "/" self._key = key self._properties = properties # Properties dict as provided by zero conf self._gcode = None self._auto_print = True self._forced_queue = False # We start with a single extruder, but update this when we get data from octoprint self._number_of_extruders_set = False self._number_of_extruders = 1 # Try to get version information from plugin.json plugin_file_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "plugin.json") try: with open(plugin_file_path) as plugin_file: plugin_info = json.load(plugin_file) plugin_version = plugin_info["version"] except: # The actual version info is not critical to have so we can continue plugin_version = "Unknown" Logger.logException( "w", "Could not get version information for the plugin") self._user_agent_header = "User-Agent".encode() self._user_agent = ( "%s/%s %s/%s" % (Application.getInstance().getApplicationName(), Application.getInstance().getVersion(), "OctoPrintPlugin", Application.getInstance().getVersion())).encode() self._api_prefix = "api/" self._api_header = "X-Api-Key".encode() self._api_key = None self._protocol = "https" if properties.get( b'useHttps') == b"true" else "http" self._base_url = "%s://%s:%d%s" % (self._protocol, self._address, self._port, self._path) self._api_url = self._base_url + self._api_prefix self._basic_auth_header = "Authorization".encode() self._basic_auth_data = None basic_auth_username = properties.get(b"userName", b"").decode("utf-8") basic_auth_password = properties.get(b"password", b"").decode("utf-8") if basic_auth_username and basic_auth_password: data = base64.b64encode( ("%s:%s" % (basic_auth_username, basic_auth_password)).encode()).decode("utf-8") self._basic_auth_data = ("basic %s" % data).encode() self._monitor_view_qml_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml") self.setPriority( 2 ) # Make sure the output device gets selected above local file output self.setName(key) self.setShortDescription( i18n_catalog.i18nc("@action:button", "Print with OctoPrint")) self.setDescription( i18n_catalog.i18nc("@properties:tooltip", "Print with OctoPrint")) self.setIconName("print") self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connected to OctoPrint on {0}").format( self._key)) # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly # hook itself into the event loop, which results in events never being fired / done. self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) ## Ensure that the qt networking stuff isn't garbage collected (unless we want it to) self._settings_reply = None self._printer_reply = None self._job_reply = None self._command_reply = None self._post_reply = None self._post_multi_part = None self._post_part = None self._progress_message = None self._error_message = None self._connection_message = None self._queued_gcode_commands = [] self._queued_gcode_timer = QTimer() self._queued_gcode_timer.setInterval(0) self._queued_gcode_timer.setSingleShot(True) self._queued_gcode_timer.timeout.connect(self._sendQueuedGcode) self._update_timer = QTimer() self._update_timer.setInterval( 2000) # TODO; Add preference for update interval self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._update) self._camera_mirror = "" self._camera_rotation = 0 self._camera_url = "" self._camera_shares_proxy = False self._sd_supported = False self._connection_state_before_timeout = None self._last_response_time = None self._last_request_time = None self._response_timeout_time = 5 self._recreate_network_manager_time = 30 # If we have no connection, re-create network manager every 30 sec. self._recreate_network_manager_count = 1 self._output_controller = GenericOutputController(self) def getProperties(self): return self._properties @pyqtSlot(str, result=str) def getProperty(self, key): key = key.encode("utf-8") if key in self._properties: return self._properties.get(key, b"").decode("utf-8") else: return "" ## Get the unique key of this machine # \return key String containing the key of the machine. @pyqtSlot(result=str) def getKey(self): return self._key ## Set the API key of this OctoPrint instance def setApiKey(self, api_key): self._api_key = api_key.encode() ## Name of the instance (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def name(self): return self._key ## Version (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def octoprintVersion(self): return self._properties.get(b"version", b"").decode("utf-8") ## IPadress of this instance @pyqtProperty(str, constant=True) def ipAddress(self): return self._address ## IPadress of this instance # Overridden from NetworkedPrinterOutputDevice because OctoPrint does not # send the ip address with zeroconf @pyqtProperty(str, constant=True) def address(self): return self._address ## port of this instance @pyqtProperty(int, constant=True) def port(self): return self._port ## path of this instance @pyqtProperty(str, constant=True) def path(self): return self._path ## absolute url of this instance @pyqtProperty(str, constant=True) def baseURL(self): return self._base_url cameraOrientationChanged = pyqtSignal() @pyqtProperty("QVariantMap", notify=cameraOrientationChanged) def cameraOrientation(self): return { "mirror": self._camera_mirror, "rotation": self._camera_rotation, } def _update(self): if self._last_response_time: time_since_last_response = time() - self._last_response_time else: time_since_last_response = 0 if self._last_request_time: time_since_last_request = time() - self._last_request_time else: time_since_last_request = float( "inf") # An irrelevantly large number of seconds # Connection is in timeout, check if we need to re-start the connection. # Sometimes the qNetwork manager incorrectly reports the network status on Mac & Windows. # Re-creating the QNetworkManager seems to fix this issue. if self._last_response_time and self._connection_state_before_timeout: if time_since_last_response > self._recreate_network_manager_time * self._recreate_network_manager_count: self._recreate_network_manager_count += 1 # It can happen that we had a very long timeout (multiple times the recreate time). # In that case we should jump through the point that the next update won't be right away. while time_since_last_response - self._recreate_network_manager_time * self._recreate_network_manager_count > self._recreate_network_manager_time: self._recreate_network_manager_count += 1 Logger.log( "d", "Timeout lasted over 30 seconds (%.1fs), re-checking connection.", time_since_last_response) self._createNetworkManager() return # Check if we have an connection in the first place. if not self._manager.networkAccessible(): if not self._connection_state_before_timeout: Logger.log( "d", "The network connection seems to be disabled. Going into timeout mode" ) self._connection_state_before_timeout = self._connection_state self.setConnectionState(ConnectionState.error) self._connection_message = Message( i18n_catalog.i18nc( "@info:status", "The connection with the network was lost.")) self._connection_message.show() # Check if we were uploading something. Abort if this is the case. # Some operating systems handle this themselves, others give weird issues. try: if self._post_reply: Logger.log( "d", "Stopping post upload because the connection was lost." ) try: self._post_reply.uploadProgress.disconnect( self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() self._progress_message.hide() except RuntimeError: self._post_reply = None # It can happen that the wrapped c++ object is already deleted. return else: if not self._connection_state_before_timeout: self._recreate_network_manager_count = 1 # Check that we aren't in a timeout state if self._last_response_time and self._last_request_time and not self._connection_state_before_timeout: if time_since_last_response > self._response_timeout_time and time_since_last_request <= self._response_timeout_time: # Go into timeout state. Logger.log( "d", "We did not receive a response for %s seconds, so it seems OctoPrint is no longer accesible.", time() - self._last_response_time) self._connection_state_before_timeout = self._connection_state self._connection_message = Message( i18n_catalog.i18nc( "@info:status", "The connection with OctoPrint was lost. Check your network-connections." )) self._connection_message.show() self.setConnectionState(ConnectionState.error) ## Request 'general' printer data self._printer_reply = self._manager.get( self._createApiRequest("printer")) ## Request print_job data self._job_reply = self._manager.get(self._createApiRequest("job")) def _createNetworkManager(self): if self._manager: self._manager.finished.disconnect(self._onRequestFinished) self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) def _createApiRequest(self, end_point): request = QNetworkRequest(QUrl(self._api_url + end_point)) request.setRawHeader(self._user_agent_header, self._user_agent) request.setRawHeader(self._api_header, self._api_key) if self._basic_auth_data: request.setRawHeader(self._basic_auth_header, self._basic_auth_data) return request def close(self): self.setConnectionState(ConnectionState.closed) if self._progress_message: self._progress_message.hide() if self._error_message: self._error_message.hide() self._update_timer.stop() def requestWrite(self, node, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): self.writeStarted.emit(self) active_build_plate = Application.getInstance().getBuildPlateModel( ).activeBuildPlate scene = Application.getInstance().getController().getScene() gcode_dict = getattr(scene, "gcode_dict", None) if not gcode_dict: return self._gcode = gcode_dict.get(active_build_plate, None) self.startPrint() ## Start requesting data from the instance def connect(self): self._createNetworkManager() self.setConnectionState(ConnectionState.connecting) self._update( ) # Manually trigger the first update, as we don't want to wait a few secs before it starts. Logger.log("d", "Connection with instance %s with url %s started", self._key, self._base_url) self._update_timer.start() self._last_response_time = None self._setAcceptsCommands(False) self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connecting to OctoPrint on {0}").format( self._key)) ## Request 'settings' dump self._settings_reply = self._manager.get( self._createApiRequest("settings")) ## Stop requesting data from the instance def disconnect(self): Logger.log("d", "Connection with instance %s with url %s stopped", self._key, self._base_url) self.close() def pausePrint(self): self._sendJobCommand("pause") def resumePrint(self): if not self._printers[0].activePrintJob: return if self._printers[0].activePrintJob.state == "paused": self._sendJobCommand("pause") else: self._sendJobCommand("start") def cancelPrint(self): self._sendJobCommand("cancel") def startPrint(self): global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not global_container_stack: return if self._error_message: self._error_message.hide() self._error_message = None if self._progress_message: self._progress_message.hide() self._progress_message = None self._auto_print = parseBool( global_container_stack.getMetaDataEntry("octoprint_auto_print", True)) self._forced_queue = False if self.activePrinter.state not in ["idle", ""]: Logger.log( "d", "Tried starting a print, but current state is %s" % self.activePrinter.state) if self.activePrinter.state == "offline": self._error_message = Message( i18n_catalog.i18nc( "@info:status", "The printer is offline. Unable to start a new job.")) elif self._auto_print: self._error_message = Message( i18n_catalog.i18nc( "@info:status", "OctoPrint is busy. Unable to start a new job.")) else: # allow queueing the job even if OctoPrint is currently busy if autoprinting is disabled self._error_message = None if self._error_message: self._error_message.addAction( "Queue", i18n_catalog.i18nc("@action:button", "Queue job"), None, i18n_catalog.i18nc( "@action:tooltip", "Queue this print job so it can be printed later")) self._error_message.actionTriggered.connect(self._queuePrint) self._error_message.show() return self._startPrint() def _queuePrint(self, message_id, action_id): if self._error_message: self._error_message.hide() self._forced_queue = True self._startPrint() def _startPrint(self): if self._auto_print and not self._forced_queue: Application.getInstance().getController().setActiveStage( "MonitorStage") try: self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to OctoPrint"), 0, False, -1) self._progress_message.addAction( "Cancel", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect( self._cancelSendGcode) self._progress_message.show() ## Mash the data into single string single_string_file_data = "" last_process_events = time() for line in self._gcode: single_string_file_data += line if time() > last_process_events + 0.05: # Ensure that the GUI keeps updated at least 20 times per second. QCoreApplication.processEvents() last_process_events = time() job_name = Application.getInstance().getPrintInformation( ).jobName.strip() if job_name is "": job_name = "untitled_print" file_name = "%s.gcode" % job_name ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (to be placed inside multipart) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) if self._auto_print and not self._forced_queue: self._post_part = QHttpPart() self._post_part.setHeader( QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader( QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) self._post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) destination = "local" if self._sd_supported and parseBool(Application.getInstance( ).getGlobalContainerStack().getMetaDataEntry( "octoprint_store_sd", False)): destination = "sdcard" ## Post request + data post_request = self._createApiRequest("files/" + destination) self._post_reply = self._manager.post(post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._gcode = None except IOError: self._progress_message.hide() self._error_message = Message( i18n_catalog.i18nc("@info:status", "Unable to send data to OctoPrint.")) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) def _cancelSendGcode(self, message_id, action_id): if self._post_reply: Logger.log("d", "Stopping upload because the user pressed cancel.") try: self._post_reply.uploadProgress.disconnect( self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() self._post_reply = None if self._progress_message: self._progress_message.hide() def sendCommand(self, command): self._queued_gcode_commands.append(command) self._queued_gcode_timer.start() # Send gcode commands that are queued in quick succession as a single batch def _sendQueuedGcode(self): if self._queued_gcode_commands: self._sendCommandToApi("printer/command", self._queued_gcode_commands) Logger.log("d", "Sent gcode command to OctoPrint instance: %s", self._queued_gcode_commands) self._queued_gcode_commands = [] def _sendJobCommand(self, command): self._sendCommandToApi("job", command) Logger.log("d", "Sent job command to OctoPrint instance: %s", command) def _sendCommandToApi(self, end_point, commands): command_request = self._createApiRequest(end_point) command_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") if isinstance(commands, list): data = json.dumps({"commands": commands}) else: data = json.dumps({"command": commands}) self._command_reply = self._manager.post(command_request, data.encode()) ## Handler for all requests that have finished. def _onRequestFinished(self, reply): if reply.error() == QNetworkReply.TimeoutError: Logger.log("w", "Received a timeout on a request to the instance") self._connection_state_before_timeout = self._connection_state self.setConnectionState(ConnectionState.error) return if self._connection_state_before_timeout and reply.error( ) == QNetworkReply.NoError: # There was a timeout, but we got a correct answer again. if self._last_response_time: Logger.log( "d", "We got a response from the instance after %s of silence", time() - self._last_response_time) self.setConnectionState(self._connection_state_before_timeout) self._connection_state_before_timeout = None if reply.error() == QNetworkReply.NoError: self._last_response_time = time() http_status_code = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) if not http_status_code: # Received no or empty reply return if reply.operation() == QNetworkAccessManager.GetOperation: if self._api_prefix + "printer" in reply.url().toString( ): # Status update from /printer. if not self._printers: self._createPrinterList() # An OctoPrint instance has a single printer. printer = self._printers[0] if http_status_code == 200: if not self.acceptsCommands: self._setAcceptsCommands(True) self.setConnectionText( i18n_catalog.i18nc( "@info:status", "Connected to OctoPrint on {0}").format( self._key)) if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from octoprint instance.") json_data = {} if "temperature" in json_data: if not self._number_of_extruders_set: self._number_of_extruders = 0 while "tool%d" % self._number_of_extruders in json_data[ "temperature"]: self._number_of_extruders += 1 if self._number_of_extruders > 1: # Recreate list of printers to match the new _number_of_extruders self._createPrinterList() printer = self._printers[0] if self._number_of_extruders > 0: self._number_of_extruders_set = True # Check for hotend temperatures for index in range(0, self._number_of_extruders): extruder = printer.extruders[index] if ("tool%d" % index) in json_data["temperature"]: hotend_temperatures = json_data["temperature"][ "tool%d" % index] extruder.updateTargetHotendTemperature( hotend_temperatures["target"]) extruder.updateHotendTemperature( hotend_temperatures["actual"]) else: extruder.updateTargetHotendTemperature(0) extruder.updateHotendTemperature(0) if "bed" in json_data["temperature"]: bed_temperatures = json_data["temperature"]["bed"] printer.updateBedTemperature( bed_temperatures["actual"]) printer.updateTargetBedTemperature( bed_temperatures["target"]) else: printer.updateBedTemperature(0) printer.updateTargetBedTemperature(0) printer_state = "offline" if "state" in json_data: if json_data["state"]["flags"]["error"]: printer_state = "error" elif json_data["state"]["flags"]["paused"]: printer_state = "paused" elif json_data["state"]["flags"]["printing"]: printer_state = "printing" elif json_data["state"]["flags"]["ready"]: printer_state = "idle" printer.updateState(printer_state) elif http_status_code == 401: printer.updateState("offline") if printer.activePrintJob: printer.activePrintJob.updateState("offline") self.setConnectionText( i18n_catalog.i18nc( "@info:status", "OctoPrint on {0} does not allow access to print"). format(self._key)) pass elif http_status_code == 409: if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) printer.updateState("offline") if printer.activePrintJob: printer.activePrintJob.updateState("offline") self.setConnectionText( i18n_catalog.i18nc( "@info:status", "The printer connected to OctoPrint on {0} is not operational" ).format(self._key)) else: printer.updateState("offline") if printer.activePrintJob: printer.activePrintJob.updateState("offline") Logger.log("w", "Received an unexpected returncode: %d", http_status_code) elif self._api_prefix + "job" in reply.url().toString( ): # Status update from /job: if not self._printers: return # Ignore the data for now, we don't have info about a printer yet. printer = self._printers[0] if http_status_code == 200: try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from octoprint instance.") json_data = {} if printer.activePrintJob is None: print_job = PrintJobOutputModel( output_controller=self._output_controller) printer.updateActivePrintJob(print_job) else: print_job = printer.activePrintJob print_job_state = "offline" if "state" in json_data: if json_data["state"] == "Error": print_job_state = "error" elif json_data["state"] == "Paused": print_job_state = "paused" elif json_data["state"] == "Printing": print_job_state = "printing" elif json_data["state"] == "Operational": print_job_state = "ready" printer.updateState("idle") print_job.updateState(print_job_state) if json_data["progress"]["printTime"]: print_job.updateTimeElapsed( json_data["progress"]["printTime"]) if json_data["progress"]["printTimeLeft"]: print_job.updateTimeTotal( json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"]) elif json_data["job"]["estimatedPrintTime"]: print_job.updateTimeTotal( max(json_data["job"]["estimatedPrintTime"], json_data["progress"]["printTime"])) elif json_data["progress"][ "completion"]: # not 0 or None or "" print_job.updateTimeTotal( json_data["progress"]["printTime"] / (json_data["progress"]["completion"] / 100)) else: print_job.updateTimeTotal(0) else: print_job.updateTimeElapsed(0) print_job.updateTimeTotal(0) print_job.updateName(json_data["job"]["file"]["name"]) else: pass # TODO: Handle errors elif self._api_prefix + "settings" in reply.url().toString( ): # OctoPrint settings dump from /settings: if http_status_code == 200: try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from octoprint instance.") json_data = {} if "feature" in json_data and "sdSupport" in json_data[ "feature"]: self._sd_supported = json_data["feature"]["sdSupport"] if "webcam" in json_data and "streamUrl" in json_data[ "webcam"]: self._camera_shares_proxy = False stream_url = json_data["webcam"]["streamUrl"] if not stream_url: #empty string or None self._camera_url = "" elif stream_url[:4].lower() == "http": # absolute uri self._camera_url = stream_url elif stream_url[:2] == "//": # protocol-relative self._camera_url = "%s:%s" % (self._protocol, stream_url) elif stream_url[: 1] == ":": # domain-relative (on another port) self._camera_url = "%s://%s%s" % ( self._protocol, self._address, stream_url) elif stream_url[: 1] == "/": # domain-relative (on same port) self._camera_url = "%s://%s:%d%s" % ( self._protocol, self._address, self._port, stream_url) self._camera_shares_proxy = True else: Logger.log("w", "Unusable stream url received: %s", stream_url) self._camera_url = "" Logger.log("d", "Set OctoPrint camera url to %s", self._camera_url) if "rotate90" in json_data["webcam"]: self._camera_rotation = -90 if json_data["webcam"][ "rotate90"] else 0 if json_data["webcam"]["flipH"] and json_data[ "webcam"]["flipV"]: self._camera_mirror = False self._camera_rotation += 180 elif json_data["webcam"]["flipH"]: self._camera_mirror = True elif json_data["webcam"]["flipV"]: self._camera_mirror = True self._camera_rotation += 180 else: self._camera_mirror = False self.cameraOrientationChanged.emit() elif reply.operation() == QNetworkAccessManager.PostOperation: if self._api_prefix + "files" in reply.url().toString( ): # Result from /files command: if http_status_code == 201: Logger.log( "d", "Resource created on OctoPrint instance: %s", reply.header( QNetworkRequest.LocationHeader).toString()) else: pass # TODO: Handle errors reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() if self._forced_queue or not self._auto_print: location = reply.header(QNetworkRequest.LocationHeader) if location: file_name = QUrl( reply.header(QNetworkRequest.LocationHeader). toString()).fileName() message = Message( i18n_catalog.i18nc( "@info:status", "Saved to OctoPrint as {0}").format(file_name)) else: message = Message( i18n_catalog.i18nc("@info:status", "Saved to OctoPrint")) message.addAction( "open_browser", i18n_catalog.i18nc("@action:button", "OctoPrint..."), "globe", i18n_catalog.i18nc("@info:tooltip", "Open the OctoPrint web interface")) message.actionTriggered.connect( self._onMessageActionTriggered) message.show() elif self._api_prefix + "job" in reply.url().toString( ): # Result from /job command (eg start/pause): if http_status_code == 204: Logger.log("d", "Octoprint job command accepted") else: pass # TODO: Handle errors elif self._api_prefix + "printer/command" in reply.url().toString( ): # Result from /printer/command (gcode statements): if http_status_code == 204: Logger.log("d", "Octoprint gcode command(s) accepted") else: pass # TODO: Handle errors else: Logger.log("d", "OctoPrintOutputDevice got an unhandled operation %s", reply.operation()) def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get # timeout responses if this happens. self._last_response_time = time() progress = bytes_sent / bytes_total * 100 if progress < 100: if progress > self._progress_message.getProgress(): self._progress_message.setProgress(progress) else: self._progress_message.hide() self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Storing data on OctoPrint"), 0, False, -1) self._progress_message.show() else: self._progress_message.setProgress(0) def _createPrinterList(self): printer = PrinterOutputModel( output_controller=self._output_controller, number_of_extruders=self._number_of_extruders) printer.setCamera(NetworkCamera(self._camera_url)) printer.updateName(self.name) self._printers = [printer] self.printersChanged.emit() def _onMessageActionTriggered(self, message, action): if action == "open_browser": QDesktopServices.openUrl(QUrl(self._base_url))
class LegacyUM3PrinterOutputController(PrinterOutputController): def __init__(self, output_device): super().__init__(output_device) self._preheat_bed_timer = QTimer() self._preheat_bed_timer.setSingleShot(True) self._preheat_bed_timer.timeout.connect( self._onPreheatBedTimerFinished) self._preheat_printer = None self.can_control_manually = False self.can_send_raw_gcode = False # Are we still waiting for a response about preheat? # We need this so we can already update buttons, so it feels more snappy. self._preheat_request_in_progress = False def isPreheatRequestInProgress(self): return self._preheat_request_in_progress def setJobState(self, job: "PrintJobOutputModel", state: str): data = "{\"target\": \"%s\"}" % state self._output_device.put("print_job/state", data, onFinished=None) def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int): data = str(temperature) self._output_device.put("printer/bed/temperature/target", data, onFinished=self._onPutBedTemperatureCompleted) def _onPutBedTemperatureCompleted(self, reply): if Version(self._preheat_printer.firmwareVersion) < Version("3.5.92"): # If it was handling a preheat, it isn't anymore. self._preheat_request_in_progress = False def _onPutPreheatBedCompleted(self, reply): self._preheat_request_in_progress = False def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed): head_pos = printer._head_position new_x = head_pos.x + x new_y = head_pos.y + y new_z = head_pos.z + z data = "{\n\"x\":%s,\n\"y\":%s,\n\"z\":%s\n}" % (new_x, new_y, new_z) self._output_device.put("printer/heads/0/position", data, onFinished=None) def homeBed(self, printer): self._output_device.put("printer/heads/0/position/z", "0", onFinished=None) def _onPreheatBedTimerFinished(self): self.setTargetBedTemperature(self._preheat_printer, 0) self._preheat_printer.updateIsPreheating(False) self._preheat_request_in_progress = True def cancelPreheatBed(self, printer: "PrinterOutputModel"): self.preheatBed(printer, temperature=0, duration=0) self._preheat_bed_timer.stop() printer.updateIsPreheating(False) def preheatBed(self, printer: "PrinterOutputModel", temperature, duration): try: temperature = round( temperature) # The API doesn't allow floating point. duration = round(duration) except ValueError: return # Got invalid values, can't pre-heat. if duration > 0: data = """{"temperature": "%i", "timeout": "%i"}""" % (temperature, duration) else: data = """{"temperature": "%i"}""" % temperature # Real bed pre-heating support is implemented from 3.5.92 and up. if Version(printer.firmwareVersion) < Version("3.5.92"): # No firmware-side duration support then, so just set target bed temp and set a timer. self.setTargetBedTemperature(printer, temperature=temperature) self._preheat_bed_timer.setInterval(duration * 1000) self._preheat_bed_timer.start() self._preheat_printer = printer printer.updateIsPreheating(True) return self._output_device.put("printer/bed/pre_heat", data, onFinished=self._onPutPreheatBedCompleted) printer.updateIsPreheating(True) self._preheat_request_in_progress = True
class FishboneConnector(RepeaterDelegate): ConnectStatus = Enum('Status', "disconnected connecting connected") _status = ConnectStatus.disconnected statusChanged = pyqtSignal(int) @pyqtProperty(ConnectStatus, notify=statusChanged) def status(self): return self._status @status.setter def status(self, status): self._status = status self.statusChanged.emit(status.value) _program = None _device = None _screenWidth = 0 _screenHeight = 0 _screenBitDepth = 0 @pyqtProperty(int) def screenWidth(self): return self._screenWidth @screenWidth.setter def screenWidth(self, value): self._screenWidth = value @pyqtProperty(int) def screenHeight(self): return self._screenHeight @screenHeight.setter def screenHeight(self, value): self._screenHeight = value @pyqtProperty(int) def screenBitDepth(self): return self._screenBitDepth @screenBitDepth.setter def screenBitDepth(self, value): self._screenBitDepth = value deviceChanged = pyqtSignal() @pyqtProperty(str, notify=deviceChanged) def device(self): return self._device @device.setter def device(self, device): if self._device != device: self._device = device self.deviceChanged.emit() def __init__(self, parent=None): super(FishboneConnector, self).__init__(parent) self._queryDeviceTimer = QTimer(self) self._queryDeviceTimer.setSingleShot(True) self._queryDeviceTimer.timeout.connect(self.onQueryDevice) self._queryDeviceTimeoutTimer = QTimer(self) self._queryDeviceTimeoutTimer.setSingleShot(True) self._queryDeviceTimeoutTimer.timeout.connect( self.onQeuryDeviceTimeout) self._launchDaemonTimeoutTimer = QTimer(self) self._launchDaemonTimeoutTimer.setSingleShot(True) self._launchDaemonTimeoutTimer.timeout.connect(self.launchDaemonFailed) self.onRepeaterStatusChanged() self._repeater.statusChanged.connect(self.onRepeaterStatusChanged) @pyqtSlot() def onRepeaterStatusChanged(self): self.status = self.ConnectStatus.connecting if self._repeater.isConnected( ) else self.ConnectStatus.disconnected if self.status == self.ConnectStatus.connecting: self._queryDeviceTimer.start(1000) elif self.status == self.ConnectStatus.disconnected: self._queryDeviceTimer.stop() self._queryDeviceTimeoutTimer.stop() @pyqtSlot() def onQueryDevice(self): self._queryDeviceTimeoutTimer.start(1000) block, ostream = self._repeater.getRequestBlock() ostream.writeQString('queryDevice') self._repeater.submitRequestBlock(block) def respQueryDevice(self, istream): self._queryDeviceTimeoutTimer.stop() self.program = istream.readQString() if self.program != 'daemon': self.startDaemon() else: self._launchDaemonTimeoutTimer.stop() version = istream.readQString() self.device = istream.readQString() self.screenWidth = istream.readInt() self.screenHeight = istream.readInt() self.screenBitDepth = istream.readInt() self.status = self.ConnectStatus.connected self._queryDeviceTimer.start() @pyqtSlot() def onQeuryDeviceTimeout(self): self.status = self.ConnectStatus.connecting self._queryDeviceTimer.start() def startDaemon(self): block, ostream = self._repeater.getRequestBlock() ostream.writeQString('startDaemon') with open("apps/bigeye-daemon", "rb") as f: ostream.writeBytes(f.read()) self._repeater.submitRequestBlock(block) # start a start daemon timeout timer to detect daemon launched failed self._launchDaemonTimeoutTimer.start(3000) self._queryDeviceTimer.start() @pyqtSlot() def launchDaemonFailed(self): # what can I do with this situation print("daemon launched failed") def stopDaemon(self): block, ostream = self._repeater.getRequestBlock() ostream.writeQString('stopDaemon') self._repeater.submitRequestBlock(block)
class TextItem(QGraphicsTextItem): DispatchSignalDelay = 50 # milliseconds def __init__(self, text=None, new=True): super().__init__(text) if new: self._editable = None self._editColor = QColor(Qt.yellow) self._colorSave = None self.setEditable(False) self.setupConnections() self._constraints = [] self.setDefaultTextColor(QColor(Qt.darkMagenta)) self.onTextChanged = [] self.setFlags(self.ItemIsMovable | self.ItemIsFocusable | self.ItemIsSelectable | self.ItemSendsGeometryChanges) self.pressedEventHandler = None self.onPositionChanged = None self._uid = uuid4() self._dispatchTimer = None def setupConnections(self): self.keyPressed = KeyPressedSignal() self.keyPressed.connect(self.dispatchOnTextChanged) self.mouseDoubleClickHandler = lambda event: self.setTextInteraction( True, select_all=True) self.setTextInteractionFlags(Qt.NoTextInteraction) # HACKFIX: gets rid of single-click selection rect around text when it first appears. Now only shows selection rect when double-clicked: self.setTextInteraction(True) self.setTextInteraction(False) def dispatchOnTextChanged(self): if self._dispatchTimer: self._dispatchTimer.stop() self._dispatchTimer = QTimer() self._dispatchTimer.setInterval(self.DispatchSignalDelay) self._dispatchTimer.timeout.connect(self._dispatchOnTextChanged) self._dispatchTimer.setSingleShot(True) self._dispatchTimer.start() def _dispatchOnTextChanged(self): text = self.toPlainText() for slot in self.onTextChanged: slot(text) def uid(self): return self._uid def __setstate__(self, data): self.__init__(data['text']) self.setFlags(unpickleGfxItemFlags(data['flags'])) self.setDefaultTextColor(data['color']) self._undoStack = data['undo stack'] self.setPos(data['pos']) def __getstate__(self): return { 'text': self.toPlainText(), 'flags': int(self.flags()), 'color': self.defaultTextColor(), 'undo stack': self._undoStack, 'pos': self.pos(), } def __deepcopy__(self, memo): copy = type(self)(new=False) copy._editable = self._editable copy._editColor = QColor(self._editColor) copy._colorSave = QColor(self._colorSave) copy.setPlainText(self.toPlainText()) memo[id(self)] = copy copy.setFlags(self.flags()) copy.setDefaultTextColor(self.defaultTextColor()) copy._constraints = deepcopy(self._constraints, memo) copy.setPos(self.pos()) copy.setupConnections() return copy def keyPressEvent(self, event): self.keyPressed.emit() super().keyPressEvent(event) def setUndoStack(self): return self._undoStack def focusOutEvent(self, event): #self.setTextInteraction(False) super().focusOutEvent(event) def setPlainText(self, text): if self.toPlainText() != text: super().setPlainText(text) if self.onTextChanged: for slot in self.onTextChanged: slot(text) def setTextInteraction(self, state, select_all=True): text = self.toPlainText() if self.editable() and state and self.textInteractionFlags( ) == Qt.NoTextInteraction: # switch on editor mode: self.setTextInteractionFlags(Qt.TextEditorInteraction) # manually do what a mouse click would do else: self.setFocus(Qt.MouseFocusReason) # give the item keyboard focus self.setSelected( True ) # ensure itemChange() gets called when we click out of the item if select_all: # option to select the whole text (e.g. after creation of the TextItem) c = self.textCursor() c.select(QTextCursor.WordUnderCursor) self.setTextCursor(c) elif not state and self.textInteractionFlags( ) == Qt.TextEditorInteraction: # turn off editor mode: self.setTextInteractionFlags(Qt.NoTextInteraction) # deselect text (else it keeps gray shade): c = self.textCursor() c.clearSelection() self.setTextCursor(c) self.clearFocus() def mouseDoubleClickEvent(self, event): if self.mouseDoubleClickHandler: self.mouseDoubleClickHandler(event) else: super().mouseDoubleClickEvent(event) def mousePressEvent(self, event): if self.pressedEventHandler: self.pressedEventHandler(event) super().mousePressEvent(event) def itemChange(self, change, value): if change == self.ItemSelectedChange: if self.textInteractionFlags( ) != Qt.NoTextInteraction and not value: # item received SelectedChange event AND is in editor mode AND is about to be deselected self.setTextInteraction(False) # leave editor mode elif change == self.ItemPositionChange: if self.onPositionChanged: self.onPositionChanged() return super().itemChange(change, value) def paint(self, painter, option, widget): painter.setRenderHints(QPainter.HighQualityAntialiasing | QPainter.Antialiasing | QPainter.TextAntialiasing) super().paint(painter, option, widget) def setSelected(self, selected): super().setSelected(selected) if selected: if self.parentItem(): self.parentItem().setSelected(False) def delete(self, deleted=None): if deleted is None: deleted = {} deleted["parent"] = self.parentItem() deleted["scene"] = self.scene() self.setParentItem(None) if self.scene(): self.scene().removeItem(self) return deleted def undelete(self, deleted): self.setParentItem(deleted["parent"]) if deleted["scene"]: deleted["scene"].addItem(self) def setEditable(self, editable): if self._editable != editable: self._editable = editable if editable: self._colorSave = self.color() self.setColor(self._editColor) else: if self._colorSave: self.setColor(self._colorSave) self._colorSave = None self.update() def setEditColor(self, color): self._editColor = color self.update() def editable(self): return self._editable def setColor(self, color): self.setDefaultTextColor(color) def color(self): return self.defaultTextColor() def constraints(self): return self._constraints def isConstrained(self): return self._constraints != [] def addConstraint(self, constraint): self._constraints.append(constraint)
def export(self): timer = QTimer(self.view) timer.setSingleShot(True) timer.timeout.connect(self.run) timer.start()
class DownloadsPage(QWidget): """ This class is responsible for managing all items on the downloads page. The downloads page shows all downloads and specific details about a download. """ received_downloads = pyqtSignal(object) def __init__(self): QWidget.__init__(self) self.export_dir = None self.filter = DOWNLOADS_FILTER_ALL self.download_widgets = {} # key: infohash, value: QTreeWidgetItem self.downloads = None self.downloads_timer = QTimer() self.downloads_timeout_timer = QTimer() self.downloads_last_update = 0 self.selected_item = None self.dialog = None self.downloads_request_mgr = TriblerRequestManager() self.request_mgr = None def showEvent(self, QShowEvent): """ When the downloads tab is clicked, we want to update the downloads list immediately. """ super(DownloadsPage, self).showEvent(QShowEvent) self.stop_loading_downloads() self.schedule_downloads_timer(True) def initialize_downloads_page(self): self.window().downloads_tab.initialize() self.window().downloads_tab.clicked_tab_button.connect( self.on_downloads_tab_button_clicked) self.window().start_download_button.clicked.connect( self.on_start_download_clicked) self.window().stop_download_button.clicked.connect( self.on_stop_download_clicked) self.window().remove_download_button.clicked.connect( self.on_remove_download_clicked) self.window().play_download_button.clicked.connect( self.on_play_download_clicked) self.window().downloads_list.itemSelectionChanged.connect( self.on_download_item_clicked) self.window().downloads_list.customContextMenuRequested.connect( self.on_right_click_item) self.window().download_details_widget.initialize_details_widget() self.window().download_details_widget.hide() self.window().downloads_filter_input.textChanged.connect( self.on_filter_text_changed) self.window().downloads_list.header().resizeSection(12, 146) if not self.window().vlc_available: self.window().play_download_button.setHidden(True) def on_filter_text_changed(self, text): self.window().downloads_list.clearSelection() self.window().download_details_widget.hide() self.update_download_visibility() def start_loading_downloads(self): self.schedule_downloads_timer(now=True) def schedule_downloads_timer(self, now=False): self.downloads_timer = QTimer() self.downloads_timer.setSingleShot(True) self.downloads_timer.timeout.connect(self.load_downloads) self.downloads_timer.start(0 if now else 1000) self.downloads_timeout_timer = QTimer() self.downloads_timeout_timer.setSingleShot(True) self.downloads_timeout_timer.timeout.connect( self.on_downloads_request_timeout) self.downloads_timeout_timer.start(16000) def on_downloads_request_timeout(self): self.downloads_request_mgr.cancel_request() self.schedule_downloads_timer() def stop_loading_downloads(self): self.downloads_timer.stop() self.downloads_timeout_timer.stop() def load_downloads(self): url = "downloads?get_pieces=1" if self.window().download_details_widget.currentIndex() == 3: url += "&get_peers=1" elif self.window().download_details_widget.currentIndex() == 1: url += "&get_files=1" if not self.isHidden() or (time.time() - self.downloads_last_update > 30): # Update if the downloads page is visible or if we haven't updated for longer than 30 seconds self.downloads_last_update = time.time() priority = "LOW" if self.isHidden() else "HIGH" self.downloads_request_mgr.cancel_request() self.downloads_request_mgr = TriblerRequestManager() self.downloads_request_mgr.perform_request( url, self.on_received_downloads, priority=priority) def on_received_downloads(self, downloads): if not downloads: return # This might happen when closing Tribler total_download = 0 total_upload = 0 self.received_downloads.emit(downloads) self.downloads = downloads download_infohashes = set() items = [] for download in downloads["downloads"]: if download["infohash"] in self.download_widgets: item = self.download_widgets[download["infohash"]] else: item = DownloadWidgetItem() self.download_widgets[download["infohash"]] = item items.append(item) item.update_with_download(download) # Update video player with download info video_infohash = self.window().video_player_page.active_infohash if video_infohash != "" and download["infohash"] == video_infohash: self.window().video_player_page.update_with_download_info( download) total_download += download["speed_down"] total_upload += download["speed_up"] download_infohashes.add(download["infohash"]) if self.window().download_details_widget.current_download is not None and \ self.window().download_details_widget.current_download["infohash"] == download["infohash"]: self.window( ).download_details_widget.current_download = download self.window().download_details_widget.update_pages() self.window().downloads_list.addTopLevelItems(items) for item in items: self.window().downloads_list.setItemWidget(item, 2, item.bar_container) # Check whether there are download that should be removed for infohash, item in self.download_widgets.items(): if infohash not in download_infohashes: index = self.window().downloads_list.indexOfTopLevelItem(item) self.window().downloads_list.takeTopLevelItem(index) del self.download_widgets[infohash] if self.window().tray_icon: self.window().tray_icon.setToolTip( "Down: %s, Up: %s" % (format_speed(total_download), format_speed(total_upload))) self.update_download_visibility() self.schedule_downloads_timer() # Update the top download management button if we have a row selected if len(self.window().downloads_list.selectedItems()) > 0: self.on_download_item_clicked() def update_download_visibility(self): for i in range(self.window().downloads_list.topLevelItemCount()): item = self.window().downloads_list.topLevelItem(i) filter_match = self.window().downloads_filter_input.text().lower( ) in item.download_info["name"].lower() is_creditmining = item.download_info["credit_mining"] if self.filter == DOWNLOADS_FILTER_CREDITMINING: item.setHidden(not is_creditmining or not filter_match) else: item.setHidden(not item.get_raw_download_status() in DOWNLOADS_FILTER_DEFINITION[self.filter] or \ not filter_match or is_creditmining) def on_downloads_tab_button_clicked(self, button_name): if button_name == "downloads_all_button": self.filter = DOWNLOADS_FILTER_ALL elif button_name == "downloads_downloading_button": self.filter = DOWNLOADS_FILTER_DOWNLOADING elif button_name == "downloads_completed_button": self.filter = DOWNLOADS_FILTER_COMPLETED elif button_name == "downloads_active_button": self.filter = DOWNLOADS_FILTER_ACTIVE elif button_name == "downloads_inactive_button": self.filter = DOWNLOADS_FILTER_INACTIVE elif button_name == "downloads_creditmining_button": self.filter = DOWNLOADS_FILTER_CREDITMINING self.window().downloads_list.clearSelection() self.window().download_details_widget.hide() self.update_download_visibility() @staticmethod def start_download_enabled(download_widget): return download_widget.get_raw_download_status() == DLSTATUS_STOPPED @staticmethod def stop_download_enabled(download_widget): status = download_widget.get_raw_download_status() return status != DLSTATUS_STOPPED and status != DLSTATUS_STOPPED_ON_ERROR @staticmethod def force_recheck_download_enabled(download_widget): status = download_widget.get_raw_download_status() return status != DLSTATUS_METADATA and status != DLSTATUS_HASHCHECKING and status != DLSTATUS_WAITING4HASHCHECK def on_download_item_clicked(self): self.window().download_details_widget.show() if len(self.window().downloads_list.selectedItems()) == 0: self.window().play_download_button.setEnabled(False) self.window().remove_download_button.setEnabled(False) self.window().start_download_button.setEnabled(False) self.window().stop_download_button.setEnabled(False) return self.selected_item = self.window().downloads_list.selectedItems()[0] self.window().play_download_button.setEnabled(True) self.window().remove_download_button.setEnabled(True) self.window().start_download_button.setEnabled( DownloadsPage.start_download_enabled(self.selected_item)) self.window().stop_download_button.setEnabled( DownloadsPage.stop_download_enabled(self.selected_item)) self.window().download_details_widget.update_with_download( self.selected_item.download_info) def on_start_download_clicked(self): infohash = self.selected_item.download_info["infohash"] self.request_mgr = TriblerRequestManager() self.request_mgr.perform_request("downloads/%s" % infohash, self.on_download_resumed, method='PATCH', data="state=resume") def on_download_resumed(self, json_result): if json_result and 'modified' in json_result: self.selected_item.download_info['status'] = "DLSTATUS_DOWNLOADING" self.selected_item.update_item() self.on_download_item_clicked() def on_stop_download_clicked(self): infohash = self.selected_item.download_info["infohash"] self.request_mgr = TriblerRequestManager() self.request_mgr.perform_request("downloads/%s" % infohash, self.on_download_stopped, method='PATCH', data="state=stop") def on_play_download_clicked(self): self.window().left_menu_button_video_player.click() self.window().video_player_page.set_torrent_infohash( self.selected_item.download_info["infohash"]) self.window().left_menu_playlist.set_loading() def on_download_stopped(self, json_result): if json_result and "modified" in json_result: self.selected_item.download_info['status'] = "DLSTATUS_STOPPED" self.selected_item.update_item() self.on_download_item_clicked() def on_remove_download_clicked(self): self.dialog = ConfirmationDialog( self, "Remove download", "Are you sure you want to remove this download?", [('remove download', BUTTON_TYPE_NORMAL), ('remove download + data', BUTTON_TYPE_NORMAL), ('cancel', BUTTON_TYPE_CONFIRM)]) self.dialog.button_clicked.connect(self.on_remove_download_dialog) self.dialog.show() def on_remove_download_dialog(self, action): if action != 2: infohash = self.selected_item.download_info["infohash"] # Reset video player if necessary before doing the actual request if self.window().video_player_page.active_infohash == infohash: self.window().video_player_page.reset_player() self.request_mgr = TriblerRequestManager() self.request_mgr.perform_request("downloads/%s" % infohash, self.on_download_removed, method='DELETE', data="remove_data=%d" % action) self.dialog.setParent(None) self.dialog = None def on_download_removed(self, json_result): if json_result and "removed" in json_result: infohash = self.selected_item.download_info["infohash"] index = self.window().downloads_list.indexOfTopLevelItem( self.selected_item) self.window().downloads_list.takeTopLevelItem(index) if infohash in self.download_widgets: # Could have been removed already through API del self.download_widgets[infohash] self.window().download_details_widget.hide() def on_force_recheck_download(self): infohash = self.selected_item.download_info["infohash"] self.request_mgr = TriblerRequestManager() self.request_mgr.perform_request("downloads/%s" % infohash, self.on_forced_recheck, method='PATCH', data='state=recheck') def on_forced_recheck(self, result): if result and "modified" in result: self.selected_item.download_info[ 'status'] = "DLSTATUS_HASHCHECKING" self.selected_item.update_item() self.on_download_item_clicked() def change_anonymity(self, hops): infohash = self.selected_item.download_info["infohash"] self.request_mgr = TriblerRequestManager() self.request_mgr.perform_request("downloads/%s" % infohash, lambda _: None, method='PATCH', data='anon_hops=%d' % hops) def on_explore_files(self): path = os.path.normpath( os.path.join( self.window().tribler_settings['downloadconfig']['saveas'], self.selected_item.download_info["destination"])) QDesktopServices.openUrl(QUrl.fromLocalFile(path)) def on_export_download(self): self.export_dir = QFileDialog.getExistingDirectory( self, "Please select the destination directory", "", QFileDialog.ShowDirsOnly) if len(self.export_dir) > 0: # Show confirmation dialog where we specify the name of the file torrent_name = self.selected_item.download_info['name'] self.dialog = ConfirmationDialog( self, "Export torrent file", "Please enter the name of the torrent file:", [('SAVE', BUTTON_TYPE_NORMAL), ('CANCEL', BUTTON_TYPE_CONFIRM)], show_input=True) self.dialog.dialog_widget.dialog_input.setPlaceholderText( 'Torrent file name') self.dialog.dialog_widget.dialog_input.setText("%s.torrent" % torrent_name) self.dialog.dialog_widget.dialog_input.setFocus() self.dialog.button_clicked.connect( self.on_export_download_dialog_done) self.dialog.show() def on_export_download_dialog_done(self, action): if action == 0: filename = self.dialog.dialog_widget.dialog_input.text() self.request_mgr = TriblerRequestManager() self.request_mgr.download_file( "downloads/%s/torrent" % self.selected_item.download_info['infohash'], lambda data: self.on_export_download_request_done( filename, data)) self.dialog.setParent(None) self.dialog = None def on_export_download_request_done(self, filename, data): dest_path = os.path.join(self.export_dir, filename) try: torrent_file = open(dest_path, "wb") torrent_file.write(data) torrent_file.close() except IOError as exc: ConfirmationDialog.show_error( self.window(), "Error when exporting file", "An error occurred when exporting the torrent file: %s" % str(exc)) else: if self.window().tray_icon: self.window().tray_icon.showMessage( "Torrent file exported", "Torrent file exported to %s" % dest_path) def on_right_click_item(self, pos): item_clicked = self.window().downloads_list.itemAt(pos) if not item_clicked: return self.selected_item = item_clicked menu = TriblerActionMenu(self) start_action = QAction('Start', self) stop_action = QAction('Stop', self) remove_download_action = QAction('Remove download', self) force_recheck_action = QAction('Force recheck', self) export_download_action = QAction('Export .torrent file', self) explore_files_action = QAction('Explore files', self) no_anon_action = QAction('No anonymity', self) one_hop_anon_action = QAction('One hop', self) two_hop_anon_action = QAction('Two hops', self) three_hop_anon_action = QAction('Three hops', self) start_action.triggered.connect(self.on_start_download_clicked) start_action.setEnabled( DownloadsPage.start_download_enabled(self.selected_item)) stop_action.triggered.connect(self.on_stop_download_clicked) stop_action.setEnabled( DownloadsPage.stop_download_enabled(self.selected_item)) remove_download_action.triggered.connect( self.on_remove_download_clicked) force_recheck_action.triggered.connect(self.on_force_recheck_download) force_recheck_action.setEnabled( DownloadsPage.force_recheck_download_enabled(self.selected_item)) export_download_action.triggered.connect(self.on_export_download) explore_files_action.triggered.connect(self.on_explore_files) no_anon_action.triggered.connect(lambda: self.change_anonymity(0)) one_hop_anon_action.triggered.connect(lambda: self.change_anonymity(1)) two_hop_anon_action.triggered.connect(lambda: self.change_anonymity(2)) three_hop_anon_action.triggered.connect( lambda: self.change_anonymity(3)) menu.addAction(start_action) menu.addAction(stop_action) if self.window().vlc_available: play_action = QAction('Play', self) play_action.triggered.connect(self.on_play_download_clicked) menu.addAction(play_action) menu.addSeparator() menu.addAction(remove_download_action) menu.addSeparator() menu.addAction(force_recheck_action) menu.addSeparator() menu.addAction(export_download_action) menu.addSeparator() menu_anon_level = menu.addMenu("Change anonymity") menu_anon_level.addAction(no_anon_action) menu_anon_level.addAction(one_hop_anon_action) menu_anon_level.addAction(two_hop_anon_action) menu_anon_level.addAction(three_hop_anon_action) menu.addAction(explore_files_action) menu.exec_(self.window().downloads_list.mapToGlobal(pos))
class CloudOutputDeviceManager: META_CLUSTER_ID = "um_cloud_cluster_id" META_NETWORK_KEY = "um_network_key" # The interval with which the remote clusters are checked CHECK_CLUSTER_INTERVAL = 30.0 # seconds # The translation catalog for this device. I18N_CATALOG = i18nCatalog("cura") # Signal emitted when the list of discovered devices changed. discoveredDevicesChanged = Signal() def __init__(self) -> None: # Persistent dict containing the remote clusters for the authenticated user. self._remote_clusters = {} # type: Dict[str, CloudOutputDevice] self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account self._api = CloudApiClient(self._account, on_error = lambda error: Logger.log("e", str(error))) self._account.loginStateChanged.connect(self._onLoginStateChanged) # Create a timer to update the remote cluster list self._update_timer = QTimer() self._update_timer.setInterval(int(self.CHECK_CLUSTER_INTERVAL * 1000)) self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._getRemoteClusters) # Ensure we don't start twice. self._running = False ## Starts running the cloud output device manager, thus periodically requesting cloud data. def start(self): if self._running: return if not self._account.isLoggedIn: return self._running = True if not self._update_timer.isActive(): self._update_timer.start() self._getRemoteClusters() ## Stops running the cloud output device manager. def stop(self): if not self._running: return self._running = False if self._update_timer.isActive(): self._update_timer.stop() self._onGetRemoteClustersFinished([]) # Make sure we remove all cloud output devices. ## Force refreshing connections. def refreshConnections(self) -> None: self._connectToActiveMachine() ## Called when the uses logs in or out def _onLoginStateChanged(self, is_logged_in: bool) -> None: if is_logged_in: self.start() else: self.stop() ## Gets all remote clusters from the API. def _getRemoteClusters(self) -> None: self._api.getClusters(self._onGetRemoteClustersFinished) ## Callback for when the request for getting the clusters is finished. def _onGetRemoteClustersFinished(self, clusters: List[CloudClusterResponse]) -> None: online_clusters = {c.cluster_id: c for c in clusters if c.is_online} # type: Dict[str, CloudClusterResponse] for device_id, cluster_data in online_clusters.items(): if device_id not in self._remote_clusters: self._onDeviceDiscovered(cluster_data) else: self._onDiscoveredDeviceUpdated(cluster_data) removed_device_keys = set(self._remote_clusters.keys()) - set(online_clusters.keys()) for device_id in removed_device_keys: self._onDiscoveredDeviceRemoved(device_id) def _onDeviceDiscovered(self, cluster_data: CloudClusterResponse) -> None: device = CloudOutputDevice(self._api, cluster_data) CuraApplication.getInstance().getDiscoveredPrintersModel().addDiscoveredPrinter( ip_address=device.key, key=device.getId(), name=device.getName(), create_callback=self._createMachineFromDiscoveredDevice, machine_type=device.printerType, device=device ) self._remote_clusters[device.getId()] = device self.discoveredDevicesChanged.emit() self._connectToActiveMachine() def _onDiscoveredDeviceUpdated(self, cluster_data: CloudClusterResponse) -> None: device = self._remote_clusters.get(cluster_data.cluster_id) if not device: return CuraApplication.getInstance().getDiscoveredPrintersModel().updateDiscoveredPrinter( ip_address=device.key, name=cluster_data.friendly_name, machine_type=device.printerType ) self.discoveredDevicesChanged.emit() def _onDiscoveredDeviceRemoved(self, device_id: str) -> None: device = self._remote_clusters.pop(device_id, None) # type: Optional[CloudOutputDevice] if not device: return device.close() CuraApplication.getInstance().getDiscoveredPrintersModel().removeDiscoveredPrinter(device.key) output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() if device.key in output_device_manager.getOutputDeviceIds(): output_device_manager.removeOutputDevice(device.key) self.discoveredDevicesChanged.emit() def _createMachineFromDiscoveredDevice(self, key: str) -> None: device = self._remote_clusters[key] if not device: return # Create a new machine and activate it. # We do not use use MachineManager.addMachine here because we need to set the cluster ID before activating it. new_machine = CuraStackBuilder.createMachine(device.name, device.printerType) if not new_machine: Logger.log("e", "Failed creating a new machine") return new_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) CuraApplication.getInstance().getMachineManager().setActiveMachine(new_machine.getId()) self._connectToOutputDevice(device, new_machine) ## Callback for when the active machine was changed by the user or a new remote cluster was found. def _connectToActiveMachine(self) -> None: active_machine = CuraApplication.getInstance().getGlobalContainerStack() if not active_machine: return output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() stored_cluster_id = active_machine.getMetaDataEntry(self.META_CLUSTER_ID) local_network_key = active_machine.getMetaDataEntry(self.META_NETWORK_KEY) for device in self._remote_clusters.values(): if device.key == stored_cluster_id: # Connect to it if the stored ID matches. self._connectToOutputDevice(device, active_machine) elif local_network_key and device.matchesNetworkKey(local_network_key): # Connect to it if we can match the local network key that was already present. self._connectToOutputDevice(device, active_machine) elif device.key in output_device_manager.getOutputDeviceIds(): # Remove device if it is not meant for the active machine. output_device_manager.removeOutputDevice(device.key) ## Connects to an output device and makes sure it is registered in the output device manager. def _connectToOutputDevice(self, device: CloudOutputDevice, machine: GlobalStack) -> None: machine.setName(device.name) machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) machine.setMetaDataEntry("group_name", device.name) machine.addConfiguredConnectionType(device.connectionType.value) if not device.isConnected(): device.connect() output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() if device.key not in output_device_manager.getOutputDeviceIds(): output_device_manager.addOutputDevice(device)
class Completer(QObject): """Completer which manages completions in a CompletionView. Attributes: _cmd: The statusbar Command object this completer belongs to. _ignore_change: Whether to ignore the next completion update. _win_id: The window ID this completer is in. _timer: The timer used to trigger the completion update. _cursor_part: The cursor part index for the next completion update. _last_cursor_pos: The old cursor position so we avoid double completion updates. _last_text: The old command text so we avoid double completion updates. """ def __init__(self, cmd, win_id, parent=None): super().__init__(parent) self._win_id = win_id self._cmd = cmd self._cmd.update_completion.connect(self.schedule_completion_update) self._cmd.textEdited.connect(self.on_text_edited) self._ignore_change = False self._empty_item_idx = None self._timer = QTimer() self._timer.setSingleShot(True) self._timer.setInterval(0) self._timer.timeout.connect(self.update_completion) self._cursor_part = None self._last_cursor_pos = None self._last_text = None def __repr__(self): return utils.get_repr(self) def _model(self): """Convienience method to get the current completion model.""" completion = objreg.get('completion', scope='window', window=self._win_id) return completion.model() def _get_completion_model(self, completion, parts, cursor_part): """Get a completion model based on an enum member. Args: completion: An usertypes.Completion member. parts: The parts currently in the commandline. cursor_part: The part the cursor is in. Return: A completion model. """ if completion == usertypes.Completion.option: section = parts[cursor_part - 1] model = instances.get(completion).get(section) elif completion == usertypes.Completion.value: section = parts[cursor_part - 2] option = parts[cursor_part - 1] try: model = instances.get(completion)[section][option] except KeyError: # No completion model for this section/option. model = None else: model = instances.get(completion) return model def _filter_cmdline_parts(self, parts, cursor_part): """Filter a list of commandline parts to exclude flags. Args: parts: A list of parts. cursor_part: The index of the part the cursor is over. Return: A (parts, cursor_part) tuple with the modified values. """ if parts == ['']: # Empty commandline, i.e. only :. return [''], 0 filtered_parts = [] for i, part in enumerate(parts): if part == '--': break elif part.startswith('-'): if cursor_part >= i: cursor_part -= 1 else: filtered_parts.append(part) return filtered_parts, cursor_part def _get_new_completion(self, parts, cursor_part): """Get a new completion. Args: parts: The command chunks to get a completion for. cursor_part: The part the cursor is over currently. Return: A completion model. """ try: if parts[cursor_part].startswith('-'): # cursor on a flag return except IndexError: pass log.completion.debug("Before filtering flags: parts {}, cursor_part " "{}".format(parts, cursor_part)) parts, cursor_part = self._filter_cmdline_parts(parts, cursor_part) log.completion.debug("After filtering flags: parts {}, cursor_part " "{}".format(parts, cursor_part)) if cursor_part == 0: # '|' or 'set|' return instances.get(usertypes.Completion.command) # delegate completion to command try: completions = cmdutils.cmd_dict[parts[0]].completion except KeyError: # entering an unknown command return None if completions is None: # command without any available completions return None dbg_completions = [c.name for c in completions] try: idx = cursor_part - 1 completion = completions[idx] except IndexError: # More arguments than completions log.completion.debug("completions: {}".format( ', '.join(dbg_completions))) return None dbg_completions[idx] = '*' + dbg_completions[idx] + '*' log.completion.debug("completions: {}".format( ', '.join(dbg_completions))) model = self._get_completion_model(completion, parts, cursor_part) return model def _quote(self, s): """Quote s if it needs quoting for the commandline. Note we don't use shlex.quote because that quotes a lot of shell metachars we don't need to have quoted. """ if not s: return "''" elif any(c in s for c in ' \'\t\n\\'): # use single quotes, and put single quotes into double quotes # the string $'b is then quoted as '$'"'"'b' return "'" + s.replace("'", "'\"'\"'") + "'" else: return s def selection_changed(self, selected, _deselected): """Change the completed part if a new item was selected. Called from the views selectionChanged method. Args: selected: New selection. _delected: Previous selection. """ indexes = selected.indexes() if not indexes: return model = self._model() data = model.data(indexes[0]) if data is None: return parts = self.split() try: needs_quoting = cmdutils.cmd_dict[parts[0]].maxsplit is None except KeyError: needs_quoting = True if needs_quoting: data = self._quote(data) if model.count() == 1 and config.get('completion', 'quick-complete'): # If we only have one item, we want to apply it immediately # and go on to the next part. self.change_completed_part(data, immediate=True) else: log.completion.debug("Will ignore next completion update.") self._ignore_change = True self.change_completed_part(data) @pyqtSlot() def schedule_completion_update(self): """Schedule updating/enabling completion. For performance reasons we don't want to block here, instead we do this in the background. """ if (self._cmd.cursorPosition() == self._last_cursor_pos and self._cmd.text() == self._last_text): log.completion.debug("Ignoring update because there were no " "changes.") else: log.completion.debug("Scheduling completion update.") self._timer.start() self._last_cursor_pos = self._cmd.cursorPosition() self._last_text = self._cmd.text() @pyqtSlot() def update_completion(self): """Check if completions are available and activate them.""" self.update_cursor_part() parts = self.split() log.completion.debug( "Updating completion - prefix {}, parts {}, cursor_part {}".format( self._cmd.prefix(), parts, self._cursor_part)) if self._ignore_change: log.completion.debug("Ignoring completion update because " "ignore_change is True.") self._ignore_change = False return completion = objreg.get('completion', scope='window', window=self._win_id) if self._cmd.prefix() != ':': # This is a search or gibberish, so we don't need to complete # anything (yet) # FIXME complete searches # https://github.com/The-Compiler/qutebrowser/issues/32 completion.hide() return model = self._get_new_completion(parts, self._cursor_part) if model != self._model(): if model is None: completion.hide() else: completion.set_model(model) if model is None: log.completion.debug("No completion model for {}.".format(parts)) return try: pattern = parts[self._cursor_part].strip() except IndexError: pattern = '' self._model().set_pattern(pattern) log.completion.debug( "New completion for {}: {}, with pattern '{}'".format( parts, model.srcmodel.__class__.__name__, pattern)) if self._model().count() == 0: completion.hide() return if completion.enabled: completion.show() def split(self, keep=False, aliases=False): """Get the text split up in parts. Args: keep: Whether to keep special chars and whitespace. aliases: Whether to resolve aliases. """ text = self._cmd.text()[len(self._cmd.prefix()):] if not text: # When only ":" is entered, we already have one imaginary part, # which just is empty at the moment. return [''] if not text.strip(): # Text is only whitespace so we treat this as a single element with # the whitespace. return [text] runner = runners.CommandRunner(self._win_id) result = runner.parse(text, fallback=True, aliases=aliases, keep=keep) parts = result.cmdline if self._empty_item_idx is not None: log.completion.debug("Empty element queued at {}, " "inserting.".format(self._empty_item_idx)) parts.insert(self._empty_item_idx, '') #log.completion.debug("Splitting '{}' -> {}".format(text, parts)) return parts @pyqtSlot() def update_cursor_part(self): """Get the part index of the commandline where the cursor is over.""" cursor_pos = self._cmd.cursorPosition() snippet = slice(cursor_pos - 1, cursor_pos + 1) if self._cmd.text()[snippet] == ' ': spaces = True else: spaces = False cursor_pos -= len(self._cmd.prefix()) parts = self.split(keep=True) log.completion.vdebug( "text: {}, parts: {}, cursor_pos after removing prefix '{}': " "{}".format(self._cmd.text(), parts, self._cmd.prefix(), cursor_pos)) skip = 0 for i, part in enumerate(parts): log.completion.vdebug("Checking part {}: {}".format(i, parts[i])) if not part: skip += 1 continue if cursor_pos <= len(part): # foo| bar self._cursor_part = i - skip if spaces: self._empty_item_idx = i - skip else: self._empty_item_idx = None log.completion.vdebug("cursor_pos {} <= len(part) {}, " "setting cursor_part {} - {} (skip), " "empty_item_idx {}".format( cursor_pos, len(part), i, skip, self._empty_item_idx)) break cursor_pos -= len(part) log.completion.vdebug( "Removing len({!r}) -> {} from cursor_pos -> {}".format( part, len(part), cursor_pos)) else: self._cursor_part = i - skip if spaces: self._empty_item_idx = i - skip else: self._empty_item_idx = None log.completion.debug("cursor_part {}, spaces {}".format( self._cursor_part, spaces)) return def change_completed_part(self, newtext, immediate=False): """Change the part we're currently completing in the commandline. Args: text: The text to set (string). immediate: True if the text should be completed immediately including a trailing space and we shouldn't continue completing the current item. """ parts = self.split() log.completion.debug("changing part {} to '{}'".format( self._cursor_part, newtext)) try: parts[self._cursor_part] = newtext except IndexError: parts.append(newtext) # We want to place the cursor directly after the part we just changed. cursor_str = self._cmd.prefix() + ' '.join( parts[:self._cursor_part + 1]) if immediate: # If we should complete immediately, we want to move the cursor by # one more char, to get to the next field. cursor_str += ' ' text = self._cmd.prefix() + ' '.join(parts) if immediate and self._cursor_part == len(parts) - 1: # If we should complete immediately and we're completing the last # part in the commandline, we automatically add a space. text += ' ' self._cmd.setText(text) log.completion.debug("Placing cursor after '{}'".format(cursor_str)) log.modes.debug("Completion triggered, focusing {!r}".format(self)) self._cmd.setCursorPosition(len(cursor_str)) self._cmd.setFocus() self._cmd.show_cmd.emit() @pyqtSlot() def on_text_edited(self): """Reset _empty_item_idx if text was edited.""" self._empty_item_idx = None
class BubblesWidget(BaseClass): """BubblesWidget(BaseClass) Provides a custom widget that shows a number of rising bubbles. Various properties are defined so that the user can customize the appearance of the widget, and change the number and behaviour of the bubbles shown. """ # We define two signals that are used to indicate changes to the status # of the widget. bubbleLeft = pyqtSignal() bubblesRemaining = pyqtSignal(int) def __init__(self, parent=None): super(BubblesWidget, self).__init__(parent) self.pen = QPen(QColor("#cccccc")) self.bubbles = [] self.backgroundColor1 = self.randomColor() self.backgroundColor2 = self.randomColor().darker(150) self.newBubble = None random.seed() self.animation_timer = QTimer(self) self.animation_timer.setSingleShot(False) self.animation_timer.timeout.connect(self.animate) self.animation_timer.start(25) self.bubbleTimer = QTimer() self.bubbleTimer.setSingleShot(False) self.bubbleTimer.timeout.connect(self.expandBubble) self.setMouseTracking(True) self.setMinimumSize(QSize(200, 200)) self.setWindowTitle("Bubble Maker") def paintEvent(self, event): background = QRadialGradient(QPointF(self.rect().topLeft()), 500, QPointF(self.rect().bottomRight())) background.setColorAt(0, self.backgroundColor1) background.setColorAt(1, self.backgroundColor2) painter = QPainter() painter.begin(self) painter.setRenderHint(QPainter.Antialiasing) painter.fillRect(event.rect(), QBrush(background)) painter.setPen(self.pen) for bubble in self.bubbles: if QRectF( bubble.position - QPointF(bubble.radius, bubble.radius), QSizeF(2 * bubble.radius, 2 * bubble.radius), ).intersects(QRectF(event.rect())): bubble.drawBubble(painter) if self.newBubble: self.newBubble.drawBubble(painter) painter.end() def mousePressEvent(self, event): if event.button() == Qt.LeftButton and self.newBubble is None: self.newBubble = Bubble( QPointF(event.pos()), 4.0, 1.0 + random.random() * 7, self.randomColor(), self.randomColor(), ) self.bubbleTimer.start(50) event.accept() def mouseMoveEvent(self, event): if self.newBubble: self.update( QRectF( self.newBubble.position - QPointF( self.newBubble.radius + 1, self.newBubble.radius + 1), QSizeF(2 * self.newBubble.radius + 2, 2 * self.newBubble.radius + 2), ).toRect()) self.newBubble.position = QPointF(event.pos()) self.update( QRectF( self.newBubble.position - QPointF( self.newBubble.radius + 1, self.newBubble.radius + 1), QSizeF(2 * self.newBubble.radius + 2, 2 * self.newBubble.radius + 2), ).toRect()) event.accept() def mouseReleaseEvent(self, event): if self.newBubble: self.bubbles.append(self.newBubble) self.newBubble = None self.bubbleTimer.stop() self.bubblesRemaining.emit(len(self.bubbles)) event.accept() def expandBubble(self): if self.newBubble: self.newBubble.radius = min(self.newBubble.radius + 4.0, self.width() / 8.0, self.height() / 8.0) self.update( QRectF( self.newBubble.position - QPointF( self.newBubble.radius + 1, self.newBubble.radius + 1), QSizeF(2 * self.newBubble.radius + 2, 2 * self.newBubble.radius + 2), ).toRect()) self.newBubble.updateBrush() def randomColor(self): red = 205 + random.random() * 50 green = 205 + random.random() * 50 blue = 205 + random.random() * 50 alpha = 91 + random.random() * 100 return QColor(red, green, blue, alpha) def animate(self): bubbles = [] left = False for bubble in self.bubbles: bubble.position = bubble.position + QPointF(0, -bubble.speed) self.update( QRectF( bubble.position - QPointF(bubble.radius + 1, bubble.radius + 1), QSizeF(2 * bubble.radius + 2, 2 * bubble.radius + 2 + bubble.speed), ).toRect()) if bubble.position.y() + bubble.radius > 0: bubbles.append(bubble) else: self.bubbleLeft.emit() left = True if self.newBubble: self.update( QRectF( self.newBubble.position - QPointF( self.newBubble.radius + 1, self.newBubble.radius + 1), QSizeF(2 * self.newBubble.radius + 2, 2 * self.newBubble.radius + 2), ).toRect()) self.bubbles = bubbles if left: self.bubblesRemaining.emit(len(self.bubbles)) def sizeHint(self): return QSize(200, 200) # We provide getter and setter methods for the numberOfBubbles property. def getBubbles(self): return len(self.bubbles) # The setBubbles() method can also be used as a slot. @pyqtSlot(int) def setBubbles(self, value): value = max(0, value) while len(self.bubbles) < value: newBubble = Bubble( QPointF(random.random() * self.width(), random.random() * self.height()), 4.0 + random.random() * 20, 1.0 + random.random() * 7, self.randomColor(), self.randomColor(), ) newBubble.updateBrush() self.bubbles.append(newBubble) self.bubbles = self.bubbles[:value] self.bubblesRemaining.emit(value) self.update() numberOfBubbles = pyqtProperty(int, getBubbles, setBubbles) # We provide getter and setter methods for the color1 and color2 # properties. The red, green and blue components for the QColor # values stored in these properties can be edited individually in # Qt Designer. def getColor1(self): return self.backgroundColor1 def setColor1(self, value): self.backgroundColor1 = QColor(value) self.update() color1 = pyqtProperty(QColor, getColor1, setColor1) def getColor2(self): return self.backgroundColor2 def setColor2(self, value): self.backgroundColor2 = QColor(value) self.update() color2 = pyqtProperty(QColor, getColor2, setColor2) # The stop() and start() slots provide simple control over the animation # of the bubbles in the widget. @pyqtSlot() def stop(self): self.animation_timer.stop() @pyqtSlot() def start(self): self.animation_timer.start(25)
class ExtrudersModel(ListModel): """Model that holds extruders. This model is designed for use by any list of extruders, but specifically intended for drop-down lists of the current machine's extruders in place of settings. """ # The ID of the container stack for the extruder. IdRole = Qt.UserRole + 1 NameRole = Qt.UserRole + 2 """Human-readable name of the extruder.""" ColorRole = Qt.UserRole + 3 """Colour of the material loaded in the extruder.""" IndexRole = Qt.UserRole + 4 """Index of the extruder, which is also the value of the setting itself. An index of 0 indicates the first extruder, an index of 1 the second one, and so on. This is the value that will be saved in instance containers. """ # The ID of the definition of the extruder. DefinitionRole = Qt.UserRole + 5 # The material of the extruder. MaterialRole = Qt.UserRole + 6 # The variant of the extruder. VariantRole = Qt.UserRole + 7 StackRole = Qt.UserRole + 8 MaterialBrandRole = Qt.UserRole + 9 ColorNameRole = Qt.UserRole + 10 EnabledRole = Qt.UserRole + 11 """Is the extruder enabled?""" MaterialTypeRole = Qt.UserRole + 12 """The type of the material (e.g. PLA, ABS, PETG, etc.).""" defaultColors = [ "#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8" ] """List of colours to display if there is no material or the material has no known colour. """ def __init__(self, parent=None): """Initialises the extruders model, defining the roles and listening for changes in the data. :param parent: Parent QtObject of this list. """ super().__init__(parent) self.addRoleName(self.IdRole, "id") self.addRoleName(self.NameRole, "name") self.addRoleName(self.EnabledRole, "enabled") self.addRoleName(self.ColorRole, "color") self.addRoleName(self.IndexRole, "index") self.addRoleName(self.DefinitionRole, "definition") self.addRoleName(self.MaterialRole, "material") self.addRoleName(self.VariantRole, "variant") self.addRoleName(self.StackRole, "stack") self.addRoleName(self.MaterialBrandRole, "material_brand") self.addRoleName(self.ColorNameRole, "color_name") self.addRoleName(self.MaterialTypeRole, "material_type") self._update_extruder_timer = QTimer() self._update_extruder_timer.setInterval(100) self._update_extruder_timer.setSingleShot(True) self._update_extruder_timer.timeout.connect(self.__updateExtruders) self._active_machine_extruders = [] # type: Iterable[ExtruderStack] self._add_optional_extruder = False # Listen to changes Application.getInstance().globalContainerStackChanged.connect( self._extrudersChanged ) # When the machine is swapped we must update the active machine extruders Application.getInstance( ).getExtruderManager().extrudersChanged.connect( self._extrudersChanged ) # When the extruders change we must link to the stack-changed signal of the new extruder Application.getInstance( ).getContainerRegistry().containerMetaDataChanged.connect( self._onExtruderStackContainersChanged ) # When meta data from a material container changes we must update self._extrudersChanged() # Also calls _updateExtruders addOptionalExtruderChanged = pyqtSignal() def setAddOptionalExtruder(self, add_optional_extruder): if add_optional_extruder != self._add_optional_extruder: self._add_optional_extruder = add_optional_extruder self.addOptionalExtruderChanged.emit() self._updateExtruders() @pyqtProperty(bool, fset=setAddOptionalExtruder, notify=addOptionalExtruderChanged) def addOptionalExtruder(self): return self._add_optional_extruder def _extrudersChanged(self, machine_id=None): """Links to the stack-changed signal of the new extruders when an extruder is swapped out or added in the current machine. :param machine_id: The machine for which the extruders changed. This is filled by the ExtruderManager.extrudersChanged signal when coming from that signal. Application.globalContainerStackChanged doesn't fill this signal; it's assumed to be the current printer in that case. """ machine_manager = Application.getInstance().getMachineManager() if machine_id is not None: if machine_manager.activeMachine is None: # No machine, don't need to update the current machine's extruders return if machine_id != machine_manager.activeMachine.getId(): # Not the current machine return # Unlink from old extruders for extruder in self._active_machine_extruders: extruder.containersChanged.disconnect( self._onExtruderStackContainersChanged) extruder.enabledChanged.disconnect(self._updateExtruders) # Link to new extruders self._active_machine_extruders = [] extruder_manager = Application.getInstance().getExtruderManager() for extruder in extruder_manager.getActiveExtruderStacks(): if extruder is None: #This extruder wasn't loaded yet. This happens asynchronously while this model is constructed from QML. continue extruder.containersChanged.connect( self._onExtruderStackContainersChanged) extruder.enabledChanged.connect(self._updateExtruders) self._active_machine_extruders.append(extruder) self._updateExtruders( ) # Since the new extruders may have different properties, update our own model. def _onExtruderStackContainersChanged(self, container): # Update when there is an empty container or material or variant change if container.getMetaDataEntry("type") in ["material", "variant", None]: # The ExtrudersModel needs to be updated when the material-name or -color changes, because the user identifies extruders by material-name self._updateExtruders() modelChanged = pyqtSignal() def _updateExtruders(self): self._update_extruder_timer.start() @UM.FlameProfiler.profile def __updateExtruders(self): """Update the list of extruders. This should be called whenever the list of extruders changes. """ extruders_changed = False if self.count != 0: extruders_changed = True items = [] global_container_stack = Application.getInstance( ).getGlobalContainerStack() if global_container_stack: # get machine extruder count for verification machine_extruder_count = global_container_stack.getProperty( "machine_extruder_count", "value") for extruder in Application.getInstance().getExtruderManager( ).getActiveExtruderStacks(): position = extruder.getMetaDataEntry("position", default="0") try: position = int(position) except ValueError: # Not a proper int. position = -1 if position >= machine_extruder_count: continue default_color = self.defaultColors[ position] if 0 <= position < len( self.defaultColors) else self.defaultColors[0] color = extruder.material.getMetaDataEntry( "color_code", default=default_color ) if extruder.material else default_color material_brand = extruder.material.getMetaDataEntry( "brand", default="generic") color_name = extruder.material.getMetaDataEntry("color_name") # construct an item with only the relevant information item = { "id": extruder.getId(), "name": extruder.getName(), "enabled": extruder.isEnabled, "color": color, "index": position, "definition": extruder.getBottom().getId(), "material": extruder.material.getName() if extruder.material else "", "variant": extruder.variant.getName() if extruder.variant else "", # e.g. print core "stack": extruder, "material_brand": material_brand, "color_name": color_name, "material_type": extruder.material.getMetaDataEntry("material") if extruder.material else "", } items.append(item) extruders_changed = True if extruders_changed: # sort by extruder index items.sort(key=lambda i: i["index"]) # We need optional extruder to be last, so add it after we do sorting. # This way we can simply interpret the -1 of the index as the last item (which it now always is) if self._add_optional_extruder: item = { "id": "", "name": catalog.i18nc("@menuitem", "Not overridden"), "enabled": True, "color": "transparent", "index": -1, "definition": "", "material": "", "variant": "", "stack": None, "material_brand": "", "color_name": "", "material_type": "", } items.append(item) if self._items != items: self.setItems(items) self.modelChanged.emit()
class CuraEngineBackend(QObject, Backend): backendError = Signal() ## Starts the back-end plug-in. # # This registers all the signal listeners and prepares for communication # with the back-end in general. # CuraEngineBackend is exposed to qml as well. def __init__(self) -> None: super().__init__() # Find out where the engine is located, and how it is called. # This depends on how Cura is packaged and which OS we are running on. executable_name = "CuraEngine" if Platform.isWindows(): executable_name += ".exe" default_engine_location = executable_name if os.path.exists( os.path.join(CuraApplication.getInstallPrefix(), "bin", executable_name)): default_engine_location = os.path.join( CuraApplication.getInstallPrefix(), "bin", executable_name) if hasattr(sys, "frozen"): default_engine_location = os.path.join( os.path.dirname(os.path.abspath(sys.executable)), executable_name) if Platform.isLinux() and not default_engine_location: if not os.getenv("PATH"): raise OSError( "There is something wrong with your Linux installation.") for pathdir in cast(str, os.getenv("PATH")).split(os.pathsep): execpath = os.path.join(pathdir, executable_name) if os.path.exists(execpath): default_engine_location = execpath break self._application = CuraApplication.getInstance( ) #type: CuraApplication self._multi_build_plate_model = None #type: Optional[MultiBuildPlateModel] self._machine_error_checker = None #type: Optional[MachineErrorChecker] if not default_engine_location: raise EnvironmentError("Could not find CuraEngine") Logger.log("i", "Found CuraEngine at: %s", default_engine_location) default_engine_location = os.path.abspath(default_engine_location) self._application.getPreferences().addPreference( "backend/location", default_engine_location) # Workaround to disable layer view processing if layer view is not active. self._layer_view_active = False #type: bool self._onActiveViewChanged() self._stored_layer_data = [] # type: List[Arcus.PythonMessage] self._stored_optimized_layer_data = { } # type: Dict[int, List[Arcus.PythonMessage]] # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob self._scene = self._application.getController().getScene( ) #type: Scene self._scene.sceneChanged.connect(self._onSceneChanged) # Triggers for auto-slicing. Auto-slicing is triggered as follows: # - auto-slicing is started with a timer # - whenever there is a value change, we start the timer # - sometimes an error check can get scheduled for a value change, in that case, we ONLY want to start the # auto-slicing timer when that error check is finished # If there is an error check, stop the auto-slicing timer, and only wait for the error check to be finished # to start the auto-slicing timer again. # self._global_container_stack = None #type: Optional[ContainerStack] # Listeners for receiving messages from the back-end. self._message_handlers["cura.proto.Layer"] = self._onLayerMessage self._message_handlers[ "cura.proto.LayerOptimized"] = self._onOptimizedLayerMessage self._message_handlers["cura.proto.Progress"] = self._onProgressMessage self._message_handlers[ "cura.proto.GCodeLayer"] = self._onGCodeLayerMessage self._message_handlers[ "cura.proto.GCodePrefix"] = self._onGCodePrefixMessage self._message_handlers[ "cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates self._message_handlers[ "cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage self._start_slice_job = None #type: Optional[StartSliceJob] self._start_slice_job_build_plate = None #type: Optional[int] self._slicing = False #type: bool # Are we currently slicing? self._restart = False #type: bool # Back-end is currently restarting? self._tool_active = False #type: bool # If a tool is active, some tasks do not have to do anything self._always_restart = True #type: bool # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness. self._process_layers_job = None #type: Optional[ProcessSlicedLayersJob] # The currently active job to process layers, or None if it is not processing layers. self._build_plates_to_be_sliced = [ ] #type: List[int] # what needs slicing? self._engine_is_fresh = True #type: bool # Is the newly started engine used before or not? self._backend_log_max_lines = 20000 #type: int # Maximum number of lines to buffer self._error_message = None #type: Optional[Message] # Pop-up message that shows errors. self._last_num_objects = defaultdict( int ) #type: Dict[int, int] # Count number of objects to see if there is something changed self._postponed_scene_change_sources = [ ] #type: List[SceneNode] # scene change is postponed (by a tool) self._slice_start_time = None #type: Optional[float] self._is_disabled = False #type: bool self._application.getPreferences().addPreference( "general/auto_slice", False) self._use_timer = False #type: bool # When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired. # This timer will group them up, and only slice for the last setting changed signal. # TODO: Properly group propertyChanged signals by whether they are triggered by the same user interaction. self._change_timer = QTimer() #type: QTimer self._change_timer.setSingleShot(True) self._change_timer.setInterval(500) self.determineAutoSlicing() self._application.getPreferences().preferenceChanged.connect( self._onPreferencesChanged) self._application.initializationFinished.connect(self.initialize) def initialize(self) -> None: self._multi_build_plate_model = self._application.getMultiBuildPlateModel( ) self._application.getController().activeViewChanged.connect( self._onActiveViewChanged) if self._multi_build_plate_model: self._multi_build_plate_model.activeBuildPlateChanged.connect( self._onActiveViewChanged) self._application.getMachineManager().globalContainerChanged.connect( self._onGlobalStackChanged) self._onGlobalStackChanged() # extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash ExtruderManager.getInstance().extrudersChanged.connect( self._extruderChanged) self.backendQuit.connect(self._onBackendQuit) self.backendConnected.connect(self._onBackendConnected) # When a tool operation is in progress, don't slice. So we need to listen for tool operations. self._application.getController().toolOperationStarted.connect( self._onToolOperationStarted) self._application.getController().toolOperationStopped.connect( self._onToolOperationStopped) self._machine_error_checker = self._application.getMachineErrorChecker( ) self._machine_error_checker.errorCheckFinished.connect( self._onStackErrorCheckFinished) ## Terminate the engine process. # # This function should terminate the engine process. # Called when closing the application. def close(self) -> None: # Terminate CuraEngine if it is still running at this point self._terminate() ## Get the command that is used to call the engine. # This is useful for debugging and used to actually start the engine. # \return list of commands and args / parameters. def getEngineCommand(self) -> List[str]: command = [ self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "" ] parser = argparse.ArgumentParser(prog="cura", add_help=False) parser.add_argument( "--debug", action="store_true", default=False, help="Turn on the debug mode by setting this option.") known_args = vars(parser.parse_known_args()[0]) if known_args["debug"]: command.append("-vvv") return command ## Emitted when we get a message containing print duration and material amount. # This also implies the slicing has finished. # \param time The amount of time the print will take. # \param material_amount The amount of material the print will use. printDurationMessage = Signal() ## Emitted when the slicing process starts. slicingStarted = Signal() ## Emitted when the slicing process is aborted forcefully. slicingCancelled = Signal() @pyqtSlot() def stopSlicing(self) -> None: self.setState(BackendState.NotStarted) if self._slicing: # We were already slicing. Stop the old job. self._terminate() self._createSocket() if self._process_layers_job is not None: # We were processing layers. Stop that, the layers are going to change soon. Logger.log("d", "Aborting process layers job...") self._process_layers_job.abort() self._process_layers_job = None if self._error_message: self._error_message.hide() ## Manually triggers a reslice @pyqtSlot() def forceSlice(self) -> None: self.markSliceAll() self.slice() ## Perform a slice of the scene. def slice(self) -> None: Logger.log("d", "Starting to slice...") self._slice_start_time = time() if not self._build_plates_to_be_sliced: self.processingProgress.emit(1.0) Logger.log( "w", "Slice unnecessary, nothing has changed that needs reslicing.") self.setState(BackendState.Done) return if self._process_layers_job: Logger.log("d", "Process layers job still busy, trying later.") return if not hasattr(self._scene, "gcode_dict"): self._scene.gcode_dict = { } #type: ignore #Because we are creating the missing attribute here. # see if we really have to slice active_build_plate = self._application.getMultiBuildPlateModel( ).activeBuildPlate build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0) Logger.log( "d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced) num_objects = self._numObjectsPerBuildPlate() self._stored_layer_data = [] if build_plate_to_be_sliced not in num_objects or num_objects[ build_plate_to_be_sliced] == 0: self._scene.gcode_dict[build_plate_to_be_sliced] = [ ] #type: ignore #Because we created this attribute above. Logger.log("d", "Build plate %s has no objects to be sliced, skipping", build_plate_to_be_sliced) if self._build_plates_to_be_sliced: self.slice() return self._stored_optimized_layer_data[build_plate_to_be_sliced] = [] if self._application.getPrintInformation( ) and build_plate_to_be_sliced == active_build_plate: self._application.getPrintInformation().setToZeroPrintInformation( build_plate_to_be_sliced) if self._process is None: # type: ignore self._createSocket() self.stopSlicing() self._engine_is_fresh = False # Yes we're going to use the engine self.processingProgress.emit(0.0) self.backendStateChange.emit(BackendState.NotStarted) self._scene.gcode_dict[build_plate_to_be_sliced] = [ ] #type: ignore #[] indexed by build plate number self._slicing = True self.slicingStarted.emit() self.determineAutoSlicing() # Switch timer on or off if appropriate slice_message = self._socket.createMessage("cura.proto.Slice") self._start_slice_job = StartSliceJob(slice_message) self._start_slice_job_build_plate = build_plate_to_be_sliced self._start_slice_job.setBuildPlate(self._start_slice_job_build_plate) self._start_slice_job.start() self._start_slice_job.finished.connect(self._onStartSliceCompleted) ## Terminate the engine process. # Start the engine process by calling _createSocket() def _terminate(self) -> None: self._slicing = False self._stored_layer_data = [] if self._start_slice_job_build_plate in self._stored_optimized_layer_data: del self._stored_optimized_layer_data[ self._start_slice_job_build_plate] if self._start_slice_job is not None: self._start_slice_job.cancel() self.slicingCancelled.emit() self.processingProgress.emit(0) Logger.log("d", "Attempting to kill the engine process") if self._application.getUseExternalBackend(): return if self._process is not None: # type: ignore Logger.log("d", "Killing engine process") try: self._process.terminate() # type: ignore Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait()) # type: ignore self._process = None # type: ignore except Exception as e: # terminating a process that is already terminating causes an exception, silently ignore this. Logger.log( "d", "Exception occurred while trying to kill the engine %s", str(e)) ## Event handler to call when the job to initiate the slicing process is # completed. # # When the start slice job is successfully completed, it will be happily # slicing. This function handles any errors that may occur during the # bootstrapping of a slice job. # # \param job The start slice job that was just finished. def _onStartSliceCompleted(self, job: StartSliceJob) -> None: if self._error_message: self._error_message.hide() # Note that cancelled slice jobs can still call this method. if self._start_slice_job is job: self._start_slice_job = None if job.isCancelled() or job.getError() or job.getResult( ) == StartJobResult.Error: self.setState(BackendState.Error) self.backendError.emit(job) return if job.getResult() == StartJobResult.MaterialIncompatible: if self._application.platformActivity: self._error_message = Message(catalog.i18nc( "@info:status", "Unable to slice with the current material as it is incompatible with the selected machine or configuration." ), title=catalog.i18nc( "@info:title", "Unable to slice")) self._error_message.show() self.setState(BackendState.Error) self.backendError.emit(job) else: self.setState(BackendState.NotStarted) return if job.getResult() == StartJobResult.SettingError: if self._application.platformActivity: if not self._global_container_stack: Logger.log( "w", "Global container stack not assigned to CuraEngineBackend!" ) return extruders = ExtruderManager.getInstance( ).getActiveExtruderStacks() error_keys = [] #type: List[str] for extruder in extruders: error_keys.extend(extruder.getErrorKeys()) if not extruders: error_keys = self._global_container_stack.getErrorKeys() error_labels = set() for key in error_keys: for stack in [ self._global_container_stack ] + extruders: #Search all container stacks for the definition of this setting. Some are only in an extruder stack. definitions = cast( DefinitionContainerInterface, stack.getBottom()).findDefinitions(key=key) if definitions: break #Found it! No need to continue search. else: #No stack has a definition for this setting. Logger.log( "w", "When checking settings for errors, unable to find definition for key: {key}" .format(key=key)) continue error_labels.add(definitions[0].label) self._error_message = Message(catalog.i18nc( "@info:status", "Unable to slice with the current settings. The following settings have errors: {0}" ).format(", ".join(error_labels)), title=catalog.i18nc( "@info:title", "Unable to slice")) self._error_message.show() self.setState(BackendState.Error) self.backendError.emit(job) else: self.setState(BackendState.NotStarted) return elif job.getResult() == StartJobResult.ObjectSettingError: errors = {} for node in DepthFirstIterator( self._application.getController().getScene().getRoot() ): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. stack = node.callDecoration("getStack") if not stack: continue for key in stack.getErrorKeys(): if not self._global_container_stack: Logger.log( "e", "CuraEngineBackend does not have global_container_stack assigned." ) continue definition = cast(DefinitionContainerInterface, self._global_container_stack.getBottom() ).findDefinitions(key=key) if not definition: Logger.log( "e", "When checking settings for errors, unable to find definition for key {key} in per-object stack." .format(key=key)) continue errors[key] = definition[0].label self._error_message = Message(catalog.i18nc( "@info:status", "Unable to slice due to some per-model settings. The following settings have errors on one or more models: {error_labels}" ).format(error_labels=", ".join(errors.values())), title=catalog.i18nc( "@info:title", "Unable to slice")) self._error_message.show() self.setState(BackendState.Error) self.backendError.emit(job) return if job.getResult() == StartJobResult.BuildPlateError: if self._application.platformActivity: self._error_message = Message(catalog.i18nc( "@info:status", "Unable to slice because the prime tower or prime position(s) are invalid." ), title=catalog.i18nc( "@info:title", "Unable to slice")) self._error_message.show() self.setState(BackendState.Error) self.backendError.emit(job) else: self.setState(BackendState.NotStarted) if job.getResult() == StartJobResult.ObjectsWithDisabledExtruder: self._error_message = Message(catalog.i18nc( "@info:status", "Unable to slice because there are objects associated with disabled Extruder %s." % job.getMessage()), title=catalog.i18nc( "@info:title", "Unable to slice")) self._error_message.show() self.setState(BackendState.Error) self.backendError.emit(job) return if job.getResult() == StartJobResult.NothingToSlice: if self._application.platformActivity: self._error_message = Message(catalog.i18nc( "@info:status", "Nothing to slice because none of the models fit the build volume or are assigned to a disabled extruder. Please scale or rotate models to fit, or enable an extruder." ), title=catalog.i18nc( "@info:title", "Unable to slice")) self._error_message.show() self.setState(BackendState.Error) self.backendError.emit(job) else: self.setState(BackendState.NotStarted) self._invokeSlice() return # Preparation completed, send it to the backend. self._socket.sendMessage(job.getSliceMessage()) # Notify the user that it's now up to the backend to do it's job self.setState(BackendState.Processing) if self._slice_start_time: Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time) ## Determine enable or disable auto slicing. Return True for enable timer and False otherwise. # It disables when # - preference auto slice is off # - decorator isBlockSlicing is found (used in g-code reader) def determineAutoSlicing(self) -> bool: enable_timer = True self._is_disabled = False if not self._application.getPreferences().getValue( "general/auto_slice"): enable_timer = False for node in DepthFirstIterator( self._scene.getRoot() ): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. if node.callDecoration("isBlockSlicing"): enable_timer = False self.setState(BackendState.Disabled) self._is_disabled = True gcode_list = node.callDecoration("getGCodeList") if gcode_list is not None: self._scene.gcode_dict[node.callDecoration( "getBuildPlateNumber" )] = gcode_list #type: ignore #Because we generate this attribute dynamically. if self._use_timer == enable_timer: return self._use_timer if enable_timer: self.setState(BackendState.NotStarted) self.enableTimer() return True else: self.disableTimer() return False ## Return a dict with number of objects per build plate def _numObjectsPerBuildPlate(self) -> Dict[int, int]: num_objects = defaultdict(int) #type: Dict[int, int] for node in DepthFirstIterator( self._scene.getRoot() ): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. # Only count sliceable objects if node.callDecoration("isSliceable"): build_plate_number = node.callDecoration("getBuildPlateNumber") if build_plate_number is not None: num_objects[build_plate_number] += 1 return num_objects ## Listener for when the scene has changed. # # This should start a slice if the scene is now ready to slice. # # \param source The scene node that was changed. def _onSceneChanged(self, source: SceneNode) -> None: if not source.callDecoration("isSliceable"): return # This case checks if the source node is a node that contains GCode. In this case the # current layer data is removed so the previous data is not rendered - CURA-4821 if source.callDecoration("isBlockSlicing") and source.callDecoration( "getLayerData"): self._stored_optimized_layer_data = {} build_plate_changed = set() source_build_plate_number = source.callDecoration( "getBuildPlateNumber") if source == self._scene.getRoot(): # we got the root node num_objects = self._numObjectsPerBuildPlate() for build_plate_number in list( self._last_num_objects.keys()) + list(num_objects.keys()): if build_plate_number not in self._last_num_objects or num_objects[ build_plate_number] != self._last_num_objects[ build_plate_number]: self._last_num_objects[build_plate_number] = num_objects[ build_plate_number] build_plate_changed.add(build_plate_number) else: # we got a single scenenode if not source.callDecoration("isGroup"): mesh_data = source.getMeshData() if mesh_data is None or mesh_data.getVertices() is None: return # There are some SceneNodes that do not have any build plate associated, then do not add to the list. if source_build_plate_number is not None: build_plate_changed.add(source_build_plate_number) if not build_plate_changed: return if self._tool_active: # do it later, each source only has to be done once if source not in self._postponed_scene_change_sources: self._postponed_scene_change_sources.append(source) return self.stopSlicing() for build_plate_number in build_plate_changed: if build_plate_number not in self._build_plates_to_be_sliced: self._build_plates_to_be_sliced.append(build_plate_number) self.printDurationMessage.emit(source_build_plate_number, {}, []) self.processingProgress.emit(0.0) self._clearLayerData(build_plate_changed) self._invokeSlice() ## Called when an error occurs in the socket connection towards the engine. # # \param error The exception that occurred. def _onSocketError(self, error: Arcus.Error) -> None: if self._application.isShuttingDown(): return super()._onSocketError(error) if error.getErrorCode() == Arcus.ErrorCode.Debug: return self._terminate() self._createSocket() if error.getErrorCode() not in [ Arcus.ErrorCode.BindFailedError, Arcus.ErrorCode.ConnectionResetError, Arcus.ErrorCode.Debug ]: Logger.log("w", "A socket error caused the connection to be reset") # _terminate()' function sets the job status to 'cancel', after reconnecting to another Port the job status # needs to be updated. Otherwise backendState is "Unable To Slice" if error.getErrorCode( ) == Arcus.ErrorCode.BindFailedError and self._start_slice_job is not None: self._start_slice_job.setIsCancelled(False) ## Remove old layer data (if any) def _clearLayerData(self, build_plate_numbers: Set = None) -> None: # Clear out any old gcode self._scene.gcode_dict = {} # type: ignore for node in DepthFirstIterator( self._scene.getRoot() ): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. if node.callDecoration("getLayerData"): if not build_plate_numbers or node.callDecoration( "getBuildPlateNumber") in build_plate_numbers: node.getParent().removeChild(node) def markSliceAll(self) -> None: for build_plate_number in range( self._application.getMultiBuildPlateModel().maxBuildPlate + 1): if build_plate_number not in self._build_plates_to_be_sliced: self._build_plates_to_be_sliced.append(build_plate_number) ## Convenient function: mark everything to slice, emit state and clear layer data def needsSlicing(self) -> None: self.determineAutoSlicing() self.stopSlicing() self.markSliceAll() self.processingProgress.emit(0.0) if not self._use_timer: # With manually having to slice, we want to clear the old invalid layer data. self._clearLayerData() ## A setting has changed, so check if we must reslice. # \param instance The setting instance that has changed. # \param property The property of the setting instance that has changed. def _onSettingChanged(self, instance: SettingInstance, property: str) -> None: if property == "value": # Only reslice if the value has changed. self.needsSlicing() self._onChanged() elif property == "validationState": if self._use_timer: self._change_timer.stop() def _onStackErrorCheckFinished(self) -> None: self.determineAutoSlicing() if self._is_disabled: return if not self._slicing and self._build_plates_to_be_sliced: self.needsSlicing() self._onChanged() ## Called when a sliced layer data message is received from the engine. # # \param message The protobuf message containing sliced layer data. def _onLayerMessage(self, message: Arcus.PythonMessage) -> None: self._stored_layer_data.append(message) ## Called when an optimized sliced layer data message is received from the engine. # # \param message The protobuf message containing sliced layer data. def _onOptimizedLayerMessage(self, message: Arcus.PythonMessage) -> None: if self._start_slice_job_build_plate is not None: if self._start_slice_job_build_plate not in self._stored_optimized_layer_data: self._stored_optimized_layer_data[ self._start_slice_job_build_plate] = [] self._stored_optimized_layer_data[ self._start_slice_job_build_plate].append(message) ## Called when a progress message is received from the engine. # # \param message The protobuf message containing the slicing progress. def _onProgressMessage(self, message: Arcus.PythonMessage) -> None: self.processingProgress.emit(message.amount) self.setState(BackendState.Processing) def _invokeSlice(self) -> None: if self._use_timer: # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice, # otherwise business as usual if self._machine_error_checker is None: self._change_timer.stop() return if self._machine_error_checker.needToWaitForResult: self._change_timer.stop() else: self._change_timer.start() ## Called when the engine sends a message that slicing is finished. # # \param message The protobuf message signalling that slicing is finished. def _onSlicingFinishedMessage(self, message: Arcus.PythonMessage) -> None: self.setState(BackendState.Done) self.processingProgress.emit(1.0) gcode_list = self._scene.gcode_dict[ self. _start_slice_job_build_plate] #type: ignore #Because we generate this attribute dynamically. for index, line in enumerate(gcode_list): replaced = line.replace( "{print_time}", str(self._application.getPrintInformation().currentPrintTime. getDisplayString(DurationFormat.Format.ISO8601))) replaced = replaced.replace( "{filament_amount}", str(self._application.getPrintInformation().materialLengths)) replaced = replaced.replace( "{filament_weight}", str(self._application.getPrintInformation().materialWeights)) replaced = replaced.replace( "{filament_cost}", str(self._application.getPrintInformation().materialCosts)) replaced = replaced.replace( "{jobname}", str(self._application.getPrintInformation().jobName)) gcode_list[index] = replaced self._slicing = False if self._slice_start_time: Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time) Logger.log("d", "Number of models per buildplate: %s", dict(self._numObjectsPerBuildPlate())) # See if we need to process the sliced layers job. active_build_plate = self._application.getMultiBuildPlateModel( ).activeBuildPlate if (self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()) and active_build_plate == self._start_slice_job_build_plate and active_build_plate not in self._build_plates_to_be_sliced): self._startProcessSlicedLayersJob(active_build_plate) # self._onActiveViewChanged() self._start_slice_job_build_plate = None Logger.log("d", "See if there is more to slice...") # Somehow this results in an Arcus Error # self.slice() # Call slice again using the timer, allowing the backend to restart if self._build_plates_to_be_sliced: self.enableTimer( ) # manually enable timer to be able to invoke slice, also when in manual slice mode self._invokeSlice() ## Called when a g-code message is received from the engine. # # \param message The protobuf message containing g-code, encoded as UTF-8. def _onGCodeLayerMessage(self, message: Arcus.PythonMessage) -> None: self._scene.gcode_dict[self._start_slice_job_build_plate].append( message.data.decode("utf-8", "replace") ) #type: ignore #Because we generate this attribute dynamically. ## Called when a g-code prefix message is received from the engine. # # \param message The protobuf message containing the g-code prefix, # encoded as UTF-8. def _onGCodePrefixMessage(self, message: Arcus.PythonMessage) -> None: self._scene.gcode_dict[self._start_slice_job_build_plate].insert( 0, message.data.decode("utf-8", "replace") ) #type: ignore #Because we generate this attribute dynamically. ## Creates a new socket connection. def _createSocket(self, protocol_file: str = None) -> None: if not protocol_file: plugin_path = PluginRegistry.getInstance().getPluginPath( self.getPluginId()) if not plugin_path: Logger.log("e", "Could not get plugin path!", self.getPluginId()) return protocol_file = os.path.abspath( os.path.join(plugin_path, "Cura.proto")) super()._createSocket(protocol_file) self._engine_is_fresh = True ## Called when anything has changed to the stuff that needs to be sliced. # # This indicates that we should probably re-slice soon. def _onChanged(self, *args: Any, **kwargs: Any) -> None: self.needsSlicing() if self._use_timer: # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice, # otherwise business as usual if self._machine_error_checker is None: self._change_timer.stop() return if self._machine_error_checker.needToWaitForResult: self._change_timer.stop() else: self._change_timer.start() ## Called when a print time message is received from the engine. # # \param message The protobuf message containing the print time per feature and # material amount per extruder def _onPrintTimeMaterialEstimates(self, message: Arcus.PythonMessage) -> None: material_amounts = [] for index in range(message.repeatedMessageCount("materialEstimates")): material_amounts.append( message.getRepeatedMessage("materialEstimates", index).material_amount) times = self._parseMessagePrintTimes(message) self.printDurationMessage.emit(self._start_slice_job_build_plate, times, material_amounts) ## Called for parsing message to retrieve estimated time per feature # # \param message The protobuf message containing the print time per feature def _parseMessagePrintTimes( self, message: Arcus.PythonMessage) -> Dict[str, float]: result = { "inset_0": message.time_inset_0, "inset_x": message.time_inset_x, "skin": message.time_skin, "infill": message.time_infill, "support_infill": message.time_support_infill, "support_interface": message.time_support_interface, "support": message.time_support, "skirt": message.time_skirt, "prime_tower": message.time_prime_tower, "travel": message.time_travel, "retract": message.time_retract, "none": message.time_none } return result ## Called when the back-end connects to the front-end. def _onBackendConnected(self) -> None: if self._restart: self._restart = False self._onChanged() ## Called when the user starts using some tool. # # When the user starts using a tool, we should pause slicing to prevent # continuously slicing while the user is dragging some tool handle. # # \param tool The tool that the user is using. def _onToolOperationStarted(self, tool: Tool) -> None: self._tool_active = True # Do not react on scene change self.disableTimer() # Restart engine as soon as possible, we know we want to slice afterwards if not self._engine_is_fresh: self._terminate() self._createSocket() ## Called when the user stops using some tool. # # This indicates that we can safely start slicing again. # # \param tool The tool that the user was using. def _onToolOperationStopped(self, tool: Tool) -> None: self._tool_active = False # React on scene change again self.determineAutoSlicing() # Switch timer on if appropriate # Process all the postponed scene changes while self._postponed_scene_change_sources: source = self._postponed_scene_change_sources.pop(0) self._onSceneChanged(source) def _startProcessSlicedLayersJob(self, build_plate_number: int) -> None: self._process_layers_job = ProcessSlicedLayersJob( self._stored_optimized_layer_data[build_plate_number]) self._process_layers_job.setBuildPlate(build_plate_number) self._process_layers_job.finished.connect( self._onProcessLayersFinished) self._process_layers_job.start() ## Called when the user changes the active view mode. def _onActiveViewChanged(self) -> None: view = self._application.getController().getActiveView() if view: active_build_plate = self._application.getMultiBuildPlateModel( ).activeBuildPlate if view.getPluginId( ) == "SimulationView": # If switching to layer view, we should process the layers if that hasn't been done yet. self._layer_view_active = True # There is data and we're not slicing at the moment # if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment. # TODO: what build plate I am slicing if (active_build_plate in self._stored_optimized_layer_data and not self._slicing and not self._process_layers_job and active_build_plate not in self._build_plates_to_be_sliced): self._startProcessSlicedLayersJob(active_build_plate) else: self._layer_view_active = False ## Called when the back-end self-terminates. # # We should reset our state and start listening for new connections. def _onBackendQuit(self) -> None: if not self._restart: if self._process: # type: ignore Logger.log( "d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait()) # type: ignore self._process = None # type: ignore ## Called when the global container stack changes def _onGlobalStackChanged(self) -> None: if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect( self._onSettingChanged) self._global_container_stack.containersChanged.disconnect( self._onChanged) extruders = list(self._global_container_stack.extruders.values()) for extruder in extruders: extruder.propertyChanged.disconnect(self._onSettingChanged) extruder.containersChanged.disconnect(self._onChanged) self._global_container_stack = self._application.getMachineManager( ).activeMachine if self._global_container_stack: self._global_container_stack.propertyChanged.connect( self._onSettingChanged ) # Note: Only starts slicing when the value changed. self._global_container_stack.containersChanged.connect( self._onChanged) extruders = list(self._global_container_stack.extruders.values()) for extruder in extruders: extruder.propertyChanged.connect(self._onSettingChanged) extruder.containersChanged.connect(self._onChanged) self._onChanged() def _onProcessLayersFinished(self, job: ProcessSlicedLayersJob) -> None: if job.getBuildPlate() in self._stored_optimized_layer_data: del self._stored_optimized_layer_data[job.getBuildPlate()] else: Logger.log( "w", "The optimized layer data was already deleted for buildplate %s", job.getBuildPlate()) self._process_layers_job = None Logger.log("d", "See if there is more to slice(2)...") self._invokeSlice() ## Connect slice function to timer. def enableTimer(self) -> None: if not self._use_timer: self._change_timer.timeout.connect(self.slice) self._use_timer = True ## Disconnect slice function from timer. # This means that slicing will not be triggered automatically def disableTimer(self) -> None: if self._use_timer: self._use_timer = False self._change_timer.timeout.disconnect(self.slice) def _onPreferencesChanged(self, preference: str) -> None: if preference != "general/auto_slice": return auto_slice = self.determineAutoSlicing() if auto_slice: self._change_timer.start() ## Tickle the backend so in case of auto slicing, it starts the timer. def tickle(self) -> None: if self._use_timer: self._change_timer.start() def _extruderChanged(self) -> None: if not self._multi_build_plate_model: Logger.log( "w", "CuraEngineBackend does not have multi_build_plate_model assigned!" ) return for build_plate_number in range( self._multi_build_plate_model.maxBuildPlate + 1): if build_plate_number not in self._build_plates_to_be_sliced: self._build_plates_to_be_sliced.append(build_plate_number) self._invokeSlice()
class DiscoverRepetierAction(MachineAction): def __init__(self, parent: QObject = None) -> None: super().__init__("DiscoverRepetierAction", catalog.i18nc("@action", "Connect Repetier")) self._qml_url = "DiscoverRepetierAction.qml" self._application = CuraApplication.getInstance() self._network_plugin = None # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly # hook itself into the event loop, which results in events never being fired / done. self._network_manager = QNetworkAccessManager() self._network_manager.finished.connect(self._onRequestFinished) self._printers = [""] self._printerlist_reply = None self._settings_reply = None self._settings_reply_timeout = None # type: Optional[NetworkReplyTimeout] self._instance_supports_appkeys = False self._appkey_reply = None # type: Optional[QNetworkReply] self._appkey_request = None # type: Optional[QNetworkRequest] self._appkey_instance_id = "" self._appkey_poll_timer = QTimer() self._appkey_poll_timer.setInterval(500) self._appkey_poll_timer.setSingleShot(True) self._appkey_poll_timer.timeout.connect(self._pollApiKey) # Try to get version information from plugin.json plugin_file_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "plugin.json") try: with open(plugin_file_path) as plugin_file: plugin_info = json.load(plugin_file) self._plugin_version = plugin_info["version"] except: # The actual version info is not critical to have so we can continue self._plugin_version = "0.0" Logger.logException( "w", "Could not get version information for the plugin") self._user_agent = ( "%s/%s %s/%s" % (self._application.getApplicationName(), self._application.getVersion(), "RepetierIntegration", self._plugin_version)).encode() self._settings_instance = None self._instance_responded = False self._instance_in_error = False self._instance_api_key_accepted = False self._instance_supports_sd = False self._instance_supports_camera = False self._instance_webcamflip_y = False self._instance_webcamflip_x = False self._instance_webcamrot90 = False self._instance_webcamrot270 = False # Load keys cache from preferences self._preferences = self._application.getPreferences() self._preferences.addPreference("Repetier/keys_cache", "") try: self._keys_cache = json.loads( self._preferences.getValue("Repetier/keys_cache")) except ValueError: self._keys_cache = {} if not isinstance(self._keys_cache, dict): self._keys_cache = {} self._additional_components = None ContainerRegistry.getInstance().containerAdded.connect( self._onContainerAdded) self._application.engineCreatedSignal.connect( self._createAdditionalComponentsView) @pyqtProperty(str, constant=True) def pluginVersion(self) -> str: return self._plugin_version @pyqtSlot() def startDiscovery(self) -> None: if not self._plugin_id: return if not self._network_plugin: self._network_plugin = cast( RepetierOutputDevicePlugin, self._application.getOutputDeviceManager(). getOutputDevicePlugin(self._plugin_id)) if not self._network_plugin: return self._network_plugin.addInstanceSignal.connect( self._onInstanceDiscovery) self._network_plugin.removeInstanceSignal.connect( self._onInstanceDiscovery) self._network_plugin.instanceListChanged.connect( self._onInstanceDiscovery) self.instancesChanged.emit() else: # Restart bonjour discovery self._network_plugin.startDiscovery() def _onInstanceDiscovery(self, *args) -> None: self.instancesChanged.emit() @pyqtSlot(str) def removeManualInstance(self, name: str) -> None: if not self._network_plugin: return self._network_plugin.removeManualInstance(name) @pyqtSlot(str, str, int, str, bool, str, str, str) def setManualInstance(self, name, address, port, path, useHttps, userName, password, repetierid): if not self._network_plugin: return # This manual printer could replace a current manual printer self._network_plugin.removeManualInstance(name) self._network_plugin.addManualInstance(name, address, port, path, useHttps, userName, password, repetierid) def _onContainerAdded(self, container: "ContainerInterface") -> None: # Add this action as a supported action to all machine definitions if (isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine" and container.getMetaDataEntry("supports_usb_connection")): self._application.getMachineActionManager().addSupportedAction( container.getId(), self.getKey()) instancesChanged = pyqtSignal() appKeysSupportedChanged = pyqtSignal() appKeyReceived = pyqtSignal() instanceIdChanged = pyqtSignal() @pyqtProperty("QVariantList", notify=instancesChanged) def discoveredInstances(self) -> List[Any]: if self._network_plugin: instances = list(self._network_plugin.getInstances().values()) instances.sort(key=lambda k: k.name) return instances else: return [] @pyqtSlot(str) def setInstanceId(self, key: str) -> None: global_container_stack = self._application.getGlobalContainerStack() if global_container_stack: global_container_stack.setMetaDataEntry("repetier_id", key) if self._network_plugin: # Ensure that the connection states are refreshed. self._network_plugin.reCheckConnections() self.instanceIdChanged.emit() @pyqtProperty(str, notify=instanceIdChanged) def instanceId(self) -> str: global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: return "" return global_container_stack.getMetaDataEntry("repetier_id", "") @pyqtSlot(str) @pyqtSlot(result=str) def getInstanceId(self) -> str: global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: Logger.log( "d", "getInstancdId - self._application.getGlobalContainerStack() returned nothing" ) return "" return global_container_stack.getMetaDataEntry("repetier_id", "") @pyqtSlot(str) def requestApiKey(self, instance_id: str) -> None: (instance, base_url, basic_auth_username, basic_auth_password) = self._getInstanceInfo(instance_id) if not base_url: return ## Request appkey self._appkey_instance_id = instance_id self._appkey_request = self._createRequest( QUrl(base_url + "plugin/appkeys/request"), basic_auth_username, basic_auth_password) self._appkey_request.setRawHeader(b"Content-Type", b"application/json") data = json.dumps({"app": "Cura"}) self._appkey_reply = self._network_manager.post( self._appkey_request, data.encode()) @pyqtSlot() def cancelApiKeyRequest(self) -> None: if self._appkey_reply: if self._appkey_reply.isRunning(): self._appkey_reply.abort() self._appkey_reply = None self._appkey_request = None # type: Optional[QNetworkRequest] self._appkey_poll_timer.stop() def _pollApiKey(self) -> None: if not self._appkey_request: return self._appkey_reply = self._network_manager.get(self._appkey_request) @pyqtSlot(str) def probeAppKeySupport(self, instance_id: str) -> None: (instance, base_url, basic_auth_username, basic_auth_password) = self._getInstanceInfo(instance_id) if not base_url or not instance: return instance.getAdditionalData() self._instance_supports_appkeys = False self.appKeysSupportedChanged.emit() appkey_probe_request = self._createRequest( QUrl(base_url + "plugin/appkeys/probe"), basic_auth_username, basic_auth_password) self._appkey_reply = self._network_manager.get(appkey_probe_request) @pyqtSlot(str) def getPrinterList(self, base_url): self._instance_responded = False url = QUrl("http://" + base_url + "/printer/info") Logger.log("d", "getPrinterList:" + url.toString()) settings_request = QNetworkRequest(url) settings_request.setRawHeader("User-Agent".encode(), self._user_agent) self._printerlist_reply = self._network_manager.get(settings_request) return self._printers @pyqtSlot(str, str, str, str, str, str) def testApiKey(self, instance_id: str, base_url, api_key, basic_auth_username="", basic_auth_password="", work_id="") -> None: (instance, base_url, basic_auth_username, basic_auth_password) = self._getInstanceInfo(instance_id) self._instance_responded = False self._instance_api_key_accepted = False self._instance_supports_sd = False self._instance_webcamflip_y = False self._instance_webcamflip_x = False self._instance_webcamrot90 = False self._instance_webcamrot270 = False self._instance_supports_camera = False self.selectedInstanceSettingsChanged.emit() if self._settings_reply: if self._settings_reply.isRunning(): self._settings_reply.abort() self._settings_reply = None if self._settings_reply_timeout: self._settings_reply_timeout = None if ((api_key != "") and (api_key != None) and (work_id != "")): Logger.log( "d", "Trying to access Repetier instance at %s with the provided API key." % base_url) Logger.log("d", "Using %s as work_id" % work_id) Logger.log("d", "Using %s as api_key" % api_key) url = QUrl(base_url + "/printer/api/" + work_id + "?a=getPrinterConfig&apikey=" + api_key) settings_request = QNetworkRequest(url) settings_request.setRawHeader("x-api-key".encode(), api_key.encode()) settings_request.setRawHeader("User-Agent".encode(), self._user_agent) if basic_auth_username and basic_auth_password: data = base64.b64encode( ("%s:%s" % (basic_auth_username, basic_auth_password)).encode()).decode("utf-8") settings_request.setRawHeader("Authorization".encode(), ("Basic %s" % data).encode()) self._settings_reply = self._network_manager.get(settings_request) self._settings_instance = instance else: self.getPrinterList(base_url) @pyqtSlot(str) def setApiKey(self, api_key: str) -> None: global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: return global_container_stack.setMetaDataEntry("repetier_api_key", api_key) self._keys_cache[self.getInstanceId()] = api_key keys_cache = base64.b64encode( json.dumps(self._keys_cache).encode("ascii")).decode("ascii") self._preferences.setValue("Repetier/keys_cache", keys_cache) if self._network_plugin: # Ensure that the connection states are refreshed. self._network_plugin.reCheckConnections() # Get the stored API key of this machine # \return key String containing the key of the machine. @pyqtSlot(str, result=str) def getApiKey(self, instance_id: str) -> str: global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: return "" Logger.log( "d", "APIKEY read %s" % global_container_stack.getMetaDataEntry("repetier_api_key", "")) if instance_id == self.getInstanceId(): api_key = global_container_stack.getMetaDataEntry( "repetier_api_key", "") else: api_key = self._keys_cache.get(instance_id, "") return api_key selectedInstanceSettingsChanged = pyqtSignal() @pyqtProperty(list) def getPrinters(self): return self._printers @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceResponded(self) -> bool: return self._instance_responded @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceInError(self) -> bool: return self._instance_in_error @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceApiKeyAccepted(self) -> bool: return self._instance_api_key_accepted @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceSupportsSd(self) -> bool: return self._instance_supports_sd @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceWebcamFlipY(self): return self._instance_webcamflip_y @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceWebcamFlipX(self): return self._instance_webcamflip_x @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceWebcamRot90(self): return self._instance_webcamrot90 @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceWebcamRot270(self): return self._instance_webcamrot270 @pyqtProperty(bool, notify=selectedInstanceSettingsChanged) def instanceSupportsCamera(self) -> bool: return self._instance_supports_camera @pyqtSlot(str, str, str) def setContainerMetaDataEntry(self, container_id: str, key: str, value: str) -> None: containers = ContainerRegistry.getInstance().findContainers( id=container_id) if not containers: Logger.log( "w", "Could not set metadata of container %s because it was not found.", container_id) return containers[0].setMetaDataEntry(key, value) @pyqtSlot(bool) def applyGcodeFlavorFix(self, apply_fix: bool) -> None: global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: return gcode_flavor = "RepRap (Marlin/Sprinter)" if apply_fix else "UltiGCode" if global_container_stack.getProperty("machine_gcode_flavor", "value") == gcode_flavor: # No need to add a definition_changes container if the setting is not going to be changed return # Make sure there is a definition_changes container to store the machine settings definition_changes_container = global_container_stack.definitionChanges if definition_changes_container == ContainerRegistry.getInstance( ).getEmptyInstanceContainer(): definition_changes_container = CuraStackBuilder.createDefinitionChangesContainer( global_container_stack, global_container_stack.getId() + "_settings") definition_changes_container.setProperty("machine_gcode_flavor", "value", gcode_flavor) # Update the has_materials metadata flag after switching gcode flavor definition = global_container_stack.getBottom() if (not definition or definition.getProperty("machine_gcode_flavor", "value") != "UltiGCode" or definition.getMetaDataEntry("has_materials", False)): # In other words: only continue for the UM2 (extended), but not for the UM2+ return has_materials = global_container_stack.getProperty( "machine_gcode_flavor", "value") != "UltiGCode" material_container = global_container_stack.material if has_materials: global_container_stack.setMetaDataEntry("has_materials", True) # Set the material container to a sane default if material_container == ContainerRegistry.getInstance( ).getEmptyInstanceContainer(): search_criteria = { "type": "material", "definition": "fdmprinter", "id": global_container_stack.getMetaDataEntry( "preferred_material") } materials = ContainerRegistry.getInstance( ).findInstanceContainers(**search_criteria) if materials: global_container_stack.material = materials[0] else: # The metadata entry is stored in an ini, and ini files are parsed as strings only. # Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False. if "has_materials" in global_container_stack.getMetaData(): global_container_stack.removeMetaDataEntry("has_materials") global_container_stack.material = ContainerRegistry.getInstance( ).getEmptyInstanceContainer() self._application.globalContainerStackChanged.emit() @pyqtSlot(str) def openWebPage(self, url: str) -> None: QDesktopServices.openUrl(QUrl(url)) def _createAdditionalComponentsView(self) -> None: Logger.log( "d", "Creating additional ui components for Repetier-connected printers." ) path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "RepetierComponents.qml") self._additional_components = self._application.createQmlComponent( path, {"manager": self}) if not self._additional_components: Logger.log( "w", "Could not create additional components for Repetier-connected printers." ) return self._application.addAdditionalComponent( "monitorButtons", self._additional_components.findChild(QObject, "openRepetierButton")) def _onRequestFailed(self, reply: QNetworkReply) -> None: if reply.operation() == QNetworkAccessManager.GetOperation: if "api/settings" in reply.url().toString( ): # Repetier settings dump from /settings: Logger.log( "w", "Connection refused or timeout when trying to access Repetier at %s" % reply.url().toString()) self._instance_in_error = True self.selectedInstanceSettingsChanged.emit() # Handler for all requests that have finished. def _onRequestFinished(self, reply: QNetworkReply) -> None: if reply.error() == QNetworkReply.TimeoutError: QMessageBox.warning(None, 'Connection Timeout', 'Connection Timeout') return http_status_code = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) if not http_status_code: #QMessageBox.warning(None,'Connection Attempt2',http_status_code) # Received no or empty reply Logger.log("d", "Received no or empty reply") return if reply.operation() == QNetworkAccessManager.GetOperation: Logger.log("d", reply.url().toString()) if "printer/info" in reply.url().toString( ): # Repetier settings dump from printer/info: if http_status_code == 200: try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) Logger.log("d", reply.url().toString()) Logger.log("d", json_data) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from Repetier instance.") json_data = {} if "printers" in json_data: Logger.log("d", "DiscoverRepetierAction: printers: %s", len(json_data["printers"])) if len(json_data["printers"]) > 0: self._printers = [""] for printerinfo in json_data["printers"]: Logger.log("d", "Slug: %s", printerinfo["slug"]) self._printers.append(printerinfo["slug"]) if "apikey" in json_data: Logger.log("d", "DiscoverRepetierAction: apikey: %s", json_data["apikey"]) global_container_stack = self._application.getGlobalContainerStack( ) if not global_container_stack: return global_container_stack.setMetaDataEntry( "repetier_api_key", json_data["apikey"]) self._keys_cache[ self.getInstanceId()] = json_data["apikey"] keys_cache = base64.b64encode( json.dumps(self._keys_cache).encode( "ascii")).decode("ascii") self._preferences.setValue("Repetier/keys_cache", keys_cache) self.appKeyReceived.emit() if self._network_plugin: # Ensure that the connection states are refreshed. self._network_plugin.reCheckConnections() if "getPrinterConfig" in reply.url().toString( ): # Repetier settings dump from getPrinterConfig: if http_status_code == 200: Logger.log("d", "API key accepted by Repetier.") self._instance_api_key_accepted = True try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) Logger.log("d", reply.url().toString()) Logger.log("d", json_data) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from Repetier instance.") json_data = {} if "general" in json_data and "sdcard" in json_data[ "general"]: self._instance_supports_sd = json_data["general"][ "sdcard"] if "webcam" in json_data and "dynamicUrl" in json_data[ "webcam"]: Logger.log( "d", "DiscoverRepetierAction: Checking streamurl") Logger.log("d", "DiscoverRepetierAction: %s", reply.url()) stream_url = json_data["webcam"]["dynamicUrl"].replace( "127.0.0.1", re.findall(r'[0-9]+(?:\.[0-9]+){3}', reply.url().toString())[0]) Logger.log("d", "DiscoverRepetierAction: stream_url: %s", stream_url) Logger.log("d", "DiscoverRepetierAction: reply_url: %s", reply.url()) if stream_url: #not empty string or None self._instance_supports_camera = True if "webcams" in json_data: Logger.log("d", "DiscoverRepetierAction: webcams: %s", len(json_data["webcams"])) if len(json_data["webcams"]) > 0: if "dynamicUrl" in json_data["webcams"][0]: Logger.log( "d", "DiscoverRepetierAction: Checking streamurl" ) stream_url = json_data["webcams"][0][ "dynamicUrl"].replace( "127.0.0.1", re.findall(r'[0-9]+(?:\.[0-9]+){3}', reply.url().toString())[0]) Logger.log( "d", "DiscoverRepetierAction: stream_url: %s", stream_url) Logger.log( "d", "DiscoverRepetierAction: reply_url: %s", reply.url()) if stream_url: #not empty string or None self._instance_supports_camera = True elif http_status_code == 401: Logger.log("d", "Invalid API key for Repetier.") self._instance_api_key_accepted = False self._instance_in_error = True self._instance_responded = True self.selectedInstanceSettingsChanged.emit() def _createRequest(self, url: str, basic_auth_username: str = "", basic_auth_password: str = "") -> QNetworkRequest: request = QNetworkRequest(url) request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) request.setRawHeader(b"User-Agent", self._user_agent) if basic_auth_username and basic_auth_password: data = base64.b64encode( ("%s:%s" % (basic_auth_username, basic_auth_password)).encode()).decode("utf-8") request.setRawHeader(b"Authorization", ("Basic %s" % data).encode()) # ignore SSL errors (eg for self-signed certificates) ssl_configuration = QSslConfiguration.defaultConfiguration() ssl_configuration.setPeerVerifyMode(QSslSocket.VerifyNone) request.setSslConfiguration(ssl_configuration) return request ## Utility handler to base64-decode a string (eg an obfuscated API key), if it has been encoded before def _deobfuscateString(self, source: str) -> str: try: return base64.b64decode(source.encode("ascii")).decode("ascii") except UnicodeDecodeError: return source def _getInstanceInfo( self, instance_id: str ) -> Tuple[Optional[RepetierOutputDevice], str, str, str]: if not self._network_plugin: return (None, "", "", "") instance = self._network_plugin.getInstanceById(instance_id) if not instance: return (None, "", "", "") return (instance, instance.baseURL, instance.getProperty("userName"), instance.getProperty("password"))
class DiscoveredPrintersModel(QObject): def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None: super().__init__(parent) self._application = application self._discovered_printer_by_ip_dict = dict( ) # type: Dict[str, DiscoveredPrinter] self._plugin_for_manual_device = None # type: Optional[OutputDevicePlugin] self._manual_device_address = "" self._manual_device_request_timeout_in_seconds = 5 # timeout for adding a manual device in seconds self._manual_device_request_timer = QTimer() self._manual_device_request_timer.setInterval( self._manual_device_request_timeout_in_seconds * 1000) self._manual_device_request_timer.setSingleShot(True) self._manual_device_request_timer.timeout.connect( self._onManualRequestTimeout) discoveredPrintersChanged = pyqtSignal() @pyqtSlot(str) def checkManualDevice(self, address: str) -> None: if self.hasManualDeviceRequestInProgress: Logger.log( "i", "A manual device request for address [%s] is still in progress, do nothing", self._manual_device_address) return priority_order = [ ManualDeviceAdditionAttempt.PRIORITY, ManualDeviceAdditionAttempt.POSSIBLE, ] # type: List[ManualDeviceAdditionAttempt] all_plugins_dict = self._application.getOutputDeviceManager( ).getAllOutputDevicePlugins() can_add_manual_plugins = [ item for item in filter( lambda plugin_item: plugin_item.canAddManualDevice(address) in priority_order, all_plugins_dict.values()) ] if not can_add_manual_plugins: Logger.log( "d", "Could not find a plugin to accept adding %s manually via address.", address) return plugin = max( can_add_manual_plugins, key=lambda p: priority_order.index(p.canAddManualDevice(address))) self._plugin_for_manual_device = plugin self._plugin_for_manual_device.addManualDevice( address, callback=self._onManualDeviceRequestFinished) self._manual_device_address = address self._manual_device_request_timer.start() self.hasManualDeviceRequestInProgressChanged.emit() @pyqtSlot() def cancelCurrentManualDeviceRequest(self) -> None: self._manual_device_request_timer.stop() if self._manual_device_address: if self._plugin_for_manual_device is not None: self._plugin_for_manual_device.removeManualDevice( self._manual_device_address, address=self._manual_device_address) self._manual_device_address = "" self._plugin_for_manual_device = None self.hasManualDeviceRequestInProgressChanged.emit() self.manualDeviceRequestFinished.emit(False) def _onManualRequestTimeout(self) -> None: Logger.log( "w", "Manual printer [%s] request timed out. Cancel the current request.", self._manual_device_address) self.cancelCurrentManualDeviceRequest() hasManualDeviceRequestInProgressChanged = pyqtSignal() @pyqtProperty(bool, notify=hasManualDeviceRequestInProgressChanged) def hasManualDeviceRequestInProgress(self) -> bool: return self._manual_device_address != "" manualDeviceRequestFinished = pyqtSignal(bool, arguments=["success"]) def _onManualDeviceRequestFinished(self, success: bool, address: str) -> None: self._manual_device_request_timer.stop() if address == self._manual_device_address: self._manual_device_address = "" self.hasManualDeviceRequestInProgressChanged.emit() self.manualDeviceRequestFinished.emit(success) @pyqtProperty("QVariantMap", notify=discoveredPrintersChanged) def discoveredPrintersByAddress(self) -> Dict[str, DiscoveredPrinter]: return self._discovered_printer_by_ip_dict @pyqtProperty("QVariantList", notify=discoveredPrintersChanged) def discoveredPrinters(self) -> List["DiscoveredPrinter"]: item_list = list(x for x in self._discovered_printer_by_ip_dict.values() if not parseBool(x.device.getProperty("temporary"))) # Split the printers into 2 lists and sort them ascending based on names. available_list = [] not_available_list = [] for item in item_list: if item.isUnknownMachineType or getattr(item.device, "clusterSize", 1) < 1: not_available_list.append(item) else: available_list.append(item) available_list.sort(key=lambda x: x.device.name) not_available_list.sort(key=lambda x: x.device.name) return available_list + not_available_list def addDiscoveredPrinter(self, ip_address: str, key: str, name: str, create_callback: Callable[[str], None], machine_type: str, device: "NetworkedPrinterOutputDevice") -> None: if ip_address in self._discovered_printer_by_ip_dict: Logger.log("e", "Printer with ip [%s] has already been added", ip_address) return discovered_printer = DiscoveredPrinter(ip_address, key, name, create_callback, machine_type, device, parent=self) self._discovered_printer_by_ip_dict[ip_address] = discovered_printer self.discoveredPrintersChanged.emit() def updateDiscoveredPrinter(self, ip_address: str, name: Optional[str] = None, machine_type: Optional[str] = None) -> None: if ip_address not in self._discovered_printer_by_ip_dict: Logger.log("w", "Printer with ip [%s] is not known", ip_address) return item = self._discovered_printer_by_ip_dict[ip_address] if name is not None: item.setName(name) if machine_type is not None: item.setMachineType(machine_type) def removeDiscoveredPrinter(self, ip_address: str) -> None: if ip_address not in self._discovered_printer_by_ip_dict: Logger.log( "w", "Key [%s] does not exist in the discovered printers list.", ip_address) return del self._discovered_printer_by_ip_dict[ip_address] self.discoveredPrintersChanged.emit() # A convenience function for QML to create a machine (GlobalStack) out of the given discovered printer. # This function invokes the given discovered printer's "create_callback" to do this. @pyqtSlot("QVariant") def createMachineFromDiscoveredPrinter( self, discovered_printer: "DiscoveredPrinter") -> None: discovered_printer.create_callback(discovered_printer.getKey())
class BeagleBoneHardware(QWidget): def __init__(self): super().__init__() self.allow_trigger = True # Set this to false to stop triggering the laser self.triggers_sent = 0 # Will record the number of trigger pulses sent before pulsing was discontinued self.pulse_count_target = None self.trigger_timer = QTimer() self.trigger_timer.timeout.connect(self.trigger_pulse) self.home_target_dialog = HomeTargetsDialog(self) # Define limits/constants for substrate motion # FIXME: All limit values are guesses and need to be set. self.sub_bottom = 0 self.sub_top = 40035 # Measured using a duty cycle test for full motion range. self.sub_position = 24000 self.sub_goal = None self.sub_dir = 'up' self.sub_steps_per_rev = 1000 self.sub_rps = 0.5 self.sub_delay_us = 3 self.current_target = 1 self.target_goal = None self.target_position = 0 self.target_pos_goal = None self.target_dir = 'ccw' self.target_steps_per_rev = 1000 self.target_rps = 2 self.target_delay_us = 1 # Define class variables for self.out_pins = { "trigger": "P8_17", "sub_dir": "P9_28", "sub_step": "P9_27", "target_dir": "P9_30", "target_step": "P9_29" } self.in_pins = {"sub_home": "P8_9", "aux": "P9_15"} self.hi_pins = {"sub_home_hi": "P8_10"} # Create timers for 50% duty non-blocking motors # Target Axis self.target_on_timer = QTimer() self.target_on_timer.setSingleShot(True) self.target_off_timer = QTimer() self.target_off_timer.setSingleShot(True) # Substrate Axis self.sub_on_timer = QTimer() self.sub_on_timer.setSingleShot(True) self.sub_off_timer = QTimer() self.sub_off_timer.setSingleShot(True) self.setup_pins() def setup_pins(self): # Set up pins (all output pins default to low (0)) for key in self.out_pins: GPIO.setup(self.out_pins[key], GPIO.OUT) GPIO.output(self.out_pins[key], GPIO.LOW) for key in self.in_pins: GPIO.setup(self.in_pins[key], GPIO.IN) for key in self.hi_pins: GPIO.setup(self.hi_pins[key], GPIO.OUT) GPIO.output(self.hi_pins[key], GPIO.HIGH) def start_pulsing(self, reprate, pulse_count=None): # Reset allow_trigger so that we don't end up breaking things/needing to restart the GUI on deposition cancel. print('Starting pulsing') rep_time = int(1000 / int(reprate)) self.trigger_timer.start(rep_time) print('Started Timer with timeout of {}'.format(rep_time)) self.pulse_count_target = pulse_count def stop_pulsing(self): self.trigger_timer.stop() print('Stop signal sent to timer') def trigger_pulse(self): if self.allow_trigger: GPIO.output(self.out_pins["trigger"], GPIO.HIGH) sleep(0.001) GPIO.output(self.out_pins["trigger"], GPIO.LOW) self.triggers_sent += 1 if self.pulse_count_target is not None: print('Checking Pulse ({}) vs triggers sent ({})'.format( self.triggers_sent, self.pulse_count_target)) if self.triggers_sent <= self.pulse_count_target: self.allow_trigger = False self.trigger_timer.stop() print( 'Stop signal sent. (Number of triggers ({}) >= target pulse count ({})' .format(self.triggers_sent, self.pulse_count_target)) def reset_trigger(self): print('Initiating trigger reset. Triggers sent: {}, Allow Trigger: {}'. format(self.triggers_sent, self.allow_trigger)) self.allow_trigger = True self.triggers_sent = 0 print('Triggering reset. Triggers sent: {}, Allow Trigger: {}'.format( self.triggers_sent, self.allow_trigger)) def home_sub(self): self.sub_goal = 'home' if not GPIO.input(self.in_pins['sub_home']): if self.get_sub_dir() != 'up': self.set_sub_dir('up') self.step_sub() def move_sub_to(self, position): self.sub_goal = position if self.sub_goal > self.sub_position: self.set_sub_dir('up') elif self.sub_goal < self.sub_position: self.set_sub_dir('down') self.step_sub() def step_sub(self): # Define function for step parts (NOTE: driver sends step on GPIO.LOW) def step_start(): if GPIO.input(self.in_pins['sub_home']): print( 'Started {} steps away from home. New home position set.'. format(abs(self.sub_position))) self.sub_goal = 0 self.sub_position = 0 return elif not GPIO.input(self.in_pins['sub_home']): GPIO.output(self.out_pins['sub_step'], GPIO.HIGH) self.sub_off_timer.start(self.sub_delay_us) def step_finish(): if self.get_sub_dir() == 'cw': self.sub_position += 1 elif self.get_sub_dir() == 'ccw': self.sub_position -= 1 GPIO.output(self.out_pins['sub_step'], GPIO.LOW) if self.sub_goal == 'home' and not GPIO.input( self.in_pins['sub_home']): self.sub_on_timer.start(self.sub_delay_us) # Connect Timeouts self.sub_on_timer.timeout.connect(step_start) self.sub_off_timer.timeout.connect(step_finish) if self.sub_goal is None: if not GPIO.input( self.in_pins['sub_home']) or self.sub_position > 0: self.sub_on_timer.start(self.sub_delay_us) elif self.sub_position == 0 or GPIO.input( self.in_pins['sub_home']): # If the substrate is at end of range warn user and offer to rehome stage if there are issues. max_range = QMessageBox.question( QWidget, 'Substrate End of Range', 'Press ok to continue or press reset to home the substrate', QMessageBox.Ok | QMessageBox.Reset, QMessageBox.Ok) if max_range == QMessageBox.Ok: pass elif max_range == QMessageBox.Reset: self.home_sub() elif self.sub_goal is not None: if self.sub_goal != self.sub_position or self.sub_goal == 'home': self.sub_on_timer.start(self.sub_delay_us) elif self.sub_goal == self.sub_position: # Clear the goal position if the substrate is in that position self.sub_goal = None def set_sub_dir(self, direction): if direction.lower() == "up": # Set direction pin so that the substrate will be driven up (CCW) self.sub_dir = 'up' GPIO.output(self.out_pins['sub_dir'], GPIO.LOW) elif direction.lower() == "down": # Set direction pin so that the substrate will be driven down (CW) self.sub_dir = 'down' GPIO.output(self.out_pins['sub_dir'], GPIO.HIGH) else: print( 'Invalid direction argument for the substrate motor supplied.') def get_sub_dir(self): return self.sub_dir def set_sub_speed(self, rps): self.sub_rps = rps # Calculate delay per step from speed self.sub_delay_us = round( ((self.sub_rps**-1) * (self.sub_steps_per_rev**-1)) / 2) def stop_sub(self): self.sub_goal = None def home_targets(self): self.home_target_dialog.exec_() if self.home_target_dialog.result() == QDialog.Accepted: self.target_position = 0 self.current_target = 1 target_home_info = QMessageBox.information( self, 'Home Set', 'New target carousel home position set', QMessageBox.Ok, QMessageBox.Ok) elif self.home_target_dialog.result() == QDialog.Rejected: target_home_info = QMessageBox.warning( self, 'Home Canceled', 'Target carousel home cancelled by user.', QMessageBox.Ok, QMessageBox.Ok) def move_to_target(self, target_goal): self.target_goal = target_goal if self.target_goal != self.current_target: delta_pos = abs(target_goal - self.current_target) if delta_pos > 3: delta_pos = ((delta_pos - 3) * 2) - delta_pos if delta_pos < 0: self.set_target_dir('cw') elif delta_pos > 0: self.set_target_dir('ccw') else: print('Target {} already selected'.format(self.target_goal)) return self.step_target() def step_target(self): # Define function for step parts (NOTE: driver sends step on GPIO.LOW) def step_start(): GPIO.output(self.out_pins['target_step'], GPIO.HIGH) self.target_off_timer.start(self.target_delay_us) def step_finish(): if self.get_target_dir() == 'cw': self.target_position += 1 elif self.get_target_dir() == 'ccw': self.target_position -= 1 self.target_position = self.target_position % 6000 GPIO.output(self.out_pins['target_step'], GPIO.LOW) # Connect Timeouts self.target_on_timer.timeout.connect(step_start) self.target_off_timer.timeout.connect(step_finish) if self.target_goal is None and self.target_pos_goal is None: self.target_on_timer.start(self.target_delay_us) elif self.target_goal is not None: # If there is a goal target # Set the target position goal if that has not already been done if self.target_pos_goal is None: self.target_pos_goal = 1000 * self.target_goal if self.target_position != self.target_pos_goal: self.target_on_timer.start(self.target_delay_us) # FIXME: Come up with logic so that the targets rotate the CW or CCW # direction to minimize positioning time: WAIT DID I Already do that in the move to? elif self.target_position == self.target_pos_goal: self.target_pos_goal = None self.target_goal = None self.target_step_timer.stop() def set_target_dir(self, direction): if direction.lower() == "ccw": # Set direction pin so that the substrate will be driven up (CCW) self.target_dir = 'ccw' GPIO.output(self.out_pins['target_dir'], GPIO.LOW) elif direction.lower() == "cw": # Set direction pin so that the substrate will be driven down (CW) self.target_dir = 'cw' GPIO.output(self.out_pins['target_dir'], GPIO.HIGH) else: print('Invalid direction argument for the target motor supplied') def set_target_speed(self, rps): self.target_rps = rps # Calculate delay per step from speed self.target_delay_us = round( ((self.target_rps**-1) * (self.target_steps_per_rev**-1)) / 2) def get_target_dir(self): return self.target_dir def stop_target(self): self.target_goal = None self.target_pos_goal = None def __del__(self): GPIO.cleanup()
class SetColumnsDialog(QtWidgets.QDialog, FORM_CLASS): def __init__(self, parent=None): """Constructor.""" super(SetColumnsDialog, self).__init__(parent) #self.setupUi(self) # Set up the user interface from Designer. # After setupUI you can access any designer object by doing # self.<objectname>, and you can use autoconnect slots - see # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html # #widgets-and-dialogs-with-auto-connect self.setupUi(self) p = self.palette() p.setColor(self.backgroundRole(), Qt.white) self.setPalette(p) self.resize(900,700) # attributes self.filename = None self.progress = QProgressBar(self) self.progress.setGeometry(20, 550, 480, 20) self.progress.setRange(0,0) self.progress.setVisible(False) self.progressInfo = QLabel(self) self.progressInfo.setText("Generando el reporte, por favor espere...") self.progressInfo.move(20, 570) newfont = QFont("Arial", 10) newfont.setItalic(True) self.progressInfo.setFont(newfont) self.progressInfo.setVisible(False) def showDialog(self, layer, columnList, fonafifoUrl): self.layer=layer self.columnList=columnList self.columnList.clear() self.fonafifoUrl = fonafifoUrl MAX_FOOTER = 600 # FONAFIFO logo pic = QLabel(self) pic.setGeometry(600, MAX_FOOTER-30, 150, 50) pixmap = QPixmap() pixmap.load(fonafifoUrl); pic.setPixmap(pixmap) self.labelHeader = QLabel(self) self.labelHeader.setText("Selección de Criterios") self.labelHeader.setStyleSheet('color: #076F00') self.labelHeader.move(10, 20) newfont = QFont("Times", 20, QFont.Bold) self.labelHeader.setFont(newfont) self.frame = QFrame(self) self.frame.setFrameShape(QFrame.HLine) self.frame.setFrameShadow(QFrame.Sunken) self.frame.move(5,55); self.frame.resize(1955,5) self.buttonDescargar = QPushButton('Cargar Tabla', self) self.buttonDescargar.move(20, 590) self.buttonDescargar.resize(200, 30) self.buttonDescargar.clicked.connect(self.looping) self.buttonCerrar = QPushButton('Cerrar', self) self.buttonCerrar.move(270, 590) self.buttonCerrar.resize(200, 30) self.buttonCerrar.clicked.connect(self.close) X = 1500 Y = 200 HEIGHT=30 WIDTH_LABEL=400 INCREMENT_Y=20 self.str_TOTAL = 'Total'; self.str_AMPHIBIA = 'Amphibia'; self.str_AVES = 'Aves'; self.str_MAMMALIA = 'Mammalia'; self.str_PLANTAE = 'Plantae'; self.str_REPTILIA = 'Reptilia'; self.str_REGISTROS_DE_PRESENCIA = 'Registros de presencia'; self.str_AREAS_DE_DISTRIBUCION = 'Áreas de distribución'; self.str_AMENAZADAS_UICN = 'Amenazadas IUCN'; self.str_EN_PELIGRO_DE_EXTINCION = 'En peligro de extinción'; self.str_CON_POBLACION_REDUCIDA = 'Con población reducida'; self.str_VEDADA = 'Vedadas'; self.str_CONT = ' --> '; self.list_class_pre = QListWidget(self) self.list_class_pre.addItem(self.str_TOTAL) self.list_class_pre.addItem(self.str_AMPHIBIA) self.list_class_pre.addItem(self.str_AVES) self.list_class_pre.addItem(self.str_MAMMALIA) self.list_class_pre.addItem(self.str_PLANTAE) self.list_class_pre.addItem(self.str_REPTILIA) self.list_class_pre.setCurrentRow(0) self.list_class_pre.setGeometry(10, 120, 150, 120) self.list_tipo_pre = QListWidget(self) self.list_tipo_pre.addItem(self.str_REGISTROS_DE_PRESENCIA) self.list_tipo_pre.addItem(self.str_AREAS_DE_DISTRIBUCION) self.list_tipo_pre.setCurrentRow(0) self.list_tipo_pre.setGeometry(10, 250, 150, 90) self.list_amenazas_pre = QListWidget(self) self.list_amenazas_pre.addItem("--") self.list_amenazas_pre.addItem(self.str_AMENAZADAS_UICN) self.list_amenazas_pre.addItem(self.str_EN_PELIGRO_DE_EXTINCION) self.list_amenazas_pre.addItem(self.str_CON_POBLACION_REDUCIDA) self.list_amenazas_pre.addItem(self.str_VEDADA) self.list_amenazas_pre.setCurrentRow(0) self.list_amenazas_pre.setGeometry(10, 350, 150, 90) self.botton_list_include = QPushButton(">>", self) self.botton_list_include.setGeometry(200, 120, 50, 50) self.botton_list_include.clicked.connect(self.chooseClassInclude) self.botton_list_exclude = QPushButton("<<", self) self.botton_list_exclude.setGeometry(200, 160, 50, 50) self.botton_list_exclude.clicked.connect(self.chooseClassExclude) self.list_class_post = QListWidget(self) self.list_class_post.setGeometry(300, 120, WIDTH_LABEL, 350) self.seleccionClass = ""; self.seleccionTipo = ""; self.seleccionAmenaza = ""; self.checkbox_presencia_total_especies = QCheckBox("Riqueza total de especies por registros de presencia", self) self.checkbox_presencia_total_especies.setGeometry(X,Y,WIDTH_LABEL,HEIGHT) Y += INCREMENT_Y self.checkbox_presencia_total_especies_amenazadas = QCheckBox("Riqueza total de especies por registros de presencia - Amenazadas UICN", self) self.checkbox_presencia_total_especies_amenazadas.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_presencia_total_especies_amenazadas_lcvs = QCheckBox("Riqueza total de especies por registros de presencia - Amenazadas LCVS", self) self.checkbox_presencia_total_especies_amenazadas_lcvs.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y Y += INCREMENT_Y self.checkbox_presencia_total_mammalia = QCheckBox("Riqueza total de MAMMALIA por registros de presencia", self) self.checkbox_presencia_total_mammalia.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_presencia_total_mammalia_amenazadas = QCheckBox("Riqueza total de MAMMALIA por registros de presencia - Amenazadas UICN", self) self.checkbox_presencia_total_mammalia_amenazadas.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_presencia_total_mammalia_amenazadas_lcvs = QCheckBox("Riqueza total de MAMMALIA por registros de presencia - Amenazadas LCVS", self) self.checkbox_presencia_total_mammalia_amenazadas_lcvs.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y Y += INCREMENT_Y self.checkbox_presencia_total_aves = QCheckBox("Riqueza total de AVES por registros de presencia", self) self.checkbox_presencia_total_aves.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_presencia_total_aves_amenazadas = QCheckBox("Riqueza total de AVES por registros de presencia - Amenazadas UICN", self) self.checkbox_presencia_total_aves_amenazadas.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_presencia_total_aves_amenazadas_lcvs = QCheckBox("Riqueza total de AVES por registros de presencia - Amenazadas LCVS", self) self.checkbox_presencia_total_aves_amenazadas_lcvs.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y Y += INCREMENT_Y self.checkbox_presencia_total_reptilia = QCheckBox("Riqueza total de REPTILIA por registros de presencia", self) self.checkbox_presencia_total_reptilia.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_presencia_total_reptilia_amenazadas = QCheckBox("Riqueza total de REPTILIA por registros de presencia - Amenazadas UICN", self) self.checkbox_presencia_total_reptilia_amenazadas.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_presencia_total_reptilia_amenazadas_lcvs = QCheckBox("Riqueza total de REPTILIA por registros de presencia - Amenazadas LCVS", self) self.checkbox_presencia_total_reptilia_amenazadas_lcvs.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y Y += INCREMENT_Y self.checkbox_presencia_total_amphibia = QCheckBox("Riqueza total de AMPHIBIA por registros de presencia", self) self.checkbox_presencia_total_amphibia.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_presencia_total_amphibia_amenazadas = QCheckBox("Riqueza total de AMPHIBIA por registros de presencia - Amenazadas UICN", self) self.checkbox_presencia_total_amphibia_amenazadas.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_presencia_total_amphibia_amenazadas_lcvs = QCheckBox("Riqueza total de AMPHIBIA por registros de presencia - Amenazadas LCVS", self) self.checkbox_presencia_total_amphibia_amenazadas_lcvs.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y Y += INCREMENT_Y self.checkbox_presencia_total_trees = QCheckBox("Riqueza total de PLANTAE por registros de presencia", self) self.checkbox_presencia_total_trees.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_presencia_total_trees_amenazadas = QCheckBox("Riqueza total de PLANTAE por registros de presencia - Amenazadas UICN", self) self.checkbox_presencia_total_trees_amenazadas.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_presencia_total_trees_amenazadas_lcvs = QCheckBox("Riqueza total de PLANTAE por registros de presencia - Amenazadas LCVS", self) self.checkbox_presencia_total_trees_amenazadas_lcvs.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y = 70 X = 1450 self.checkbox_distribucion_total_especies = QCheckBox("Riqueza total de especies por áreas de distribución", self) self.checkbox_distribucion_total_especies.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_distribucion_total_especies_amenazadas = QCheckBox("Riqueza total de especies por áreas de distribución - Amenazadas UICN", self) self.checkbox_distribucion_total_especies_amenazadas.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_distribucion_total_especies_amenazadas_lcvs = QCheckBox("Riqueza total de especies por áreas de distribución - Amenazadas LCVS", self) self.checkbox_distribucion_total_especies_amenazadas_lcvs.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y Y += INCREMENT_Y self.checkbox_distribucion_total_mammalia = QCheckBox("Riqueza total de MAMMALIA por áreas de distribución", self) self.checkbox_distribucion_total_mammalia.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_distribucion_total_mammalia_amenazadas = QCheckBox("Riqueza total de MAMMALIA por áreas de distribución - Amenazadas UICN", self) self.checkbox_distribucion_total_mammalia_amenazadas.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_distribucion_total_mammalia_amenazadas_lcvs = QCheckBox("Riqueza total de MAMMALIA por áreas de distribución - Amenazadas LCVS", self) self.checkbox_distribucion_total_mammalia_amenazadas_lcvs.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y Y += INCREMENT_Y self.checkbox_distribucion_total_aves = QCheckBox("Riqueza total de AVES por áreas de distribución", self) self.checkbox_distribucion_total_aves.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_distribucion_total_aves_amenazadas = QCheckBox("Riqueza total de AVES por áreas de distribución - Amenazadas UICN", self) self.checkbox_distribucion_total_aves_amenazadas.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_distribucion_total_aves_amenazadas_lcvs = QCheckBox("Riqueza total de AVES por áreas de distribución - Amenazadas LCVS", self) self.checkbox_distribucion_total_aves_amenazadas_lcvs.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y Y += INCREMENT_Y self.checkbox_distribucion_total_reptilia = QCheckBox("Riqueza total de REPTILIA por áreas de distribución", self) self.checkbox_distribucion_total_reptilia.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_distribucion_total_reptilia_amenazadas = QCheckBox("Riqueza total de REPTILIA por áreas de distribución - Amenazadas UICN", self) self.checkbox_distribucion_total_reptilia_amenazadas.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_distribucion_total_reptilia_amenazadas_lcvs = QCheckBox("Riqueza total de REPTILIA por áreas de distribución - Amenazadas LCVS", self) self.checkbox_distribucion_total_reptilia_amenazadas_lcvs.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y Y += INCREMENT_Y self.checkbox_distribucion_total_amphibia = QCheckBox("Riqueza total de AMPHIBIA por áreas de distribución", self) self.checkbox_distribucion_total_amphibia.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_distribucion_total_amphibia_amenazadas = QCheckBox("Riqueza total de AMPHIBIA por áreas de distribución - Amenazadas UICN", self) self.checkbox_distribucion_total_amphibia_amenazadas.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_distribucion_total_amphibia_amenazadas_lcvs = QCheckBox("Riqueza total de AMPHIBIA por áreas de distribución - Amenazadas LCVS", self) self.checkbox_distribucion_total_amphibia_amenazadas_lcvs.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y Y += INCREMENT_Y self.checkbox_distribucion_total_trees = QCheckBox("Riqueza total de PLANTAE por áreas de distribución", self) self.checkbox_distribucion_total_trees.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_distribucion_total_trees_amenazadas = QCheckBox("Riqueza total de PLANTAE por áreas de distribución - Amenazadas UICN", self) self.checkbox_distribucion_total_trees_amenazadas.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) Y += INCREMENT_Y self.checkbox_distribucion_total_trees_amenazadas_lcvs = QCheckBox("Riqueza total de PLANTAE por áreas de distribución - Amenazadas LCVS", self) self.checkbox_distribucion_total_trees_amenazadas_lcvs.setGeometry(X, Y, WIDTH_LABEL, HEIGHT) self.show() def looping(self): self.progressInfo.setVisible(True) self.progress.setVisible(True) self.looptimer = QTimer() self.looptimer.setSingleShot(True) self.looptimer.timeout.connect(self.cargarTabla) self.looptimer.start(5000) def rellenarText(self, strClass): if (self.seleccionClass == strClass): if (self.seleccionTipo == self.str_REGISTROS_DE_PRESENCIA): const_REG_PRESENCIA = strClass + self.str_CONT + self.str_REGISTROS_DE_PRESENCIA if (self.seleccionAmenaza == '--'): self.list_class_post.addItem(const_REG_PRESENCIA) if (self.seleccionAmenaza == self.str_AMENAZADAS_UICN): self.list_class_post.addItem(const_REG_PRESENCIA + self.str_CONT + self.str_AMENAZADAS_UICN); if (self.seleccionAmenaza == self.str_EN_PELIGRO_DE_EXTINCION): self.list_class_post.addItem(const_REG_PRESENCIA + self.str_CONT + self.str_EN_PELIGRO_DE_EXTINCION); if (self.seleccionAmenaza == self.str_CON_POBLACION_REDUCIDA): self.list_class_post.addItem(const_REG_PRESENCIA + self.str_CONT + self.str_CON_POBLACION_REDUCIDA); if (self.seleccionAmenaza == self.str_VEDADA): if((self.seleccionClass == self.str_PLANTAE) | (self.seleccionClass == self.str_TOTAL)): self.list_class_post.addItem(const_REG_PRESENCIA + self.str_CONT + self.str_VEDADA); else: QMessageBox.about(self, 'Alerta!', 'La categoria "VEDADA" es exclusiva para Plantae'); self.agregarColumnas(strClass, self.seleccionAmenaza, "occurrence"); if (self.seleccionTipo == self.str_AREAS_DE_DISTRIBUCION): const_REG_DISTRIBUCION = strClass + self.str_CONT + self.str_AREAS_DE_DISTRIBUCION if (self.seleccionAmenaza == '--'): self.list_class_post.addItem(const_REG_DISTRIBUCION) if (self.seleccionAmenaza == self.str_AMENAZADAS_UICN): self.list_class_post.addItem(const_REG_DISTRIBUCION + self.str_CONT + self.str_AMENAZADAS_UICN); if (self.seleccionAmenaza == self.str_EN_PELIGRO_DE_EXTINCION): self.list_class_post.addItem(const_REG_DISTRIBUCION + self.str_CONT + self.str_EN_PELIGRO_DE_EXTINCION); if (self.seleccionAmenaza == self.str_CON_POBLACION_REDUCIDA): self.list_class_post.addItem(const_REG_DISTRIBUCION + self.str_CONT + self.str_CON_POBLACION_REDUCIDA); if (self.seleccionAmenaza == self.str_VEDADA): if((self.seleccionClass == self.str_PLANTAE) | (self.seleccionClass == self.str_TOTAL)): self.list_class_post.addItem(const_REG_DISTRIBUCION + self.str_CONT + self.str_VEDADA); else: QMessageBox.about(self, 'Alerta!', 'La categoria "VEDADA" es exclusiva para Plantae'); self.agregarColumnas(strClass, self.seleccionAmenaza, "distribution"); def agregarColumnas(self, strClass, strStatus, strType): if (strClass == self.str_TOTAL): if (strStatus == '--'): self.columnList.append("all_" + strType); self.columnList.append("all_" + strType + "_names"); if (strStatus == self.str_AMENAZADAS_UICN): self.columnList.append("all_iucn_threatened_" + strType); self.columnList.append("all_iucn_threatened_" + strType + "_names"); if (strStatus == self.str_EN_PELIGRO_DE_EXTINCION): self.columnList.append("all_lcvs_pe_" + strType); self.columnList.append("all_lcvs_pe_" + strType + "_names"); if (strStatus == self.str_CON_POBLACION_REDUCIDA): self.columnList.append("all_lcvs_pr_" + strType); self.columnList.append("all_lcvs_pr_" + strType + "_names"); if (strStatus == self.str_VEDADA): self.columnList.append("all_lcvs_ve_" + strType); self.columnList.append("all_lcvs_ve_" + strType + "_names"); if (strClass == self.str_AMPHIBIA): if (strStatus == '--'): self.columnList.append("amphibia_" + strType); self.columnList.append("amphibia_" + strType + "_names"); if (strStatus == self.str_AMENAZADAS_UICN): self.columnList.append("amphibia_iucn_threatened_" + strType); self.columnList.append("amphibia_iucn_threatened_" + strType + "_names"); if (strStatus == self.str_EN_PELIGRO_DE_EXTINCION): self.columnList.append("amphibia_lcvs_pe_" + strType); self.columnList.append("amphibia_lcvs_pe_" + strType + "_names"); if (strStatus == self.str_CON_POBLACION_REDUCIDA): self.columnList.append("amphibia_lcvs_pr_" + strType); self.columnList.append("amphibia_lcvs_pr_" + strType + "_names"); if (strClass == self.str_AVES): if (strStatus == '--'): self.columnList.append("aves_" + strType); self.columnList.append("aves_occurrence_names"); if (strStatus == self.str_AMENAZADAS_UICN): self.columnList.append("aves_iucn_threatened_" + strType); self.columnList.append("aves_iucn_threatened_" + strType + "_names"); if (strStatus == self.str_EN_PELIGRO_DE_EXTINCION): self.columnList.append("aves_lcvs_pe_" + strType); self.columnList.append("aves_lcvs_pe_" + strType + "_names"); if (strStatus == self.str_CON_POBLACION_REDUCIDA): self.columnList.append("aves_lcvs_pr_" + strType); self.columnList.append("aves_lcvs_pr_" + strType + "_names"); if (strClass == self.str_MAMMALIA): if (strStatus == '--'): self.columnList.append("mammalia_" + strType); self.columnList.append("mammalia_" + strType + "_names"); if (strStatus == self.str_AMENAZADAS_UICN): self.columnList.append("mammalia_iucn_threatened_" + strType); self.columnList.append("mammalia_iucn_threatened_" + strType + "_names"); if (strStatus == self.str_EN_PELIGRO_DE_EXTINCION): self.columnList.append("mammalia_lcvs_pe_" + strType); self.columnList.append("mammalia_lcvs_pe_" + strType + "_names"); if (strStatus == self.str_CON_POBLACION_REDUCIDA): self.columnList.append("mammalia_lcvs_pr_" + strType); self.columnList.append("mammalia_lcvs_pr_" + strType + "_names"); if (strClass == self.str_PLANTAE): if (strStatus == '--'): self.columnList.append("plantae_" + strType); self.columnList.append("plantae_" + strType + "_names"); if (strStatus == self.str_AMENAZADAS_UICN): self.columnList.append("plantae_iucn_threatened_" + strType); self.columnList.append("plantae_iucn_threatened_" + strType + "_names"); if (strStatus == self.str_EN_PELIGRO_DE_EXTINCION): self.columnList.append("plantae_lcvs_pe_" + strType); self.columnList.append("plantae_lcvs_pe_" + strType + "_names"); if (strStatus == self.str_CON_POBLACION_REDUCIDA): self.columnList.append("plantae_lcvs_pr_" + strType); self.columnList.append("plantae_lcvs_pr_" + strType + "_names"); if (strStatus == self.str_VEDADA): self.columnList.append("plantae_lcvs_ve_" + strType); self.columnList.append("plantae_lcvs_ve_" + strType + "_names"); if (strClass == self.str_REPTILIA): if (strStatus == '--'): self.columnList.append("reptilia_" + strType); self.columnList.append("reptilia_" + strType + "_names"); if (strStatus == self.str_AMENAZADAS_UICN): self.columnList.append("reptilia_iucn_threatened_" + strType); self.columnList.append("reptilia_iucn_threatened_" + strType + "_names"); if (strStatus == self.str_EN_PELIGRO_DE_EXTINCION): self.columnList.append("reptilia_lcvs_pe_" + strType); self.columnList.append("reptilia_lcvs_pe_" + strType + "_names"); if (strStatus == self.str_CON_POBLACION_REDUCIDA): self.columnList.append("reptilia_lcvs_pr_" + strType); self.columnList.append("reptilia_lcvs_pr_" + strType + "_names"); def chooseClassInclude(self): for selectedItem in self.list_class_pre.selectedItems(): self.seleccionClass = self.list_class_pre.currentItem().text(); self.seleccionTipo = self.list_tipo_pre.currentItem().text(); self.seleccionAmenaza = self.list_amenazas_pre.currentItem().text(); self.rellenarText(self.str_TOTAL); self.rellenarText(self.str_AMPHIBIA); self.rellenarText(self.str_AVES); self.rellenarText(self.str_MAMMALIA); self.rellenarText(self.str_PLANTAE); self.rellenarText(self.str_REPTILIA); #self.list_class_post.addItem(self.list_class_pre.currentItem().text()) #self.list_class_pre.takeItem(self.list_class_pre.row(selectedItem)) def chooseClassExclude(self): for selectedItem in self.list_class_post.selectedItems(): self.list_class_pre.addItem(self.list_class_post.currentItem().text()) self.list_class_post.takeItem(self.list_class_post.row(selectedItem)) def cargarTabla(self): ############################################################################### ### VALIDACIONES ############################################################################### if self.checkbox_presencia_total_especies.isChecked(): self.columnList.append("spp_all_richness_occurrence") self.columnList.append("spp_all_richness_occurrence_names") if self.checkbox_presencia_total_especies_amenazadas.isChecked(): self.columnList.append("spp_all_threatened_richness_occurrence") self.columnList.append("spp_all_threatened_richness_occurrence_names") if self.checkbox_presencia_total_especies_amenazadas_lcvs.isChecked(): self.columnList.append("spp_all_lcvs_richness_occurrence") self.columnList.append("spp_all_lcvs_richness_occurrence_names") if self.checkbox_presencia_total_mammalia.isChecked(): self.columnList.append("spp_mammalia_richness_occurrence") self.columnList.append("spp_mammalia_richness_occurrence_names") if self.checkbox_presencia_total_mammalia_amenazadas.isChecked(): self.columnList.append("spp_mammalia_threatened_richness_occurrence") self.columnList.append("spp_mammalia_threatened_richness_occurrence_names") if self.checkbox_presencia_total_mammalia_amenazadas_lcvs.isChecked(): self.columnList.append("spp_mammalia_lcvs_richness_occurrence") self.columnList.append("spp_mammalia_lcvs_richness_occurrence_names") if self.checkbox_presencia_total_aves.isChecked(): self.columnList.append("spp_aves_richness_occurrence") self.columnList.append("spp_aves_richness_occurrence_names") if self.checkbox_presencia_total_aves_amenazadas.isChecked(): self.columnList.append("spp_aves_threatened_richness_occurrence") self.columnList.append("spp_aves_threatened_richness_occurrence_names") if self.checkbox_presencia_total_aves_amenazadas_lcvs.isChecked(): self.columnList.append("spp_aves_lcvs_richness_occurrence") self.columnList.append("spp_aves_lcvs_richness_occurrence_names") if self.checkbox_presencia_total_reptilia.isChecked(): self.columnList.append("spp_reptilia_richness_occurrence") self.columnList.append("spp_reptilia_richness_occurrence_names") if self.checkbox_presencia_total_reptilia_amenazadas.isChecked(): self.columnList.append("spp_reptilia_threatened_richness_occurrence") self.columnList.append("spp_reptilia_threatened_richness_occurrence_names") if self.checkbox_presencia_total_reptilia_amenazadas_lcvs.isChecked(): self.columnList.append("spp_reptilia_lcvs_richness_occurrence") self.columnList.append("spp_reptilia_lcvs_richness_occurrence_names") if self.checkbox_presencia_total_amphibia.isChecked(): self.columnList.append("spp_amphibia_richness_occurrence") self.columnList.append("spp_amphibia_richness_occurrence_names") if self.checkbox_presencia_total_amphibia_amenazadas.isChecked(): self.columnList.append("spp_amphibia_threatened_richness_occurrence") self.columnList.append("spp_amphibia_threatened_richness_occurrence_names") if self.checkbox_presencia_total_amphibia_amenazadas_lcvs.isChecked(): self.columnList.append("spp_amphibia_lcvs_richness_occurrence") self.columnList.append("spp_amphibia_lcvs_richness_occurrence_names") if self.checkbox_presencia_total_trees.isChecked(): self.columnList.append("spp_trees_richness_occurrence") self.columnList.append("spp_trees_richness_occurrence_names") if self.checkbox_presencia_total_trees_amenazadas.isChecked(): self.columnList.append("spp_trees_threatened_richness_occurrence") self.columnList.append("spp_trees_threatened_richness_occurrence_names") if self.checkbox_presencia_total_trees_amenazadas_lcvs.isChecked(): self.columnList.append("spp_trees_lcvs_richness_occurrence") self.columnList.append("spp_trees_lcvs_richness_occurrence_names") # DISTRIBUTION if self.checkbox_distribucion_total_especies.isChecked(): self.columnList.append("spp_all_richness_distribution") self.columnList.append("spp_all_richness_distribution_names") if self.checkbox_distribucion_total_especies_amenazadas.isChecked(): self.columnList.append("spp_all_threatened_richness_distribution") self.columnList.append("spp_all_threatened_richness_distribution_names") if self.checkbox_distribucion_total_especies_amenazadas_lcvs.isChecked(): self.columnList.append("spp_all_lcvs_richness_distribution") self.columnList.append("spp_all_lcvs_richness_distribution_names") if self.checkbox_distribucion_total_mammalia.isChecked(): self.columnList.append("spp_mammalia_richness_distribution") self.columnList.append("spp_mammalia_richness_distribution_names") if self.checkbox_distribucion_total_mammalia_amenazadas.isChecked(): self.columnList.append("spp_mammalia_threatened_richness_distribution") self.columnList.append("spp_mammalia_threatened_richness_distribution_names") if self.checkbox_distribucion_total_mammalia_amenazadas_lcvs.isChecked(): self.columnList.append("spp_mammalia_lcvs_richness_distribution") self.columnList.append("spp_mammalia_lcvs_richness_distribution_names") if self.checkbox_distribucion_total_aves.isChecked(): self.columnList.append("spp_aves_richness_distribution") self.columnList.append("spp_aves_richness_distribution_names") if self.checkbox_distribucion_total_aves_amenazadas.isChecked(): self.columnList.append("spp_aves_threatened_richness_distribution") self.columnList.append("spp_aves_threatened_richness_distribution_names") if self.checkbox_distribucion_total_aves_amenazadas_lcvs.isChecked(): self.columnList.append("spp_aves_lcvs_richness_distribution") self.columnList.append("spp_aves_lcvs_richness_distribution_names") if self.checkbox_distribucion_total_reptilia.isChecked(): self.columnList.append("spp_reptilia_richness_distribution") self.columnList.append("spp_reptilia_richness_distribution_names") if self.checkbox_distribucion_total_reptilia_amenazadas.isChecked(): self.columnList.append("spp_reptilia_threatened_richness_distribution") self.columnList.append("spp_reptilia_threatened_richness_distribution_names") if self.checkbox_distribucion_total_reptilia_amenazadas_lcvs.isChecked(): self.columnList.append("spp_reptilia_lcvs_richness_distribution") self.columnList.append("spp_reptilia_lcvs_richness_distribution_names") if self.checkbox_distribucion_total_amphibia.isChecked(): self.columnList.append("spp_amphibia_richness_distribution") self.columnList.append("spp_amphibia_richness_distribution_names") if self.checkbox_distribucion_total_amphibia_amenazadas.isChecked(): self.columnList.append("spp_amphibia_threatened_richness_distribution") self.columnList.append("spp_amphibia_threatened_richness_distribution_names") if self.checkbox_distribucion_total_amphibia_amenazadas_lcvs.isChecked(): self.columnList.append("spp_amphibia_lcvs_richness_distribution") self.columnList.append("spp_amphibia_lcvs_richness_distribution_names") if self.checkbox_distribucion_total_trees.isChecked(): self.columnList.append("spp_trees_richness_distribution") self.columnList.append("spp_trees_richness_distribution_names") if self.checkbox_distribucion_total_trees_amenazadas.isChecked(): self.columnList.append("spp_trees_threatened_richness_distribution") self.columnList.append("spp_trees_threatened_richness_distribution_names") if self.checkbox_distribucion_total_trees_amenazadas_lcvs.isChecked(): self.columnList.append("spp_trees_lcvs_richness_distribution") self.columnList.append("spp_trees_lcvs_richness_distribution_names") self.dlgIdentificarPoligono = IdentifyToolDialog() self.dlgIdentificarPoligono.showDialog(self.layer, self.columnList, self.progress, self.progressInfo, self.fonafifoUrl)
class ElectrumGui(BaseElectrumGui, Logger): network_dialog: Optional['NetworkDialog'] lightning_dialog: Optional['LightningDialog'] watchtower_dialog: Optional['WatchtowerDialog'] @profiler def __init__(self, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'): set_language(config.get('language', get_default_language())) BaseElectrumGui.__init__(self, config=config, daemon=daemon, plugins=plugins) Logger.__init__(self) self.logger.info( f"Qt GUI starting up... Qt={QtCore.QT_VERSION_STR}, PyQt={QtCore.PYQT_VERSION_STR}" ) # Uncomment this call to verify objects are being properly # GC-ed when windows are closed #network.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer, # ElectrumWindow], interval=5)]) QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"): QtCore.QCoreApplication.setAttribute( QtCore.Qt.AA_ShareOpenGLContexts) if hasattr(QGuiApplication, 'setDesktopFileName'): QGuiApplication.setDesktopFileName('electrum.desktop') self.gui_thread = threading.current_thread() self.windows = [] # type: List[ElectrumWindow] self.efilter = OpenFileEventFilter(self.windows) self.app = QElectrumApplication(sys.argv) self.app.installEventFilter(self.efilter) self.app.setWindowIcon(read_QIcon("electrum.png")) self._cleaned_up = False # timer self.timer = QTimer(self.app) self.timer.setSingleShot(False) self.timer.setInterval(500) # msec self.network_dialog = None self.lightning_dialog = None self.watchtower_dialog = None self.network_updated_signal_obj = QNetworkUpdatedSignalObject() self._num_wizards_in_progress = 0 self._num_wizards_lock = threading.Lock() self.dark_icon = self.config.get("dark_icon", False) self.tray = None self._init_tray() self.app.new_window_signal.connect(self.start_new_window) self.app.quit_signal.connect(self.app.quit, Qt.QueuedConnection) self.set_dark_theme_if_needed() run_hook('init_qt', self) def _init_tray(self): self.tray = QSystemTrayIcon(self.tray_icon(), None) self.tray.setToolTip('Electrum') self.tray.activated.connect(self.tray_activated) self.build_tray_menu() self.tray.show() def set_dark_theme_if_needed(self): use_dark_theme = self.config.get('qt_gui_color_theme', 'default') == 'dark' if use_dark_theme: try: import qdarkstyle self.app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) except BaseException as e: use_dark_theme = False self.logger.warning(f'Error setting dark theme: {repr(e)}') # Apply any necessary stylesheet patches patch_qt_stylesheet(use_dark_theme=use_dark_theme) # Even if we ourselves don't set the dark theme, # the OS/window manager/etc might set *a dark theme*. # Hence, try to choose colors accordingly: ColorScheme.update_from_widget(QWidget(), force_dark=use_dark_theme) def build_tray_menu(self): if not self.tray: return # Avoid immediate GC of old menu when window closed via its action if self.tray.contextMenu() is None: m = QMenu() self.tray.setContextMenu(m) else: m = self.tray.contextMenu() m.clear() network = self.daemon.network m.addAction(_("Network"), self.show_network_dialog) if network and network.lngossip: m.addAction(_("Lightning Network"), self.show_lightning_dialog) if network and network.local_watchtower: m.addAction(_("Local Watchtower"), self.show_watchtower_dialog) for window in self.windows: name = window.wallet.basename() submenu = m.addMenu(name) submenu.addAction(_("Show/Hide"), window.show_or_hide) submenu.addAction(_("Close"), window.close) m.addAction(_("Dark/Light"), self.toggle_tray_icon) m.addSeparator() m.addAction(_("Exit Electrum"), self.app.quit) def tray_icon(self): if self.dark_icon: return read_QIcon('electrum_dark_icon.png') else: return read_QIcon('electrum_light_icon.png') def toggle_tray_icon(self): if not self.tray: return self.dark_icon = not self.dark_icon self.config.set_key("dark_icon", self.dark_icon, True) self.tray.setIcon(self.tray_icon()) def tray_activated(self, reason): if reason == QSystemTrayIcon.DoubleClick: if all([w.is_hidden() for w in self.windows]): for w in self.windows: w.bring_to_top() else: for w in self.windows: w.hide() def _cleanup_before_exit(self): if self._cleaned_up: return self._cleaned_up = True self.app.new_window_signal.disconnect() self.efilter = None # If there are still some open windows, try to clean them up. for window in list(self.windows): window.close() window.clean_up() if self.network_dialog: self.network_dialog.close() self.network_dialog.clean_up() self.network_dialog = None self.network_updated_signal_obj = None if self.lightning_dialog: self.lightning_dialog.close() self.lightning_dialog = None if self.watchtower_dialog: self.watchtower_dialog.close() self.watchtower_dialog = None # Shut down the timer cleanly self.timer.stop() self.timer = None # clipboard persistence. see http://www.mail-archive.com/[email protected]/msg17328.html event = QtCore.QEvent(QtCore.QEvent.Clipboard) self.app.sendEvent(self.app.clipboard(), event) if self.tray: self.tray.hide() self.tray.deleteLater() self.tray = None def _maybe_quit_if_no_windows_open(self) -> None: """Check if there are any open windows and decide whether we should quit.""" # keep daemon running after close if self.config.get('daemon'): return # check if a wizard is in progress with self._num_wizards_lock: if self._num_wizards_in_progress > 0 or len(self.windows) > 0: return self.app.quit() def new_window(self, path, uri=None): # Use a signal as can be called from daemon thread self.app.new_window_signal.emit(path, uri) def show_lightning_dialog(self): if not self.daemon.network.has_channel_db(): return if not self.lightning_dialog: self.lightning_dialog = LightningDialog(self) self.lightning_dialog.bring_to_top() def show_watchtower_dialog(self): if not self.watchtower_dialog: self.watchtower_dialog = WatchtowerDialog(self) self.watchtower_dialog.bring_to_top() def show_network_dialog(self): if self.network_dialog: self.network_dialog.on_update() self.network_dialog.show() self.network_dialog.raise_() return self.network_dialog = NetworkDialog( network=self.daemon.network, config=self.config, network_updated_signal_obj=self.network_updated_signal_obj) self.network_dialog.show() def _create_window_for_wallet(self, wallet): w = ElectrumWindow(self, wallet) self.windows.append(w) self.build_tray_menu() w.warn_if_testnet() w.warn_if_watching_only() return w def count_wizards_in_progress(func): def wrapper(self: 'ElectrumGui', *args, **kwargs): with self._num_wizards_lock: self._num_wizards_in_progress += 1 try: return func(self, *args, **kwargs) finally: with self._num_wizards_lock: self._num_wizards_in_progress -= 1 self._maybe_quit_if_no_windows_open() return wrapper @count_wizards_in_progress def start_new_window(self, path, uri, *, app_is_starting=False) -> Optional[ElectrumWindow]: '''Raises the window for the wallet if it is open. Otherwise opens the wallet and creates a new window for it''' wallet = None try: wallet = self.daemon.load_wallet(path, None) except Exception as e: self.logger.exception('') custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), text=_('Cannot load wallet') + ' (1):\n' + repr(e)) # if app is starting, still let wizard to appear if not app_is_starting: return if not wallet: try: wallet = self._start_wizard_to_select_or_create_wallet(path) except (WalletFileException, BitcoinException) as e: self.logger.exception('') custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), text=_('Cannot load wallet') + ' (2):\n' + repr(e)) if not wallet: return # create or raise window try: for window in self.windows: if window.wallet.storage.path == wallet.storage.path: break else: window = self._create_window_for_wallet(wallet) except Exception as e: self.logger.exception('') custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), text=_('Cannot create window for wallet') + ':\n' + repr(e)) if app_is_starting: wallet_dir = os.path.dirname(path) path = os.path.join(wallet_dir, get_new_wallet_name(wallet_dir)) self.start_new_window(path, uri) return if uri: window.pay_to_URI(uri) window.bring_to_top() window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) window.activateWindow() return window def _start_wizard_to_select_or_create_wallet( self, path) -> Optional[Abstract_Wallet]: wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self) try: path, storage = wizard.select_storage(path, self.daemon.get_wallet) # storage is None if file does not exist if storage is None: wizard.path = path # needed by trustedcoin plugin wizard.run('new') storage, db = wizard.create_storage(path) else: db = WalletDB(storage.read(), manual_upgrades=False) wizard.run_upgrades(storage, db) except (UserCancelled, GoBack): return except WalletAlreadyOpenInMemory as e: return e.wallet finally: wizard.terminate() # return if wallet creation is not complete if storage is None or db.get_action(): return wallet = Wallet(db, storage, config=self.config) wallet.start_network(self.daemon.network) self.daemon.add_wallet(wallet) return wallet def close_window(self, window: ElectrumWindow): if window in self.windows: self.windows.remove(window) self.build_tray_menu() # save wallet path of last open window if not self.windows: self.config.save_last_wallet(window.wallet) run_hook('on_close_window', window) self.daemon.stop_wallet(window.wallet.storage.path) def init_network(self): # Show network dialog if config does not exist if self.daemon.network: if self.config.get('auto_connect') is None: wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self) wizard.init_network(self.daemon.network) wizard.terminate() def main(self): # setup Ctrl-C handling and tear-down code first, so that user can easily exit whenever self.app.setQuitOnLastWindowClosed( False) # so _we_ can decide whether to quit self.app.lastWindowClosed.connect(self._maybe_quit_if_no_windows_open) self.app.aboutToQuit.connect(self._cleanup_before_exit) signal.signal(signal.SIGINT, lambda *args: self.app.quit()) # hook for crash reporter Exception_Hook.maybe_setup(config=self.config) # first-start network-setup try: self.init_network() except UserCancelled: return except GoBack: return except Exception as e: self.logger.exception('') return # start wizard to select/create wallet self.timer.start() path = self.config.get_wallet_path(use_gui_last_wallet=True) try: if not self.start_new_window( path, self.config.get('url'), app_is_starting=True): return except Exception as e: self.logger.error( "error loading wallet (or creating window for it)") send_exception_to_crash_reporter(e) # Let Qt event loop start properly so that crash reporter window can appear. # We will shutdown when the user closes that window, via lastWindowClosed signal. # main loop self.logger.info("starting Qt main loop") self.app.exec_() # on some platforms the exec_ call may not return, so use _cleanup_before_exit def stop(self): self.logger.info('closing GUI') self.app.quit_signal.emit()
class QEAppController(QObject): userNotify = pyqtSignal(str) def __init__(self, qedaemon): super().__init__() self.logger = get_logger(__name__) self._qedaemon = qedaemon # set up notification queue and notification_timer self.user_notification_queue = queue.Queue() self.user_notification_last_time = 0 self.notification_timer = QTimer(self) self.notification_timer.setSingleShot(False) self.notification_timer.setInterval(500) # msec self.notification_timer.timeout.connect(self.on_notification_timer) self._qedaemon.walletLoaded.connect(self.on_wallet_loaded) self.userNotify.connect(self.notifyAndroid) def on_wallet_loaded(self): qewallet = self._qedaemon.currentWallet if not qewallet: return # attach to the wallet user notification events # connect only once try: qewallet.userNotify.disconnect(self.on_wallet_usernotify) except: pass qewallet.userNotify.connect(self.on_wallet_usernotify) def on_wallet_usernotify(self, wallet, message): self.logger.debug(message) self.user_notification_queue.put(message) if not self.notification_timer.isActive(): self.logger.debug('starting app notification timer') self.notification_timer.start() def on_notification_timer(self): if self.user_notification_queue.qsize() == 0: self.logger.debug('queue empty, stopping app notification timer') self.notification_timer.stop() return now = time.time() rate_limit = 20 # seconds if self.user_notification_last_time + rate_limit > now: return self.user_notification_last_time = now self.logger.info("Notifying GUI about new user notifications") try: self.userNotify.emit(self.user_notification_queue.get_nowait()) except queue.Empty: pass def notifyAndroid(self, message): try: # TODO: lazy load not in UI thread please global notification if not notification: from plyer import notification icon = (os.path.dirname(os.path.realpath(__file__)) + '/../icons/electrum.png') notification.notify('Electrum', message, app_icon=icon, app_name='Electrum') except ImportError: self.logger.warning( 'Notification: needs plyer; `sudo python3 -m pip install plyer`' ) except Exception as e: self.logger.error(repr(e)) @pyqtSlot(str, str) def doShare(self, data, title): #if platform != 'android': #return try: from jnius import autoclass, cast except ImportError: self.logger.error('Share: needs jnius. Platform not Android?') return JS = autoclass('java.lang.String') Intent = autoclass('android.content.Intent') sendIntent = Intent() sendIntent.setAction(Intent.ACTION_SEND) sendIntent.setType("text/plain") sendIntent.putExtra(Intent.EXTRA_TEXT, JS(data)) pythonActivity = autoclass('org.kivy.android.PythonActivity') currentActivity = cast('android.app.Activity', pythonActivity.mActivity) it = Intent.createChooser(sendIntent, cast('java.lang.CharSequence', JS(title))) currentActivity.startActivity(it) @pyqtSlot('QString') def textToClipboard(self, text): QGuiApplication.clipboard().setText(text) @pyqtSlot(result='QString') def clipboardToText(self): return QGuiApplication.clipboard().text()
class login_window(QWidget, Ui_LoginForm): def __init__(self, mode=0, parent=None): super(login_window, self).__init__(parent) self.mode = mode self.setupUi(self) self.setWindowTitle("登陆") self.setWindowIcon(QtGui.QIcon('./resource/favicon.ico')) ###### 登录页面头图设置 完美显示图片,并自适应大小 pix = QtGui.QPixmap("./resource/bg7699.png") self.login_top_bg_label.setPixmap(pix) self.login_top_bg_label.setScaledContents(True) ###### 显示窗口在屏幕中间 self.center() ###### 初始化登录信息 self.init_login_info() ###### 自动登录 self.timer = QTimer(self) self.timer.timeout.connect(self.goto_autologin) self.timer.setSingleShot(True) self.timer.start(1000) ###### 创建资产文件夹 try: os.mkdir('Assets_Host') os.mkdir('Assets_URL') os.mkdir('log') except Exception as e: pass ###### 检查配置文件 try: with open('config.ini') as content: pass self.read_config() except Exception as e: output_log("[!] 配置文件未找到,初次使用请配置扫描器信息...") settings = QSettings("config.ini", QSettings.IniFormat) settings.setValue("host", "") settings.setValue("port", "") settings.setValue("account", "") settings.setValue("password", "") settings.setValue("remeberpassword", "") settings.setValue("autologin", "") self.open_config_pane() ###### 显示窗口在屏幕中间 def center(self): qr = self.frameGeometry() cp = QDesktopWidget().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft()) ###### 从配置文件取出扫描器的配置信息 def read_config(self): settings = QSettings("config.ini", QSettings.IniFormat) self.host = settings.value("host") self.port = settings.value("port") global SCANNER_URL if self.port == '443': SCANNER_URL = f'https://{self.host}' else: SCANNER_URL = f'https://{self.host}:{self.port}' ###### 跳转配置信息页面 def open_config_pane(self): dialog = configdialog() if dialog.exec_() == QDialog.Accepted: output_log("[+] 扫描器配置信息保存成功。") self.read_config() ###### 自动登录 and 记住密码 联动 def auto_login(self, checked): if checked: self.remember_passwd_checkBox.setChecked(True) ###### 记住密码 and 自动登录 联动 def remember_pwd(self, checked): if not checked: self.Auto_login_checkBox.setChecked(False) ###### 账户 and 密码 and 登录按钮 有效性联动 def enable_login_btn(self): account = self.username_lineEdit.text() passwd = self.passwd_lineEdit.text() if len(account) > 0 and len(passwd) > 0: self.login_pushButton.setEnabled(True) else: self.login_pushButton.setEnabled(False) ###### 登录按钮事件 def ckeck_login(self): self.on_pushButton_enter_clicked() ###### 网页登录按钮事件 def open_url_link(self): QDesktopServices.openUrl(QUrl(SCANNER_URL)) ###### 自动登录 def goto_autologin(self): if self.Auto_login_checkBox.isChecked() == True and self.mode == 0: self.on_pushButton_enter_clicked() ###### 保存登录信息 def save_login_info(self): settings = QSettings("config.ini", QSettings.IniFormat) settings.setValue("host", self.host) settings.setValue("port", self.port) settings.setValue("account", self.username_lineEdit.text()) settings.setValue("password", self.passwd_lineEdit.text()) settings.setValue("remeberpassword", self.remember_passwd_checkBox.isChecked()) settings.setValue("autologin", self.Auto_login_checkBox.isChecked()) ###### 初始化登录信息 def init_login_info(self): settings = QSettings("config.ini", QSettings.IniFormat) the_account = settings.value("account") the_password = settings.value("password") the_remeberpassword = settings.value("remeberpassword") the_autologin = settings.value("autologin") self.username_lineEdit.setText(the_account) if the_remeberpassword == "true" or the_remeberpassword == True: self.remember_passwd_checkBox.setChecked(True) self.passwd_lineEdit.setText(the_password) if the_autologin == "true" or the_autologin == True: self.Auto_login_checkBox.setChecked(True) windowList = [] ###### 登录事件执行 def on_pushButton_enter_clicked(self): self.username = self.username_lineEdit.text() self.passwd = self.passwd_lineEdit.text() output_log( f'[+] 配置信息读取成功,用户名:{self.username} 扫描器地址:{self.host} 扫描器端口:{self.port}' ) try: cooker = RSAS_Requests.RSAS_Login(SCANNER_URL, self.username, self.passwd) if cooker.headers['location'] == '/': self.save_login_info() output_log('[+] 扫描器登录成功!') try: output_log('[+] 正在连接扫描器获取参数,初始化主页面...') task_window = main_window(SCANNER_URL, self.username, self.passwd) self.windowList.append(task_window) self.close() task_window.show() except Exception as e: output_log('[!] 主页面初始化失败!') QtWidgets.QMessageBox.information( None, "初始化失败!", f"{e}", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.Yes) except Exception as e: output_log('[!] 扫描器登录失败!') QtWidgets.QMessageBox.about(None, '登录失败!', '密码错误!')
class CuraSplashScreen(QSplashScreen): def __init__(self): super().__init__() self._scale = 0.7 splash_image = QPixmap(Resources.getPath(Resources.Images, "cura.png")) self.setPixmap(splash_image) self._current_message = "" self._loading_image_rotation_angle = 0 self._to_stop = False self._change_timer = QTimer() self._change_timer.setInterval(50) self._change_timer.setSingleShot(False) self._change_timer.timeout.connect(self.updateLoadingImage) def show(self): super().show() self._change_timer.start() def updateLoadingImage(self): if self._to_stop: return self._loading_image_rotation_angle -= 10 self.repaint() # Override the mousePressEvent so the splashscreen doesn't disappear when clicked def mousePressEvent(self, mouse_event): pass def drawContents(self, painter): if self._to_stop: return painter.save() painter.setPen(QColor(255, 255, 255, 255)) painter.setRenderHint(QPainter.Antialiasing) painter.setRenderHint(QPainter.Antialiasing, True) version = Application.getInstance().getVersion().split("-") buildtype = Application.getInstance().getBuildType() if buildtype: version[0] += " (%s)" % buildtype # draw version text font = QFont() # Using system-default font here font.setPixelSize(37) painter.setFont(font) painter.drawText(215, 66, 330 * self._scale, 230 * self._scale, Qt.AlignLeft | Qt.AlignTop, version[0]) if len(version) > 1: font.setPointSize(12) painter.setFont(font) painter.setPen(QColor(200, 200, 200, 255)) painter.drawText(247, 105, 330 * self._scale, 255 * self._scale, Qt.AlignLeft | Qt.AlignTop, version[1]) painter.setPen(QColor(255, 255, 255, 255)) # draw the loading image pen = QPen() pen.setWidth(6 * self._scale) pen.setColor(QColor(32, 166, 219, 255)) painter.setPen(pen) painter.drawArc(60, 150, 32 * self._scale, 32 * self._scale, self._loading_image_rotation_angle * 16, 300 * 16) # draw message text if self._current_message: font = QFont() # Using system-default font here font.setPixelSize(13) pen = QPen() pen.setColor(QColor(255, 255, 255, 255)) painter.setPen(pen) painter.setFont(font) painter.drawText(100, 128, 170, 64, Qt.AlignLeft | Qt.AlignVCenter | Qt.TextWordWrap, self._current_message) painter.restore() super().drawContents(painter) def showMessage(self, message, *args, **kwargs): if self._to_stop: return self._current_message = message self.messageChanged.emit(message) QCoreApplication.flush() self.repaint() def close(self): # set stop flags self._to_stop = True self._change_timer.stop() super().close()