Example #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)
Example #2
0
def main():
    # Create main app
    myApp = QGuiApplication(sys.argv)
    # myApp.setWindowIcon(QIcon('./images/icon.png'))

    # Create a View and set its properties
    appView = QQuickView()
    appView.setMinimumHeight(640)
    appView.setMinimumWidth(1024)
    appView.setTitle('Authorize Kerberos')

    engine = appView.engine()
    engine.quit.connect(myApp.quit)
    context = engine.rootContext()

    # add paths
    appDir = 'file:///' + QDir.currentPath()
    context.setContextProperty('appDir', appDir)

    # add controllers
    mainController = MainController()
    context.setContextProperty('PyConsole', mainController)
    context.setContextProperty('mainController', mainController)

    # Show the View
    appView.setSource(QUrl('./qml/main.qml'))
    appView.showMaximized()

    # Execute the Application and Exit
    myApp.exec_()
    sys.exit()
Example #3
0
File: main.py Project: drewdru/AOI
from controllers.noiseGeneratorController import NoiseGeneratorController
from controllers.colorCorrectorController import ColorCorrectorController
from controllers.filtersController import FiltersController
from controllers.binarizeController import BinarizeController
from controllers.morphologyController import MorphologyController
from controllers.segmentationController import SegmentationController

if __name__ == '__main__':
    # Create main app
    myApp = QApplication(sys.argv)
    myApp.setWindowIcon(QIcon('./images/icon.png'))

    # Create a View and set its properties
    appView = QQuickView()
    appView.setMinimumHeight(640)
    appView.setMinimumWidth(1024)
    appView.setTitle('roadLaneFinding')

    engine = appView.engine()
    engine.quit.connect(myApp.quit)
    context = engine.rootContext()

    # add paths
    # appDir = os.getcwd()
    # print(QDir.currentPath())
    appDir = 'file:///' + QDir.currentPath()
    # print(appDir)
    # # print('appDir:', appDir)
    # appDir = 'file:///h:/QtDocuments/AOI'
    # print(appDir)
    context.setContextProperty('appDir', appDir)
Example #4
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()
Example #5
0
class PluginsStore(QDialog):

    def __init__(self, parent=None):
        super(PluginsStore, self).__init__(parent, Qt.Dialog)
       # QDialog.__init__(self, parent, Qt.Dialog | Qt.FramelessWindowHint)
        self.setWindowTitle(self.tr("Plugins Store"))
        self.setMaximumSize(QSize(0, 0))

        vbox = QVBoxLayout(self)
        vbox.setContentsMargins(0, 0, 0, 0)
        vbox.setSpacing(0)
        self.view = QQuickView()
        self.view.setMinimumWidth(800)
        self.view.setMinimumHeight(600)
        self.view.setResizeMode(QQuickView.SizeRootObjectToView)
        self.view.setSource(ui_tools.get_qml_resource("PluginsStore.qml"))
        self.root = self.view.rootObject()
        vbox.addWidget(self.view)
        self._plugins = {}
        self._plugins_inflate = []
        self._plugins_by_tag = collections.defaultdict(list)
        self._plugins_by_author = collections.defaultdict(list)
        self._base_color = QColor("white")
        self._counter = 0
        self._counter_callback = None
        self._inflating_plugins = []
        self._categoryTags = True
        self._search = []
        self.status = None

        self.connect(self.root, SIGNAL("loadPluginsGrid()"),
                     self._load_by_name)
        self.connect(self.root, SIGNAL("close()"),
                     self.close)
        self.connect(self.root, SIGNAL("showPluginDetails(int)"),
                     self.show_plugin_details)
        self.connect(self.root, SIGNAL("loadTagsGrid()"),
                     self._load_tags_grid)
        self.connect(self.root, SIGNAL("loadAuthorGrid()"),
                     self._load_author_grid)
        self.connect(self.root, SIGNAL("search(QString)"),
                     self._load_search_results)
        self.connect(self.root, SIGNAL("loadPluginsForCategory(QString)"),
                     self._load_plugins_for_category)
        self.connect(self, SIGNAL("processCompleted(PyQt_PyObject)"),
                     self._process_complete)

        self.nenv = nenvironment.NenvEggSearcher()
        self.connect(self.nenv,
                     SIGNAL("searchCompleted(PyQt_PyObject)"),
                     self.callback)
        self.status = self.nenv.do_search()

    def _load_by_name(self):
        if self._plugins:
            self.root.showGridPlugins()
            for plugin in list(self._plugins.values()):
                self.root.addPlugin(plugin.identifier, plugin.name,
                                    plugin.summary, plugin.version)

    def _load_plugins_for_category(self, name):
        self.root.showGridPlugins()
        if self._categoryTags:
            for plugin in self._plugins_by_tag[name]:
                self.root.addPlugin(plugin.identifier, plugin.name,
                                    plugin.summary, plugin.version)
        else:
            for plugin in self._plugins_by_author[name]:
                self.root.addPlugin(plugin.identifier, plugin.name,
                                    plugin.summary, plugin.version)

    def callback(self, values):
        self.root.showGridPlugins()
        for i, plugin in enumerate(values):
            plugin.identifier = i + 1
            self.root.addPlugin(plugin.identifier, plugin.name,
                                plugin.summary, plugin.version)
            self._plugins[plugin.identifier] = plugin

    def show_plugin_details(self, identifier):
        plugin = self._plugins[identifier]
        self._counter = 1
        self._counter_callback = self._show_details

        if plugin.shallow:
            self.connect(plugin,
                         SIGNAL("pluginMetadataInflated(PyQt_PyObject)"),
                         self._update_content)
            self._plugins_inflate.append(plugin.inflate())
        else:
            self._update_content(plugin)

    def _load_tags_grid(self):
        self._categoryTags = True
        self._counter = len(self._plugins)
        self.root.updateCategoryCounter(self._counter)
        self._counter_callback = self._show_tags_grid
        self._inflating_plugins = list(self._plugins.values())
        self._loading_function()

    def _load_author_grid(self):
        self._categoryTags = False
        self._counter = len(self._plugins)
        self.root.updateCategoryCounter(self._counter)
        self._counter_callback = self._show_author_grid
        self._inflating_plugins = list(self._plugins.values())
        self._loading_function()

    def _load_search_results(self, search):
        self._search = search.lower().split()
        self._counter = len(self._plugins)
        self.root.updateCategoryCounter(self._counter)
        self._counter_callback = self._show_search_grid
        self._inflating_plugins = list(self._plugins.values())
        self._loading_function()

    def _loading_function(self):
        plugin = self._inflating_plugins.pop()
        if plugin.shallow:
            self.connect(plugin,
                         SIGNAL("pluginMetadataInflated(PyQt_PyObject)"),
                         self._update_content)
            self._plugins_inflate.append(plugin.inflate())
        else:
            self._process_complete(plugin)

    def _process_complete(self, plugin=None):
        self._counter -= 1
        self.root.updateCategoryCounter(self._counter)
        if self._counter == 0:
            self._counter_callback(plugin)
        else:
            self._loading_function()

    def _show_search_grid(self, plugin=None):
        self.root.showGridPlugins()
        for plugin in list(self._plugins.values()):
            keywords = plugin.keywords.lower().split() + [plugin.name.lower()]
            for word in self._search:
                if word in keywords:
                    self.root.addPlugin(plugin.identifier, plugin.name,
                                        plugin.summary, plugin.version)

    def _show_details(self, plugin):
        self.root.displayDetails(plugin.identifier)

    def _show_tags_grid(self, plugin=None):
        tags = sorted(self._plugins_by_tag.keys())
        for tag in tags:
            color = self._get_random_color(self._base_color)
            self.root.addCategory(color.name(), tag)
        self.root.loadingComplete()

    def _show_author_grid(self, plugin=None):
        authors = sorted(self._plugins_by_author.keys())
        for author in authors:
            color = self._get_random_color(self._base_color)
            self.root.addCategory(color.name(), author)
        self.root.loadingComplete()

    def _update_content(self, plugin):
        self.root.updatePlugin(
            plugin.identifier, plugin.author, plugin.author_email,
            plugin.description, plugin.download_url, plugin.home_page,
            plugin.license)
        keywords = plugin.keywords.split()
        for key in keywords:
            plugins = self._plugins_by_tag[key]
            if plugin not in plugins:
                plugins.append(plugin)
                self._plugins_by_tag[key] = plugins
        plugins = self._plugins_by_author[plugin.author]
        if plugin not in plugins:
            plugins.append(plugin)
            self._plugins_by_author[plugin.author] = plugins
        self.emit(SIGNAL("processCompleted(PyQt_PyObject)"), plugin)

    def _get_random_color(self, mix=None):
        red = random.randint(0, 256)
        green = random.randint(0, 256)
        blue = random.randint(0, 256)

        # mix the color
        if mix:
            red = (red + mix.red()) / 2
            green = (green + mix.green()) / 2
            blue = (blue + mix.blue()) / 2

        color = QColor(red, green, blue)
        return color