class CentralWidget(QWidget, Ui_CentralWidget): NOTIFICATION_EVENTS = [ "backend.connection.changed", "mountpoint.stopped", "mountpoint.remote_error", "mountpoint.unhandled_error", "sharing.updated", "fs.entry.file_update_conflicted", ] connection_state_changed = pyqtSignal(object, object) logout_requested = pyqtSignal() new_notification = pyqtSignal(str, str) def __init__(self, core, jobs_ctx, event_bus, **kwargs): super().__init__(**kwargs) self.setupUi(self) self.jobs_ctx = jobs_ctx self.core = core self.event_bus = event_bus self.menu = MenuWidget(parent=self) self.widget_menu.layout().addWidget(self.menu) for e in self.NOTIFICATION_EVENTS: self.event_bus.connect(e, self.handle_event) self.label_mountpoint.setText(str(self.core.config.mountpoint_base_dir)) self.label_mountpoint.clicked.connect(self.open_mountpoint) self.menu.organization = self.core.device.organization_addr.organization_id self.menu.username = self.core.device.user_id self.menu.device = self.core.device.device_name self.menu.organization_url = str(self.core.device.organization_addr) self.new_notification.connect(self.on_new_notification) self.menu.files_clicked.connect(self.show_mount_widget) self.menu.users_clicked.connect(self.show_users_widget) self.menu.devices_clicked.connect(self.show_devices_widget) self.menu.logout_clicked.connect(self.logout_requested.emit) self.connection_state_changed.connect(self._on_connection_state_changed) self.widget_title2.hide() self.widget_title3.hide() self.title2_icon.apply_style() self.title3_icon.apply_style() self.icon_mountpoint.apply_style() effect = QGraphicsDropShadowEffect(self) effect.setColor(QColor(100, 100, 100)) effect.setBlurRadius(4) effect.setXOffset(-2) effect.setYOffset(2) self.widget_notif.setGraphicsEffect(effect) self.mount_widget = MountWidget(self.core, self.jobs_ctx, self.event_bus, parent=self) self.widget_central.layout().insertWidget(0, self.mount_widget) self.mount_widget.folder_changed.connect(self._on_folder_changed) self.users_widget = UsersWidget(self.core, self.jobs_ctx, self.event_bus, parent=self) self.widget_central.layout().insertWidget(0, self.users_widget) self.devices_widget = DevicesWidget(self.core, self.jobs_ctx, self.event_bus, parent=self) self.widget_central.layout().insertWidget(0, self.devices_widget) self._on_connection_state_changed( self.core.backend_conn.status, self.core.backend_conn.status_exc ) self.show_mount_widget() def _on_folder_changed(self, workspace_name, path): if workspace_name and path: self.widget_title2.show() self.label_title2.setText(workspace_name) self.widget_title3.show() self.label_title3.setText(path) else: self.widget_title2.hide() self.widget_title3.hide() def open_mountpoint(self, path): desktop.open_file(path) def handle_event(self, event, **kwargs): if event == "backend.connection.changed": self.connection_state_changed.emit(kwargs["status"], kwargs["status_exc"]) elif event == "mountpoint.stopped": self.new_notification.emit("WARNING", _("NOTIF_WARN_MOUNTPOINT_UNMOUNTED")) elif event == "mountpoint.remote_error": exc = kwargs["exc"] path = kwargs["path"] if isinstance(exc, FSWorkspaceNoReadAccess): msg = _("NOTIF_WARN_WORKSPACE_READ_ACCESS_LOST_{}").format(path) elif isinstance(exc, FSWorkspaceNoWriteAccess): msg = _("NOTIF_WARN_WORKSPACE_WRITE_ACCESS_LOST_{}").format(path) elif isinstance(exc, FSWorkspaceInMaintenance): msg = _("NOTIF_WARN_WORKSPACE_IN_MAINTENANCE_{}").format(path) else: msg = _("NOTIF_WARN_MOUNTPOINT_REMOTE_ERROR_{}_{}").format(path, str(exc)) self.new_notification.emit("WARNING", msg) elif event == "mountpoint.unhandled_error": exc = kwargs["exc"] path = kwargs["path"] operation = kwargs["operation"] self.new_notification.emit( "ERROR", _("NOTIF_ERR_MOUNTPOINT_UNEXPECTED_ERROR_{}_{}_{}").format( operation, path, str(exc) ), ) elif event == "sharing.updated": new_entry = kwargs["new_entry"] previous_entry = kwargs["previous_entry"] new_role = getattr(new_entry, "role", None) previous_role = getattr(previous_entry, "role", None) if new_role is not None and previous_role is None: self.new_notification.emit( "INFO", _("NOTIF_INFO_WORKSPACE_SHARED_{}").format(new_entry.name) ) elif new_role is not None and previous_role is not None: self.new_notification.emit( "INFO", _("NOTIF_INFO_WORKSPACE_ROLE_UPDATED_{}").format(new_entry.name) ) elif new_role is None and previous_role is not None: self.new_notification.emit( "INFO", _("NOTIF_INFO_WORKSPACE_UNSHARED_{}").format(previous_entry.name) ) elif event == "fs.entry.file_update_conflicted": self.new_notification.emit( "WARNING", _("NOTIF_WARN_SYNC_CONFLICT_{}").format(kwargs["path"]) ) def _on_connection_state_changed(self, status, status_exc): text = None icon = None tooltip = None notif = None if status in (BackendConnStatus.READY, BackendConnStatus.INITIALIZING): tooltip = text = _("TEXT_BACKEND_STATE_CONNECTED") icon = QPixmap(":/icons/images/material/cloud_queue.svg") elif status == BackendConnStatus.LOST: tooltip = text = _("TEXT_BACKEND_STATE_DISCONNECTED") icon = QPixmap(":/icons/images/material/cloud_off.svg") elif status == BackendConnStatus.REFUSED: cause = status_exc.__cause__ if isinstance(cause, HandshakeAPIVersionError): tooltip = _("TEXT_BACKEND_STATE_API_MISMATCH_versions").format( versions=", ".join([v.version for v in cause.backend_versions]) ) elif isinstance(cause, HandshakeRevokedDevice): tooltip = _("TEXT_BACKEND_STATE_REVOKED_DEVICE") elif isinstance(cause, HandshakeOrganizationExpired): tooltip = _("TEXT_BACKEND_STATE_ORGANIZATION_EXPIRED") else: tooltip = _("TEXT_BACKEND_STATE_UNKNOWN") text = _("TEXT_BACKEND_STATE_DISCONNECTED") icon = QPixmap(":/icons/images/material/cloud_off.svg") notif = ("WARNING", tooltip) elif status == BackendConnStatus.CRASHED: text = _("TEXT_BACKEND_STATE_DISCONNECTED") tooltip = _("TEXT_BACKEND_STATE_CRASHED_cause").format(cause=str(status_exc.__cause__)) icon = QPixmap(":/icons/images/material/cloud_off.svg") notif = ("ERROR", tooltip) self.menu.set_connection_state(text, tooltip, icon) if notif: self.new_notification.emit(*notif) def on_new_notification(self, notif_type, msg): pass def show_mount_widget(self): self.clear_widgets() self.menu.activate_files() self.label_title.setText(_("ACTION_MENU_DOCUMENTS")) self.mount_widget.show() self.mount_widget.show_workspaces_widget() def show_users_widget(self): self.clear_widgets() self.menu.activate_users() self.label_title.setText(_("ACTION_MENU_USERS")) self.users_widget.show() def show_devices_widget(self): self.clear_widgets() self.menu.activate_devices() self.label_title.setText(_("ACTION_MENU_DEVICES")) self.devices_widget.show() def clear_widgets(self): self.widget_title2.hide() self.widget_title3.hide() self.users_widget.hide() self.mount_widget.hide() self.devices_widget.hide()
class CentralWidget(QWidget, Ui_CentralWidget): # type: ignore[misc] NOTIFICATION_EVENTS = [ CoreEvent.BACKEND_CONNECTION_CHANGED, CoreEvent.MOUNTPOINT_STOPPED, CoreEvent.MOUNTPOINT_REMOTE_ERROR, CoreEvent.MOUNTPOINT_UNHANDLED_ERROR, CoreEvent.MOUNTPOINT_TRIO_DEADLOCK_ERROR, CoreEvent.MOUNTPOINT_READONLY, CoreEvent.SHARING_UPDATED, ] organization_stats_success = pyqtSignal(QtToTrioJob) organization_stats_error = pyqtSignal(QtToTrioJob) connection_state_changed = pyqtSignal(object, object) logout_requested = pyqtSignal() new_notification = pyqtSignal(str, str) REFRESH_ORGANIZATION_STATS_DELAY = 5 # 5s def __init__( self, core: LoggedCore, jobs_ctx: QtToTrioJobScheduler, event_bus: EventBus, systray_notification: pyqtBoundSignal, file_link_addr: Optional[BackendOrganizationFileLinkAddr] = None, parent: Optional[QWidget] = None, ): super().__init__(parent=parent) self.setupUi(self) self.jobs_ctx = jobs_ctx self.core = core self.event_bus = event_bus self.systray_notification = systray_notification self.last_notification = 0.0 self.desync_notified = False self.menu = MenuWidget(parent=self) self.widget_menu.layout().addWidget(self.menu) for e in self.NOTIFICATION_EVENTS: self.event_bus.connect(e, cast(EventCallback, self.handle_event)) self.event_bus.connect(CoreEvent.FS_ENTRY_SYNCED, self._on_vlobs_updated) self.event_bus.connect(CoreEvent.BACKEND_REALM_VLOBS_UPDATED, self._on_vlobs_updated) self.set_user_info() menu = QMenu() if self.core.device.is_admin and is_saas_addr(self.core.device.organization_addr): update_sub_act = menu.addAction(_("ACTION_UPDATE_SUBSCRIPTION")) update_sub_act.triggered.connect(self._on_update_subscription_clicked) copy_backend_addr_act = menu.addAction(_("ACTION_COPY_BACKEND_ADDR")) copy_backend_addr_act.triggered.connect(self._on_copy_backend_addr) menu.addSeparator() change_auth_act = menu.addAction(_("ACTION_DEVICE_MENU_CHANGE_AUTHENTICATION")) change_auth_act.triggered.connect(self.change_authentication) menu.addSeparator() log_out_act = menu.addAction(_("ACTION_LOG_OUT")) log_out_act.triggered.connect(self.logout_requested.emit) self.button_user.setMenu(menu) pix = Pixmap(":/icons/images/material/person.svg") pix.replace_color(QColor(0, 0, 0), QColor(0x00, 0x92, 0xFF)) self.button_user.setIcon(QIcon(pix)) self.button_user.clicked.connect(self._show_user_menu) self.new_notification.connect(self.on_new_notification) self.menu.button_enrollment.setVisible( self.core.device.is_admin and is_pki_enrollment_available() ) if self.core.device.is_outsider: self.menu.button_users.hide() self.menu.files_clicked.connect(self.show_mount_widget) self.menu.users_clicked.connect(self.show_users_widget) self.menu.devices_clicked.connect(self.show_devices_widget) self.menu.enrollment_clicked.connect(self.show_enrollment_widget) self.connection_state_changed.connect(self._on_connection_state_changed) self.navigation_bar_widget.clear() self.navigation_bar_widget.route_clicked.connect(self._on_route_clicked) effect = QGraphicsDropShadowEffect(self) effect.setColor(QColor(100, 100, 100)) effect.setBlurRadius(4) effect.setXOffset(-2) effect.setYOffset(2) self.widget_notif.setGraphicsEffect(effect) self.mount_widget = MountWidget(self.core, self.jobs_ctx, self.event_bus, parent=self) self.widget_central.layout().insertWidget(0, self.mount_widget) self.mount_widget.folder_changed.connect(self._on_folder_changed) self.organization_stats_success.connect(self._on_organization_stats_success) self.organization_stats_error.connect(self._on_organization_stats_error) self.users_widget = UsersWidget(self.core, self.jobs_ctx, self.event_bus, parent=self) self.users_widget.filter_shared_workspaces_request.connect(self.show_mount_widget) self.widget_central.layout().insertWidget(0, self.users_widget) self.devices_widget = DevicesWidget(self.core, self.jobs_ctx, self.event_bus, parent=self) self.widget_central.layout().insertWidget(0, self.devices_widget) self.enrollment_widget = EnrollmentWidget( self.core, self.jobs_ctx, self.event_bus, parent=self ) self.widget_central.layout().insertWidget(0, self.enrollment_widget) self._on_connection_state_changed( self.core.backend_status, self.core.backend_status_exc, allow_systray=False ) if file_link_addr is not None: try: self.go_to_file_link(file_link_addr) except GoToFileLinkBadWorkspaceIDError: show_error( self, _("TEXT_FILE_LINK_WORKSPACE_NOT_FOUND_organization").format( organization=file_link_addr.organization_id ), ) self.show_mount_widget() except GoToFileLinkPathDecryptionError: show_error(self, _("TEXT_INVALID_URL")) self.show_mount_widget() except GoToFileLinkBadOrganizationIDError: show_error( self, _("TEXT_FILE_LINK_NOT_IN_ORG_organization").format( organization=file_link_addr.organization_id ), ) self.show_mount_widget() else: self.show_mount_widget() def _show_user_menu(self) -> None: self.button_user.showMenu() def _on_update_subscription_clicked(self) -> None: desktop.open_url(_("TEXT_SAAS_UPDATE_SUBSCRIPTION_URL")) def _on_copy_backend_addr(self) -> None: desktop.copy_to_clipboard(self.core.device.organization_addr.to_url()) SnackbarManager.inform(_("TEXT_BACKEND_ADDR_COPIED_TO_CLIPBOARD")) def set_user_info(self) -> None: org = self.core.device.organization_id username = self.core.device.short_user_display user_text = f"{org}\n{username}" self.button_user.setText(user_text) self.button_user.setToolTip(self.core.device.organization_addr.to_url()) async def change_authentication(self) -> None: await AuthenticationChangeWidget.show_modal( core=self.core, jobs_ctx=self.jobs_ctx, parent=self, on_finished=None ) def _on_route_clicked(self, path: FsPath) -> None: self.mount_widget.load_path(path) def _on_folder_changed(self, workspace_name: Optional[EntryName], path: Optional[str]) -> None: if workspace_name and path: self.navigation_bar_widget.from_path(workspace_name, path) else: self.navigation_bar_widget.clear() def handle_event(self, event: CoreEvent, **kwargs: object) -> None: if event == CoreEvent.BACKEND_CONNECTION_CHANGED: assert isinstance(kwargs["status"], BackendConnStatus) assert kwargs["status_exc"] is None or isinstance(kwargs["status_exc"], Exception) self.connection_state_changed.emit(kwargs["status"], kwargs["status_exc"]) elif event == CoreEvent.MOUNTPOINT_READONLY: current_time = time.time() if self.last_notification == 0.0 or current_time >= self.last_notification + 3: # 3s self.systray_notification.emit( "", _("TEXT_NOTIF_INFO_WORKSPACE_READ_ONLY"), 3000 ) # 3000ms self.last_notification = current_time elif event == CoreEvent.MOUNTPOINT_STOPPED: pass # Not taking this event into account since we cannot yet distinguish # whether the workspace has been unmounted by the user or not. # self.new_notification.emit("INFO", _("NOTIF_WARN_MOUNTPOINT_UNMOUNTED")) elif event == CoreEvent.MOUNTPOINT_REMOTE_ERROR: assert isinstance(kwargs["exc"], Exception) assert isinstance(kwargs["mountpoint"], PurePath) assert isinstance(kwargs["path"], FsPath) exc = kwargs["exc"] mountpoint = kwargs["mountpoint"] if isinstance(exc, FSWorkspaceNoReadAccess): msg = _("TEXT_NOTIF_WARN_WORKSPACE_READ_ACCESS_LOST_workspace").format( workspace=str(mountpoint) ) elif isinstance(exc, FSWorkspaceNoWriteAccess): msg = _("TEXT_NOTIF_WARN_WORKSPACE_WRITE_ACCESS_LOST_workspace").format( workspace=str(mountpoint) ) elif isinstance(exc, FSWorkspaceInMaintenance): msg = _("TEXT_NOTIF_WARN_WORKSPACE_IN_MAINTENANCE_workspace").format( workspace=str(mountpoint) ) else: msg = _("TEXT_NOTIF_WARN_MOUNTPOINT_REMOTE_ERROR_workspace-error").format( workspace=str(mountpoint), error=str(exc) ) self.new_notification.emit("WARN", msg) elif event in ( CoreEvent.MOUNTPOINT_UNHANDLED_ERROR, CoreEvent.MOUNTPOINT_TRIO_DEADLOCK_ERROR, ): assert isinstance(kwargs["exc"], Exception) assert isinstance(kwargs["operation"], str) assert isinstance(kwargs["mountpoint"], PurePath) assert isinstance(kwargs["path"], FsPath) exc = kwargs["exc"] self.new_notification.emit( "WARN", _("TEXT_NOTIF_ERR_MOUNTPOINT_UNEXPECTED_ERROR_workspace_operation_error").format( operation=kwargs["operation"], workspace=str(kwargs["mountpoint"]), error=str(kwargs["exc"]), ), ) elif event == CoreEvent.SHARING_UPDATED: assert isinstance(kwargs["new_entry"], WorkspaceEntry) assert kwargs["previous_entry"] is None or isinstance( kwargs["previous_entry"], WorkspaceEntry ) new_entry: WorkspaceEntry = kwargs["new_entry"] previous_entry: Optional[WorkspaceEntry] = kwargs["previous_entry"] new_role = new_entry.role previous_role = previous_entry.role if previous_entry is not None else None if new_role is not None and previous_role is None: self.new_notification.emit( "INFO", _("TEXT_NOTIF_INFO_WORKSPACE_SHARED_workspace").format( workspace=new_entry.name ), ) elif new_role is not None and previous_role is not None and new_role != previous_role: self.new_notification.emit( "INFO", _("TEXT_NOTIF_INFO_WORKSPACE_ROLE_UPDATED_workspace").format( workspace=new_entry.name ), ) elif new_role is None and previous_role is not None: name = previous_entry.name # type: ignore self.new_notification.emit( "INFO", _("TEXT_NOTIF_INFO_WORKSPACE_UNSHARED_workspace").format(workspace=name) ) def _load_organization_stats(self, delay: float = 0) -> None: self.jobs_ctx.submit_throttled_job( "central_widget.load_organization_stats", delay, self.organization_stats_success, self.organization_stats_error, _do_get_organization_stats, core=self.core, ) def _on_vlobs_updated(self, *args: object, **kwargs: object) -> None: self._load_organization_stats(delay=self.REFRESH_ORGANIZATION_STATS_DELAY) def _on_connection_state_changed( self, status: BackendConnStatus, status_exc: Optional[Exception], allow_systray: bool = True ) -> None: text = None icon = None tooltip = None notif = None disconnected = None self.menu.label_organization_name.hide() self.menu.label_organization_size.clear() if status in (BackendConnStatus.READY, BackendConnStatus.INITIALIZING): if status == BackendConnStatus.READY and self.core.device.is_admin: self._load_organization_stats() tooltip = text = _("TEXT_BACKEND_STATE_CONNECTED") icon = QPixmap(":/icons/images/material/cloud_queue.svg") elif status == BackendConnStatus.LOST: tooltip = text = _("TEXT_BACKEND_STATE_DISCONNECTED") icon = QPixmap(":/icons/images/material/cloud_off.svg") disconnected = True elif status == BackendConnStatus.REFUSED: disconnected = True text = _("TEXT_BACKEND_STATE_DISCONNECTED") icon = QPixmap(":/icons/images/material/cloud_off.svg") assert isinstance(status_exc, Exception) cause = status_exc.__cause__ if isinstance(cause, HandshakeAPIVersionError): tooltip = text = _("TEXT_BACKEND_STATE_API_MISMATCH_versions").format( versions=", ".join([str(v.version) for v in cause.backend_versions]) ) elif isinstance(cause, HandshakeRevokedDevice): tooltip = _("TEXT_BACKEND_STATE_REVOKED_DEVICE") notif = ("WARN", tooltip) elif isinstance(cause, HandshakeOrganizationExpired): tooltip = _("TEXT_BACKEND_STATE_ORGANIZATION_EXPIRED") notif = ("WARN", tooltip) else: tooltip = _("TEXT_BACKEND_STATE_UNKNOWN") notif = ("WARN", tooltip) elif status == BackendConnStatus.CRASHED: assert isinstance(status_exc, Exception) text = _("TEXT_BACKEND_STATE_DISCONNECTED") tooltip = _("TEXT_BACKEND_STATE_CRASHED_cause").format(cause=str(status_exc.__cause__)) icon = QPixmap(":/icons/images/material/cloud_off.svg") notif = ("WARN", tooltip) disconnected = True elif status == BackendConnStatus.DESYNC: assert isinstance(status_exc, Exception) text = _("TEXT_BACKEND_STATE_DISCONNECTED") tooltip = _("TEXT_BACKEND_STATE_DESYNC") icon = QPixmap(":/icons/images/material/cloud_off.svg") notif = None disconnected = False # The disconnection for being out-of-sync with the backend # is only shown once per login. This is useful in the case # of backends with API version 2.3 and older as it's going # to successfully connect every 10 seconds before being # thrown off by the sync monitor. if not self.desync_notified: self.desync_notified = True notif = ("WARN", tooltip) disconnected = True self.menu.set_connection_state(text, tooltip, icon) if notif: self.new_notification.emit(*notif) if allow_systray and disconnected: self.systray_notification.emit( "Parsec", _("TEXT_SYSTRAY_BACKEND_DISCONNECT_organization").format( organization=self.core.device.organization_id ), 5000, ) def _on_organization_stats_success(self, job: QtToTrioJob) -> None: assert job.is_finished() assert job.status == "ok" organization_stats = job.ret self.menu.show_organization_stats( organization_id=self.core.device.organization_id, organization_stats=organization_stats ) def _on_organization_stats_error(self, job: QtToTrioJob) -> None: assert job.is_finished() assert job.status != "ok" self.menu.label_organization_name.hide() self.menu.label_organization_size.clear() def on_new_notification(self, notif_type: str, msg: str) -> None: if notif_type == "CONGRATULATE": SnackbarManager.congratulate(msg) elif notif_type == "WARN": SnackbarManager.warn(msg) else: SnackbarManager.inform(msg) def go_to_file_link(self, addr: BackendOrganizationFileLinkAddr, mount: bool = True) -> None: """ Raises: GoToFileLinkBadOrganizationIDError GoToFileLinkBadWorkspaceIDError GoToFileLinkPathDecryptionError """ if addr.organization_id != self.core.device.organization_id: raise GoToFileLinkBadOrganizationIDError try: workspace = self.core.user_fs.get_workspace(addr.workspace_id) except FSWorkspaceNotFoundError as exc: raise GoToFileLinkBadWorkspaceIDError from exc try: path = workspace.decrypt_file_link_path(addr) except ValueError as exc: raise GoToFileLinkPathDecryptionError from exc self.show_mount_widget() self.mount_widget.show_files_widget(workspace, path, selected=True, mount_it=mount) def show_mount_widget(self, user_info: Optional[UserInfo] = None) -> None: self.clear_widgets() self.menu.activate_files() self.label_title.setText(_("ACTION_MENU_DOCUMENTS")) if user_info is not None: self.mount_widget.workspaces_widget.set_user_info(user_info) self.mount_widget.show() self.mount_widget.show_workspaces_widget() def show_users_widget(self) -> None: self.clear_widgets() self.menu.activate_users() self.label_title.setText(_("ACTION_MENU_USERS")) self.users_widget.show() def show_devices_widget(self) -> None: self.clear_widgets() self.menu.activate_devices() self.label_title.setText(_("ACTION_MENU_DEVICES")) self.devices_widget.show() def show_enrollment_widget(self) -> None: self.clear_widgets() self.menu.activate_enrollment() self.label_title.setText(_("ACTION_MENU_ENROLLMENT")) self.enrollment_widget.show() def clear_widgets(self) -> None: self.navigation_bar_widget.clear() self.users_widget.hide() self.mount_widget.hide() self.devices_widget.hide() self.enrollment_widget.hide()
class CentralWidget(QWidget, Ui_CentralWidget): NOTIFICATION_EVENTS = [ CoreEvent.BACKEND_CONNECTION_CHANGED, CoreEvent.MOUNTPOINT_STOPPED, CoreEvent.MOUNTPOINT_REMOTE_ERROR, CoreEvent.MOUNTPOINT_UNHANDLED_ERROR, CoreEvent.SHARING_UPDATED, CoreEvent.FS_ENTRY_FILE_UPDATE_CONFLICTED, ] organization_stats_success = pyqtSignal(QtToTrioJob) organization_stats_error = pyqtSignal(QtToTrioJob) connection_state_changed = pyqtSignal(object, object) vlobs_updated_qt = pyqtSignal(object, object) logout_requested = pyqtSignal() new_notification = pyqtSignal(str, str) RESET_TIMER_STATS = 5000 # ms def __init__(self, core, jobs_ctx, event_bus, systray_notification, action_addr=None, **kwargs): super().__init__(**kwargs) self.setupUi(self) self.jobs_ctx = jobs_ctx self.core = core self.event_bus = event_bus self.systray_notification = systray_notification self.menu = MenuWidget(parent=self) self.widget_menu.layout().addWidget(self.menu) for e in self.NOTIFICATION_EVENTS: self.event_bus.connect(e, self.handle_event) self.event_bus.connect(CoreEvent.FS_ENTRY_SYNCED, self._on_vlobs_updated_trio) self.event_bus.connect(CoreEvent.BACKEND_REALM_VLOBS_UPDATED, self._on_vlobs_updated_trio) self.vlobs_updated_qt.connect(self._on_vlobs_updated_qt) self.organization_stats_timer = QTimer() self.organization_stats_timer.setInterval(self.RESET_TIMER_STATS) self.organization_stats_timer.setSingleShot(True) self.organization_stats_timer.timeout.connect( self._get_organization_stats) self.set_user_info() menu = QMenu() log_out_act = menu.addAction(_("ACTION_LOG_OUT")) log_out_act.triggered.connect(self.logout_requested.emit) self.button_user.setMenu(menu) pix = Pixmap(":/icons/images/material/person.svg") pix.replace_color(QColor(0, 0, 0), QColor(0x00, 0x92, 0xFF)) self.button_user.setIcon(QIcon(pix)) self.button_user.clicked.connect(self._show_user_menu) self.new_notification.connect(self.on_new_notification) self.menu.files_clicked.connect(self.show_mount_widget) self.menu.users_clicked.connect(self.show_users_widget) self.menu.devices_clicked.connect(self.show_devices_widget) self.connection_state_changed.connect( self._on_connection_state_changed) self.widget_title2.hide() self.icon_title3.hide() self.label_title3.setText("") self.icon_title3.apply_style() self.icon_title3.apply_style() effect = QGraphicsDropShadowEffect(self) effect.setColor(QColor(100, 100, 100)) effect.setBlurRadius(4) effect.setXOffset(-2) effect.setYOffset(2) self.widget_notif.setGraphicsEffect(effect) self.mount_widget = MountWidget(self.core, self.jobs_ctx, self.event_bus, parent=self) self.widget_central.layout().insertWidget(0, self.mount_widget) self.mount_widget.folder_changed.connect(self._on_folder_changed) self.organization_stats_success.connect( self._on_organization_stats_success) self.organization_stats_error.connect( self._on_organization_stats_error) self.users_widget = UsersWidget(self.core, self.jobs_ctx, self.event_bus, parent=self) self.users_widget.filter_shared_workspaces_request.connect( self.show_mount_widget) self.widget_central.layout().insertWidget(0, self.users_widget) self.devices_widget = DevicesWidget(self.core, self.jobs_ctx, self.event_bus, parent=self) self.widget_central.layout().insertWidget(0, self.devices_widget) self._on_connection_state_changed(self.core.backend_status, self.core.backend_status_exc, allow_systray=False) if action_addr is not None: try: self.go_to_file_link(action_addr.workspace_id, action_addr.path) except FSWorkspaceNotFoundError: show_error( self, _("TEXT_FILE_LINK_WORKSPACE_NOT_FOUND_organization"). format(organization=action_addr.organization_id), ) self.show_mount_widget() else: self.show_mount_widget() def _show_user_menu(self): self.button_user.showMenu() def set_user_info(self): org = self.core.device.organization_id username = self.core.device.short_user_display user_text = f"{org}\n{username}" self.button_user.setText(user_text) self.button_user.setToolTip( self.core.device.organization_addr.to_url()) def _on_folder_changed(self, workspace_name, path): if workspace_name and path: self.widget_title2.show() self.label_title2.setText(workspace_name) self.icon_title3.show() self.label_title3.setText(path) else: self.widget_title2.hide() self.icon_title3.hide() self.label_title3.setText("") def handle_event(self, event, **kwargs): if event == CoreEvent.BACKEND_CONNECTION_CHANGED: self.connection_state_changed.emit(kwargs["status"], kwargs["status_exc"]) elif event == CoreEvent.MOUNTPOINT_STOPPED: self.new_notification.emit("WARNING", _("NOTIF_WARN_MOUNTPOINT_UNMOUNTED")) elif event == CoreEvent.MOUNTPOINT_REMOTE_ERROR: exc = kwargs["exc"] path = kwargs["path"] if isinstance(exc, FSWorkspaceNoReadAccess): msg = _("NOTIF_WARN_WORKSPACE_READ_ACCESS_LOST_{}").format( path) elif isinstance(exc, FSWorkspaceNoWriteAccess): msg = _("NOTIF_WARN_WORKSPACE_WRITE_ACCESS_LOST_{}").format( path) elif isinstance(exc, FSWorkspaceInMaintenance): msg = _("NOTIF_WARN_WORKSPACE_IN_MAINTENANCE_{}").format(path) else: msg = _("NOTIF_WARN_MOUNTPOINT_REMOTE_ERROR_{}_{}").format( path, str(exc)) self.new_notification.emit("WARNING", msg) elif event == CoreEvent.MOUNTPOINT_UNHANDLED_ERROR: exc = kwargs["exc"] path = kwargs["path"] operation = kwargs["operation"] self.new_notification.emit( "ERROR", _("NOTIF_ERR_MOUNTPOINT_UNEXPECTED_ERROR_{}_{}_{}").format( operation, path, str(exc)), ) elif event == CoreEvent.SHARING_UPDATED: new_entry = kwargs["new_entry"] previous_entry = kwargs["previous_entry"] new_role = getattr(new_entry, "role", None) previous_role = getattr(previous_entry, "role", None) if new_role is not None and previous_role is None: self.new_notification.emit( "INFO", _("NOTIF_INFO_WORKSPACE_SHARED_{}").format(new_entry.name)) elif new_role is not None and previous_role is not None: self.new_notification.emit( "INFO", _("NOTIF_INFO_WORKSPACE_ROLE_UPDATED_{}").format( new_entry.name)) elif new_role is None and previous_role is not None: self.new_notification.emit( "INFO", _("NOTIF_INFO_WORKSPACE_UNSHARED_{}").format( previous_entry.name)) elif event == CoreEvent.FS_ENTRY_FILE_UPDATE_CONFLICTED: self.new_notification.emit( "WARNING", _("NOTIF_WARN_SYNC_CONFLICT_{}").format(kwargs["path"])) def _get_organization_stats(self): self.jobs_ctx.submit_job( ThreadSafeQtSignal(self, "organization_stats_success", QtToTrioJob), ThreadSafeQtSignal(self, "organization_stats_error", QtToTrioJob), _do_get_organization_stats, core=self.core, ) def _on_vlobs_updated_trio(self, event, workspace_id=None, id=None, *args, **kwargs): self.vlobs_updated_qt.emit(event, id) def _on_vlobs_updated_qt(self, event, uuid): if not self.organization_stats_timer.isActive(): self.organization_stats_timer.start() self._get_organization_stats() def _on_connection_state_changed(self, status, status_exc, allow_systray=True): text = None icon = None tooltip = None notif = None disconnected = None self.menu.label_organization_name.hide() self.menu.label_organization_size.clear() if status in (BackendConnStatus.READY, BackendConnStatus.INITIALIZING): if status == BackendConnStatus.READY and self.core.device.is_admin: self._get_organization_stats() tooltip = text = _("TEXT_BACKEND_STATE_CONNECTED") icon = QPixmap(":/icons/images/material/cloud_queue.svg") elif status == BackendConnStatus.LOST: tooltip = text = _("TEXT_BACKEND_STATE_DISCONNECTED") icon = QPixmap(":/icons/images/material/cloud_off.svg") disconnected = True elif status == BackendConnStatus.REFUSED: disconnected = True cause = status_exc.__cause__ if isinstance(cause, HandshakeAPIVersionError): tooltip = _("TEXT_BACKEND_STATE_API_MISMATCH_versions").format( versions=", ".join( [str(v.version) for v in cause.backend_versions])) elif isinstance(cause, HandshakeRevokedDevice): tooltip = _("TEXT_BACKEND_STATE_REVOKED_DEVICE") notif = ("REVOKED", tooltip) self.new_notification.emit(*notif) elif isinstance(cause, HandshakeOrganizationExpired): tooltip = _("TEXT_BACKEND_STATE_ORGANIZATION_EXPIRED") notif = ("EXPIRED", tooltip) self.new_notification.emit(*notif) else: tooltip = _("TEXT_BACKEND_STATE_UNKNOWN") text = _("TEXT_BACKEND_STATE_DISCONNECTED") icon = QPixmap(":/icons/images/material/cloud_off.svg") notif = ("WARNING", tooltip) elif status == BackendConnStatus.CRASHED: text = _("TEXT_BACKEND_STATE_DISCONNECTED") tooltip = _("TEXT_BACKEND_STATE_CRASHED_cause").format( cause=str(status_exc.__cause__)) icon = QPixmap(":/icons/images/material/cloud_off.svg") notif = ("ERROR", tooltip) disconnected = True self.menu.set_connection_state(text, tooltip, icon) if notif: self.new_notification.emit(*notif) if allow_systray and disconnected: self.systray_notification.emit( "Parsec", _("TEXT_SYSTRAY_BACKEND_DISCONNECT_organization").format( organization=self.core.device.organization_id), 5000, ) def _on_organization_stats_success(self, job): assert job.is_finished() assert job.status == "ok" organization_stats = job.ret self.menu.show_organization_stats( organization_id=self.core.device.organization_id, organization_stats=organization_stats) def _on_organization_stats_error(self, job): assert job.is_finished() assert job.status != "ok" self.menu.label_organization_name.hide() self.menu.label_organization_size.clear() def on_new_notification(self, notif_type, msg): if notif_type in ["REVOKED", "EXPIRED"]: show_error(self, msg) def go_to_file_link(self, workspace_id, path, mount=False): self.show_mount_widget() self.mount_widget.show_files_widget( self.core.user_fs.get_workspace(workspace_id), path, selected=True, mount_it=True) def show_mount_widget(self, user_info=None): self.clear_widgets() self.menu.activate_files() self.label_title.setText(_("ACTION_MENU_DOCUMENTS")) if user_info is not None: self.mount_widget.workspaces_widget.set_user_info(user_info) self.mount_widget.show() self.mount_widget.show_workspaces_widget(user_info=user_info) def show_users_widget(self): self.clear_widgets() self.menu.activate_users() self.label_title.setText(_("ACTION_MENU_USERS")) self.users_widget.show() def show_devices_widget(self): self.clear_widgets() self.menu.activate_devices() self.label_title.setText(_("ACTION_MENU_DEVICES")) self.devices_widget.show() def clear_widgets(self): self.widget_title2.hide() self.icon_title3.hide() self.label_title3.setText("") self.users_widget.hide() self.mount_widget.hide() self.devices_widget.hide()