예제 #1
0
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)
예제 #2
0
def quickView(url: str, parent=None):
    view = QQuickView(parent)
    view.setResizeMode(QQuickView.SizeRootObjectToView)

    view.setSource(QUrl(url))
    view.show()
    return view
예제 #3
0
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_())
예제 #4
0
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()
예제 #5
0
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)
예제 #6
0
def test_qquickview():
    app = QApplication(sys.argv)

    view = QQuickView()
    view.show()

    sys.exit(app.exec_())
예제 #7
0
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_()
예제 #8
0
def main():
    app = QGuiApplication(sys.argv)
    view = QQuickView()
    view.setSource(QUrl.fromLocalFile('scene3d.qml'))
    view.setResizeMode(QQuickView.SizeRootObjectToView)
    view.show()
    sys.exit(app.exec_())
예제 #9
0
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)
예제 #10
0
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_())
예제 #11
0
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_())
예제 #12
0
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_())
예제 #13
0
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_()
예제 #14
0
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_())
예제 #15
0
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_())
예제 #16
0
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_())
예제 #17
0
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_()
예제 #18
0
파일: gui.py 프로젝트: HenryHu/xmradio
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()
예제 #19
0
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'
예제 #20
0
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_()
예제 #21
0
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)
예제 #22
0
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()
예제 #23
0
파일: run.py 프로젝트: Endle/quick
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_())
예제 #24
0
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_())
예제 #25
0
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)
예제 #26
0
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_())
예제 #27
0
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_())
예제 #28
0
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()
예제 #29
0
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_()
예제 #30
0
파일: main.py 프로젝트: bgr/qml_hot_reload
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)
예제 #31
0
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_())
예제 #32
0
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
예제 #33
0
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()
예제 #34
0
파일: main.py 프로젝트: harry159821/PyQt_3D
    @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_())
예제 #35
0
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)



예제 #38
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
예제 #39
0
파일: test.py 프로젝트: Jamesits/recharify
#!/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()
예제 #40
0
파일: main.py 프로젝트: harry159821/PyQt_3D
# -*- 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_())
예제 #41
0
def main():
    print("start")
    app = QApplication(sys.argv)
    v = QQuickView(QUrl("main.qml"))
    v.show()
    sys.exit(app.exec_())
예제 #42
0
## 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_())
예제 #43
0
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