def _debug_show_file_status(self): from nxdrive.gui.status_dialog import StatusDialog sender = self.sender() engine = sender.data().toPyObject() self.statusDialog = StatusDialog(engine.get_dao()) self.statusDialog.show()
def show_file_status(self): from nxdrive.gui.status_dialog import StatusDialog for _, engine in self.manager.get_engines().iteritems(): self.statusDialog = StatusDialog(engine.get_dao()) self.statusDialog.show() return
class Application(SimpleApplication): """Main Nuxeo drive application controlled by a system tray icon + menu""" def __init__(self, manager, options, argv=()): super(Application, self).__init__(manager, options, list(argv)) self.setQuitOnLastWindowClosed(False) self._delegator = None from nxdrive.scripting import DriveUiScript self.manager.set_script_object(DriveUiScript(manager, self)) self.mainEngine = None self.filters_dlg = None self._conflicts_modals = dict() self.current_notification = None # Make dialog unique self.uniqueDialogs = dict() for _, engine in self.manager.get_engines().iteritems(): self.mainEngine = engine break if self.mainEngine is not None and options.debug: from nxdrive.engine.engine import EngineLogger self.engineLogger = EngineLogger(self.mainEngine) self.engineWidget = None self.aboutToQuit.connect(self.manager.stop) self.manager.dropEngine.connect(self.dropped_engine) # Timer to spin the transferring icon self.icon_spin_timer = QtCore.QTimer() self.icon_spin_timer.timeout.connect(self.spin_transferring_icon) self.icon_spin_count = 0 # Application update self.manager.get_updater().appUpdated.connect(self.app_updated) self.updated_version = None # This is a windowless application mostly using the system tray self.setQuitOnLastWindowClosed(False) self.setup_systray() # Direct Edit conflict self.manager.get_drive_edit().driveEditConflict.connect( self._direct_edit_conflict) # Check if actions is required, separate method so it can be override self.init_checks() self.engineWidget = None # Setup notification center for Mac if AbstractOSIntegration.is_mac(): if AbstractOSIntegration.os_version_above("10.8"): self._setup_notification_center() @QtCore.pyqtSlot(str, str, str) def _direct_edit_conflict(self, filename, ref, digest): try: log.trace('Entering _direct_edit_conflict for %r / %r', filename, ref) filename = unicode(filename) log.trace('Unicode filename: %r', filename) 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 info = dict() info["name"] = filename dlg = WebModal( self, Translator.get("DIRECT_EDIT_CONFLICT_MESSAGE", info)) dlg.add_button("OVERWRITE", Translator.get("DIRECT_EDIT_CONFLICT_OVERWRITE")) dlg.add_button("CANCEL", Translator.get("DIRECT_EDIT_CONFLICT_CANCEL")) res = dlg.exec_() if res == "OVERWRITE": self.manager.get_drive_edit().force_update( unicode(ref), unicode(digest)) del self._conflicts_modals[filename] except Exception: log.exception( 'Error while displaying Direct Edit conflict modal dialog for %r', filename) @QtCore.pyqtSlot() def _root_deleted(self): engine = self.sender() info = dict() log.debug("Root has been deleted for engine: %s", engine.get_uid()) info["folder"] = engine.get_local_folder() dlg = WebModal(self, Translator.get("DRIVE_ROOT_DELETED", info)) dlg.add_button("RECREATE", Translator.get("DRIVE_ROOT_RECREATE"), style="primary") dlg.add_button("DISCONNECT", Translator.get("DRIVE_ROOT_DISCONNECT"), style="danger") res = dlg.exec_() if res == "DISCONNECT": self.manager.unbind_engine(engine.get_uid()) elif res == "RECREATE": engine.reinit() engine.start() @QtCore.pyqtSlot(str) def _root_moved(self, new_path): engine = self.sender() info = dict() log.debug("Root has been moved for engine: %s to '%s'", engine.get_uid(), new_path) info["folder"] = engine.get_local_folder() info["new_folder"] = new_path dlg = WebModal(self, Translator.get("DRIVE_ROOT_MOVED", info)) dlg.add_button("MOVE", Translator.get("DRIVE_ROOT_UPDATE"), style="primary") dlg.add_button("RECREATE", Translator.get("DRIVE_ROOT_RECREATE")) dlg.add_button("DISCONNECT", Translator.get("DRIVE_ROOT_DISCONNECT"), style="danger") res = dlg.exec_() if res == "DISCONNECT": self.manager.unbind_engine(engine.get_uid()) elif res == "RECREATE": engine.reinit() engine.start() elif res == "MOVE": engine.set_local_folder(unicode(new_path)) engine.start() def get_cache_folder(self): return os.path.join(self.manager.get_configuration_folder(), "cache", "wui") def _get_skin(self): return 'ui5' def get_window_icon(self): return find_icon('nuxeo_drive_icon_64.png') def get_htmlpage(self, page): import nxdrive nxdrive_path = os.path.dirname(nxdrive.__file__) ui_path = os.path.join(nxdrive_path, 'data', self._get_skin()) return os.path.join(find_resource_dir(self._get_skin(), ui_path), page).replace("\\", "/") def _init_translator(self): from nxdrive.wui.translator import Translator Translator(self.manager, self.get_htmlpage('i18n.js'), self.manager.get_config("locale", self.options.locale)) @QtCore.pyqtSlot(object) def dropped_engine(self, engine): # Update icon in case the engine dropped was syncing self.change_systray_icon() @QtCore.pyqtSlot() def change_systray_icon(self): syncing = False engines = self.manager.get_engines() invalid_credentials = True paused = True offline = True for _, engine in engines.iteritems(): syncing = syncing | engine.is_syncing() invalid_credentials = invalid_credentials & engine.has_invalid_credentials( ) paused = paused & engine.is_paused() offline = offline & engine.is_offline() new_state = "asleep" if len(engines) == 0 or paused or offline: new_state = "disabled" elif invalid_credentials: new_state = 'stopping' elif syncing: new_state = 'transferring' log.trace("Should change icon to %s", new_state) self.set_icon_state(new_state) def _get_settings_dialog(self, section): from nxdrive.wui.settings import WebSettingsDialog return WebSettingsDialog(self, section) def _get_conflicts_dialog(self, engine): from nxdrive.wui.dialog import WebDialog from nxdrive.wui.conflicts import WebConflictsApi return WebDialog(self, "conflicts.html", api=WebConflictsApi(self, engine)) @QtCore.pyqtSlot() def show_conflicts_resolution(self, engine): conflicts = self._get_unique_dialog("conflicts") if conflicts is None: conflicts = self._get_conflicts_dialog(engine) self._create_unique_dialog("conflicts", conflicts) else: conflicts._api.set_engine(engine) self._show_window(conflicts) @QtCore.pyqtSlot() def show_settings(self, section="Accounts"): if section is None: section = "Accounts" settings = self._get_unique_dialog("settings") if settings is None: settings = self._get_settings_dialog(section) self._create_unique_dialog("settings", settings) else: settings.set_section(section) self._show_window(settings) def _show_window(self, window): window.show() window.raise_() def _get_unique_dialog(self, name): if name in self.uniqueDialogs: return self.uniqueDialogs[name] return None def _destroy_dialog(self): sender = self.sender() name = str(sender.objectName()) if name in self.uniqueDialogs: del self.uniqueDialogs[name] def _create_unique_dialog(self, name, dialog): self.uniqueDialogs[name] = dialog dialog.setObjectName(name) dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose) dialog.destroyed.connect(self._destroy_dialog) @QtCore.pyqtSlot() def destroyed_filters_dialog(self): self.filters_dlg = None def _get_filters_dialog(self, engine): from nxdrive.gui.folders_dialog import FiltersDialog return FiltersDialog(self, engine) @QtCore.pyqtSlot() def show_filters(self, engine): if self.filters_dlg is not None: self.filters_dlg.close() self.filters_dlg = None self.filters_dlg = self._get_filters_dialog(engine) self.filters_dlg.destroyed.connect(self.destroyed_filters_dialog) self.filters_dlg.show() def show_file_status(self): from nxdrive.gui.status_dialog import StatusDialog for _, engine in self.manager.get_engines().iteritems(): self.statusDialog = StatusDialog(engine.get_dao()) self.statusDialog.show() return def show_activities(self): from nxdrive.wui.activity import WebActivityDialog self.webEngineWidget = WebActivityDialog(self) self.webEngineWidget.show() @QtCore.pyqtSlot(object) def _connect_engine(self, engine): 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) @QtCore.pyqtSlot() def _debug_toggle_invalid_credentials(self): sender = self.sender() engine = sender.data().toPyObject() engine.set_invalid_credentials(not engine.has_invalid_credentials(), reason='debug') @QtCore.pyqtSlot() def _debug_show_file_status(self): from nxdrive.gui.status_dialog import StatusDialog sender = self.sender() engine = sender.data().toPyObject() self.statusDialog = StatusDialog(engine.get_dao()) self.statusDialog.show() def _create_debug_engine_menu(self, engine, parent): menuDebug = QtGui.QMenu(parent) action = QtGui.QAction(Translator.get("DEBUG_INVALID_CREDENTIALS"), menuDebug) action.setCheckable(True) action.setChecked(engine.has_invalid_credentials()) action.setData(engine) action.triggered.connect(self._debug_toggle_invalid_credentials) menuDebug.addAction(action) action = QtGui.QAction(Translator.get("DEBUG_FILE_STATUS"), menuDebug) action.setData(engine) action.triggered.connect(self._debug_show_file_status) menuDebug.addAction(action) return menuDebug def create_debug_menu(self, parent): menuDebug = QtGui.QMenu(parent) menuDebug.addAction(Translator.get("DEBUG_WINDOW"), self.show_debug_window) for engine in self.manager.get_engines().values(): action = QtGui.QAction(engine._name, menuDebug) action.setMenu(self._create_debug_engine_menu(engine, menuDebug)) action.setData(engine) menuDebug.addAction(action) return menuDebug def _get_debug_dialog(self): from nxdrive.debug.wui.engine import EngineDialog return EngineDialog(self) @QtCore.pyqtSlot() def show_debug_window(self): debug = self._get_unique_dialog("debug") if debug is None: debug = self._get_debug_dialog() self._create_unique_dialog("debug", debug) self._show_window(debug) def init_checks(self): if self.manager.is_debug(): self.show_debug_window() for _, engine in self.manager.get_engines().iteritems(): self._connect_engine(engine) self.manager.newEngine.connect(self._connect_engine) self.manager.get_notification_service().newNotification.connect( self._new_notification) self.manager.get_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() @QtCore.pyqtSlot() def _update_notification(self): replacements = dict() replacements["version"] = self.manager.get_updater().get_status()[1] notification = Notification( uuid="AutoUpdate", flags=Notification.FLAG_BUBBLE | Notification.FLAG_VOLATILE | Notification.FLAG_UNIQUE, title=Translator.get("AUTOUPDATE_NOTIFICATION_TITLE", replacements), description=Translator.get("AUTOUPDATE_NOTIFICATION_MESSAGE", replacements)) self.manager.get_notification_service().send_notification(notification) @QtCore.pyqtSlot() def _message_clicked(self): if self.current_notification is None: return self.manager.get_notification_service().trigger_notification( self.current_notification.get_uid()) def _setup_notification_center(self): from nxdrive.osi.darwin.pyNotificationCenter import setup_delegator, NotificationDelegator if self._delegator is None: self._delegator = NotificationDelegator.alloc().init() self._delegator._manager = self.manager setup_delegator(self._delegator) @QtCore.pyqtSlot(object) def _new_notification(self, notification): if not notification.is_bubble(): return if AbstractOSIntegration.is_mac(): if AbstractOSIntegration.os_version_above("10.8"): from nxdrive.osi.darwin.pyNotificationCenter import notify, NotificationDelegator if self._delegator is None: self._delegator = NotificationDelegator.alloc().init() self._delegator._manager = self.manager # Use notification center userInfo = dict() userInfo["uuid"] = notification.get_uid() return notify(notification.get_title(), None, notification.get_description(), userInfo=userInfo) self.current_notification = notification icon = QtGui.QSystemTrayIcon.Information if (notification.get_level() == Notification.LEVEL_WARNING): icon = QtGui.QSystemTrayIcon.Warning elif (notification.get_level() == Notification.LEVEL_ERROR): icon = QtGui.QSystemTrayIcon.Critical self.show_message(notification.get_title(), notification.get_description(), icon=icon) def get_systray_menu(self): from nxdrive.wui.systray import WebSystray return WebSystray(self, self._tray_icon) def set_icon_state(self, state): """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.get_icon_state() == state: # Nothing to update return False self._tray_icon.setToolTip(self.get_tooltip()) # Handle animated transferring icon if state == 'transferring': self.icon_spin_timer.start(150) else: self.icon_spin_timer.stop() icon = find_icon('nuxeo_drive_systray_icon_%s_18.png' % state) if icon is not None: self._tray_icon.setIcon(QtGui.QIcon(icon)) else: log.warning('Icon not found: %s', icon) self._icon_state = state log.debug('Updated icon state to: %s', state) return True def get_icon_state(self): return getattr(self, '_icon_state', None) def spin_transferring_icon(self): icon = find_icon('nuxeo_drive_systray_icon_transferring_%s.png' % (self.icon_spin_count + 1)) self._tray_icon.setIcon(QtGui.QIcon(icon)) self.icon_spin_count = (self.icon_spin_count + 1) % 10 def get_osi(self): return self.manager.get_osi() def update_tooltip(self): # Update also the file self._tray_icon.setToolTip(self.get_tooltip()) def get_default_tooltip(self): return self.manager.get_appname() def get_tooltip(self): actions = Action.get_actions() if actions is None or len(actions) == 0: return self.get_default_tooltip() # Display only the first action for now # TODO Get all actions ? or just file action action = actions.itervalues().next() if action is None: return self.get_default_tooltip() if isinstance(action, FileAction): if action.get_percent() is not None: return ("%s - %s - %s - %d%%" % (self.get_default_tooltip(), action.type, action.filename, action.get_percent())) else: return ( "%s - %s - %s" % (self.get_default_tooltip(), action.type, action.filename)) elif action.get_percent() is not None: return ("%s - %s - %d%%" % (self.get_default_tooltip(), action.type, action.get_percent())) else: return ("%s - %s" % (self.get_default_tooltip(), action.type)) @QtCore.pyqtSlot(str) def app_updated(self, updated_version): self.updated_version = str(updated_version) log.info('Quitting Nuxeo Drive and restarting updated version %s', self.updated_version) self.manager.stopped.connect(self.restart) log.debug("Exiting Qt application") self.quit() @QtCore.pyqtSlot() def restart(self): """ Restart application by loading updated executable into current process""" current_version = self.manager.get_updater().get_active_version() log.info("Current application version: %s", current_version) log.info("Updated application version: %s", self.updated_version) executable = sys.executable # TODO NXP-13818: better handle this! if sys.platform == 'darwin': executable = executable.replace('python', self.get_mac_app()) log.info("Current executable is: %s", executable) updated_executable = executable.replace(current_version, self.updated_version) log.info("Updated executable is: %s", updated_executable) args = [updated_executable] args.extend(sys.argv[1:]) log.info("Opening subprocess with args: %r", args) subprocess.Popen(args) def get_mac_app(self): return 'ndrive' def show_message(self, title, message, icon=QtGui.QSystemTrayIcon.Information, timeout=10000): self._tray_icon.showMessage(title, message, icon, timeout) def show_dialog(self, url): from nxdrive.wui.dialog import WebDialog dialog = WebDialog(self, url) dialog.show() def show_metadata(self, file_path): from nxdrive.wui.metadata import CreateMetadataWebDialog self._metadata_dialog = CreateMetadataWebDialog( self.manager, file_path) self._metadata_dialog.show() def setup_systray(self): self._tray_icon = DriveSystrayIcon() self._tray_icon.setToolTip(self.manager.get_appname()) self.set_icon_state("disabled") self._tray_icon.show() self.tray_icon_menu = self.get_systray_menu() self._tray_icon.setContextMenu(self.tray_icon_menu) self._tray_icon.messageClicked.connect(self._message_clicked) def event(self, event): """Handle URL scheme events under OSX""" if hasattr(event, 'url'): url = str(event.url().toString()) log.debug("Event URL: %s", url) try: info = parse_protocol_url(url) log.debug("URL info: %r", info) if info is not None: log.debug("Received nxdrive URL scheme event: %s", url) if info.get('command') == 'download_edit': # This is a quick operation, no need to fork a QThread self.manager.get_drive_edit().edit( info['server_url'], info['doc_id'], user=info['user'], download_url=info['download_url']) elif info.get('command') == 'edit': # Kept for backward compatibility self.manager.get_drive_edit().edit( info['server_url'], info['item_id']) except: log.error("Error handling URL event: %s", url, exc_info=True) return super(Application, self).event(event)
class Application(QApplication): """Main Nuxeo drive application controlled by a system tray icon + menu""" def __init__(self, manager, options, argv=()): super(Application, self).__init__(list(argv)) self.setQuitOnLastWindowClosed(False) self.manager = manager self.options = options self.mainEngine = None self.filters_dlg = None # Make dialog unique self.uniqueDialogs = dict() # Init translator self._init_translator() for _, engine in self.manager.get_engines().iteritems(): self.mainEngine = engine break if self.mainEngine is not None and options.debug: from nxdrive.engine.engine import EngineLogger self.engineLogger = EngineLogger(self.mainEngine) self.engineWidget = None self.aboutToQuit.connect(self.manager.stop) self.manager.dropEngine.connect(self.dropped_engine) # Timer to spin the transferring icon self.icon_spin_timer = QtCore.QTimer() self.icon_spin_timer.timeout.connect(self.spin_transferring_icon) self.icon_spin_count = 0 # Application update self.manager.get_updater().appUpdated.connect(self.app_updated) self.updated_version = None # This is a windowless application mostly using the system tray self.setQuitOnLastWindowClosed(False) self.setup_systray() # Check if actions is required, separate method so it can be override self.init_checks() self.engineWidget = None def get_cache_folder(self): return os.path.join(self.manager.get_configuration_folder(), "cache", "wui") def _get_skin(self): return 'ui5' def get_window_icon(self): return find_icon('nuxeo_drive_icon_64.png') def get_htmlpage(self, page): import nxdrive nxdrive_path = os.path.dirname(nxdrive.__file__) ui_path = os.path.join(nxdrive_path, 'data', self._get_skin()) return os.path.join(find_resource_dir(self._get_skin(), ui_path), page).replace("\\","/") def _init_translator(self): from nxdrive.wui.translator import Translator Translator(self.manager, self.get_htmlpage('i18n.js'), self.manager.get_config("locale", self.options.locale)) @QtCore.pyqtSlot(object) def dropped_engine(self, engine): # Update icon in case the engine dropped was syncing self.change_systray_icon() @QtCore.pyqtSlot() def change_systray_icon(self): syncing = False engines = self.manager.get_engines() invalid_credentials = True paused = True offline = True for _, engine in engines.iteritems(): syncing = syncing | engine.is_syncing() invalid_credentials = invalid_credentials & engine.has_invalid_credentials() paused = paused & engine.is_paused() offline = offline & engine.is_offline() new_state = "asleep" if len(engines) == 0 or paused or offline: new_state = "disabled" elif invalid_credentials: new_state = 'stopping' elif syncing: new_state = 'transferring' log.trace("Should change icon to %s", new_state) self.set_icon_state(new_state) def _get_settings_dialog(self, section): from nxdrive.wui.settings import WebSettingsDialog return WebSettingsDialog(self, section) def _get_conflicts_dialog(self, engine): from nxdrive.wui.dialog import WebDialog from nxdrive.wui.conflicts import WebConflictsApi return WebDialog(self, "conflicts.html", api=WebConflictsApi(self, engine)) @QtCore.pyqtSlot() def show_conflicts_resolution(self, engine): conflicts = self._get_unique_dialog("conflicts") if conflicts is None: conflicts = self._get_conflicts_dialog(engine) self._create_unique_dialog("conflicts", conflicts) else: conflicts._api.set_engine(engine) self._show_window(conflicts) @QtCore.pyqtSlot() def show_settings(self, section="Accounts"): if section is None: section = "Accounts" settings = self._get_unique_dialog("settings") if settings is None: settings = self._get_settings_dialog(section) self._create_unique_dialog("settings", settings) else: settings.set_section(section) self._show_window(settings) def _show_window(self, window): window.show() window.raise_() def _get_unique_dialog(self, name): if name in self.uniqueDialogs: return self.uniqueDialogs[name] return None def _destroy_dialog(self): sender = self.sender() name = str(sender.objectName()) if name in self.uniqueDialogs: del self.uniqueDialogs[name] def _create_unique_dialog(self, name, dialog): self.uniqueDialogs[name] = dialog dialog.setObjectName(name) dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose) dialog.destroyed.connect(self._destroy_dialog) @QtCore.pyqtSlot() def destroyed_filters_dialog(self): self.filters_dlg = None def _get_filters_dialog(self, engine): from nxdrive.gui.folders_dialog import FiltersDialog return FiltersDialog(self, engine) @QtCore.pyqtSlot() def show_filters(self, engine): if self.filters_dlg is not None: self.filters_dlg.close() self.filters_dlg = None self.filters_dlg = self._get_filters_dialog(engine) self.filters_dlg.destroyed.connect(self.destroyed_filters_dialog) self.filters_dlg.show() def show_file_status(self): from nxdrive.gui.status_dialog import StatusDialog for _, engine in self.manager.get_engines().iteritems(): self.statusDialog = StatusDialog(engine.get_dao()) self.statusDialog.show() return def show_activities(self): from nxdrive.wui.activity import WebActivityDialog self.webEngineWidget = WebActivityDialog(self) self.webEngineWidget.show() @QtCore.pyqtSlot(object) def _connect_engine(self, engine): 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) @QtCore.pyqtSlot() def _debug_toggle_invalid_credentials(self): sender = self.sender() engine = sender.data().toPyObject() engine.set_invalid_credentials(not engine.has_invalid_credentials(), reason='debug') @QtCore.pyqtSlot() def _debug_show_file_status(self): from nxdrive.gui.status_dialog import StatusDialog sender = self.sender() engine = sender.data().toPyObject() self.statusDialog = StatusDialog(engine.get_dao()) self.statusDialog.show() def _create_debug_engine_menu(self, engine, parent): menuDebug = QtGui.QMenu(parent) action = QtGui.QAction(Translator.get("DEBUG_INVALID_CREDENTIALS"), menuDebug) action.setCheckable(True) action.setChecked(engine.has_invalid_credentials()) action.setData(engine) action.triggered.connect(self._debug_toggle_invalid_credentials) menuDebug.addAction(action) action = QtGui.QAction(Translator.get("DEBUG_FILE_STATUS"), menuDebug) action.setData(engine) action.triggered.connect(self._debug_show_file_status) menuDebug.addAction(action) return menuDebug def create_debug_menu(self, parent): menuDebug = QtGui.QMenu(parent) menuDebug.addAction(Translator.get("DEBUG_WINDOW"), self.show_debug_window) for engine in self.manager.get_engines().values(): action = QtGui.QAction(engine._name, menuDebug) action.setMenu(self._create_debug_engine_menu(engine, menuDebug)) action.setData(engine) menuDebug.addAction(action) return menuDebug def _get_debug_dialog(self): from nxdrive.debug.wui.engine import EngineDialog return EngineDialog(self) @QtCore.pyqtSlot() def show_debug_window(self): debug = self._get_unique_dialog("debug") if debug is None: debug = self._get_debug_dialog() self._create_unique_dialog("debug", debug) self._show_window(debug) def init_checks(self): if self.manager.is_debug(): self.show_debug_window() for _, engine in self.manager.get_engines().iteritems(): self._connect_engine(engine) self.manager.newEngine.connect(self._connect_engine) 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() def get_systray_menu(self): from nxdrive.wui.systray import WebSystray return WebSystray(self, self._tray_icon) def set_icon_state(self, state): """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.get_icon_state() == state: # Nothing to update return False self._tray_icon.setToolTip(self.get_tooltip()) # Handle animated transferring icon if state == 'transferring': self.icon_spin_timer.start(150) else: self.icon_spin_timer.stop() icon = find_icon('nuxeo_drive_systray_icon_%s_18.png' % state) if icon is not None: self._tray_icon.setIcon(QtGui.QIcon(icon)) else: log.warning('Icon not found: %s', icon) self._icon_state = state log.debug('Updated icon state to: %s', state) return True def get_icon_state(self): return getattr(self, '_icon_state', None) def spin_transferring_icon(self): icon = find_icon('nuxeo_drive_systray_icon_transferring_%s.png' % (self.icon_spin_count + 1)) self._tray_icon.setIcon(QtGui.QIcon(icon)) self.icon_spin_count = (self.icon_spin_count + 1) % 10 def update_tooltip(self): # Update also the file self._tray_icon.setToolTip(self.get_tooltip()) def get_default_tooltip(self): return self.manager.get_appname() def get_tooltip(self): actions = Action.get_actions() if actions is None or len(actions) == 0: return self.get_default_tooltip() # Display only the first action for now # TODO Get all actions ? or just file action action = actions.itervalues().next() if action is None: return self.get_default_tooltip() if isinstance(action, FileAction): if action.get_percent() is not None: return ("%s - %s - %s - %d%%" % (self.get_default_tooltip(), action.type, action.filename, action.get_percent())) else: return ("%s - %s - %s" % (self.get_default_tooltip(), action.type, action.filename)) elif action.get_percent() is not None: return ("%s - %s - %d%%" % (self.get_default_tooltip(), action.type, action.get_percent())) else: return ("%s - %s" % (self.get_default_tooltip(), action.type)) @QtCore.pyqtSlot(str) def app_updated(self, updated_version): self.updated_version = str(updated_version) log.info('Quitting Nuxeo Drive and restarting updated version %s', self.updated_version) self.manager.stopped.connect(self.restart) log.debug("Exiting Qt application") self.quit() @QtCore.pyqtSlot() def restart(self): """ Restart application by loading updated executable into current process""" current_version = self.manager.get_updater().get_active_version() log.info("Current application version: %s", current_version) log.info("Updated application version: %s", self.updated_version) executable = sys.executable # TODO NXP-13818: better handle this! if sys.platform == 'darwin': executable = executable.replace('python', self.get_mac_app()) log.info("Current executable is: %s", executable) updated_executable = executable.replace(current_version, self.updated_version) log.info("Updated executable is: %s", updated_executable) args = [updated_executable] args.extend(sys.argv[1:]) log.info("Opening subprocess with args: %r", args) subprocess.Popen(args) def get_mac_app(self): return 'ndrive' def show_metadata(self, file_path): from nxdrive.wui.metadata import CreateMetadataWebDialog self._metadata_dialog = CreateMetadataWebDialog(self.manager, file_path) self._metadata_dialog.show() def setup_systray(self): self._tray_icon = QtGui.QSystemTrayIcon() self._tray_icon.setToolTip(self.manager.get_appname()) self.set_icon_state("disabled") self._tray_icon.show() self.tray_icon_menu = self.get_systray_menu() self._tray_icon.setContextMenu(self.tray_icon_menu) def event(self, event): """Handle URL scheme events under OSX""" if hasattr(event, 'url'): url = str(event.url().toString()) log.debug("Event URL: %s", url) try: info = parse_protocol_url(url) log.debug("URL info: %r", info) if info is not None: log.debug("Received nxdrive URL scheme event: %s", url) if info.get('command') == 'download_edit': # This is a quick operation, no need to fork a QThread self.manager.get_drive_edit().edit( info['server_url'], info['doc_id'], info['filename'], user=info['user'], download_url=info['download_url']) elif info.get('command') == 'edit': # TO_REVIEW Still used ? self.manager.get_drive_edit().edit( info['server_url'], info['item_id']) except: log.error("Error handling URL event: %s", url, exc_info=True) return super(Application, self).event(event)
class Application(SimpleApplication): """Main Nuxeo drive application controlled by a system tray icon + menu""" def __init__(self, manager, options, argv=()): super(Application, self).__init__(manager, options, list(argv)) self.setQuitOnLastWindowClosed(False) self._delegator = None from nxdrive.scripting import DriveUiScript self.manager.set_script_object(DriveUiScript(manager, self)) self.mainEngine = None self.filters_dlg = None self._conflicts_modals = dict() self.current_notification = None # Make dialog unique self.uniqueDialogs = dict() for _, engine in self.manager.get_engines().iteritems(): self.mainEngine = engine break if self.mainEngine is not None and options.debug: from nxdrive.engine.engine import EngineLogger self.engineLogger = EngineLogger(self.mainEngine) self.engineWidget = None self.aboutToQuit.connect(self.manager.stop) self.manager.dropEngine.connect(self.dropped_engine) # Timer to spin the transferring icon self.icon_spin_timer = QtCore.QTimer() self.icon_spin_timer.timeout.connect(self.spin_transferring_icon) self.icon_spin_count = 0 # Application update self.manager.get_updater().appUpdated.connect(self.app_updated) self.updated_version = None # This is a windowless application mostly using the system tray self.setQuitOnLastWindowClosed(False) self.setup_systray() # Direct Edit conflict self.manager.get_drive_edit().driveEditConflict.connect(self._direct_edit_conflict) # Check if actions is required, separate method so it can be override self.init_checks() self.engineWidget = None # Setup notification center for Mac if AbstractOSIntegration.is_mac(): if AbstractOSIntegration.os_version_above("10.8"): self._setup_notification_center() @QtCore.pyqtSlot(str, str, str) def _direct_edit_conflict(self, filename, ref, digest): try: log.trace("Entering _direct_edit_conflict for %r / %r", filename, ref) filename = unicode(filename) log.trace("Unicode filename: %r", filename) 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 info = dict() info["name"] = filename dlg = WebModal(self, Translator.get("DIRECT_EDIT_CONFLICT_MESSAGE", info)) dlg.add_button("OVERWRITE", Translator.get("DIRECT_EDIT_CONFLICT_OVERWRITE")) dlg.add_button("CANCEL", Translator.get("DIRECT_EDIT_CONFLICT_CANCEL")) res = dlg.exec_() if res == "OVERWRITE": self.manager.get_drive_edit().force_update(unicode(ref), unicode(digest)) del self._conflicts_modals[filename] except Exception: log.exception("Error while displaying Direct Edit conflict modal dialog for %r", filename) @QtCore.pyqtSlot() def _root_deleted(self): engine = self.sender() info = dict() log.debug("Root has been deleted for engine: %s", engine.get_uid()) info["folder"] = engine.get_local_folder() dlg = WebModal(self, Translator.get("DRIVE_ROOT_DELETED", info)) dlg.add_button("RECREATE", Translator.get("DRIVE_ROOT_RECREATE"), style="primary") dlg.add_button("DISCONNECT", Translator.get("DRIVE_ROOT_DISCONNECT"), style="danger") res = dlg.exec_() if res == "DISCONNECT": self.manager.unbind_engine(engine.get_uid()) elif res == "RECREATE": engine.reinit() engine.start() @QtCore.pyqtSlot(str) def _root_moved(self, new_path): engine = self.sender() info = dict() log.debug("Root has been moved for engine: %s to '%s'", engine.get_uid(), new_path) info["folder"] = engine.get_local_folder() info["new_folder"] = new_path dlg = WebModal(self, Translator.get("DRIVE_ROOT_MOVED", info)) dlg.add_button("MOVE", Translator.get("DRIVE_ROOT_UPDATE"), style="primary") dlg.add_button("RECREATE", Translator.get("DRIVE_ROOT_RECREATE")) dlg.add_button("DISCONNECT", Translator.get("DRIVE_ROOT_DISCONNECT"), style="danger") res = dlg.exec_() if res == "DISCONNECT": self.manager.unbind_engine(engine.get_uid()) elif res == "RECREATE": engine.reinit() engine.start() elif res == "MOVE": engine.set_local_folder(unicode(new_path)) engine.start() def get_cache_folder(self): return os.path.join(self.manager.get_configuration_folder(), "cache", "wui") def _get_skin(self): return "ui5" def get_window_icon(self): return find_icon("nuxeo_drive_icon_64.png") def get_htmlpage(self, page): import nxdrive nxdrive_path = os.path.dirname(nxdrive.__file__) ui_path = os.path.join(nxdrive_path, "data", self._get_skin()) return os.path.join(find_resource_dir(self._get_skin(), ui_path), page).replace("\\", "/") def _init_translator(self): from nxdrive.wui.translator import Translator Translator(self.manager, self.get_htmlpage("i18n.js"), self.manager.get_config("locale", self.options.locale)) @QtCore.pyqtSlot(object) def dropped_engine(self, engine): # Update icon in case the engine dropped was syncing self.change_systray_icon() @QtCore.pyqtSlot() def change_systray_icon(self): syncing = False engines = self.manager.get_engines() invalid_credentials = True paused = True offline = True for _, engine in engines.iteritems(): syncing = syncing | engine.is_syncing() invalid_credentials = invalid_credentials & engine.has_invalid_credentials() paused = paused & engine.is_paused() offline = offline & engine.is_offline() new_state = "asleep" if len(engines) == 0 or paused or offline: new_state = "disabled" elif invalid_credentials: new_state = "stopping" elif syncing: new_state = "transferring" log.trace("Should change icon to %s", new_state) self.set_icon_state(new_state) def _get_settings_dialog(self, section): from nxdrive.wui.settings import WebSettingsDialog return WebSettingsDialog(self, section) def _get_conflicts_dialog(self, engine): from nxdrive.wui.dialog import WebDialog from nxdrive.wui.conflicts import WebConflictsApi return WebDialog(self, "conflicts.html", api=WebConflictsApi(self, engine)) @QtCore.pyqtSlot() def show_conflicts_resolution(self, engine): conflicts = self._get_unique_dialog("conflicts") if conflicts is None: conflicts = self._get_conflicts_dialog(engine) self._create_unique_dialog("conflicts", conflicts) else: conflicts._api.set_engine(engine) self._show_window(conflicts) @QtCore.pyqtSlot() def show_settings(self, section="Accounts"): if section is None: section = "Accounts" settings = self._get_unique_dialog("settings") if settings is None: settings = self._get_settings_dialog(section) self._create_unique_dialog("settings", settings) else: settings.set_section(section) self._show_window(settings) def _show_window(self, window): window.show() window.raise_() def _get_unique_dialog(self, name): if name in self.uniqueDialogs: return self.uniqueDialogs[name] return None def _destroy_dialog(self): sender = self.sender() name = str(sender.objectName()) if name in self.uniqueDialogs: del self.uniqueDialogs[name] def _create_unique_dialog(self, name, dialog): self.uniqueDialogs[name] = dialog dialog.setObjectName(name) dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose) dialog.destroyed.connect(self._destroy_dialog) @QtCore.pyqtSlot() def destroyed_filters_dialog(self): self.filters_dlg = None def _get_filters_dialog(self, engine): from nxdrive.gui.folders_dialog import FiltersDialog return FiltersDialog(self, engine) @QtCore.pyqtSlot() def show_filters(self, engine): if self.filters_dlg is not None: self.filters_dlg.close() self.filters_dlg = None self.filters_dlg = self._get_filters_dialog(engine) self.filters_dlg.destroyed.connect(self.destroyed_filters_dialog) self.filters_dlg.show() def show_file_status(self): from nxdrive.gui.status_dialog import StatusDialog for _, engine in self.manager.get_engines().iteritems(): self.statusDialog = StatusDialog(engine.get_dao()) self.statusDialog.show() return def show_activities(self): from nxdrive.wui.activity import WebActivityDialog self.webEngineWidget = WebActivityDialog(self) self.webEngineWidget.show() @QtCore.pyqtSlot(object) def _connect_engine(self, engine): 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) @QtCore.pyqtSlot() def _debug_toggle_invalid_credentials(self): sender = self.sender() engine = sender.data().toPyObject() engine.set_invalid_credentials(not engine.has_invalid_credentials(), reason="debug") @QtCore.pyqtSlot() def _debug_show_file_status(self): from nxdrive.gui.status_dialog import StatusDialog sender = self.sender() engine = sender.data().toPyObject() self.statusDialog = StatusDialog(engine.get_dao()) self.statusDialog.show() def _create_debug_engine_menu(self, engine, parent): menuDebug = QtGui.QMenu(parent) action = QtGui.QAction(Translator.get("DEBUG_INVALID_CREDENTIALS"), menuDebug) action.setCheckable(True) action.setChecked(engine.has_invalid_credentials()) action.setData(engine) action.triggered.connect(self._debug_toggle_invalid_credentials) menuDebug.addAction(action) action = QtGui.QAction(Translator.get("DEBUG_FILE_STATUS"), menuDebug) action.setData(engine) action.triggered.connect(self._debug_show_file_status) menuDebug.addAction(action) return menuDebug def create_debug_menu(self, parent): menuDebug = QtGui.QMenu(parent) menuDebug.addAction(Translator.get("DEBUG_WINDOW"), self.show_debug_window) for engine in self.manager.get_engines().values(): action = QtGui.QAction(engine._name, menuDebug) action.setMenu(self._create_debug_engine_menu(engine, menuDebug)) action.setData(engine) menuDebug.addAction(action) return menuDebug def _get_debug_dialog(self): from nxdrive.debug.wui.engine import EngineDialog return EngineDialog(self) @QtCore.pyqtSlot() def show_debug_window(self): debug = self._get_unique_dialog("debug") if debug is None: debug = self._get_debug_dialog() self._create_unique_dialog("debug", debug) self._show_window(debug) def init_checks(self): if self.manager.is_debug(): self.show_debug_window() for _, engine in self.manager.get_engines().iteritems(): self._connect_engine(engine) self.manager.newEngine.connect(self._connect_engine) self.manager.get_notification_service().newNotification.connect(self._new_notification) self.manager.get_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() @QtCore.pyqtSlot() def _update_notification(self): replacements = dict() replacements["version"] = self.manager.get_updater().get_status()[1] notification = Notification( uuid="AutoUpdate", flags=Notification.FLAG_BUBBLE | Notification.FLAG_VOLATILE | Notification.FLAG_UNIQUE, title=Translator.get("AUTOUPDATE_NOTIFICATION_TITLE", replacements), description=Translator.get("AUTOUPDATE_NOTIFICATION_MESSAGE", replacements), ) self.manager.get_notification_service().send_notification(notification) @QtCore.pyqtSlot() def _message_clicked(self): if self.current_notification is None: return self.manager.get_notification_service().trigger_notification(self.current_notification.get_uid()) def _setup_notification_center(self): from nxdrive.osi.darwin.pyNotificationCenter import setup_delegator, NotificationDelegator if self._delegator is None: self._delegator = NotificationDelegator.alloc().init() self._delegator._manager = self.manager setup_delegator(self._delegator) @QtCore.pyqtSlot(object) def _new_notification(self, notification): if not notification.is_bubble(): return if AbstractOSIntegration.is_mac(): if AbstractOSIntegration.os_version_above("10.8"): from nxdrive.osi.darwin.pyNotificationCenter import notify, NotificationDelegator if self._delegator is None: self._delegator = NotificationDelegator.alloc().init() self._delegator._manager = self.manager # Use notification center userInfo = dict() userInfo["uuid"] = notification.get_uid() return notify(notification.get_title(), None, notification.get_description(), userInfo=userInfo) self.current_notification = notification icon = QtGui.QSystemTrayIcon.Information if notification.get_level() == Notification.LEVEL_WARNING: icon = QtGui.QSystemTrayIcon.Warning elif notification.get_level() == Notification.LEVEL_ERROR: icon = QtGui.QSystemTrayIcon.Critical self.show_message(notification.get_title(), notification.get_description(), icon=icon) def get_systray_menu(self): from nxdrive.wui.systray import WebSystray return WebSystray(self, self._tray_icon) def set_icon_state(self, state): """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.get_icon_state() == state: # Nothing to update return False self._tray_icon.setToolTip(self.get_tooltip()) # Handle animated transferring icon if state == "transferring": self.icon_spin_timer.start(150) else: self.icon_spin_timer.stop() icon = find_icon("nuxeo_drive_systray_icon_%s_18.png" % state) if icon is not None: self._tray_icon.setIcon(QtGui.QIcon(icon)) else: log.warning("Icon not found: %s", icon) self._icon_state = state log.debug("Updated icon state to: %s", state) return True def get_icon_state(self): return getattr(self, "_icon_state", None) def spin_transferring_icon(self): icon = find_icon("nuxeo_drive_systray_icon_transferring_%s.png" % (self.icon_spin_count + 1)) self._tray_icon.setIcon(QtGui.QIcon(icon)) self.icon_spin_count = (self.icon_spin_count + 1) % 10 def get_osi(self): return self.manager.get_osi() def update_tooltip(self): # Update also the file self._tray_icon.setToolTip(self.get_tooltip()) def get_default_tooltip(self): return self.manager.get_appname() def get_tooltip(self): actions = Action.get_actions() if actions is None or len(actions) == 0: return self.get_default_tooltip() # Display only the first action for now # TODO Get all actions ? or just file action action = actions.itervalues().next() if action is None: return self.get_default_tooltip() if isinstance(action, FileAction): if action.get_percent() is not None: return "%s - %s - %s - %d%%" % ( self.get_default_tooltip(), action.type, action.filename, action.get_percent(), ) else: return "%s - %s - %s" % (self.get_default_tooltip(), action.type, action.filename) elif action.get_percent() is not None: return "%s - %s - %d%%" % (self.get_default_tooltip(), action.type, action.get_percent()) else: return "%s - %s" % (self.get_default_tooltip(), action.type) @QtCore.pyqtSlot(str) def app_updated(self, updated_version): self.updated_version = str(updated_version) log.info("Quitting Nuxeo Drive and restarting updated version %s", self.updated_version) self.manager.stopped.connect(self.restart) log.debug("Exiting Qt application") self.quit() @QtCore.pyqtSlot() def restart(self): """ Restart application by loading updated executable into current process""" current_version = self.manager.get_updater().get_active_version() log.info("Current application version: %s", current_version) log.info("Updated application version: %s", self.updated_version) executable = sys.executable # TODO NXP-13818: better handle this! if sys.platform == "darwin": executable = executable.replace("python", self.get_mac_app()) log.info("Current executable is: %s", executable) updated_executable = executable.replace(current_version, self.updated_version) log.info("Updated executable is: %s", updated_executable) args = [updated_executable] args.extend(sys.argv[1:]) log.info("Opening subprocess with args: %r", args) subprocess.Popen(args) def get_mac_app(self): return "ndrive" def show_message(self, title, message, icon=QtGui.QSystemTrayIcon.Information, timeout=10000): self._tray_icon.showMessage(title, message, icon, timeout) def show_dialog(self, url): from nxdrive.wui.dialog import WebDialog dialog = WebDialog(self, url) dialog.show() def show_metadata(self, file_path): from nxdrive.wui.metadata import CreateMetadataWebDialog self._metadata_dialog = CreateMetadataWebDialog(self.manager, file_path) self._metadata_dialog.show() def setup_systray(self): self._tray_icon = DriveSystrayIcon() self._tray_icon.setToolTip(self.manager.get_appname()) self.set_icon_state("disabled") self._tray_icon.show() self.tray_icon_menu = self.get_systray_menu() self._tray_icon.setContextMenu(self.tray_icon_menu) self._tray_icon.messageClicked.connect(self._message_clicked) def event(self, event): """Handle URL scheme events under OSX""" if hasattr(event, "url"): url = str(event.url().toString()) log.debug("Event URL: %s", url) try: info = parse_protocol_url(url) log.debug("URL info: %r", info) if info is not None: log.debug("Received nxdrive URL scheme event: %s", url) if info.get("command") == "download_edit": # This is a quick operation, no need to fork a QThread self.manager.get_drive_edit().edit( info["server_url"], info["doc_id"], user=info["user"], download_url=info["download_url"] ) elif info.get("command") == "edit": # Kept for backward compatibility self.manager.get_drive_edit().edit(info["server_url"], info["item_id"]) except: log.error("Error handling URL event: %s", url, exc_info=True) return super(Application, self).event(event)
class Application(SimpleApplication): """Main Nuxeo drive application controlled by a system tray icon + menu""" tray_icon = None icon_state = None def __init__(self, manager, *args): super(Application, self).__init__(manager, *args) self.setQuitOnLastWindowClosed(False) self._delegator = None from nxdrive.scripting import DriveUiScript self.manager.set_script_object(DriveUiScript(manager, self)) self.mainEngine = None self.filters_dlg = None self._conflicts_modals = dict() self.current_notification = None self.default_tooltip = self.manager.app_name for _, engine in self.manager.get_engines().iteritems(): self.mainEngine = engine break if self.mainEngine is not None and Options.debug: from nxdrive.engine.engine import EngineLogger self.engineLogger = EngineLogger(self.mainEngine) self.engineWidget = None self.aboutToQuit.connect(self.manager.stop) self.manager.dropEngine.connect(self.dropped_engine) # Timer to spin the transferring icon self.icon_spin_timer = QtCore.QTimer() self.icon_spin_timer.timeout.connect(self.spin_transferring_icon) self.icon_spin_count = 0 # Application update self.manager.get_updater().appUpdated.connect(self.app_updated) self.updated_version = None # This is a windowless application mostly using the system tray self.setQuitOnLastWindowClosed(False) self.setup_systray() # Direct Edit conflict self.manager.direct_edit.directEditConflict.connect( self._direct_edit_conflict) # Check if actions is required, separate method so it can be override self.init_checks() self.engineWidget = None # Setup notification center for macOS if (AbstractOSIntegration.is_mac() and AbstractOSIntegration.os_version_above('10.8')): self._setup_notification_center() @QtCore.pyqtSlot(str, str, str) def _direct_edit_conflict(self, filename, ref, digest): log.trace('Entering _direct_edit_conflict for %r / %r', filename, ref) try: filename = unicode(filename) 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 info = dict(name=filename) dlg = WebModal( self, Translator.get('DIRECT_EDIT_CONFLICT_MESSAGE', info), ) dlg.add_button('OVERWRITE', Translator.get('DIRECT_EDIT_CONFLICT_OVERWRITE')) dlg.add_button('CANCEL', Translator.get('DIRECT_EDIT_CONFLICT_CANCEL')) res = dlg.exec_() if res == 'OVERWRITE': self.manager.direct_edit.force_update(unicode(ref), unicode(digest)) del self._conflicts_modals[filename] except: log.exception( 'Error while displaying Direct Edit' ' conflict modal dialog for %r', filename) @QtCore.pyqtSlot() def _root_deleted(self): engine = self.sender() info = dict() log.debug('Root has been deleted for engine: %s', engine.uid) info['folder'] = engine.local_folder dlg = WebModal(self, Translator.get('DRIVE_ROOT_DELETED', info)) dlg.add_button('RECREATE', Translator.get('DRIVE_ROOT_RECREATE'), style='primary') dlg.add_button('DISCONNECT', Translator.get('DRIVE_ROOT_DISCONNECT'), style='danger') res = dlg.exec_() if res == 'DISCONNECT': self.manager.unbind_engine(engine.uid) elif res == 'RECREATE': engine.reinit() engine.start() @QtCore.pyqtSlot() def _no_space_left(self): dialog = WebModal(self, Translator.get('NO_SPACE_LEFT_ON_DEVICE')) dialog.add_button('OK', Translator.get('OK')) dialog.exec_() @QtCore.pyqtSlot(str) def _root_moved(self, new_path): engine = self.sender() log.debug('Root has been moved for engine: %s to %r', engine.uid, new_path) info = { 'folder': engine.local_folder, 'new_folder': new_path, } dlg = WebModal(self, Translator.get('DRIVE_ROOT_MOVED', info)) dlg.add_button('MOVE', Translator.get('DRIVE_ROOT_UPDATE'), style='primary') dlg.add_button('RECREATE', Translator.get('DRIVE_ROOT_RECREATE')) dlg.add_button('DISCONNECT', Translator.get('DRIVE_ROOT_DISCONNECT'), style='danger') res = dlg.exec_() if res == 'DISCONNECT': self.manager.unbind_engine(engine.uid) elif res == 'RECREATE': engine.reinit() engine.start() elif res == 'MOVE': engine.set_local_folder(unicode(new_path)) engine.start() def get_cache_folder(self): return os.path.join(self.manager.nxdrive_home, 'cache', 'wui') def _init_translator(self): Translator( self.manager, self.get_htmlpage('i18n.js'), self.manager.get_config('locale', Options.locale), ) @QtCore.pyqtSlot(object) def dropped_engine(self, engine): # Update icon in case the engine dropped was syncing self.change_systray_icon() @QtCore.pyqtSlot() def change_systray_icon(self): syncing = False engines = self.manager.get_engines() invalid_credentials = True paused = True offline = True for engine in engines.itervalues(): syncing |= engine.is_syncing() invalid_credentials &= engine.has_invalid_credentials() paused &= engine.is_paused() offline &= engine.is_offline() if offline: new_state = 'stopping' Action(Translator.get('OFFLINE')) elif invalid_credentials: new_state = 'stopping' Action(Translator.get('INVALID_CREDENTIALS')) elif not engines or paused: new_state = 'disabled' Action.finish_action() elif syncing: new_state = 'transferring' else: new_state = 'asleep' Action.finish_action() self.set_icon_state(new_state) def _get_settings_dialog(self, section): from nxdrive.wui.settings import WebSettingsDialog return WebSettingsDialog(self, section) def _get_conflicts_dialog(self, engine): from nxdrive.wui.dialog import WebDialog from nxdrive.wui.conflicts import WebConflictsApi return WebDialog( self, 'conflicts.html', api=WebConflictsApi(self, engine), ) @QtCore.pyqtSlot() def show_conflicts_resolution(self, engine): conflicts = self._get_unique_dialog('conflicts') if conflicts is None: conflicts = self._get_conflicts_dialog(engine) self._create_unique_dialog('conflicts', conflicts) else: conflicts.api.set_engine(engine) self._show_window(conflicts) @QtCore.pyqtSlot() def show_settings(self, section='Accounts'): if section is None: section = 'Accounts' settings = self._get_unique_dialog('settings') if settings is None: settings = self._get_settings_dialog(section) self._create_unique_dialog('settings', settings) else: settings.set_section(section) self._show_window(settings) @QtCore.pyqtSlot() def open_help(self): self.manager.open_help() @QtCore.pyqtSlot() def destroyed_filters_dialog(self): self.filters_dlg = None def _get_filters_dialog(self, engine): from nxdrive.gui.folders_dialog import FiltersDialog return FiltersDialog(self, engine) @QtCore.pyqtSlot() def show_filters(self, engine): if self.filters_dlg is not None: self.filters_dlg.close() self.filters_dlg = None self.filters_dlg = self._get_filters_dialog(engine) self.filters_dlg.destroyed.connect(self.destroyed_filters_dialog) self.filters_dlg.show() def show_file_status(self): from nxdrive.gui.status_dialog import StatusDialog for _, engine in self.manager.get_engines().iteritems(): self.status = StatusDialog(engine.get_dao()) self.status.show() break def show_activities(self): from nxdrive.wui.activity import WebActivityDialog self.activities = WebActivityDialog(self) self.activities.show() @QtCore.pyqtSlot(object) def _connect_engine(self, engine): 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() @QtCore.pyqtSlot() def _debug_toggle_invalid_credentials(self): sender = self.sender() engine = sender.data().toPyObject() engine.set_invalid_credentials(not engine.has_invalid_credentials(), reason='debug') @QtCore.pyqtSlot() def _debug_show_file_status(self): from nxdrive.gui.status_dialog import StatusDialog sender = self.sender() engine = sender.data().toPyObject() self.status_dialog = StatusDialog(engine.get_dao()) self.status_dialog.show() def _create_debug_engine_menu(self, engine, parent): menu = QtGui.QMenu(parent) action = QtGui.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 = QtGui.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): menu.addAction(Translator.get('DEBUG_WINDOW'), self.show_debug_window) for engine in self.manager.get_engines().values(): action = QtGui.QAction(engine.name, menu) action.setMenu(self._create_debug_engine_menu(engine, menu)) action.setData(engine) menu.addAction(action) @QtCore.pyqtSlot() def show_debug_window(self): debug = self._get_unique_dialog('debug') if debug is None: from nxdrive.debug.wui.engine import EngineDialog debug = EngineDialog(self) self._create_unique_dialog('debug', debug) self._show_window(debug) def init_checks(self): if Options.debug: self.show_debug_window() for _, engine in self.manager.get_engines().iteritems(): self._connect_engine(engine) self.manager.newEngine.connect(self._connect_engine) self.manager.notification_service.newNotification.connect( self._new_notification) self.manager.get_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() @QtCore.pyqtSlot() def _update_notification(self): replacements = dict(version=self.manager.get_updater().get_status()[1]) notification = Notification( uuid='AutoUpdate', flags=(Notification.FLAG_BUBBLE | Notification.FLAG_VOLATILE | Notification.FLAG_UNIQUE), title=Translator.get('AUTOUPDATE_NOTIFICATION_TITLE', replacements), description=Translator.get('AUTOUPDATE_NOTIFICATION_MESSAGE', replacements), ) self.manager.notification_service.send_notification(notification) @QtCore.pyqtSlot() def message_clicked(self): if self.current_notification: self.manager.notification_service.trigger_notification( self.current_notification.uid) def _setup_notification_center(self): from nxdrive.osi.darwin.pyNotificationCenter import setup_delegator, NotificationDelegator if self._delegator is None: self._delegator = NotificationDelegator.alloc().init() self._delegator._manager = self.manager setup_delegator(self._delegator) @QtCore.pyqtSlot(object) def _new_notification(self, notif): if not notif.is_bubble(): return if self._delegator is not None: # Use notification center from nxdrive.osi.darwin.pyNotificationCenter import notify return notify( notif.title, None, notif.description, user_info={'uuid': notif.uid}, ) icon = QtGui.QSystemTrayIcon.Information if notif.level == Notification.LEVEL_WARNING: icon = QtGui.QSystemTrayIcon.Warning elif notif.level == Notification.LEVEL_ERROR: icon = QtGui.QSystemTrayIcon.Critical self.current_notification = notif self.tray_icon.showMessage(notif.title, notif.description, icon, 10000) def set_icon_state(self, state): """ 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()) # Handle animated transferring icon if state == 'transferring': self.icon_spin_timer.start(150) else: self.icon_spin_timer.stop() icon = find_icon('nuxeo_drive_systray_icon_%s_18.png' % state) self.tray_icon.setIcon(QtGui.QIcon(icon)) self.icon_state = state return True def spin_transferring_icon(self): icon = find_icon('nuxeo_drive_systray_icon_transferring_%s.png' % (self.icon_spin_count + 1)) self.tray_icon.setIcon(QtGui.QIcon(icon)) self.icon_spin_count = (self.icon_spin_count + 1) % 10 def get_tooltip(self): actions = Action.get_actions() if not actions: return self.default_tooltip # Display only the first action for now for action in actions.itervalues(): if action 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, ) @QtCore.pyqtSlot(str) def app_updated(self, updated_version): self.updated_version = str(updated_version) log.info('Quitting Nuxeo Drive and restarting updated version %s', self.updated_version) self.manager.stopped.connect(self.restart) log.debug('Exiting Qt application') self.quit() @QtCore.pyqtSlot() def restart(self): """ Restart application by loading updated executable into current process. """ current_version = self.manager.get_updater().get_active_version() log.info('Current application version: %s', current_version) log.info('Updated application version: %s', self.updated_version) executable = sys.executable # TODO NXP-13818: better handle this! if sys.platform == 'darwin': executable = executable.replace('python', self.get_mac_app()) log.info('Current executable is: %s', executable) updated_executable = executable.replace(current_version, self.updated_version) log.info('Updated executable is: %s', updated_executable) args = [updated_executable] args.extend(sys.argv[1:]) log.info('Opening subprocess with args: %r', args) subprocess.Popen(args, close_fds=True) @staticmethod def get_mac_app(): return 'ndrive' def show_dialog(self, url): from nxdrive.wui.dialog import WebDialog WebDialog(self, url).show() def show_metadata(self, file_path): self.manager.open_metadata_window(file_path) def setup_systray(self): self.tray_icon = DriveSystrayIcon(self) self.tray_icon.setToolTip(self.manager.app_name) self.set_icon_state('disabled') self.tray_icon.show() def event(self, event): """Handle URL scheme events under OSX""" if hasattr(event, 'url'): url = str(event.url().toString()) try: info = parse_protocol_url(url) log.debug('Event url=%s, info=%r', url, info) if info is not None: log.debug('Received nxdrive URL scheme event: %s', url) if info.get('command') == 'download_edit': # This is a quick operation, no need to fork a QThread self.manager.direct_edit.edit( info['server_url'], info['doc_id'], user=info['user'], download_url=info['download_url'], ) elif info.get('command') == 'edit': # Kept for backward compatibility self.manager.direct_edit.edit(info['server_url'], info['item_id']) except: log.exception('Error handling URL event: %s', url) return super(Application, self).event(event)
class Application(SimpleApplication): """Main Nuxeo drive application controlled by a system tray icon + menu""" tray_icon = None icon_state = None def __init__(self, manager, *args): super(Application, self).__init__(manager, *args) self.setQuitOnLastWindowClosed(False) self._delegator = None from nxdrive.scripting import DriveUiScript self.manager.set_script_object(DriveUiScript(manager, self)) self.mainEngine = None self.filters_dlg = None self._conflicts_modals = dict() self.current_notification = None self.default_tooltip = self.manager.app_name for _, engine in self.manager.get_engines().iteritems(): self.mainEngine = engine break if self.mainEngine is not None and Options.debug: from nxdrive.engine.engine import EngineLogger self.engineLogger = EngineLogger(self.mainEngine) self.engineWidget = None self.aboutToQuit.connect(self.manager.stop) self.manager.dropEngine.connect(self.dropped_engine) # Timer to spin the transferring icon self.icon_spin_timer = QtCore.QTimer() self.icon_spin_timer.timeout.connect(self.spin_transferring_icon) self.icon_spin_count = 0 # Application update self.manager.get_updater().appUpdated.connect(self.app_updated) self.updated_version = None # This is a windowless application mostly using the system tray self.setQuitOnLastWindowClosed(False) self.setup_systray() # Direct Edit conflict self.manager.direct_edit.directEditConflict.connect(self._direct_edit_conflict) # Check if actions is required, separate method so it can be override self.init_checks() self.engineWidget = None # Setup notification center for macOS if (AbstractOSIntegration.is_mac() and AbstractOSIntegration.os_version_above('10.8')): self._setup_notification_center() @QtCore.pyqtSlot(str, str, str) def _direct_edit_conflict(self, filename, ref, digest): log.trace('Entering _direct_edit_conflict for %r / %r', filename, ref) try: filename = unicode(filename) 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 info = dict(name=filename) dlg = WebModal( self, Translator.get('DIRECT_EDIT_CONFLICT_MESSAGE', info), ) dlg.add_button('OVERWRITE', Translator.get('DIRECT_EDIT_CONFLICT_OVERWRITE')) dlg.add_button('CANCEL', Translator.get('DIRECT_EDIT_CONFLICT_CANCEL')) res = dlg.exec_() if res == 'OVERWRITE': self.manager.direct_edit.force_update(unicode(ref), unicode(digest)) del self._conflicts_modals[filename] except: log.exception('Error while displaying Direct Edit' ' conflict modal dialog for %r', filename) @QtCore.pyqtSlot() def _root_deleted(self): engine = self.sender() info = dict() log.debug('Root has been deleted for engine: %s', engine.uid) info['folder'] = engine.local_folder dlg = WebModal(self, Translator.get('DRIVE_ROOT_DELETED', info)) dlg.add_button('RECREATE', Translator.get('DRIVE_ROOT_RECREATE'), style='primary') dlg.add_button('DISCONNECT', Translator.get('DRIVE_ROOT_DISCONNECT'), style='danger') res = dlg.exec_() if res == 'DISCONNECT': self.manager.unbind_engine(engine.uid) elif res == 'RECREATE': engine.reinit() engine.start() @QtCore.pyqtSlot() def _no_space_left(self): dialog = WebModal(self, Translator.get('NO_SPACE_LEFT_ON_DEVICE')) dialog.add_button('OK', Translator.get('OK')) dialog.exec_() @QtCore.pyqtSlot(str) def _root_moved(self, new_path): engine = self.sender() info = dict() log.debug('Root has been moved for engine: %s to %r', engine.uid, new_path) info['folder'] = engine.local_folder info['new_folder'] = new_path dlg = WebModal(self, Translator.get('DRIVE_ROOT_MOVED', info)) dlg.add_button('MOVE', Translator.get('DRIVE_ROOT_UPDATE'), style='primary') dlg.add_button('RECREATE', Translator.get('DRIVE_ROOT_RECREATE')) dlg.add_button('DISCONNECT', Translator.get('DRIVE_ROOT_DISCONNECT'), style='danger') res = dlg.exec_() if res == 'DISCONNECT': self.manager.unbind_engine(engine.uid) elif res == 'RECREATE': engine.reinit() engine.start() elif res == 'MOVE': engine.set_local_folder(unicode(new_path)) engine.start() def get_cache_folder(self): return os.path.join(self.manager.nxdrive_home, 'cache', 'wui') def _init_translator(self): Translator( self.manager, self.get_htmlpage('i18n.js'), self.manager.get_config('locale', Options.locale), ) @QtCore.pyqtSlot(object) def dropped_engine(self, engine): # Update icon in case the engine dropped was syncing self.change_systray_icon() @QtCore.pyqtSlot() def change_systray_icon(self): syncing = False engines = self.manager.get_engines() invalid_credentials = True paused = True offline = True for engine in engines.itervalues(): syncing |= engine.is_syncing() invalid_credentials &= engine.has_invalid_credentials() paused &= engine.is_paused() offline &= engine.is_offline() if offline: new_state = 'stopping' Action(Translator.get('OFFLINE')) elif invalid_credentials: new_state = 'stopping' Action(Translator.get('INVALID_CREDENTIALS')) elif not engines or paused: new_state = 'disabled' Action.finish_action() elif syncing: new_state = 'transferring' else: new_state = 'asleep' Action.finish_action() self.set_icon_state(new_state) def _get_settings_dialog(self, section): from nxdrive.wui.settings import WebSettingsDialog return WebSettingsDialog(self, section) def _get_conflicts_dialog(self, engine): from nxdrive.wui.dialog import WebDialog from nxdrive.wui.conflicts import WebConflictsApi return WebDialog( self, 'conflicts.html', api=WebConflictsApi(self, engine), ) @QtCore.pyqtSlot() def show_conflicts_resolution(self, engine): conflicts = self._get_unique_dialog('conflicts') if conflicts is None: conflicts = self._get_conflicts_dialog(engine) self._create_unique_dialog('conflicts', conflicts) else: conflicts.api.set_engine(engine) self._show_window(conflicts) @QtCore.pyqtSlot() def show_settings(self, section='Accounts'): if section is None: section = 'Accounts' settings = self._get_unique_dialog('settings') if settings is None: settings = self._get_settings_dialog(section) self._create_unique_dialog('settings', settings) else: settings.set_section(section) self._show_window(settings) @QtCore.pyqtSlot() def open_help(self): self.manager.open_help() @QtCore.pyqtSlot() def destroyed_filters_dialog(self): self.filters_dlg = None def _get_filters_dialog(self, engine): from nxdrive.gui.folders_dialog import FiltersDialog return FiltersDialog(self, engine) @QtCore.pyqtSlot() def show_filters(self, engine): if self.filters_dlg is not None: self.filters_dlg.close() self.filters_dlg = None self.filters_dlg = self._get_filters_dialog(engine) self.filters_dlg.destroyed.connect(self.destroyed_filters_dialog) self.filters_dlg.show() def show_file_status(self): from nxdrive.gui.status_dialog import StatusDialog for _, engine in self.manager.get_engines().iteritems(): self.status = StatusDialog(engine.get_dao()) self.status.show() break def show_activities(self): from nxdrive.wui.activity import WebActivityDialog self.activities = WebActivityDialog(self) self.activities.show() @QtCore.pyqtSlot(object) def _connect_engine(self, engine): 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() @QtCore.pyqtSlot() def _debug_toggle_invalid_credentials(self): sender = self.sender() engine = sender.data().toPyObject() engine.set_invalid_credentials(not engine.has_invalid_credentials(), reason='debug') @QtCore.pyqtSlot() def _debug_show_file_status(self): from nxdrive.gui.status_dialog import StatusDialog sender = self.sender() engine = sender.data().toPyObject() self.status_dialog = StatusDialog(engine.get_dao()) self.status_dialog.show() def _create_debug_engine_menu(self, engine, parent): menu = QtGui.QMenu(parent) action = QtGui.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 = QtGui.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): menu.addAction(Translator.get('DEBUG_WINDOW'), self.show_debug_window) for engine in self.manager.get_engines().values(): action = QtGui.QAction(engine.name, menu) action.setMenu(self._create_debug_engine_menu(engine, menu)) action.setData(engine) menu.addAction(action) @QtCore.pyqtSlot() def show_debug_window(self): debug = self._get_unique_dialog('debug') if debug is None: from nxdrive.debug.wui.engine import EngineDialog debug = EngineDialog(self) self._create_unique_dialog('debug', debug) self._show_window(debug) def init_checks(self): if Options.debug: self.show_debug_window() for _, engine in self.manager.get_engines().iteritems(): self._connect_engine(engine) self.manager.newEngine.connect(self._connect_engine) self.manager.notification_service.newNotification.connect(self._new_notification) self.manager.get_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() @QtCore.pyqtSlot() def _update_notification(self): replacements = dict(version=self.manager.get_updater().get_status()[1]) notification = Notification( uuid='AutoUpdate', flags=(Notification.FLAG_BUBBLE | Notification.FLAG_VOLATILE | Notification.FLAG_UNIQUE), title=Translator.get('AUTOUPDATE_NOTIFICATION_TITLE', replacements), description=Translator.get('AUTOUPDATE_NOTIFICATION_MESSAGE', replacements), ) self.manager.notification_service.send_notification(notification) @QtCore.pyqtSlot() def message_clicked(self): if self.current_notification: self.manager.notification_service.trigger_notification(self.current_notification.uid) def _setup_notification_center(self): from nxdrive.osi.darwin.pyNotificationCenter import setup_delegator, NotificationDelegator if self._delegator is None: self._delegator = NotificationDelegator.alloc().init() self._delegator._manager = self.manager setup_delegator(self._delegator) @QtCore.pyqtSlot(object) def _new_notification(self, notif): if not notif.is_bubble(): return if self._delegator is not None: # Use notification center from nxdrive.osi.darwin.pyNotificationCenter import notify return notify( notif.title, None, notif.description, user_info={'uuid': notif.uid}, ) icon = QtGui.QSystemTrayIcon.Information if notif.level == Notification.LEVEL_WARNING: icon = QtGui.QSystemTrayIcon.Warning elif notif.level == Notification.LEVEL_ERROR: icon = QtGui.QSystemTrayIcon.Critical self.current_notification = notif self.tray_icon.showMessage(notif.title, notif.description, icon, 10000) def set_icon_state(self, state): """ 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()) # Handle animated transferring icon if state == 'transferring': self.icon_spin_timer.start(150) else: self.icon_spin_timer.stop() icon = find_icon('nuxeo_drive_systray_icon_%s_18.png' % state) self.tray_icon.setIcon(QtGui.QIcon(icon)) self.icon_state = state return True def spin_transferring_icon(self): icon = find_icon('nuxeo_drive_systray_icon_transferring_%s.png' % (self.icon_spin_count + 1)) self.tray_icon.setIcon(QtGui.QIcon(icon)) self.icon_spin_count = (self.icon_spin_count + 1) % 10 def get_tooltip(self): actions = Action.get_actions() if not actions: return self.default_tooltip # Display only the first action for now for action in actions.itervalues(): if action 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, ) @QtCore.pyqtSlot(str) def app_updated(self, updated_version): self.updated_version = str(updated_version) log.info('Quitting Nuxeo Drive and restarting updated version %s', self.updated_version) self.manager.stopped.connect(self.restart) log.debug('Exiting Qt application') self.quit() @QtCore.pyqtSlot() def restart(self): """ Restart application by loading updated executable into current process. """ current_version = self.manager.get_updater().get_active_version() log.info('Current application version: %s', current_version) log.info('Updated application version: %s', self.updated_version) executable = sys.executable # TODO NXP-13818: better handle this! if sys.platform == 'darwin': executable = executable.replace('python', self.get_mac_app()) log.info('Current executable is: %s', executable) updated_executable = executable.replace(current_version, self.updated_version) log.info('Updated executable is: %s', updated_executable) args = [updated_executable] args.extend(sys.argv[1:]) log.info('Opening subprocess with args: %r', args) subprocess.Popen(args, close_fds=True) @staticmethod def get_mac_app(): return 'ndrive' def show_dialog(self, url): from nxdrive.wui.dialog import WebDialog WebDialog(self, url).show() def show_metadata(self, file_path): self.manager.open_metadata_window(file_path) def setup_systray(self): self.tray_icon = DriveSystrayIcon(self) self.tray_icon.setToolTip(self.manager.app_name) self.set_icon_state('disabled') self.tray_icon.show() def event(self, event): """Handle URL scheme events under OSX""" if hasattr(event, 'url'): url = str(event.url().toString()) try: info = parse_protocol_url(url) log.debug('Event url=%s, info=%r', url, info) if info is not None: log.debug('Received nxdrive URL scheme event: %s', url) if info.get('command') == 'download_edit': # This is a quick operation, no need to fork a QThread self.manager.direct_edit.edit( info['server_url'], info['doc_id'], user=info['user'], download_url=info['download_url'], ) elif info.get('command') == 'edit': # Kept for backward compatibility self.manager.direct_edit.edit( info['server_url'], info['item_id']) except: log.exception('Error handling URL event: %s', url) return super(Application, self).event(event)