def main(): # start QT UI sargv = sys.argv + ['--style', 'material'] app = QApplication(sargv) font = QFont() font.setFamily("Ariel") app.setFont(font) # manually detect lid open close event from the start ui_view = QQuickView() ui_view.setSource(QUrl.fromLocalFile('RobotPanelSelect.qml')) ui_view.setWidth(800) ui_view.setHeight(480) ui_view.setMaximumHeight(480) ui_view.setMaximumWidth(800) ui_view.setMinimumHeight(480) ui_view.setMinimumWidth(800) # ui_view = UIController.get_ui() ui_view.show() ret = app.exec_() # Teminate sys.exit(ret)
def quickView(url: str, parent=None): view = QQuickView(parent) view.setResizeMode(QQuickView.SizeRootObjectToView) view.setSource(QUrl(url)) view.show() return view
def main(): app = QGuiApplication([]) try: path = QUrl(sys.argv[1]) except IndexError: print("Usage: pyqmlscene <filename>") sys.exit(1) engine = QQmlApplicationEngine() # Procedure similar to # https://github.com/qt/qtdeclarative/blob/0e9ab20b6a41bfd40aff63c9d3e686606e51e798/tools/qmlscene/main.cpp component = QQmlComponent(engine) component.loadUrl(path) root_object = component.create() if isinstance(root_object, QQuickWindow): # Display window object root_object.show() elif isinstance(root_object, QQuickItem): # Display arbitrary QQuickItems by reloading the source since # reparenting the existing root object to the view did not have any # effect. Neither does the QQuickView class have a setContent() method view = QQuickView(path) view.show() else: raise SystemExit("Error displaying {}".format(root_object)) sys.exit(app.exec_())
def create_main_app(): # create the application main_app = QApplication(sys.argv) # internationalisation # see http://doc.qt.io/qt-5/internationalization.html # see http://pyqt.sourceforge.net/Docs/PyQt5/i18n.html translator = QTranslator() translator.load("arpi/res/i18n/arpi_" + QLocale.system().name()) main_app.installTranslator(translator) # create config global_config = GlobalConfig() # create speech output class global_config.say = Say(global_config) # create quick view view = QQuickView() view.setResizeMode(QQuickView.SizeRootObjectToView) # start program AppOverview(view, loaded_apps, global_config).activate() view.show() # clean up main_app.exec_() sys.exit()
def main(): argv = sys.argv # Trick to set the style / not found how to do it in pythonic way argv.extend(["-style", "universal"]) app = QGuiApplication(argv) qmlRegisterType(FigureCanvasQTAgg, "Backend", 1, 0, "FigureCanvas") view = QQuickView() view.setResizeMode(QQuickView.SizeRootObjectToView) view.setSource( QUrl( os.path.join(os.path.dirname(__file__), 'backend_qtquick5', 'Figure.qml'))) view.show() win = view.rootObject() fig = win.findChild(QObject, "figure").getFigure() print(fig) ax = fig.add_subplot(111) x = np.linspace(-5, 5) ax.plot(x, np.sin(x)) rc = app.exec_() # There is some trouble arising when deleting all the objects here # but I have not figure out how to solve the error message. # It looks like 'app' is destroyed before some QObject sys.exit(rc)
def test_qquickview(): app = QApplication(sys.argv) view = QQuickView() view.show() sys.exit(app.exec_())
def main(): global app # sys.argv.extend(['-platform', 'eglfs']) # Qt Charts uses Qt Graphics View Framework for drawing, therefore QApplication must be used. app = QApplication(sys.argv) viewer = QQuickView() # The following are needed to make examples run without having to install the module # in desktop environments. extraImportPath = QGuiApplication.applicationDirPath() if sys.platform == 'win32': extraImportPath += "/../../../../qml" else: extraImportPath += "/../../../qml" viewer.engine().addImportPath(extraImportPath) viewer.engine().quit.connect(app.quit) viewer.setTitle("QML Oscilloscope") dataSource = datasource.DataSource(viewer) viewer.rootContext().setContextProperty("dataSource", dataSource) main_qml = path.dirname(__file__) + "/qml/qmloscilloscope/main.qml" viewer.setSource(QUrl(main_qml)) viewer.setResizeMode(QQuickView.SizeRootObjectToView) viewer.setColor(QColor("#404040")) viewer.show() return app.exec_()
def main(): app = QGuiApplication(sys.argv) view = QQuickView() view.setSource(QUrl.fromLocalFile('scene3d.qml')) view.setResizeMode(QQuickView.SizeRootObjectToView) view.show() sys.exit(app.exec_())
def main(): argv = sys.argv app = QGuiApplication(argv) qmlRegisterType(FigureCanvasQTAggToolbar, "Backend", 1, 0, "FigureToolbar") imgProvider = MatplotlibIconProvider() view = QQuickView() view.engine().addImageProvider("mplIcons", imgProvider) view.setResizeMode(QQuickView.SizeRootObjectToView) view.setSource( QUrl( os.path.join(os.path.dirname(__file__), 'backend_qtquick5', 'FigureToolbar.qml'))) win = view.rootObject() fig = win.findChild(QObject, "figure").getFigure() ax = fig.add_subplot(111) x = np.linspace(-5, 5) ax.plot(x, np.sin(x)) view.show() rc = app.exec_() # There is some trouble arising when deleting all the objects here # but I have not figure out how to solve the error message. # It looks like 'app' is destroyed before some QObject sys.exit(rc)
def main(): print("start") app = QApplication(sys.argv) qmlRegisterType(Person, 'People', 1, 0, 'Person') v = QQuickView(QUrl("main.qml")) v.show() sys.exit(app.exec_())
def run_qml(qmlpath): app = QGuiApplication(sys.argv) register_qml() view = QQuickView() view.setResizeMode(QQuickView.SizeRootObjectToView) view.setSource(QUrl(qmlpath)) view.show() sys.exit(app.exec_())
def run_qml(qmlpath): app = QGuiApplication(sys.argv) qmlRegisterType(QQuickGLItem, 'GLItem', 1, 0, 'GLItem') view = QQuickView() view.setResizeMode(QQuickView.SizeRootObjectToView) view.setSource(QUrl(qmlpath)) view.show() sys.exit(app.exec_())
def qt_t1(): app = QGuiApplication([]) view = QQuickView() path = './qml/side3/side3.qml' # 加载的QML文件 view.engine().quit.connect(app.quit) view.setSource(QUrl(path)) view.show() root = view.rootObject() root.updateRotater() # 调用QML函数 app.exec_()
def run_qml(qmlpath): app = QGuiApplication(sys.argv) qmlRegisterType(VideoView, 'PyQt5GLfwTest', 1, 0, 'VideoView') view = QQuickView() view.setResizeMode(QQuickView.SizeRootObjectToView) view.setSource(QUrl(qmlpath)) view.show() sys.exit(app.exec_())
def qml_tutorial(): from PyQt5.QtCore import QUrl from PyQt5.QtQuick import QQuickView app = QApplication([]) url = QUrl('./view.qml') view = QQuickView() view.setSource(url) view.show() sys.exit(app.exec_())
def main(arguments): app = QGuiApplication(sys.argv) view = QQuickView() f = QFile(':/default.txt') f.open(QIODevice.ReadOnly) model = TreeModel(f.readAll()) f.close() rootContext = view.rootContext().setContextProperty('model', model) view.setSource(QUrl.fromLocalFile('TreeModel.qml')) view.show() sys.exit(app.exec_())
def main(): app = QGuiApplication(sys.argv) dir_path = os.path.dirname(os.path.realpath(__file__)) qmlRegisterType(Hello, 'Hello', 1, 0, 'Hello') view = QQuickView() view.setSource(QUrl(dir_path + "/qml/Main.qml")) view.show() return app.exec_()
class LoginWin(QtCore.QObject): def __init__(self, state, app): QtCore.QObject.__init__(self) self.state = state self.app = app # Create the QML user interface. self.login_win = QQuickView() self.login_win.setTitle(self.tr("Xiami Login")) self.login_win.setSource(QUrl('login.qml')) self.login_win.setResizeMode(QQuickView.SizeRootObjectToView) self.login_win.show() # Connect signals self.root_obj = self.login_win.rootObject() self.root_obj.loginClicked.connect(self.login_clicked) self.root_obj.exitClicked.connect(self.exit_clicked) def set_state(self, msg): self.root_obj.setStatus(msg) def exit_clicked(self): sys.exit(0) def login_clicked(self, username, password): code = self.root_obj.getVerificationCode() if code != "": try: login.login_with_code(self.state, self.key, code) except Exception as e: self.set_state(e.message) self.root_obj.hideCode() return self.ok() else: try: ret = login.login(self.state, username, password) except Exception as e: self.set_state(e.message) return if not ret[0]: with open(login.img_path, 'wb') as imgf: imgf.write(ret[2]) self.set_state(self.tr("Please enter verification code")) self.root_obj.setVerificationImage("file://%s" % login.img_path) self.key = ret[1] else: self.ok() def ok(self): self.login_win.close() self.app.auth_ok()
def buildandsave(site, exit_on_save=True): agent = Agent(reactor) curator = User().get_by_role(site, keys.entity_twitter) leagues = [] deferreds_league = [] for l in curator[user_keys.user_site_leagues]: league = Entity().get_item(league=l, profile='league:' + l) d = agent.request( "HEAD", str('http://' + league[keys.entity_site] + '/tw/' + league[keys.entity_twitter_id] + '/avatar_large.png')) d.addCallback(add_redirect, league, leagues, 'large') deferreds_league.append(d) yield defer.DeferredList(deferreds_league) print 'leagues length:', len(leagues) players = [] deferreds_small = [] for p in Entity().query_2(index=Entity.index_site_profile, site__eq=curator[user_keys.user_role], query_filter={'twitter__null': False}, limit=200): d = agent.request( "HEAD", str('http://' + p[keys.entity_site] + '/tw/' + p[keys.entity_twitter_id] + '/avatar_small.png')) d.addCallback(add_redirect, p, players, 'small') deferreds_small.append(d) yield defer.DeferredList(deferreds_small) print 'players length:', len(players) view = QQuickView() view.setSource(QUrl('qml/render/curator_twitter_bg.qml')) view.rootObject().setProperty('bgcolor', 'black') view.setWidth(1500) view.setHeight(500) view.show() view.rootObject().setProperty('curator', curator._data) view.rootObject().setProperty('leagues', leagues) view.rootObject().setProperty('players', players) yield task.deferLater(reactor, 30, screenshot, view, site, curator) if exit_on_save: print 'exit on save' reactor.callLater(0, reactor.stop) else: print 'done'
def main(): app = QGuiApplication(sys.argv) view = QQuickView() schema = [ "pyLabel", "pyColor", ] model = Model(schema) items = [{ "pyLabel": "First Item", "pyColor": "white", }, { "pyLabel": "Second Item", "pyColor": "white", }] for item in items: model.append(item) engine = view.engine() context = engine.rootContext() context.setContextProperty("pyModel", model) view.setSource(QUrl("app.qml")) view.setResizeMode(view.SizeRootObjectToView) view.show() # Appending to the model QTimer.singleShot( 2000, lambda: model.append({ "pyLabel": "Third Item", "pyColor": "steelblue" })) # Modifying an item in the model QTimer.singleShot( 3000, lambda: model.setData( model.createIndex(1, 0), # 1th item, 0th column "New pLabel!", schema.index("pyLabel"), )) app.exec_()
class TabletShortcuts(QGuiApplication): def __init__(self, argv): QGuiApplication.__init__(self, argv) self.view = QQuickView() self.bus = QDBusConnection.sessionBus() self.server = MyDBUSServer(self) self.bus.registerObject("/app", self.server) self.bus.registerService("sevanteri.TabletShortcuts") self.view.setTitle("TabletShortcuts") self.view.setResizeMode(QQuickView.SizeRootObjectToView) self.view.setSource(QUrl('main.qml')) self.root = self.view.rootObject() self.showView() self.root.runCommand.connect(self.run) self.root.hideView.connect(self.view.hide) self.view.engine().quit.connect(self.quit) def run(self, cmd): return Popen(shlex.split(cmd)) def quit(self): self.exit() def showView(self): if self.view.isVisible(): self.view.hide() else: # width, height = TabletShortcuts.getScreenGeometry() # self.view.setGeometry(1, 1, width, height) self.view.show() def getScreenGeometry(): output = Popen("xrandr | grep 'current'", shell=True, stdout=PIPE)\ .communicate()[0].decode('UTF-8') m = re.search('current.([0-9]+).x.([0-9]+)', output) width = int(m.group(1)) height = int(m.group(2)) return (width, height)
def main(): """Main Function Entry.""" app = QApplication(sys.argv) # Create a label and set its properties applable = QQuickView() applable.setSource(QUrl('basic.qml')) conn = SlotClass() context = applable.rootContext() context.setContextProperty("conn", conn) # Show the Label applable.show() # Execute the Application and Exit app.exec_() sys.exit()
def main(): global VIEW global APP APP = QGuiApplication(sys.argv) VIEW = QQuickView() url = QUrl('main.qml') VIEW.setSource(url) submit = submitUserInput() context = VIEW.rootContext() context.setContextProperty("submit", submit) VIEW.show() sys.exit(APP.exec_())
def main(): import sys QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling) QCoreApplication.setOrganizationName("QtExamples") app = QGuiApplication(sys.argv) view = QQuickView() view.engine().quit.connect(app.quit) view.setSource(QUrl("qrc:/demos/clocks/clocks.qml")) if view.status() == QQuickView.Error: sys.exit(-1) view.setResizeMode(QQuickView.SizeRootObjectToView) view.show() sys.exit(app.exec_())
class CountdownApp(QObject): QMLFILE = 'main.qml' def __init__(self): super(QObject, self).__init__() self.app = QGuiApplication(sys.argv) self.view = QQuickView() self.view.setResizeMode(QQuickView.SizeRootObjectToView) #self.view.engine().quit.connect(self.app.quit) self.cds = CountdownList() self.cdt = [] for name, targetDatetime in { "silvester": datetime(2018, 1, 1, 0, 0, 0), "geburtstag": datetime(2018, 3, 12, 0, 0, 0) }.items(): cdobj = CountdownData() countdown = CountdownTimer(cdobj, targetDatetime, name) countdown.start() self.cds.append(cdobj) self.cdt.append(countdown) self.view.rootContext().setContextProperty('countdowns', self.cds) self.view.setSource(QUrl(self.QMLFILE)) self.t = QTimer() self.t.timeout.connect(self.addCountdown) self.t.start(10000) def run(self): self.view.show() sys.exit(self.app.exec_()) @pyqtSlot() def addCountdown(self): for name, targetDatetime in { "antrittsvorlesung": datetime(2018, 1, 19, 0, 0, 0) }.items(): cdobj = CountdownData() countdown = CountdownTimer(cdobj, targetDatetime, name) countdown.start() self.cds.append(cdobj) self.cdt.append(countdown)
def run_app(): app = QGuiApplication(sys.argv) app.setApplicationName("Worship Prototype") view = QQuickView() view.setResizeMode(QQuickView.SizeRootObjectToView) view.setSource(QUrl.fromLocalFile(os.path.join(os.path.dirname(__file__), 'main.qml'))) view.show() root = view.rootObject() preview = DefaultScreen() preview.wire_to_gui(root, 'previewScreen') preview.show_background(VideoBackground(os.path.join(os.path.dirname(__file__), '../echo.mp4'))) # preview_live = DefaultScreen() # live = DefaultScreen() modules = [ LyricsModule(SongsList(), root, preview), ] sys.exit(app.exec_())
def main(): # os.environ["QML_IMPORT_TRACE"] = "1" app = QGuiApplication(argv) qmlRegisterType(MainBusModel, 'Snowman', 1, 0, 'MainBusModel') qmlRegisterType(DsksModel, 'Snowman', 1, 0, 'DsksModel') view = QQuickView() view.setResizeMode(QQuickView.SizeRootObjectToView) sourceFile = os.path.join(os.path.dirname(__file__), 'app.qml') view.setSource(QUrl.fromLocalFile(sourceFile)) mainBus = view.rootObject().findChild(QQuickItem, 'mainBusModel') mainBus.setProperty('manager', ManagerConnection(5555, 5556)) dsks = view.rootObject().findChild(QQuickItem, 'dsksModel') dsks.setProperty('manager', ManagerConnection(5555, 5556)) view.show() exit(app.exec_())
class MainWindow(QtCore.QObject): def __init__(self): QtCore.QObject.__init__(self) self._controller = Controller() self.view = QQuickView() full_path = os.path.realpath(__file__) folder = os.path.dirname(full_path) qml_file = os.path.join(folder, 'qml', 'App.qml') qml_qurl = QtCore.QUrl.fromLocalFile(qml_file) self.view.setSource(qml_qurl) # Add context properties to use this objects from qml rc = self.view.rootContext() rc.setContextProperty('controller', self._controller) def show(self): self.view.show()
def main(): app = QGuiApplication(sys.argv) app.setApplicationName('InfiniteCopy') openDataBase() view = QQuickView() clipboardItemModel = ClipboardItemModel() clipboardItemModel.create() filterProxyModel = QSortFilterProxyModel() filterProxyModel.setSourceModel(clipboardItemModel) clipboard = Clipboard() clipboard.setFormats([ mimeText, mimeHtml, mimePng, mimeSvg ]) clipboard.changed.connect(clipboardItemModel.addItem) engine = view.engine() imageProvider = ClipboardItemModelImageProvider(clipboardItemModel) engine.addImageProvider("items", imageProvider) context = view.rootContext() context.setContextProperty('clipboardItemModel', clipboardItemModel) context.setContextProperty('clipboardItemModelFilterProxy', filterProxyModel) context.setContextProperty('clipboard', clipboard) view.setSource(QUrl.fromLocalFile('qml/MainWindow.qml')) view.setGeometry(100, 100, 400, 240) view.show() engine.quit.connect(QGuiApplication.quit) return app.exec_()
def run(): signal.signal(signal.SIGINT, signal.SIG_DFL) app = QGuiApplication(sys.argv) view = QQuickView() view.setTitle('Hot reloading demo') qml_engine = view.rootContext().engine() qml_engine.addImportPath(lib_dir_path) notifier = HotReloadNotifier(demo_dir_path, qml_engine, parent=app) view.rootContext().setContextProperty('hotReloadNotifier', notifier) qml_url = QUrl.fromLocalFile(os.path.join(demo_dir_path, 'Demo.qml')) view.setSource(qml_url) view.show() exit_code = app.exec_() # notifier.stop() # seems like this is not needed sys.exit(exit_code)
class VVSQMLApp(QObject): QMLFILE = 'gui.qml' def __init__(self, connections): super(QObject, self).__init__() self.app = QGuiApplication(sys.argv) self.view = QQuickView() self.view.setResizeMode(QQuickView.SizeRootObjectToView) if settings['alwaysOnTop']: self.view.setFlags(Qt.WindowStaysOnTopHint) self.con = [] for connection in connections: updaterThread = VVSConnectionUpdater( connection[0], connection[1], connection[2], updateDelay=settings['updateDelay']) updaterThread.start() self.con.append(updaterThread) #print(connection) #self.con = VVSConnectionUpdater('5006021', 'X60', 'Leonberg Bf') #self.con.start() #print(self.con) self.view.rootContext().setContextProperty('con', self.con) self.view.setSource(QUrl(self.QMLFILE)) #Setup notifications VVSNotifier.setup(self.con) def run(self): if settings['fullscreen']: self.view.showFullScreen() else: self.view.show() sys.exit(self.app.exec_())
class Application(QApplication): """Main Nuxeo Drive application controlled by a system tray icon + menu""" tray_icon = None icon_state = None def __init__(self, manager: "Manager", *args: Any) -> None: super().__init__(list(*args)) self.manager = manager self.dialogs = dict() self.osi = self.manager.osi self.setWindowIcon(QIcon(self.get_window_icon())) self.setApplicationName(manager.app_name) self._init_translator() self.setQuitOnLastWindowClosed(False) self._delegator = None self.filters_dlg = None self._conflicts_modals = dict() self.current_notification = None self.default_tooltip = self.manager.app_name font = QFont("Helvetica, Arial, sans-serif", 12) self.setFont(font) self.ratio = sqrt(QFontMetricsF(font).height() / 12) self.init_gui() self.aboutToQuit.connect(self.manager.stop) self.manager.dropEngine.connect(self.dropped_engine) # This is a windowless application mostly using the system tray self.setQuitOnLastWindowClosed(False) self.setup_systray() # Direct Edit self.manager.direct_edit.directEditConflict.connect( self._direct_edit_conflict) self.manager.direct_edit.directEditError.connect( self._direct_edit_error) # Check if actions is required, separate method so it can be overridden self.init_checks() # Setup notification center for macOS if MAC: self._setup_notification_center() # Application update self.manager.updater.appUpdated.connect(self.quit) # Display release notes on new version if self.manager.old_version != self.manager.version: self.show_release_notes(self.manager.version) # Listen for nxdrive:// sent by a new instance self.init_nxdrive_listener() def init_gui(self) -> None: self.api = QMLDriveApi(self) self.conflicts_model = FileModel() self.errors_model = FileModel() self.engine_model = EngineModel() self.file_model = FileModel() self.ignoreds_model = FileModel() self.language_model = LanguageModel() self.add_engines(list(self.manager._engines.values())) self.engine_model.statusChanged.connect(self.update_status) self.language_model.addLanguages(Translator.languages()) flags = Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint if WINDOWS: self.conflicts_window = QQuickView() self.settings_window = QQuickView() self.systray_window = SystrayWindow() self._fill_qml_context(self.conflicts_window.rootContext()) self._fill_qml_context(self.settings_window.rootContext()) self._fill_qml_context(self.systray_window.rootContext()) self.systray_window.rootContext().setContextProperty( "systrayWindow", self.systray_window) self.conflicts_window.setSource( QUrl.fromLocalFile(find_resource("qml", "Conflicts.qml"))) self.settings_window.setSource( QUrl.fromLocalFile(find_resource("qml", "Settings.qml"))) self.systray_window.setSource( QUrl.fromLocalFile(find_resource("qml", "Systray.qml"))) flags |= Qt.Popup else: self.app_engine = QQmlApplicationEngine() self._fill_qml_context(self.app_engine.rootContext()) self.app_engine.load( QUrl.fromLocalFile(find_resource("qml", "Main.qml"))) root = self.app_engine.rootObjects()[0] self.conflicts_window = root.findChild(QQuickWindow, "conflictsWindow") self.settings_window = root.findChild(QQuickWindow, "settingsWindow") self.systray_window = root.findChild(SystrayWindow, "systrayWindow") if LINUX: flags |= Qt.Drawer self.systray_window.setFlags(flags) self.manager.newEngine.connect(self.add_engines) self.manager.initEngine.connect(self.add_engines) self.manager.dropEngine.connect(self.remove_engine) self._window_root(self.conflicts_window).changed.connect( self.refresh_conflicts) self._window_root(self.systray_window).appUpdate.connect( self.api.app_update) self._window_root(self.systray_window).getLastFiles.connect( self.get_last_files) self.api.setMessage.connect( self._window_root(self.settings_window).setMessage) if self.manager.get_engines(): current_uid = self.engine_model.engines_uid[0] self.get_last_files(current_uid) self.update_status(self.engine_model.engines[current_uid]) self.manager.updater.updateAvailable.connect( self._window_root(self.systray_window).updateAvailable) self.manager.updater.updateProgress.connect( self._window_root(self.systray_window).updateProgress) def add_engines(self, engines: Union["Engine", List["Engine"]]) -> None: if not engines: return engines = engines if isinstance(engines, list) else [engines] for engine in engines: self.engine_model.addEngine(engine) def remove_engine(self, uid: str) -> None: self.engine_model.removeEngine(uid) def _fill_qml_context(self, context: "QQmlContext") -> None: """ Fill the context of a QML element with the necessary resources. """ context.setContextProperty("ConflictsModel", self.conflicts_model) context.setContextProperty("ErrorsModel", self.errors_model) context.setContextProperty("EngineModel", self.engine_model) context.setContextProperty("FileModel", self.file_model) context.setContextProperty("IgnoredsModel", self.ignoreds_model) context.setContextProperty("languageModel", self.language_model) context.setContextProperty("api", self.api) context.setContextProperty("application", self) context.setContextProperty("currentLanguage", self.current_language()) context.setContextProperty("manager", self.manager) context.setContextProperty("updater", self.manager.updater) context.setContextProperty("ratio", self.ratio) context.setContextProperty("isFrozen", Options.is_frozen) context.setContextProperty("tl", Translator._singleton) context.setContextProperty( "nuxeoVersionText", f"{self.manager.app_name} {self.manager.version}") metrics = self.manager.get_metrics() context.setContextProperty( "modulesVersionText", (f'Python {metrics["python_version"]}, ' f'Qt {metrics["qt_version"]}, ' f'SIP {metrics["sip_version"]}'), ) colors = { "darkBlue": "#1F28BF", "nuxeoBlue": "#0066FF", "lightBlue": "#00ADED", "teal": "#73D2CF", "purple": "#8400FF", "red": "#C02828", "orange": "#FF9E00", "darkGray": "#495055", "mediumGray": "#7F8284", "lightGray": "#BCBFBF", "lighterGray": "#F5F5F5", } for name, value in colors.items(): context.setContextProperty(name, value) def _window_root(self, window): if WINDOWS: return window.rootObject() return window def translate(self, message: str, values: dict = None) -> str: return Translator.get(message, values) def _show_window(self, window: "QWindow") -> None: window.show() window.raise_() def _destroy_dialog(self) -> None: sender = self.sender() name = sender.objectName() self.dialogs.pop(name, None) def _create_unique_dialog(self, name: str, dialog: QDialog) -> None: dialog.setObjectName(name) dialog.destroyed.connect(self._destroy_dialog) self.dialogs[name] = dialog def _init_translator(self) -> None: locale = Options.force_locale or Options.locale Translator( self.manager, find_resource("i18n"), self.manager.get_config("locale", locale), ) self.installTranslator(Translator._singleton) @staticmethod def get_window_icon() -> str: return find_icon("app_icon.svg") @pyqtSlot(str, str, str) def _direct_edit_conflict(self, filename: str, ref: str, digest: str) -> None: log.trace("Entering _direct_edit_conflict for %r / %r", filename, ref) try: if filename in self._conflicts_modals: log.trace("Filename already in _conflicts_modals: %r", filename) return log.trace("Putting filename in _conflicts_modals: %r", filename) self._conflicts_modals[filename] = True msg = QMessageBox() msg.setInformativeText( Translator.get("DIRECT_EDIT_CONFLICT_MESSAGE", [short_name(filename)])) overwrite = msg.addButton( Translator.get("DIRECT_EDIT_CONFLICT_OVERWRITE"), QMessageBox.AcceptRole) msg.addButton(Translator.get("CANCEL"), QMessageBox.RejectRole) msg.setIcon(QMessageBox.Warning) if msg.clickedButton() == overwrite: self.manager.direct_edit.force_update(ref, digest) del self._conflicts_modals[filename] except: log.exception( "Error while displaying Direct Edit conflict modal dialog for %r", filename, ) @pyqtSlot(str, list) def _direct_edit_error(self, message: str, values: List[str]) -> None: """ Display a simple Direct Edit error message. """ msg = QMessageBox() msg.setWindowTitle("Direct Edit - {self.manager.app_name}") msg.setWindowIcon(QIcon(self.get_window_icon())) msg.setIcon(QMessageBox.Warning) msg.setTextFormat(Qt.RichText) msg.setText(self.translate(message, values)) msg.exec_() @pyqtSlot() def _root_deleted(self) -> None: engine = self.sender() log.debug("Root has been deleted for engine: %s", engine.uid) msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setWindowIcon(QIcon(self.get_window_icon())) msg.setText(Translator.get("DRIVE_ROOT_DELETED", [engine.local_folder])) recreate = msg.addButton(Translator.get("DRIVE_ROOT_RECREATE"), QMessageBox.AcceptRole) disconnect = msg.addButton(Translator.get("DRIVE_ROOT_DISCONNECT"), QMessageBox.RejectRole) msg.exec_() res = msg.clickedButton() if res == disconnect: self.manager.unbind_engine(engine.uid) elif res == recreate: engine.reinit() engine.start() @pyqtSlot() def _no_space_left(self) -> None: msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowIcon(QIcon(self.get_window_icon())) msg.setText(Translator.get("NO_SPACE_LEFT_ON_DEVICE")) msg.addButton(Translator.get("OK"), QMessageBox.AcceptRole) msg.exec_() @pyqtSlot(str) def _root_moved(self, new_path: str) -> None: engine = self.sender() log.debug("Root has been moved for engine: %s to %r", engine.uid, new_path) info = [engine.local_folder, new_path] msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setWindowIcon(QIcon(self.get_window_icon())) msg.setText(Translator.get("DRIVE_ROOT_MOVED", info)) move = msg.addButton(Translator.get("DRIVE_ROOT_UPDATE"), QMessageBox.AcceptRole) recreate = msg.addButton(Translator.get("DRIVE_ROOT_RECREATE"), QMessageBox.AcceptRole) disconnect = msg.addButton(Translator.get("DRIVE_ROOT_DISCONNECT"), QMessageBox.RejectRole) msg.exec_() res = msg.clickedButton() if res == disconnect: self.manager.unbind_engine(engine.uid) elif res == recreate: engine.reinit() engine.start() elif res == move: engine.set_local_folder(new_path) engine.start() @pyqtSlot(object) def dropped_engine(self, engine: "Engine") -> None: # Update icon in case the engine dropped was syncing self.change_systray_icon() @pyqtSlot() def change_systray_icon(self) -> None: # Update status has the precedence over other ones if self.manager.updater.status not in ( UPDATE_STATUS_UNAVAILABLE_SITE, UPDATE_STATUS_UP_TO_DATE, ): self.set_icon_state("update") return syncing = conflict = False engines = self.manager.get_engines() invalid_credentials = paused = offline = True for engine in engines.values(): syncing |= engine.is_syncing() invalid_credentials &= engine.has_invalid_credentials() paused &= engine.is_paused() offline &= engine.is_offline() conflict |= bool(engine.get_conflicts()) if offline: new_state = "error" Action(Translator.get("OFFLINE")) elif invalid_credentials: new_state = "error" Action(Translator.get("AUTH_EXPIRED")) elif not engines: new_state = "disabled" Action.finish_action() elif paused: new_state = "paused" Action.finish_action() elif syncing: new_state = "syncing" elif conflict: new_state = "conflict" else: new_state = "idle" Action.finish_action() self.set_icon_state(new_state) def refresh_conflicts(self, uid: str) -> None: """ Update the content of the conflicts/errors window. """ self.conflicts_model.empty() self.errors_model.empty() self.ignoreds_model.empty() self.conflicts_model.addFiles(self.api.get_conflicts(uid)) self.errors_model.addFiles(self.api.get_errors(uid)) self.ignoreds_model.addFiles(self.api.get_unsynchronizeds(uid)) @pyqtSlot() def show_conflicts_resolution(self, engine: "Engine") -> None: """ Display the conflicts/errors window. """ self.refresh_conflicts(engine.uid) self._window_root(self.conflicts_window).setEngine.emit(engine.uid) self.conflicts_window.show() self.conflicts_window.requestActivate() @pyqtSlot() def show_settings(self, section: str = "General") -> None: sections = {"General": 0, "Accounts": 1, "About": 2} self._window_root(self.settings_window).setSection.emit( sections[section]) self.settings_window.show() self.settings_window.requestActivate() @pyqtSlot() def show_systray(self) -> None: icon = self.tray_icon.geometry() if not icon or icon.isEmpty(): # On Ubuntu it's likely we can't retrieve the geometry. # We're simply displaying the systray in the top right corner. screen = self.desktop().screenGeometry() pos_x = screen.right() - self.systray_window.width() - 20 pos_y = 30 else: pos_x = max(0, icon.x() + icon.width() - self.systray_window.width()) pos_y = icon.y() - self.systray_window.height() if pos_y < 0: pos_y = icon.y() + icon.height() self.systray_window.setX(pos_x) self.systray_window.setY(pos_y) self.systray_window.show() self.systray_window.raise_() @pyqtSlot() def hide_systray(self): self.systray_window.hide() @pyqtSlot() def open_help(self) -> None: self.manager.open_help() @pyqtSlot() def destroyed_filters_dialog(self) -> None: self.filters_dlg = None @pyqtSlot() def show_filters(self, engine: "Engine") -> None: if self.filters_dlg: self.filters_dlg.close() self.filters_dlg = None from ..gui.folders_dialog import FiltersDialog self.filters_dlg = FiltersDialog(self, engine) self.filters_dlg.destroyed.connect(self.destroyed_filters_dialog) self.filters_dlg.show() def show_file_status(self) -> None: from ..gui.status_dialog import StatusDialog for _, engine in self.manager.get_engines().items(): self.status = StatusDialog(engine.get_dao()) self.status.show() break def show_activities(self) -> None: return # TODO: Create activities window self.activities.show() @pyqtSlot(str, object) def _open_authentication_dialog(self, url: str, callback_params: Dict[str, str]) -> None: self.api._callback_params = callback_params if Options.is_frozen: """ Authenticate through the browser. This authentication requires the server's Nuxeo Drive addon to include NXP-25519. Instead of opening the server's login page in a WebKit view through the app, it opens in the browser and retrieves the login token by opening an nxdrive:// URL. """ self.manager.open_local_file(url) else: self._web_auth_not_frozen(url) def _web_auth_not_frozen(self, url: str) -> None: """ Open a dialog box to fill the credentials. Then a request will be done using the Python client to get a token. This is used when the application is not frozen as there is no custom protocol handler in this case. """ from PyQt5.QtWidgets import QLineEdit from nuxeo.client import Nuxeo dialog = QDialog() dialog.setWindowTitle( self.translate("WEB_AUTHENTICATION_WINDOW_TITLE")) dialog.setWindowIcon(QIcon(self.get_window_icon())) dialog.resize(250, 100) layout = QVBoxLayout() username = QLineEdit("Administrator", parent=dialog) password = QLineEdit("Administrator", parent=dialog) password.setEchoMode(QLineEdit.Password) layout.addWidget(username) layout.addWidget(password) def auth() -> None: """Retrieve a token and create the account.""" user = str(username.text()) pwd = str(password.text()) nuxeo = Nuxeo(host=url, auth=(user, pwd)) try: token = nuxeo.client.request_auth_token( device_id=self.manager.device_id, app_name=APP_NAME, permission=TOKEN_PERMISSION, device=get_device(), ) except Exception as exc: log.error(f"Connection error: {exc}") token = "" finally: del nuxeo self.api.handle_token(token, user) dialog.close() buttons = QDialogButtonBox() buttons.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok) buttons.accepted.connect(auth) buttons.rejected.connect(dialog.close) layout.addWidget(buttons) dialog.setLayout(layout) dialog.exec_() @pyqtSlot(object) def _connect_engine(self, engine: "Engine") -> None: engine.syncStarted.connect(self.change_systray_icon) engine.syncCompleted.connect(self.change_systray_icon) engine.invalidAuthentication.connect(self.change_systray_icon) engine.syncSuspended.connect(self.change_systray_icon) engine.syncResumed.connect(self.change_systray_icon) engine.offline.connect(self.change_systray_icon) engine.online.connect(self.change_systray_icon) engine.rootDeleted.connect(self._root_deleted) engine.rootMoved.connect(self._root_moved) engine.noSpaceLeftOnDevice.connect(self._no_space_left) self.change_systray_icon() @pyqtSlot() def _debug_toggle_invalid_credentials(self) -> None: sender = self.sender() engine = sender.data() engine.set_invalid_credentials(not engine.has_invalid_credentials(), reason="debug") @pyqtSlot() def _debug_show_file_status(self) -> None: from ..gui.status_dialog import StatusDialog sender = self.sender() engine = sender.data() self.status_dialog = StatusDialog(engine.get_dao()) self.status_dialog.show() def _create_debug_engine_menu(self, engine: "Engine", parent: QMenu) -> QMenu: menu = QMenu(parent) action = QAction(Translator.get("DEBUG_INVALID_CREDENTIALS"), menu) action.setCheckable(True) action.setChecked(engine.has_invalid_credentials()) action.setData(engine) action.triggered.connect(self._debug_toggle_invalid_credentials) menu.addAction(action) action = QAction(Translator.get("DEBUG_FILE_STATUS"), menu) action.setData(engine) action.triggered.connect(self._debug_show_file_status) menu.addAction(action) return menu def create_debug_menu(self, menu: QMenu) -> None: menu.addAction(Translator.get("DEBUG_WINDOW"), self.show_debug_window) for engine in self.manager.get_engines().values(): action = QAction(engine.name, menu) action.setMenu(self._create_debug_engine_menu(engine, menu)) action.setData(engine) menu.addAction(action) @pyqtSlot() def show_debug_window(self) -> None: return debug = self.dialogs.get("debug") # TODO: if not debug: Create debug window self._show_window(debug) def init_checks(self) -> None: if Options.debug: self.show_debug_window() for _, engine in self.manager.get_engines().items(): self._connect_engine(engine) self.manager.newEngine.connect(self._connect_engine) self.manager.notification_service.newNotification.connect( self._new_notification) self.manager.updater.updateAvailable.connect(self._update_notification) if not self.manager.get_engines(): self.show_settings() else: for engine in self.manager.get_engines().values(): # Prompt for settings if needed if engine.has_invalid_credentials(): self.show_settings("Accounts_" + engine.uid) break self.manager.start() @pyqtSlot() @if_frozen def _update_notification(self) -> None: self.change_systray_icon() # Display a notification status, version = self.manager.updater.status, self.manager.updater.version msg = ( "AUTOUPDATE_UPGRADE", "AUTOUPDATE_DOWNGRADE")[status == UPDATE_STATUS_DOWNGRADE_NEEDED] description = Translator.get(msg, [version]) flags = Notification.FLAG_BUBBLE | Notification.FLAG_UNIQUE if LINUX: description += " " + Translator.get("AUTOUPDATE_MANUAL") flags |= Notification.FLAG_SYSTRAY log.warning(description) notification = Notification( uuid="AutoUpdate", flags=flags, title=Translator.get("NOTIF_UPDATE_TITLE", [version]), description=description, ) self.manager.notification_service.send_notification(notification) @pyqtSlot() def message_clicked(self) -> None: if self.current_notification: self.manager.notification_service.trigger_notification( self.current_notification.uid) def _setup_notification_center(self) -> None: from ..osi.darwin.pyNotificationCenter import ( setup_delegator, NotificationDelegator, ) if not self._delegator: self._delegator = NotificationDelegator.alloc().init() self._delegator._manager = self.manager setup_delegator(self._delegator) @pyqtSlot(object) def _new_notification(self, notif: Notification) -> None: if not notif.is_bubble(): return if self._delegator is not None: # Use notification center from ..osi.darwin.pyNotificationCenter import notify return notify(notif.title, None, notif.description, user_info={"uuid": notif.uid}) icon = QSystemTrayIcon.Information if notif.level == Notification.LEVEL_WARNING: icon = QSystemTrayIcon.Warning elif notif.level == Notification.LEVEL_ERROR: icon = QSystemTrayIcon.Critical self.current_notification = notif self.tray_icon.showMessage(notif.title, notif.description, icon, 10000) def set_icon_state(self, state: str) -> bool: """ Execute systray icon change operations triggered by state change. The synchronization thread can update the state info but cannot directly call QtGui widget methods. This should be executed by the main thread event loop, hence the delegation to this method that is triggered by a signal to allow for message passing between the 2 threads. Return True of the icon has changed state. """ if self.icon_state == state: # Nothing to update return False self.tray_icon.setToolTip(self.get_tooltip()) self.tray_icon.setIcon(self.icons[state]) self.icon_state = state return True def get_tooltip(self) -> str: actions = Action.get_actions() if not actions: return self.default_tooltip # Display only the first action for now for action in actions.values(): if action and action.type and not action.type.startswith("_"): break else: return self.default_tooltip if isinstance(action, FileAction): if action.get_percent() is not None: return "%s - %s - %s - %d%%" % ( self.default_tooltip, action.type, action.filename, action.get_percent(), ) return "%s - %s - %s" % (self.default_tooltip, action.type, action.filename) elif action.get_percent() is not None: return "%s - %s - %d%%" % ( self.default_tooltip, action.type, action.get_percent(), ) return "%s - %s" % (self.default_tooltip, action.type) @if_frozen def show_release_notes(self, version: str) -> None: """ Display release notes of a given version. """ beta = self.manager.get_beta_channel() log.debug("Showing release notes, version=%r beta=%r", version, beta) # For now, we do care about beta only if not beta: return url = ("https://api.github.com/repos/nuxeo/nuxeo-drive" "/releases/tags/release-" + version) if beta: version += " beta" try: content = requests.get(url) except requests.HTTPError as exc: if exc.response.status_code == 404: log.error("[%s] Release does not exist", version) else: log.exception( "[%s] Network error while fetching release notes", version) return except: log.exception("[%s] Unknown error while fetching release notes", version) return try: data = content.json() except ValueError: log.exception("[%s] Invalid release notes", version) return finally: del content try: html = markdown(data["body"]) except KeyError: log.error("[%s] Release notes is missing its body", version) return except (UnicodeDecodeError, ValueError): log.exception("[%s] Release notes conversion error", version) return dialog = QDialog() dialog.setWindowTitle( f"{self.manager.app_name} {version} - Release notes") dialog.setWindowIcon(QIcon(self.get_window_icon())) dialog.resize(600, 400) notes = QTextEdit() notes.setStyleSheet("background-color: #eee; border: none;") notes.setReadOnly(True) notes.setHtml(html) buttons = QDialogButtonBox() buttons.setStandardButtons(QDialogButtonBox.Ok) buttons.clicked.connect(dialog.accept) layout = QVBoxLayout() layout.addWidget(notes) layout.addWidget(buttons) dialog.setLayout(layout) dialog.exec_() def show_metadata(self, file_path: str) -> None: self.manager.ctx_edit_metadata(file_path) def setup_systray(self) -> None: icons = {} for state in { "conflict", "disabled", "error", "idle", "notification", "paused", "syncing", "update", }: name = "{}{}.svg".format(state, "_light" if WINDOWS else "") icon = QIcon() icon.addFile(find_icon(name)) if MAC: icon.addFile(find_icon("active.svg"), mode=QIcon.Selected) icons[state] = icon setattr(self, "icons", icons) self.tray_icon = DriveSystrayIcon(self) if not self.tray_icon.isSystemTrayAvailable(): log.critical("There is no system tray available!") else: self.tray_icon.setToolTip(self.manager.app_name) self.set_icon_state("disabled") self.tray_icon.show() def event(self, event: QEvent) -> bool: """ Handle URL scheme events under macOS. """ url = getattr(event, "url", None) if not url: # This is not an event for us! return super().event(event) try: final_url = unquote(event.url().toString()) return self._handle_nxdrive_url(final_url) except: log.exception("Error handling URL event %r", url) return False def _handle_nxdrive_url(self, url: str) -> bool: """ Handle an nxdrive protocol URL. """ info = parse_protocol_url(url) if not info: return False cmd = info["command"] path = info.get("filepath", None) manager = self.manager log.debug("Event URL=%s, info=%r", url, info) # Event fired by a context menu item func = { "access-online": manager.ctx_access_online, "copy-share-link": manager.ctx_copy_share_link, "edit-metadata": manager.ctx_edit_metadata, }.get(cmd, None) if func: func(path) elif "edit" in cmd: manager.direct_edit.edit( info["server_url"], info["doc_id"], user=info["user"], download_url=info["download_url"], ) elif cmd == "trigger-watch": for engine in manager._engine_definitions: manager.osi.watch_folder(engine.local_folder) elif cmd == "token": self.api.handle_token(info["token"], info["username"]) else: log.warning("Unknown event URL=%r, info=%r", url, info) return False return True @if_frozen def init_nxdrive_listener(self) -> None: """ Set up a QLocalServer to listen to nxdrive protocol calls. On Windows, when an nxdrive:// URL is opened, it creates a new instance of Nuxeo Drive. As we want the already running instance to receive this call (particularly during the login process), we set up a QLocalServer in that instance to listen to the new ones who will send their data. The Qt implementation of QLocalSocket on Windows makes use of named pipes. We just need to connect a handler to the newConnection signal to process the URLs. """ self._nxdrive_listener = QLocalServer() self._nxdrive_listener.newConnection.connect(self._handle_connection) self._nxdrive_listener.listen("com.nuxeo.drive.protocol") self.aboutToQuit.connect(self._nxdrive_listener.close) @if_frozen def _handle_connection(self) -> None: """ Retrieve the connection with other instances and handle the incoming data. """ con: QLocalSocket = self._nxdrive_listener.nextPendingConnection() log.debug("Receiving socket connection for nxdrive protocol handling") if not con or not con.waitForConnected(): log.error(f"Unable to open server socket: {con.errorString()}") return if con.waitForReadyRead(): payload = con.readAll() url = force_decode(payload.data()) self._handle_nxdrive_url(url) con.disconnectFromServer() con.waitForDisconnected() del con log.debug("Successfully closed server socket") # Bring settings window to front self.settings_window.show() def update_status(self, engine: "Engine") -> None: """ Update the systray status for synchronization, conflicts/errors and software updates. """ sync_state = error_state = update_state = "" update_state = self.manager.updater.status self.refresh_conflicts(engine.uid) # Check synchronization state if engine.is_paused(): sync_state = "suspended" elif engine.is_syncing(): sync_state = "syncing" # Check error state if engine.has_invalid_credentials(): error_state = "auth_expired" elif self.conflicts_model.count: error_state = "conflicted" elif self.errors_model.count: error_state = "error" self._window_root(self.systray_window).setStatus.emit( sync_state, error_state, update_state) @pyqtSlot(str) def get_last_files(self, uid: str) -> None: files = self.api.get_last_files(uid, 10, "", None) self.file_model.empty() self.file_model.addFiles(files) def current_language(self) -> Optional[str]: lang = Translator.locale() for tag, name in self.language_model.languages: if tag == lang: return name return None
class Application(QApplication): """Main Nuxeo Drive application controlled by a system tray icon + menu""" icon = QIcon(str(find_icon("app_icon.svg"))) icons: Dict[str, QIcon] = {} icon_state = None use_light_icons = None filters_dlg: Optional[FiltersDialog] = None _delegator: Optional["NotificationDelegator"] = None tray_icon: DriveSystrayIcon def __init__(self, manager: "Manager", *args: Any) -> None: super().__init__(list(*args)) self.manager = manager self.osi = self.manager.osi self.setWindowIcon(self.icon) self.setApplicationName(APP_NAME) self._init_translator() self.setQuitOnLastWindowClosed(False) self.ask_for_metrics_approval() self._conflicts_modals: Dict[str, bool] = dict() self.current_notification: Optional[Notification] = None self.default_tooltip = APP_NAME font = QFont("Helvetica, Arial, sans-serif", 12) self.setFont(font) self.ratio = sqrt(QFontMetricsF(font).height() / 12) self.init_gui() self.manager.dropEngine.connect(self.dropped_engine) self.setup_systray() self.manager.reloadIconsSet.connect(self.load_icons_set) # Direct Edit self.manager.direct_edit.directEditConflict.connect(self._direct_edit_conflict) self.manager.direct_edit.directEditError.connect(self._direct_edit_error) # Check if actions is required, separate method so it can be overridden self.init_checks() # Setup notification center for macOS if MAC: self._setup_notification_center() # Application update self.manager.updater.appUpdated.connect(self.quit) self.manager.updater.serverIncompatible.connect(self._server_incompatible) self.manager.updater.wrongChannel.connect(self._wrong_channel) # Display release notes on new version if self.manager.old_version != self.manager.version: self.show_release_notes(self.manager.version) # Listen for nxdrive:// sent by a new instance self.init_nxdrive_listener() # Connect this slot last so the other slots connected # to self.aboutToQuit can run beforehand. self.aboutToQuit.connect(self.manager.stop) @if_frozen def add_qml_import_path(self, view: QQuickView) -> None: """ Manually set the path to the QML folder to fix errors with unicode paths. This is needed only on Windows when packaged with Nuitka. """ if Options.freezer != "nuitka": return qml_dir = Options.res_dir.parent / "PyQt5" / "Qt" / "qml" log.debug(f"Setting QML import path for {view} to {qml_dir!r}") view.engine().addImportPath(str(qml_dir)) def init_gui(self) -> None: self.api = QMLDriveApi(self) self.conflicts_model = FileModel() self.errors_model = FileModel() self.engine_model = EngineModel(self) self.action_model = ActionModel() self.file_model = FileModel() self.ignoreds_model = FileModel() self.language_model = LanguageModel() self.add_engines(list(self.manager._engines.values())) self.engine_model.statusChanged.connect(self.update_status) self.language_model.addLanguages(Translator.languages()) flags = Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint if WINDOWS: self.conflicts_window = QQuickView() self.add_qml_import_path(self.conflicts_window) self.conflicts_window.setMinimumWidth(550) self.conflicts_window.setMinimumHeight(600) self.settings_window = QQuickView() self.add_qml_import_path(self.settings_window) self.settings_window.setMinimumWidth(640) self.settings_window.setMinimumHeight(520) self.systray_window = SystrayWindow() self.add_qml_import_path(self.systray_window) self._fill_qml_context(self.conflicts_window.rootContext()) self._fill_qml_context(self.settings_window.rootContext()) self._fill_qml_context(self.systray_window.rootContext()) self.systray_window.rootContext().setContextProperty( "systrayWindow", self.systray_window ) self.conflicts_window.setSource( QUrl.fromLocalFile(str(find_resource("qml", "Conflicts.qml"))) ) self.settings_window.setSource( QUrl.fromLocalFile(str(find_resource("qml", "Settings.qml"))) ) self.systray_window.setSource( QUrl.fromLocalFile(str(find_resource("qml", "Systray.qml"))) ) flags |= Qt.Popup else: self.app_engine = QQmlApplicationEngine() self._fill_qml_context(self.app_engine.rootContext()) self.app_engine.load( QUrl.fromLocalFile(str(find_resource("qml", "Main.qml"))) ) root = self.app_engine.rootObjects()[0] self.conflicts_window = root.findChild(QQuickWindow, "conflictsWindow") self.settings_window = root.findChild(QQuickWindow, "settingsWindow") self.systray_window = root.findChild(SystrayWindow, "systrayWindow") if LINUX: flags |= Qt.Drawer self.systray_window.setFlags(flags) self.manager.newEngine.connect(self.add_engines) self.manager.initEngine.connect(self.add_engines) self.manager.dropEngine.connect(self.remove_engine) self._window_root(self.conflicts_window).changed.connect(self.refresh_conflicts) self._window_root(self.systray_window).appUpdate.connect(self.api.app_update) self._window_root(self.systray_window).getLastFiles.connect(self.get_last_files) self.api.setMessage.connect(self._window_root(self.settings_window).setMessage) if self.manager.get_engines(): current_uid = self.engine_model.engines_uid[0] self.get_last_files(current_uid) self.update_status(self.manager._engines[current_uid]) self.manager.updater.updateAvailable.connect( self._window_root(self.systray_window).updateAvailable ) self.manager.updater.updateProgress.connect( self._window_root(self.systray_window).updateProgress ) @pyqtSlot(Action) def action_started(self, action: Action) -> None: self.refresh_actions() @pyqtSlot(Action) def action_progressing(self, action: Action) -> None: self.action_model.set_progress(action.export()) @pyqtSlot(Action) def action_done(self, action: Action) -> None: self.refresh_actions() def add_engines(self, engines: Union[Engine, List[Engine]]) -> None: if not engines: return engines = engines if isinstance(engines, list) else [engines] for engine in engines: self.engine_model.addEngine(engine.uid) def remove_engine(self, uid: str) -> None: self.engine_model.removeEngine(uid) def _fill_qml_context(self, context: QQmlContext) -> None: """ Fill the context of a QML element with the necessary resources. """ context.setContextProperty("ConflictsModel", self.conflicts_model) context.setContextProperty("ErrorsModel", self.errors_model) context.setContextProperty("EngineModel", self.engine_model) context.setContextProperty("ActionModel", self.action_model) context.setContextProperty("FileModel", self.file_model) context.setContextProperty("IgnoredsModel", self.ignoreds_model) context.setContextProperty("languageModel", self.language_model) context.setContextProperty("api", self.api) context.setContextProperty("application", self) context.setContextProperty("currentLanguage", self.current_language()) context.setContextProperty("manager", self.manager) context.setContextProperty("osi", self.osi) context.setContextProperty("updater", self.manager.updater) context.setContextProperty("ratio", self.ratio) context.setContextProperty("update_check_delay", Options.update_check_delay) context.setContextProperty("isFrozen", Options.is_frozen) context.setContextProperty("WINDOWS", WINDOWS) context.setContextProperty("tl", Translator._singleton) context.setContextProperty( "nuxeoVersionText", f"{APP_NAME} {self.manager.version}" ) metrics = self.manager.get_metrics() versions = ( f'Python {metrics["python_version"]}, ' f'Qt {metrics["qt_version"]}, ' f'SIP {metrics["sip_version"]}' ) if Options.system_wide: versions += " [admin]" context.setContextProperty("modulesVersionText", versions) colors = { "darkBlue": "#1F28BF", "nuxeoBlue": "#0066FF", "lightBlue": "#00ADED", "teal": "#73D2CF", "purple": "#8400FF", "red": "#C02828", "orange": "#FF9E00", "darkGray": "#495055", "mediumGray": "#7F8284", "lightGray": "#BCBFBF", "lighterGray": "#F5F5F5", } for name, value in colors.items(): context.setContextProperty(name, value) def _window_root(self, window): if WINDOWS: return window.rootObject() return window def translate(self, message: str, values: List[Any] = None) -> str: return Translator.get(message, values) def _show_window(self, window: QWindow) -> None: window.show() window.raise_() window.requestActivate() def _init_translator(self) -> None: locale = Options.force_locale or Options.locale Translator(find_resource("i18n"), self.manager.get_config("locale", locale)) # Make sure that a language change changes external values like # the text in the contextual menu Translator.on_change(self._handle_language_change) # Trigger it now self.osi.register_contextual_menu() self.installTranslator(Translator._singleton) @pyqtSlot(str, Path, str) def _direct_edit_conflict(self, filename: str, ref: Path, digest: str) -> None: log.debug(f"Entering _direct_edit_conflict for {filename!r} / {ref!r}") try: if filename in self._conflicts_modals: log.debug(f"Filename already in _conflicts_modals: {filename!r}") return log.debug(f"Putting filename in _conflicts_modals: {filename!r}") self._conflicts_modals[filename] = True msg = QMessageBox() msg.setInformativeText( Translator.get("DIRECT_EDIT_CONFLICT_MESSAGE", [short_name(filename)]) ) overwrite = msg.addButton( Translator.get("DIRECT_EDIT_CONFLICT_OVERWRITE"), QMessageBox.AcceptRole ) msg.addButton(Translator.get("CANCEL"), QMessageBox.RejectRole) msg.setIcon(QMessageBox.Warning) msg.exec_() if msg.clickedButton() == overwrite: self.manager.direct_edit.force_update(ref, digest) del self._conflicts_modals[filename] except: log.exception( f"Error while displaying Direct Edit conflict modal dialog for {filename!r}" ) @pyqtSlot(str, list) def _direct_edit_error(self, message: str, values: List[str]) -> None: """ Display a simple Direct Edit error message. """ msg_text = self.translate(message, values) log.warning(f"DirectEdit error message: '{msg_text}', values={values}") msg = QMessageBox() msg.setWindowTitle(f"Direct Edit - {APP_NAME}") msg.setWindowIcon(self.icon) msg.setIcon(QMessageBox.Warning) msg.setTextFormat(Qt.RichText) msg.setText(msg_text) msg.exec_() @pyqtSlot() def _root_deleted(self) -> None: engine = self.sender() log.info(f"Root has been deleted for engine: {engine.uid}") msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setWindowIcon(self.icon) msg.setText(Translator.get("DRIVE_ROOT_DELETED", [engine.local_folder])) recreate = msg.addButton( Translator.get("DRIVE_ROOT_RECREATE"), QMessageBox.AcceptRole ) disconnect = msg.addButton( Translator.get("DRIVE_ROOT_DISCONNECT"), QMessageBox.RejectRole ) msg.exec_() res = msg.clickedButton() if res == disconnect: self.manager.unbind_engine(engine.uid) elif res == recreate: engine.reinit() engine.start() @pyqtSlot() def _no_space_left(self) -> None: msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowIcon(self.icon) msg.setText(Translator.get("NO_SPACE_LEFT_ON_DEVICE")) msg.addButton(Translator.get("OK"), QMessageBox.AcceptRole) msg.exec_() @pyqtSlot(Path) def _root_moved(self, new_path: Path) -> None: engine = self.sender() log.info(f"Root has been moved for engine: {engine.uid} to {new_path!r}") info = [engine.local_folder, str(new_path)] msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setWindowIcon(self.icon) msg.setText(Translator.get("DRIVE_ROOT_MOVED", info)) move = msg.addButton( Translator.get("DRIVE_ROOT_UPDATE"), QMessageBox.AcceptRole ) recreate = msg.addButton( Translator.get("DRIVE_ROOT_RECREATE"), QMessageBox.AcceptRole ) disconnect = msg.addButton( Translator.get("DRIVE_ROOT_DISCONNECT"), QMessageBox.RejectRole ) msg.exec_() res = msg.clickedButton() if res == disconnect: self.manager.unbind_engine(engine.uid) elif res == recreate: engine.reinit() engine.start() elif res == move: engine.set_local_folder(new_path) engine.start() def confirm_deletion(self, path: Path) -> DelAction: msg = QMessageBox() msg.setIcon(QMessageBox.Question) msg.setWindowIcon(self.icon) cb = QCheckBox(Translator.get("DONT_ASK_AGAIN")) msg.setCheckBox(cb) mode = self.manager.get_deletion_behavior() unsync = None if mode is DelAction.DEL_SERVER: descr = "DELETION_BEHAVIOR_CONFIRM_DELETE" confirm_text = "DELETE_FOR_EVERYONE" unsync = msg.addButton( Translator.get("JUST_UNSYNC"), QMessageBox.RejectRole ) elif mode is DelAction.UNSYNC: descr = "DELETION_BEHAVIOR_CONFIRM_UNSYNC" confirm_text = "UNSYNC" msg.setText( Translator.get(descr, [str(path), Translator.get("SELECT_SYNC_FOLDERS")]) ) msg.addButton(Translator.get("CANCEL"), QMessageBox.RejectRole) confirm = msg.addButton(Translator.get(confirm_text), QMessageBox.AcceptRole) msg.exec_() res = msg.clickedButton() if cb.isChecked(): self.manager._dao.store_bool("show_deletion_prompt", False) if res == confirm: return mode if res == unsync: msg = QMessageBox() msg.setIcon(QMessageBox.Question) msg.setWindowIcon(self.icon) msg.setText(Translator.get("DELETION_BEHAVIOR_SWITCH")) msg.addButton(Translator.get("NO"), QMessageBox.RejectRole) confirm = msg.addButton(Translator.get("YES"), QMessageBox.AcceptRole) msg.exec_() res = msg.clickedButton() if res == confirm: self.manager.set_deletion_behavior(DelAction.UNSYNC) return DelAction.UNSYNC return DelAction.ROLLBACK @pyqtSlot(Path) def _doc_deleted(self, path: Path) -> None: engine: Engine = self.sender() mode = self.confirm_deletion(path) if mode is DelAction.ROLLBACK: # Re-sync the document engine.rollback_delete(path) else: # Delete or filter out the document engine.delete_doc(path, mode) @pyqtSlot(Path, Path) def _file_already_exists(self, oldpath: Path, newpath: Path) -> None: msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setWindowIcon(self.icon) msg.setText(Translator.get("FILE_ALREADY_EXISTS", values=[str(oldpath)])) replace = msg.addButton(Translator.get("REPLACE"), QMessageBox.AcceptRole) msg.addButton(Translator.get("CANCEL"), QMessageBox.RejectRole) msg.exec_() if msg.clickedButton() == replace: oldpath.unlink() normalize_event_filename(newpath) else: newpath.unlink() @pyqtSlot(object) def dropped_engine(self, engine: "Engine") -> None: # Update icon in case the engine dropped was syncing self.change_systray_icon() @pyqtSlot() def change_systray_icon(self) -> None: # Update status has the precedence over other ones if self.manager.updater.status not in ( UPDATE_STATUS_UNAVAILABLE_SITE, UPDATE_STATUS_UP_TO_DATE, ): self.set_icon_state("update") return syncing = conflict = False engines = self.manager.get_engines() invalid_credentials = paused = offline = True for engine in engines.values(): syncing |= engine.is_syncing() invalid_credentials &= engine.has_invalid_credentials() paused &= engine.is_paused() offline &= engine.is_offline() conflict |= bool(engine.get_conflicts()) if offline: new_state = "error" Action(Translator.get("OFFLINE")) elif invalid_credentials: new_state = "error" Action(Translator.get("AUTH_EXPIRED")) elif not engines: new_state = "disabled" Action.finish_action() elif paused: new_state = "paused" Action.finish_action() elif syncing: new_state = "syncing" elif conflict: new_state = "conflict" else: new_state = "idle" Action.finish_action() self.set_icon_state(new_state) def refresh_conflicts(self, uid: str) -> None: """ Update the content of the conflicts/errors window. """ self.conflicts_model.empty() self.errors_model.empty() self.ignoreds_model.empty() self.conflicts_model.addFiles(self.api.get_conflicts(uid)) self.errors_model.addFiles(self.api.get_errors(uid)) self.ignoreds_model.addFiles(self.api.get_unsynchronizeds(uid)) @pyqtSlot() def show_conflicts_resolution(self, engine: "Engine") -> None: """ Display the conflicts/errors window. """ self.refresh_conflicts(engine.uid) self._window_root(self.conflicts_window).setEngine.emit(engine.uid) self.conflicts_window.show() self.conflicts_window.requestActivate() @pyqtSlot() def show_settings(self, section: str = "General") -> None: sections = {"General": 0, "Accounts": 1, "About": 2} self._window_root(self.settings_window).setSection.emit(sections[section]) self.settings_window.show() self.settings_window.requestActivate() @pyqtSlot() def show_systray(self) -> None: icon = self.tray_icon.geometry() if not icon or icon.isEmpty(): # On Ubuntu it's likely we can't retrieve the geometry. # We're simply displaying the systray in the top right corner. screen = self.desktop().screenGeometry() pos_x = screen.right() - self.systray_window.width() - 20 pos_y = 30 else: dpi_ratio = self.primaryScreen().devicePixelRatio() if WINDOWS else 1 pos_x = max( 0, (icon.x() + icon.width()) / dpi_ratio - self.systray_window.width() ) pos_y = icon.y() / dpi_ratio - self.systray_window.height() if pos_y < 0: pos_y = (icon.y() + icon.height()) / dpi_ratio self.systray_window.setX(pos_x) self.systray_window.setY(pos_y) self.systray_window.show() self.systray_window.raise_() @pyqtSlot() def hide_systray(self): self.systray_window.hide() @pyqtSlot() def open_help(self) -> None: self.manager.open_help() @pyqtSlot() def destroyed_filters_dialog(self) -> None: self.filters_dlg = None @pyqtSlot() def show_filters(self, engine: "Engine") -> None: if self.filters_dlg: self.filters_dlg.close() self.filters_dlg = None self.filters_dlg = FiltersDialog(self, engine) self.filters_dlg.destroyed.connect(self.destroyed_filters_dialog) # Close the settings window at the same time of the filters one if hasattr(self, "close_settings_too"): self.filters_dlg.destroyed.connect(self.settings_window.close) delattr(self, "close_settings_too") self.filters_dlg.show() self._show_window(self.settings_window) @pyqtSlot(str, object) def _open_authentication_dialog( self, url: str, callback_params: Dict[str, str] ) -> None: self.api._callback_params = callback_params if Options.is_frozen: """ Authenticate through the browser. This authentication requires the server's Nuxeo Drive addon to include NXP-25519. Instead of opening the server's login page in a WebKit view through the app, it opens in the browser and retrieves the login token by opening an nxdrive:// URL. """ self.manager.open_local_file(url) else: self._web_auth_not_frozen(url) def _web_auth_not_frozen(self, url: str) -> None: """ Open a dialog box to fill the credentials. Then a request will be done using the Python client to get a token. This is used when the application is not frozen as there is no custom protocol handler in this case. """ from PyQt5.QtWidgets import QLineEdit from nuxeo.client import Nuxeo dialog = QDialog() dialog.setWindowTitle(self.translate("WEB_AUTHENTICATION_WINDOW_TITLE")) dialog.setWindowIcon(self.icon) dialog.resize(250, 100) layout = QVBoxLayout() username = QLineEdit("Administrator", parent=dialog) password = QLineEdit("Administrator", parent=dialog) password.setEchoMode(QLineEdit.Password) layout.addWidget(username) layout.addWidget(password) def auth() -> None: """Retrieve a token and create the account.""" user = str(username.text()) pwd = str(password.text()) nuxeo = Nuxeo( host=url, auth=(user, pwd), proxies=self.manager.proxy.settings(url=url), verify=Options.ca_bundle or not Options.ssl_no_verify, ) try: token = nuxeo.client.request_auth_token( device_id=self.manager.device_id, app_name=APP_NAME, permission=TOKEN_PERMISSION, device=get_device(), ) except Exception as exc: log.error(f"Connection error: {exc}") token = "" finally: del nuxeo # Check we have a token and not a HTML response if "\n" in token: token = "" self.api.handle_token(token, user) dialog.close() buttons = QDialogButtonBox() buttons.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok) buttons.accepted.connect(auth) buttons.rejected.connect(dialog.close) layout.addWidget(buttons) dialog.setLayout(layout) dialog.exec_() @pyqtSlot(object) def _connect_engine(self, engine: "Engine") -> None: engine.syncStarted.connect(self.change_systray_icon) engine.syncCompleted.connect(self.change_systray_icon) engine.invalidAuthentication.connect(self.change_systray_icon) engine.syncSuspended.connect(self.change_systray_icon) engine.syncResumed.connect(self.change_systray_icon) engine.offline.connect(self.change_systray_icon) engine.online.connect(self.change_systray_icon) engine.rootDeleted.connect(self._root_deleted) engine.rootMoved.connect(self._root_moved) engine.docDeleted.connect(self._doc_deleted) engine.fileAlreadyExists.connect(self._file_already_exists) engine.noSpaceLeftOnDevice.connect(self._no_space_left) self.change_systray_icon() def init_checks(self) -> None: for engine in self.manager.get_engines().values(): self._connect_engine(engine) self.manager.newEngine.connect(self._connect_engine) self.manager.notification_service.newNotification.connect( self._new_notification ) self.manager.notification_service.triggerNotification.connect( self._handle_notification_action ) self.manager.updater.updateAvailable.connect(self._update_notification) self.manager.updater.noSpaceLeftOnDevice.connect(self._no_space_left) if not self.manager.get_engines(): self.show_settings() else: for engine in self.manager.get_engines().values(): # Prompt for settings if needed if engine.has_invalid_credentials(): self.show_settings("Accounts") # f"Account_{engine.uid}" break self.manager.start() @pyqtSlot() @if_frozen def _update_notification(self) -> None: self.change_systray_icon() # Display a notification status, version = self.manager.updater.status, self.manager.updater.version msg = ("AUTOUPDATE_UPGRADE", "AUTOUPDATE_DOWNGRADE")[ status == UPDATE_STATUS_INCOMPATIBLE_SERVER ] description = Translator.get(msg, [version]) flags = Notification.FLAG_BUBBLE | Notification.FLAG_UNIQUE if LINUX: description += " " + Translator.get("AUTOUPDATE_MANUAL") flags |= Notification.FLAG_SYSTRAY log.warning(description) notification = Notification( uuid="AutoUpdate", flags=flags, title=Translator.get("NOTIF_UPDATE_TITLE", [version]), description=description, ) self.manager.notification_service.send_notification(notification) @pyqtSlot() def _server_incompatible(self) -> None: version = self.manager.version downgrade_version = self.manager.updater.version or "" msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowIcon(self.icon) msg.setText(Translator.get("SERVER_INCOMPATIBLE", [version, downgrade_version])) if downgrade_version: msg.addButton( Translator.get("CONTINUE_USING", [version]), QMessageBox.RejectRole ) downgrade = msg.addButton( Translator.get("DOWNGRADE_TO", [downgrade_version]), QMessageBox.AcceptRole, ) else: msg.addButton(Translator.get("CONTINUE"), QMessageBox.RejectRole) msg.exec_() res = msg.clickedButton() if downgrade_version and res == downgrade: self.manager.updater.update(downgrade_version) @pyqtSlot() def _wrong_channel(self) -> None: if self.manager.prompted_wrong_channel: log.debug( "Not prompting for wrong channel, already showed it since startup" ) return self.manager.prompted_wrong_channel = True version = self.manager.version downgrade_version = self.manager.updater.version or "" version_channel = self.manager.updater.get_version_channel(version) current_channel = self.manager.get_update_channel() msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowIcon(self.icon) msg.setText( Translator.get("WRONG_CHANNEL", [version, version_channel, current_channel]) ) switch_channel = msg.addButton( Translator.get("USE_CHANNEL", [version_channel]), QMessageBox.AcceptRole ) downgrade = msg.addButton( Translator.get("DOWNGRADE_TO", [downgrade_version]), QMessageBox.AcceptRole ) msg.exec_() res = msg.clickedButton() if downgrade_version and res == downgrade: self.manager.updater.update(downgrade_version) elif res == switch_channel: self.manager.set_update_channel(version_channel) @pyqtSlot() def message_clicked(self) -> None: if self.current_notification: self.manager.notification_service.trigger_notification( self.current_notification.uid ) def _setup_notification_center(self) -> None: if not self._delegator: self._delegator = NotificationDelegator.alloc().init() if self._delegator: self._delegator._manager = self.manager setup_delegator(self._delegator) @pyqtSlot(object) def _new_notification(self, notif: Notification) -> None: if not notif.is_bubble(): return if self._delegator is not None: # Use notification center from ..osi.darwin.pyNotificationCenter import notify user_info = {"uuid": notif.uid} if notif.uid else None return notify(notif.title, "", notif.description, user_info=user_info) icon = QSystemTrayIcon.Information if notif.level == Notification.LEVEL_WARNING: icon = QSystemTrayIcon.Warning elif notif.level == Notification.LEVEL_ERROR: icon = QSystemTrayIcon.Critical self.current_notification = notif self.tray_icon.showMessage(notif.title, notif.description, icon, 10000) @pyqtSlot(str, str) def _handle_notification_action(self, action: str, engine_uid: str) -> None: func = getattr(self.api, action, None) if not func: log.error(f"Action {action}() is not defined in {self.api}") return func(engine_uid) def set_icon_state(self, state: str, force: bool = False) -> bool: """ Execute systray icon change operations triggered by state change. The synchronization thread can update the state info but cannot directly call QtGui widget methods. This should be executed by the main thread event loop, hence the delegation to this method that is triggered by a signal to allow for message passing between the 2 threads. Return True of the icon has changed state or if force is True. """ if not force and self.icon_state == state: # Nothing to update return False self.tray_icon.setToolTip(self.get_tooltip()) self.tray_icon.setIcon(self.icons[state]) self.icon_state = state return True def get_tooltip(self) -> str: actions = Action.get_actions() if not actions: return self.default_tooltip # Display only the first action for now for action in actions.values(): if action and action.type and not action.type.startswith("_"): break else: return self.default_tooltip return f"{self.default_tooltip} - {action!r}" @if_frozen def show_release_notes(self, version: str) -> None: """ Display release notes of a given version. """ channel = self.manager.get_update_channel() log.info(f"Showing release notes, version={version!r} channel={channel}") # For now, we do care about beta only if channel != "beta": return url = ( "https://api.github.com/repos/nuxeo/nuxeo-drive" f"/releases/tags/release-{version}" ) if channel != "release": version += f" {channel}" try: # No need for the `verify` kwarg here as GitHub will never use a bad certificate. with requests.get(url) as resp: data = resp.json() html = markdown(data["body"]) except Exception: log.warning(f"[{version}] Release notes retrieval error") return dialog = QDialog() dialog.setWindowTitle(f"{APP_NAME} {version} - Release notes") dialog.setWindowIcon(self.icon) dialog.resize(600, 400) notes = QTextEdit() notes.setStyleSheet("background-color: #eee; border: none;") notes.setReadOnly(True) notes.setHtml(html) buttons = QDialogButtonBox() buttons.setStandardButtons(QDialogButtonBox.Ok) buttons.clicked.connect(dialog.accept) layout = QVBoxLayout() layout.addWidget(notes) layout.addWidget(buttons) dialog.setLayout(layout) dialog.exec_() def accept_unofficial_ssl_cert(self, hostname: str) -> bool: """Ask the user to bypass the SSL certificate verification.""" from ..utils import get_certificate_details def signature(sig: str) -> str: """ Format the certificate signature. >>> signature("0F4019D1E6C52EF9A3A929B6D5613816") 0f:40:19:d1:e6:c5:2e:f9:a3:a9:29:b6:d5:61:38:16 """ from textwrap import wrap return str.lower(":".join(wrap(sig, 2))) cert = get_certificate_details(hostname=hostname) if not cert: return False subject = [ f"<li>{details[0][0]}: {details[0][1]}</li>" for details in sorted(cert["subject"]) ] issuer = [ f"<li>{details[0][0]}: {details[0][1]}</li>" for details in sorted(cert["issuer"]) ] urls = [ f"<li><a href='{details}'>{details}</a></li>" for details in cert["caIssuers"] ] sig = f"<code><small>{signature(cert['serialNumber'])}</small></code>" message = f""" <h2>{Translator.get("SSL_CANNOT_CONNECT", [hostname])}</h2> <p style="color:red">{Translator.get("SSL_HOSTNAME_ERROR")}</p> <h2>{Translator.get("SSL_CERTIFICATE")}</h2> <ul> {"".join(subject)} <li style="margin-top: 10px;">{Translator.get("SSL_SERIAL_NUMBER")} {sig}</li> <li style="margin-top: 10px;">{Translator.get("SSL_DATE_FROM")} {cert["notBefore"]}</li> <li>{Translator.get("SSL_DATE_EXPIRATION")} {cert["notAfter"]}</li> </ul> <h2>{Translator.get("SSL_ISSUER")}</h2> <ul style="list-style-type:square;">{"".join(issuer)}</ul> <h2>{Translator.get("URL")}</h2> <ul>{"".join(urls)}</ul> """ dialog = QDialog() dialog.setWindowTitle(Translator.get("SSL_UNTRUSTED_CERT_TITLE")) dialog.setWindowIcon(self.icon) dialog.resize(600, 650) notes = QTextEdit() notes.setReadOnly(True) notes.setHtml(message) continue_with_bad_ssl_cert = False def accept() -> None: nonlocal continue_with_bad_ssl_cert continue_with_bad_ssl_cert = True dialog.accept() buttons = QDialogButtonBox() buttons.setStandardButtons(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.button(QDialogButtonBox.Ok).setEnabled(False) buttons.accepted.connect(accept) buttons.rejected.connect(dialog.close) def bypass_triggered(state: int) -> None: """Enable the OK button only when the checkbox is checked.""" buttons.button(QDialogButtonBox.Ok).setEnabled(bool(state)) bypass = QCheckBox(Translator.get("SSL_TRUST_ANYWAY")) bypass.stateChanged.connect(bypass_triggered) layout = QVBoxLayout() layout.addWidget(notes) layout.addWidget(bypass) layout.addWidget(buttons) dialog.setLayout(layout) dialog.exec_() return continue_with_bad_ssl_cert def show_metadata(self, path: Path) -> None: self.manager.ctx_edit_metadata(path) @pyqtSlot(bool) def load_icons_set(self, use_light_icons: bool = False) -> None: """Load a given icons set (either the default one "dark", or the light one).""" if self.use_light_icons is use_light_icons: return suffix = ("", "_light")[use_light_icons] mask = str(find_icon("active.svg")) # Icon mask for macOS for state in { "conflict", "disabled", "error", "idle", "notification", "paused", "syncing", "update", }: icon = QIcon() icon.addFile(str(find_icon(f"{state}{suffix}.svg"))) if MAC: icon.addFile(mask, mode=QIcon.Selected) self.icons[state] = icon self.use_light_icons = use_light_icons self.manager.set_config("light_icons", use_light_icons) # Reload the current showed icon if self.icon_state: self.set_icon_state(self.icon_state, force=True) def initial_icons_set(self) -> bool: """ Try to guess the most appropriate icons set at start. The user will still have the possibility to change that in Settings. """ use_light_icons = self.manager.get_config("light_icons", default=None) if use_light_icons is None: # Default value for GNU/Linux, macOS ans Windows 7 use_light_icons = False if WINDOWS: win_ver = sys.getwindowsversion() version = (win_ver.major, win_ver.minor) if version > (6, 1): # Windows 7 # Windows 8+ has a dark them by default use_light_icons = True else: # The value stored in DTB as a string '0' or '1', convert to boolean use_light_icons = bool(int(use_light_icons)) return use_light_icons def setup_systray(self) -> None: """Setup the icon system tray and its associated menu.""" self.load_icons_set(use_light_icons=self.initial_icons_set()) self.tray_icon = DriveSystrayIcon(self) if not self.tray_icon.isSystemTrayAvailable(): log.critical("There is no system tray available!") else: self.tray_icon.setToolTip(APP_NAME) self.set_icon_state("disabled") self.tray_icon.show() def _handle_language_change(self) -> None: self.manager.set_config("locale", Translator.locale()) if not MAC: self.tray_icon.setContextMenu(self.tray_icon.get_context_menu()) self.osi.register_contextual_menu() def event(self, event: QEvent) -> bool: """ Handle URL scheme events under macOS. """ url = getattr(event, "url", None) if not url: # This is not an event for us! return super().event(event) final_url = unquote(event.url().toString()) try: return self._handle_nxdrive_url(final_url) except: log.exception(f"Error handling URL event {final_url!r}") return False def _show_msgbox_restart_needed(self) -> None: msg = QMessageBox() msg.setIcon(QMessageBox.Information) msg.setText(Translator.get("RESTART_NEEDED_MSG", values=[APP_NAME])) msg.setWindowTitle(APP_NAME) msg.addButton(Translator.get("OK"), QMessageBox.AcceptRole) msg.exec_() def _handle_nxdrive_url(self, url: str) -> bool: """ Handle an nxdrive protocol URL. """ info = parse_protocol_url(url) if not info: return False cmd = info["command"] path = normalized_path(info.get("filepath", "")) manager = self.manager log.info(f"Event URL={url}, info={info!r}") # Event fired by a context menu item func = { "access-online": manager.ctx_access_online, "copy-share-link": manager.ctx_copy_share_link, "edit-metadata": manager.ctx_edit_metadata, }.get(cmd, None) if func: func(path) elif "edit" in cmd: if self.manager.restart_needed: self._show_msgbox_restart_needed() return False manager.direct_edit.edit( info["server_url"], info["doc_id"], user=info["user"], download_url=info["download_url"], ) elif cmd == "token": self.api.handle_token(info["token"], info["username"]) else: log.warning(f"Unknown event URL={url}, info={info!r}") return False return True def init_nxdrive_listener(self) -> None: """ Set up a QLocalServer to listen to nxdrive protocol calls. On Windows, when an nxdrive:// URL is opened, it creates a new instance of Nuxeo Drive. As we want the already running instance to receive this call (particularly during the login process), we set up a QLocalServer in that instance to listen to the new ones who will send their data. The Qt implementation of QLocalSocket on Windows makes use of named pipes. We just need to connect a handler to the newConnection signal to process the URLs. """ named_pipe = f"{BUNDLE_IDENTIFIER}.protocol.{os.getpid()}" server = QLocalServer() server.setSocketOptions(QLocalServer.WorldAccessOption) server.newConnection.connect(self._handle_connection) try: server.listen(named_pipe) log.info(f"Listening for nxdrive:// calls on {server.fullServerName()}") except: log.info( f"Unable to start local server on {named_pipe}: {server.errorString()}" ) self._nxdrive_listener = server self.aboutToQuit.connect(self._nxdrive_listener.close) def _handle_connection(self) -> None: """ Retrieve the connection with other instances and handle the incoming data. """ con: QLocalSocket = None try: con = self._nxdrive_listener.nextPendingConnection() log.info("Receiving socket connection for nxdrive protocol handling") if not con or not con.waitForConnected(): log.error(f"Unable to open server socket: {con.errorString()}") return if con.waitForReadyRead(): payload = con.readAll() url = force_decode(payload.data()) self._handle_nxdrive_url(url) con.disconnectFromServer() if con.state() == QLocalSocket.ConnectedState: con.waitForDisconnected() finally: del con log.info("Successfully closed server socket") def update_status(self, engine: "Engine") -> None: """ Update the systray status for synchronization, conflicts/errors and software updates. """ sync_state = error_state = update_state = "" update_state = self.manager.updater.status self.refresh_conflicts(engine.uid) # Check synchronization state if self.manager.restart_needed: sync_state = "restart" elif engine.is_paused(): sync_state = "suspended" elif engine.is_syncing(): sync_state = "syncing" # Check error state if engine.has_invalid_credentials(): error_state = "auth_expired" elif self.conflicts_model.count: error_state = "conflicted" elif self.errors_model.count: error_state = "error" self._window_root(self.systray_window).setStatus.emit( sync_state, error_state, update_state ) @pyqtSlot() def refresh_actions(self) -> None: actions = self.api.get_actions() if actions != self.action_model.actions: self.action_model.set_actions(actions) self.action_model.fileChanged.emit() @pyqtSlot(str) def get_last_files(self, uid: str) -> None: files = self.api.get_last_files(uid, 10, "") if files != self.file_model.files: self.file_model.empty() self.file_model.addFiles(files) self.file_model.fileChanged.emit() def current_language(self) -> Optional[str]: lang = Translator.locale() for tag, name in self.language_model.languages: if tag == lang: return name return None def show_metrics_acceptance(self) -> None: """ Display a "friendly" dialog box to ask user for metrics approval. """ tr = Translator.get dialog = QDialog() dialog.setWindowTitle(tr("SHARE_METRICS_TITLE", [APP_NAME])) dialog.setWindowIcon(self.icon) dialog.setStyleSheet("background-color: #ffffff;") layout = QVBoxLayout() info = QLabel(tr("SHARE_METRICS_MSG", [COMPANY])) info.setTextFormat(Qt.RichText) info.setWordWrap(True) layout.addWidget(info) def analytics_choice(state) -> None: Options.use_analytics = bool(state) def errors_choice(state) -> None: Options.use_sentry = bool(state) # Checkboxes em_analytics = QCheckBox(tr("SHARE_METRICS_ERROR_REPORTING")) em_analytics.setChecked(True) em_analytics.stateChanged.connect(errors_choice) layout.addWidget(em_analytics) cb_analytics = QCheckBox(tr("SHARE_METRICS_ANALYTICS")) cb_analytics.stateChanged.connect(analytics_choice) layout.addWidget(cb_analytics) # Buttons buttons = QDialogButtonBox() buttons.setStandardButtons(QDialogButtonBox.Apply) buttons.clicked.connect(dialog.close) layout.addWidget(buttons) dialog.setLayout(layout) dialog.resize(400, 200) dialog.show() dialog.exec_() states = [] if Options.use_analytics: states.append("analytics") if Options.use_sentry: states.append("sentry") (Options.nxdrive_home / "metrics.state").write_text("\n".join(states)) def ask_for_metrics_approval(self) -> None: """Should we setup and use Sentry and/or Google Analytics?""" # Check the user choice first Options.nxdrive_home.mkdir(parents=True, exist_ok=True) STATE_FILE = Options.nxdrive_home / "metrics.state" if STATE_FILE.is_file(): lines = STATE_FILE.read_text().splitlines() Options.use_sentry = "sentry" in lines Options.use_analytics = "analytics" in lines # Abort now, the user already decided to use Sentry or not return # The user did not choose yet, display a message box self.show_metrics_acceptance()
@pyqtSlot() def sync(self): if not self.m_renderer: print("sync<----------------") self.m_renderer = SquircleRenderer() # self.window()) self.window().beforeRendering.connect(self.m_renderer.paint, Qt.DirectConnection) self.m_renderer.setViewportSize(self.window().size() * self.window().devicePixelRatio()) self.m_renderer.setT(self._t) self.m_renderer.setWin(self.window()) # @pyqtSlot(QQuickWindow) def handleWindowChanged(self, win): if win: win.beforeSynchronizing.connect(self.sync, Qt.DirectConnection) win.sceneGraphInvalidated.connect(self.cleanup, Qt.DirectConnection) win.setClearBeforeRendering(False) if __name__ == "__main__": app = QApplication(sys.argv) qmlRegisterType(Squircle, "OpenGLUnderQML", 1, 0, "Squircle") viewer = QQuickView(QUrl.fromLocalFile("main.qml")) viewer.show() sys.exit(app.exec_())
class PhotoBoothGUI(QObject): def run(self): # Create main app self.myApp = QApplication(sys.argv) # Create a label and set its properties self.appLabel = QQuickView() #self.appLabel.setSource(QUrl('loading.qml')) # Show the Label self.appLabel.show() # Initialize PhotoBoothEngine. self.pbengine = PhotoBoothEngine() self.pbengine.on_change_url.connect(self.update_url_signal) self.pbengine.on_connect_signal.connect(self.connect_signal) self.pbengine.change_qml(0) self.pbengine.connect_state(0) print("UPDATE") #self.pbengine.on_status.connect(self.appLabel.rootObject().status) #self.pbengine.on_update_filter_preview.connect(self.appLabel.rootObject().updateImageFilterPreview) self.appLabel.rootContext().setContextProperty('pbengine', self.pbengine) self.setup_text_status_fly_component() self.pbengine.start_state_thread(0) # Execute the Application and Exit self.myApp.exec_() sys.exit() def setup_text_status_fly_component(self): # Create a component factory and load the QML script. print("Hello") self.component = QQmlComponent(self.appLabel.engine()) self.component.loadUrl(QUrl('qml/TextStatusFly.qml')) print("Hello2") self.statuswidget = self.component.create(self.appLabel.rootContext()) print("Hello3") self.statuswidget.setParentItem(self.appLabel.rootObject()) self.statuswidget.setParent(self.appLabel.rootObject()) print("Hello4") #statuswidget.setProperty("targetX", 100) self.statuswidget.setProperty("objectName", "textStatusBar") print("Hello5") self.appLabel.rootContext().setContextProperty('textStatusBar', self.statuswidget) self.statuswidget.setProperty("parentSet", True) def update_url_signal(self, url): print(" ** Updating URL: %s" % url) #self.pbengine.on_change_url.disconnect() #self.pbengine.on_connect_signal.disconnect() self.appLabel.rootContext().setContextProperty('textStatusBar', None) self.appLabel.setSource(QUrl()) self.appLabel.engine().clearComponentCache() self.appLabel.setSource(QUrl(url)) self.setup_text_status_fly_component() self.appLabel.show() # Reconnect #self.pbengine.on_change_url.connect(self.update_url_signal) #self.pbengine.on_connect_signal.connect(self.connect_signal) def connect_signal(self, signal, target): print(" ** Binding signal %s to target %s!" % (str(signal), target)) print(" ** (getattr(self.appLabel, target) = %s)" % (str(getattr(self.appLabel.rootObject(), target)))) signal.connect(getattr(self.appLabel.rootObject(), target))
painter.setRenderHints(QPainter.Antialiasing, True) rect = QRectF(0, 0, self.width(), self.height()).adjusted(1, 1, -1, -1) painter.drawPie(rect, 90 * 16, 290 * 16) @pyqtSlot() def clearChart(self): self.color = QColor(Qt.transparent) self.update() self.chartCleared.emit() if __name__ == '__main__': import os import sys app = QGuiApplication(sys.argv) qmlRegisterType(PieChart, "Charts", 1, 0, "PieChart") view = QQuickView() view.setResizeMode(QQuickView.SizeRootObjectToView) view.setSource( QUrl.fromLocalFile( os.path.join(os.path.dirname(os.path.abspath(__file__)), 'app.qml'))) view.show() sys.exit(app.exec_())
#!/usr/bin/python3 # -*- coding: utf-8 -*- # Copyright (C) 2014 Daniele Simonetti import sys from OpenGL import GL from PyQt5.QtCore import pyqtProperty, QCoreApplication, QObject, QUrl from PyQt5.QtGui import QGuiApplication from PyQt5.QtQuick import QQuickView from PyQt5.QtQml import qmlRegisterType, QQmlComponent, QQmlEngine if __name__ == '__main__': # Create the application instance. app = QGuiApplication(sys.argv) view = QQuickView() view.setSource(QUrl.fromLocalFile("main.qml")); view.show(); try: app.exec_() except KeyboardInterrupt: app.exit(0)
class View(object): shapes = ["rectangle", "ellipse", "image"] edgetypes = ["line", "curve"] def __init__(self): self._controller = Controller(self) self._gui = QGuiApplication(sys.argv) self._qml_dir = os.path.dirname(os.path.realpath(__file__)) self._main = QQuickView() self._main.setResizeMode(QQuickView.SizeRootObjectToView) self._main.setSource(QUrl(self._qml_dir + '/main.qml')) self._main.rootObject().create_node.connect( self._controller.create_node) self._main.rootObject().mouse_position.connect( self._controller.mouse_position) self._main.rootObject().save.connect( self._controller.save) self._main.rootObject().load.connect( self._controller.load) self._main.rootObject().lose_focus.connect( self._controller.lose_focus) self._main.rootObject().node_color_sel.connect( self._controller.node_color_sel) self._main.rootObject().edge_color_sel.connect( self._controller.edge_color_sel) self._main.rootObject().workspace_height_changed.connect( self._controller.workspace_height_changed) self._main.rootObject().workspace_width_changed.connect( self._controller.workspace_width_changed) self._main.rootObject().edge_type_sel.connect( self._controller.edge_type_sel) self._main.rootObject().node_shape_sel.connect( self._controller.node_shape_sel) self._main.rootObject().clear_workspace.connect( self._controller.clear_workspace) self._main.rootObject().node_width_changed.connect( self._controller.node_width_changed) self._main.rootObject().node_height_changed.connect( self._controller.node_height_changed) self._main.rootObject().node_text_color_sel.connect( self._controller.node_text_color_sel) self._main.rootObject().node_text_size_changed.connect( self._controller.node_text_size_changed) self._main.rootObject().edge_thickness_changed.connect( self._controller.edge_thickness_changed) self._main.rootObject().show_edge_controls.connect( self._controller.show_edge_controls) self._main.rootObject().hide_edge_controls.connect( self._controller.hide_edge_controls) self._main.rootObject().exporting.connect( self._controller.exporting) self._main.setProperty( "width", self._controller.project.workspace_width) self._main.setProperty( "height", self._controller.project.workspace_height) self._main.show() def run(self): return self._gui.exec_() def create_node(self, node): # Creates new node from source QML and puts it inside of main window qml_node = QQuickView(QUrl(self._qml_dir + '/shapes/' + self.shapes[node.shape] + '.qml'), self._main) workspace = self._main.rootObject().findChild(QQuickItem, "workspace") # Sets all properties qml_node.rootObject().setProperty("parent", workspace) qml_node.rootObject().setProperty("objectId", str(node.id)) qml_node.rootObject().setProperty("background", str(node.background)) qml_node.rootObject().setProperty("width", str(node.width)) qml_node.rootObject().setProperty("height", str(node.height)) qml_node.rootObject().setProperty("text", str(node.text.text)) qml_node.rootObject().setProperty("textFont", str(node.text.font)) qml_node.rootObject().setProperty("textSize", str(node.text.size)) qml_node.rootObject().setProperty("textColor", str(node.text.color)) # Sets drag boundaries qml_node.rootObject().setProperty("workspaceWidth", str(workspace.property("width"))) qml_node.rootObject().setProperty("workspaceHeight", str(workspace.property("height"))) # Signal connection qml_node.rootObject().node_delete.connect( self._controller.node_delete) qml_node.rootObject().node_text_changed.connect( self._controller.node_text_changed) qml_node.rootObject().node_position_changed.connect( self._controller.node_position_changed) qml_node.rootObject().node_connect.connect( self._controller.node_connect) qml_node.rootObject().node_focus.connect( self._controller.node_focus) if node.shape == 2: qml_node.rootObject().node_image_loaded.connect( self._controller.node_image_loaded) # Position to mouse click qml_node.rootObject().setX(node.x - node.width / 2) qml_node.rootObject().setY(node.y - node.height / 2) qml_node.rootObject().setZ(2) return qml_node def create_edge(self, edge, node1, node2): qml_edge = QQuickView(QUrl(self._qml_dir + '/edges/' + self.edgetypes[edge.type] + '.qml'), self._main) workspace = self._main.rootObject().findChild(QQuickItem, "workspace") qml_edge.rootObject().setProperty("parent", workspace) qml_edge.rootObject().setProperty("objectId", str(edge.id)) qml_edge.rootObject().setZ(1) qml_edge.rootObject().setProperty( "width", workspace.property("width")) qml_edge.rootObject().setProperty( "height", workspace.property("height")) qml_edge.rootObject().setProperty("ctrlX", str(edge.x)) qml_edge.rootObject().setProperty("ctrlY", str(edge.y)) qml_edge.rootObject().setProperty("startX", str(node1.x)) qml_edge.rootObject().setProperty("startY", str(node1.y)) qml_edge.rootObject().setProperty("endX", str(node2.x)) qml_edge.rootObject().setProperty("endY", str(node2.y)) qml_edge.rootObject().setProperty("color", str(edge.color)) qml_edge.rootObject().setProperty("thickness", str(edge.thickness)) qml_edge.rootObject().setProperty("spiked", str(edge.spiked)) qml_edge.rootObject().setProperty("arrow", str(edge.arrow)) # Sets drag boundaries qml_edge.rootObject().setProperty("workspaceWidth", str(workspace.property("width"))) qml_edge.rootObject().setProperty("workspaceHeight", str(workspace.property("height"))) # Signal connection qml_edge.rootObject().edge_delete.connect( self._controller.edge_delete) qml_edge.rootObject().edge_position_changed.connect( self._controller.edge_position_changed) qml_edge.rootObject().edge_focus.connect( self._controller.edge_focus) return qml_edge def node_update(self, node): pass
#!/usr/bin/env python3 import sys from PyQt5.QtCore import QUrl from PyQt5.QtWidgets import QApplication from PyQt5.QtQuick import QQuickView # Main Function if __name__ == '__main__': # Create main app myApp = QApplication(sys.argv) # Create a label and set its properties appLabel = QQuickView() appLabel.setSource(QUrl('2.qml')) # Show the Label appLabel.show() # Execute the Application and Exit myApp.exec_() sys.exit()
# -*- coding: utf-8 -*- import os, sys, re from PyQt5.QtNetwork import * from fboinsgrenderer import * from textureinsgnode_rc import * from PyQt5.QtGui import QSurfaceFormat from PyQt5.QtWidgets import QApplication from PyQt5.QtCore import (QVariant, QUrl, QDir, QSortFilterProxyModel, pyqtProperty, QSize, Q_ENUMS, QObject, QRegExp, QAbstractItemModel, pyqtSignal, Qt, QModelIndex, QByteArray) from PyQt5.QtQml import (QQmlApplicationEngine, QQmlEngine, QQmlFileSelector, qmlRegisterType, QQmlParserStatus, QJSValue) from PyQt5.QtQuick import QQuickView, QQuickItem, QQuickWindow if __name__ == '__main__': app = QApplication(sys.argv) qmlRegisterType(FboInSGRenderer, "SceneGraphRendering", 1, 0, "Renderer") widgetWindow = QQuickView() widgetWindow.setResizeMode(QQuickView.SizeRootObjectToView) widgetWindow.setSource(QUrl("qrc:///main.qml")) widgetWindow.show() sys.exit(app.exec_())
def main(): print("start") app = QApplication(sys.argv) v = QQuickView(QUrl("main.qml")) v.show() sys.exit(app.exec_())
## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ## $QT_END_LICENSE$ ## ############################################################################# import sys import os.path from PyQt5.QtCore import QUrl from PyQt5.QtGui import QGuiApplication from PyQt5.QtQuick import QQuickView #animation 폴더의 animation_rc 와 shared 폴더의 shared_rc를 import해온다. from shared_rc import * from animation_rc import * if len(sys.argv) is 2:# 실행 옵션으로 파이썬도움말 절대경로 제공시 os.chdir(sys.argv[1]) app = QGuiApplication(sys.argv) view = QQuickView() view.engine().quit.connect(app.quit) view.setSource(QUrl('animation.qml')) view.show() sys.exit(app.exec_())
class USBPrinterManager(QObject, SignalEmitter, Extension): def __init__(self, parent = None): super().__init__(parent) self._serial_port_list = [] self._printer_connections = [] self._check_ports_thread = threading.Thread(target = self._updateConnectionList) self._check_ports_thread.daemon = True self._check_ports_thread.start() self._progress = 0 self._control_view = None self._firmware_view = None self._extruder_temp = 0 self._bed_temp = 0 self._error_message = "" ## Add menu item to top menu of the application. self.setMenuName("Firmware") self.addMenuItem(i18n_catalog.i18n("Update Firmware"), self.updateAllFirmware) pyqtError = pyqtSignal(str, arguments = ["amount"]) processingProgress = pyqtSignal(float, arguments = ["amount"]) pyqtExtruderTemperature = pyqtSignal(float, arguments = ["amount"]) pyqtBedTemperature = pyqtSignal(float, arguments = ["amount"]) ## Show firmware interface. # This will create the view if its not already created. def spawnFirmwareInterface(self, serial_port): if self._firmware_view is None: self._firmware_view = QQuickView() self._firmware_view.engine().rootContext().setContextProperty("manager",self) self._firmware_view.setSource(QUrl("plugins/USBPrinting/FirmwareUpdateWindow.qml")) self._firmware_view.show() ## Show control interface. # This will create the view if its not already created. def spawnControlInterface(self,serial_port): if self._control_view is None: self._control_view = QQuickView() self._control_view.engine().rootContext().setContextProperty("manager",self) self._control_view.setSource(QUrl("plugins/USBPrinting/ControlWindow.qml")) self._control_view.show() @pyqtProperty(float,notify = processingProgress) def progress(self): return self._progress @pyqtProperty(float,notify = pyqtExtruderTemperature) def extruderTemperature(self): return self._extruder_temp @pyqtProperty(float,notify = pyqtBedTemperature) def bedTemperature(self): return self._bed_temp @pyqtProperty(str,notify = pyqtError) def error(self): return self._error_message ## Check all serial ports and create a PrinterConnection object for them. # Note that this does not validate if the serial ports are actually usable! # This (the validation) is only done when the connect function is called. def _updateConnectionList(self): while True: temp_serial_port_list = self.getSerialPortList(only_list_usb = True) if temp_serial_port_list != self._serial_port_list: # Something changed about the list since we last changed something. disconnected_ports = [port for port in self._serial_port_list if port not in temp_serial_port_list ] self._serial_port_list = temp_serial_port_list for serial_port in self._serial_port_list: if self.getConnectionByPort(serial_port) is None: # If it doesn't already exist, add it if not os.path.islink(serial_port): # Only add the connection if it's a non symbolic link connection = PrinterConnection.PrinterConnection(serial_port) connection.connect() connection.connectionStateChanged.connect(self.serialConectionStateCallback) connection.progressChanged.connect(self.onProgress) connection.onExtruderTemperatureChange.connect(self.onExtruderTemperature) connection.onBedTemperatureChange.connect(self.onBedTemperature) connection.onError.connect(self.onError) self._printer_connections.append(connection) for serial_port in disconnected_ports: # Close connections and remove them from list. connection = self.getConnectionByPort(serial_port) if connection != None: self._printer_connections.remove(connection) connection.close() time.sleep(5) # Throttle, as we don"t need this information to be updated every single second. def updateAllFirmware(self): self.spawnFirmwareInterface("") for printer_connection in self._printer_connections: printer_connection.updateFirmware(Resources.getPath(Resources.FirmwareLocation, self._getDefaultFirmwareName())) def updateFirmwareBySerial(self, serial_port): printer_connection = self.getConnectionByPort(serial_port) if printer_connection is not None: self.spawnFirmwareInterface(printer_connection.getSerialPort()) printer_connection.updateFirmware(Resources.getPath(Resources.FirmwareLocation, self._getDefaultFirmwareName())) def _getDefaultFirmwareName(self): machine_type = Application.getInstance().getActiveMachine().getTypeID() firmware_name = "" baudrate = 250000 if sys.platform.startswith("linux"): baudrate = 115200 if machine_type == "ultimaker_original": firmware_name = "MarlinUltimaker" firmware_name += "-%d" % (baudrate) elif machine_type == "ultimaker_original_plus": firmware_name = "MarlinUltimaker-UMOP-%d" % (baudrate) elif machine_type == "Witbox": return "MarlinWitbox.hex" elif machine_type == "ultimaker2go": return "MarlinUltimaker2go.hex" elif machine_type == "ultimaker2extended": return "MarlinUltimaker2extended.hex" elif machine_type == "ultimaker2": return "MarlinUltimaker2.hex" ##TODO: Add check for multiple extruders if firmware_name != "": firmware_name += ".hex" return firmware_name ## Callback for extruder temperature change def onExtruderTemperature(self, serial_port, index, temperature): self._extruder_temp = temperature self.pyqtExtruderTemperature.emit(temperature) ## Callback for bed temperature change def onBedTemperature(self, serial_port,temperature): self._bed_temperature = temperature self.pyqtBedTemperature.emit(temperature) ## Callback for error def onError(self, error): self._error_message = error self.pyqtError.emit(error) ## Callback for progress change def onProgress(self, progress, serial_port): self._progress = progress self.processingProgress.emit(progress) ## Attempt to connect with all possible connections. def connectAllConnections(self): for connection in self._printer_connections: connection.connect() ## Send gcode to printer and start printing def sendGCodeByPort(self, serial_port, gcode_list): printer_connection = self.getConnectionByPort(serial_port) if printer_connection is not None: printer_connection.printGCode(gcode_list) return True return False @pyqtSlot() def cancelPrint(self): for printer_connection in self.getActiveConnections(): printer_connection.cancelPrint() ## Send gcode to all active printers. # \return True if there was at least one active connection. def sendGCodeToAllActive(self, gcode_list): for printer_connection in self.getActiveConnections(): printer_connection.printGCode(gcode_list) if len(self.getActiveConnections()): return True else: return False ## Send a command to a printer indentified by port # \param serial port String indentifieing the port # \param command String with the g-code command to send. # \return True if connection was found, false otherwise def sendCommandByPort(self, serial_port, command): printer_connection = self.getConnectionByPort(serial_port) if printer_connection is not None: printer_connection.sendCommand(command) return True return False ## Send a command to all active (eg; connected) printers # \param command String with the g-code command to send. # \return True if at least one connection was found, false otherwise def sendCommandToAllActive(self, command): for printer_connection in self.getActiveConnections(): printer_connection.sendCommand(command) if len(self.getActiveConnections()): return True else: return False ## Callback if the connection state of a connection is changed. # This adds or removes the connection as a possible output device. def serialConectionStateCallback(self, serial_port): connection = self.getConnectionByPort(serial_port) if connection.isConnected(): Application.getInstance().addOutputDevice(serial_port, { "id": serial_port, "function": self.spawnControlInterface, "description": "Write to USB {0}".format(serial_port), "icon": "print_usb", "priority": 1 }) else: Application.getInstance().removeOutputDevice(serial_port) @pyqtSlot() def startPrint(self): gcode_list = getattr(Application.getInstance().getController().getScene(), "gcode_list", None) if gcode_list: final_list = [] for gcode in gcode_list: final_list += gcode.split("\n") self.sendGCodeToAllActive(gcode_list) ## Get a list of printer connection objects that are connected. def getActiveConnections(self): return [connection for connection in self._printer_connections if connection.isConnected()] ## Get a printer connection object by serial port def getConnectionByPort(self, serial_port): for printer_connection in self._printer_connections: if serial_port == printer_connection.getSerialPort(): return printer_connection return None ## Create a list of serial ports on the system. # \param only_list_usb If true, only usb ports are listed def getSerialPortList(self,only_list_usb=False): base_list = [] if platform.system() == "Windows": import winreg try: key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,"HARDWARE\\DEVICEMAP\\SERIALCOMM") i = 0 while True: values = winreg.EnumValue(key, i) if not base_list or "USBSER" in values[0]: base_list += [values[1]] i += 1 except Exception as e: pass if base_list: base_list = base_list + glob.glob("/dev/ttyUSB*") + glob.glob("/dev/ttyACM*") + glob.glob("/dev/cu.usb*") base_list = filter(lambda s: "Bluetooth" not in s, base_list) # Filter because mac sometimes puts them in the list else: base_list = base_list + glob.glob("/dev/ttyUSB*") + glob.glob("/dev/ttyACM*") + glob.glob("/dev/cu.*") + glob.glob("/dev/tty.usb*") + glob.glob("/dev/rfcomm*") + glob.glob("/dev/serial/by-id/*") return base_list