Пример #1
0
class MovableSlider(DoubleSlider, MovableWidget):
    def __init__(self, name, minSlider, maxSlider, stepSlider, label,
                 shortcutPlusKey, shortcutMinusKey, startValue, **kwargs):
        MovableWidget.__init__(self, name, label)
        DoubleSlider.__init__(self, Qt.Horizontal, name=name)

        self.parameter = kwargs.get('parameter', None)
        self.module = kwargs.get('module', None)

        self.minSlider = minSlider
        self.maxSlider = maxSlider
        self.stepSlider = stepSlider
        self.label = label

        self.shortcutPlus = QShortcut(self)
        self.shortcutPlus.setKey(shortcutPlusKey)
        self.shortcutPlus.activated.connect(
            lambda: self.setValue(self.value + float(self.stepSlider)))
        self.shortcutMinus = QShortcut(self)
        self.shortcutMinus.setKey(shortcutMinusKey)
        self.shortcutMinus.activated.connect(
            lambda: self.setValue(self.value - float(self.stepSlider)))

        self.startValue = startValue
        self.setTracking(False)

        self.updateData()

    def getData(self):
        data = dict()

        data['widgetType'] = 'Slider'
        data['name'] = self.widgetName
        data['minSlider'] = self.minSlider
        data['maxSlider'] = self.maxSlider
        data['stepSlider'] = self.stepSlider
        data['module'] = self.module
        data['parameter'] = self.parameter
        data['shortcutPlus'] = self.shortcutPlus.key().toString()
        data['shortcutMinus'] = self.shortcutMinus.key().toString()

        return data

    def updateData(self):
        self.setMinimum(float(self.minSlider))
        self.setMaximum(float(self.maxSlider))
        self.setTickInterval(float(self.stepSlider))
        self.setPageStep(1)
        self.setSingleStep(1)
        self.setValue(float(self.startValue))
        self.label.setText(self.widgetName + ': ' + str(self.startValue))
Пример #2
0
class Shortcut(QObject):
    def __init__(self, id, name, shortcut_key, callback, widget):
        self._id = id
        self._name = name
        self._shortcut = QShortcut(QKeySequence(shortcut_key), widget)
        self._shortcut.activated.connect(callback)
        self._callback = callback

    def id(self):
        return self._id

    def name(self):
        return self._name

    def setName(self, name):
        self._name = name

    def shortcut(self):
        return self._shortcut

    def key(self):
        return self._shortcut.key().toString()

    def setKey(self, key):
        self._shortcut.setKey(QKeySequence(key))

    def setShortcut(self, shortcut):
        self._shortcut = shortcut

    def callback(self):
        return self._callback
Пример #3
0
    def defineShortcuts(self):
        self.shortcuts = []
        for x in EnumCamModes:
            hotkey = QShortcut(QKeySequence(Qt.Key_F1 + int(x)), self)
            self.shortcuts.append(hotkey)

            key = (hotkey.key().toString())

            if key == 'F1':
                self.shortcuts[0].activated.connect(lambda: self.setCamera(
                    int(EnumCamModes.CAM_MODE_MATCH_UP)))
            elif key == 'F2':
                self.shortcuts[1].activated.connect(lambda: self.setCamera(
                    int(EnumCamModes.CAM_MODE_MATCH_SIDE)))
            elif key == 'F3':
                self.shortcuts[2].activated.connect(lambda: self.setCamera(
                    int(EnumCamModes.CAM_MODE_BROADCASTER)))
            elif key == 'F4':
                self.shortcuts[3].activated.connect(
                    lambda: self.setCamera(int(EnumCamModes.CAM_MODE_CROWD)))
            elif key == 'F5':
                self.shortcuts[4].activated.connect(
                    lambda: self.setCamera(int(EnumCamModes.CAM_MODE_FREE)))
            elif key == 'F6':
                self.shortcuts[5].activated.connect(
                    lambda: self.setCamera(int(EnumCamModes.CAM_MODE_LOGO)))
Пример #4
0
class MovablePushButton(QPushButton, MovableWidget):
    def __init__(self, name, valueOn, shortcutKey, **kwargs):
        MovableWidget.__init__(self, name)
        QPushButton.__init__(self, name=name)
        self.valueOn = valueOn

        self.parameter = kwargs.get('parameter', None)
        self.module = kwargs.get('module', None)

        self.shortcut = QShortcut(self)
        self.shortcut.setKey(shortcutKey)
        self.shortcut.setAutoRepeat(False)
        self.shortcut.activated.connect(lambda: self.animateClick())

        self.updateData()

    def getData(self):
        data = dict()

        data['widgetType'] = 'PushButton'
        data['name'] = self.widgetName
        data['valueOn'] = self.valueOn
        data['module'] = self.module
        data['parameter'] = self.parameter
        data['shortcut'] = self.shortcut.key().toString()

        return data

    def updateData(self):
        self.setText(self.widgetName + '\n' + self.valueOn)
Пример #5
0
class MainWindow(QMainWindow, Ui_MainWindow):
    foreground_needed = pyqtSignal()
    new_instance_needed = pyqtSignal(object)
    systray_notification = pyqtSignal(str, str, int)

    TAB_NOTIFICATION_COLOR = QColor(46, 146, 208)
    TAB_NOT_SELECTED_COLOR = QColor(123, 132, 163)
    TAB_SELECTED_COLOR = QColor(12, 65, 159)

    def __init__(self,
                 jobs_ctx,
                 event_bus,
                 config,
                 minimize_on_close: bool = False,
                 **kwargs):
        super().__init__(**kwargs)
        self.setupUi(self)

        self.setMenuBar(None)
        self.jobs_ctx = jobs_ctx
        self.event_bus = event_bus
        self.config = config
        self.minimize_on_close = minimize_on_close
        # Explain only once that the app stays in background
        self.minimize_on_close_notif_already_send = False
        self.force_close = False
        self.need_close = False
        self.event_bus.connect(CoreEvent.GUI_CONFIG_CHANGED,
                               self.on_config_updated)
        self.setWindowTitle(
            _("TEXT_PARSEC_WINDOW_TITLE_version").format(
                version=PARSEC_VERSION))
        self.foreground_needed.connect(self._on_foreground_needed)
        self.new_instance_needed.connect(self._on_new_instance_needed)
        self.tab_center.tabCloseRequested.connect(self.close_tab)

        self.menu_button = Button()
        self.menu_button.setCursor(Qt.PointingHandCursor)
        self.menu_button.setIcon(QIcon(":/icons/images/material/menu.svg"))
        self.menu_button.setIconSize(QSize(24, 24))
        self.menu_button.setText(_("ACTION_MAIN_MENU_SHOW"))
        self.menu_button.setObjectName("MenuButton")
        self.menu_button.setProperty("color", QColor(0x00, 0x92, 0xFF))
        self.menu_button.setProperty("hover_color", QColor(0x00, 0x70, 0xDD))
        self.menu_button.setStyleSheet(
            "#MenuButton {background: none; border: none; color: #0092FF;}"
            "#MenuButton:hover {color: #0070DD;}")
        self.menu_button.apply_style()
        self.menu_button.clicked.connect(self._show_menu)
        self.tab_center.setCornerWidget(self.menu_button, Qt.TopRightCorner)

        self.add_tab_button = Button()
        self.add_tab_button.setCursor(Qt.PointingHandCursor)
        self.add_tab_button.setIcon(QIcon(":/icons/images/material/add.svg"))
        self.add_tab_button.setIconSize(QSize(24, 24))
        self.add_tab_button.setProperty("color", QColor(0x00, 0x92, 0xFF))
        self.add_tab_button.setProperty("hover_color",
                                        QColor(0x00, 0x70, 0xDD))
        self.add_tab_button.setStyleSheet("background: none; border: none;")
        self.add_tab_button.apply_style()
        self.add_tab_button.clicked.connect(self._on_add_instance_clicked)
        self.tab_center.setCornerWidget(self.add_tab_button, Qt.TopLeftCorner)

        self.tab_center.currentChanged.connect(self.on_current_tab_changed)
        self._define_shortcuts()
        self.ensurePolished()

    def _define_shortcuts(self):
        self.shortcut_close = QShortcut(QKeySequence(QKeySequence.Close), self)
        self.shortcut_close.activated.connect(
            self._shortcut_proxy(self.close_current_tab))
        self.shortcut_new_tab = QShortcut(QKeySequence(QKeySequence.AddTab),
                                          self)
        self.shortcut_new_tab.activated.connect(
            self._shortcut_proxy(self._on_add_instance_clicked))
        self.shortcut_settings = QShortcut(QKeySequence(_("Ctrl+K")), self)
        self.shortcut_settings.activated.connect(
            self._shortcut_proxy(self._show_settings))
        self.shortcut_menu = QShortcut(QKeySequence(_("Alt+E")), self)
        self.shortcut_menu.activated.connect(
            self._shortcut_proxy(self._show_menu))
        self.shortcut_help = QShortcut(QKeySequence(QKeySequence.HelpContents),
                                       self)
        self.shortcut_help.activated.connect(
            self._shortcut_proxy(self._on_show_doc_clicked))
        self.shortcut_quit = QShortcut(QKeySequence(QKeySequence.Quit), self)
        self.shortcut_quit.activated.connect(
            self._shortcut_proxy(self.close_app))
        self.shortcut_create_org = QShortcut(QKeySequence(QKeySequence.New),
                                             self)
        self.shortcut_create_org.activated.connect(
            self._shortcut_proxy(self._on_create_org_clicked))
        self.shortcut_join_org = QShortcut(QKeySequence(QKeySequence.Open),
                                           self)
        self.shortcut_join_org.activated.connect(
            self._shortcut_proxy(self._on_join_org_clicked))
        shortcut = QShortcut(QKeySequence(QKeySequence.NextChild), self)
        shortcut.activated.connect(self._shortcut_proxy(self._cycle_tabs(1)))
        shortcut = QShortcut(QKeySequence(QKeySequence.PreviousChild), self)
        shortcut.activated.connect(self._shortcut_proxy(self._cycle_tabs(-1)))

    def _shortcut_proxy(self, funct):
        def _inner_proxy():
            if ParsecApp.has_active_modal():
                return
            funct()

        return _inner_proxy

    def _cycle_tabs(self, offset):
        def _inner_cycle_tabs():
            idx = self.tab_center.currentIndex()
            idx += offset
            if idx >= self.tab_center.count():
                idx = 0
            if idx < 0:
                idx = self.tab_center.count() - 1
            self.switch_to_tab(idx)

        return _inner_cycle_tabs

    def _toggle_add_tab_button(self):
        if self._get_login_tab_index() == -1:
            self.add_tab_button.setDisabled(False)
        else:
            self.add_tab_button.setDisabled(True)

    def resizeEvent(self, event):
        super().resizeEvent(event)
        for win in self.children():
            if win.objectName() == "GreyedDialog":
                win.resize(event.size())
                win.move(0, 0)

    def _show_menu(self):
        menu = QMenu(self)
        menu.setObjectName("MainMenu")
        action = None

        idx = self._get_login_tab_index()
        action = menu.addAction(_("ACTION_MAIN_MENU_ADD_INSTANCE"))
        action.triggered.connect(self._on_add_instance_clicked)
        action.setShortcut(self.shortcut_new_tab.key())
        action.setShortcutVisibleInContextMenu(True)

        if idx != -1:
            action.setDisabled(True)

        action = menu.addAction(_("ACTION_MAIN_MENU_CREATE_ORGANIZATION"))
        action.triggered.connect(self._on_create_org_clicked)
        action.setShortcut(self.shortcut_create_org.key())
        action.setShortcutVisibleInContextMenu(True)

        action = menu.addAction(_("ACTION_MAIN_MENU_JOIN_ORGANIZATION"))
        action.triggered.connect(self._on_join_org_clicked)
        action.setShortcut(self.shortcut_join_org.key())
        action.setShortcutVisibleInContextMenu(True)

        menu.addSeparator()

        action = menu.addAction(_("ACTION_MAIN_MENU_SETTINGS"))
        action.triggered.connect(self._show_settings)
        action.setShortcut(self.shortcut_settings.key())
        action.setShortcutVisibleInContextMenu(True)

        action = menu.addAction(_("ACTION_MAIN_MENU_OPEN_DOCUMENTATION"))
        action.triggered.connect(self._on_show_doc_clicked)
        action.setShortcut(self.shortcut_help.key())
        action.setShortcutVisibleInContextMenu(True)

        action = menu.addAction(_("ACTION_MAIN_MENU_ABOUT"))
        action.triggered.connect(self._show_about)
        action = menu.addAction(_("ACTION_MAIN_MENU_CHANGELOG"))
        action.triggered.connect(self._show_changelog)
        action = menu.addAction(_("ACTION_MAIN_MENU_LICENSE"))
        action.triggered.connect(self._show_license)
        action = menu.addAction(_("ACTION_MAIN_MENU_FEEDBACK_SEND"))
        action.triggered.connect(self._on_send_feedback_clicked)
        menu.addSeparator()
        action = menu.addAction(_("ACTION_MAIN_MENU_QUIT_PARSEC"))
        action.triggered.connect(self.close_app)
        action.setShortcut(self.shortcut_quit.key())
        action.setShortcutVisibleInContextMenu(True)

        pos = self.menu_button.pos()
        pos.setY(pos.y() + self.menu_button.size().height())
        pos = self.mapToGlobal(pos)
        menu.exec_(pos)
        menu.setParent(None)

    def _show_about(self):
        w = AboutWidget()
        d = GreyedDialog(w, title="", parent=self, width=1000)
        d.exec_()

    def _show_license(self):
        w = LicenseWidget()
        d = GreyedDialog(w,
                         title=_("TEXT_LICENSE_TITLE"),
                         parent=self,
                         width=1000)
        d.exec_()

    def _show_changelog(self):
        w = ChangelogWidget()
        d = GreyedDialog(w,
                         title=_("TEXT_CHANGELOG_TITLE"),
                         parent=self,
                         width=1000)
        d.exec_()

    def _show_settings(self):
        w = SettingsWidget(self.config, self.jobs_ctx, self.event_bus)
        d = GreyedDialog(w,
                         title=_("TEXT_SETTINGS_TITLE"),
                         parent=self,
                         width=1000)
        d.exec_()

    def _on_show_doc_clicked(self):
        desktop.open_doc_link()

    def _on_send_feedback_clicked(self):
        desktop.open_feedback_link()

    def _on_add_instance_clicked(self):
        self.add_instance()

    def _on_create_org_clicked(self, addr=None):
        def _on_finished(ret):
            if ret is None:
                return
            self.reload_login_devices()
            self.try_login(ret[0], ret[1])

        CreateOrgWidget.show_modal(self.jobs_ctx,
                                   self.config,
                                   self,
                                   on_finished=_on_finished,
                                   start_addr=addr)

    def _on_join_org_clicked(self):
        url = get_text_input(
            parent=self,
            title=_("TEXT_JOIN_ORG_URL_TITLE"),
            message=_("TEXT_JOIN_ORG_URL_INSTRUCTIONS"),
            placeholder=_("TEXT_JOIN_ORG_URL_PLACEHOLDER"),
        )
        if url is None:
            return
        elif url == "":
            show_error(self, _("TEXT_JOIN_ORG_INVALID_URL"))
            return

        action_addr = None
        try:
            action_addr = BackendActionAddr.from_url(url)
        except ValueError as exc:
            show_error(self, _("TEXT_INVALID_URL"), exception=exc)
            return

        if isinstance(action_addr, BackendOrganizationBootstrapAddr):
            self._on_create_org_clicked(action_addr)
        elif isinstance(action_addr, BackendInvitationAddr):
            if action_addr.invitation_type == InvitationType.USER:
                self._on_claim_user_clicked(action_addr)
            elif action_addr.invitation_type == InvitationType.DEVICE:
                self._on_claim_device_clicked(action_addr)
            else:
                show_error(self, _("TEXT_INVALID_URL"))
                return
        else:
            show_error(self, _("TEXT_INVALID_URL"))
            return

    def _on_claim_user_clicked(self, action_addr):
        widget = None

        def _on_finished():
            nonlocal widget
            if not widget.status:
                return
            login, password = widget.status
            self.reload_login_devices()
            self.try_login(login, password)

        widget = ClaimUserWidget.show_modal(
            jobs_ctx=self.jobs_ctx,
            config=self.config,
            addr=action_addr,
            parent=self,
            on_finished=_on_finished,
        )

    def _on_claim_device_clicked(self, action_addr):
        widget = None

        def _on_finished():
            nonlocal widget
            if not widget.status:
                return
            login, password = widget.status
            self.reload_login_devices()
            self.try_login(login, password)

        widget = ClaimDeviceWidget.show_modal(
            jobs_ctx=self.jobs_ctx,
            config=self.config,
            addr=action_addr,
            parent=self,
            on_finished=_on_finished,
        )

    def try_login(self, device, password):
        idx = self._get_login_tab_index()
        tab = None
        if idx == -1:
            tab = self.add_new_tab()
        else:
            tab = self.tab_center.widget(idx)
        kf = get_key_file(self.config.config_dir, device)
        tab.login_with_password(kf, password)

    def reload_login_devices(self):
        idx = self._get_login_tab_index()
        if idx == -1:
            return
        w = self.tab_center.widget(idx)
        if not w:
            return
        w.show_login_widget()

    def on_current_tab_changed(self, index):
        for i in range(self.tab_center.tabBar().count()):
            if i != index:
                if self.tab_center.tabBar().tabTextColor(
                        i) != MainWindow.TAB_NOTIFICATION_COLOR:
                    self.tab_center.tabBar().setTabTextColor(
                        i, MainWindow.TAB_NOT_SELECTED_COLOR)
            else:
                self.tab_center.tabBar().setTabTextColor(
                    i, MainWindow.TAB_SELECTED_COLOR)

    def _on_foreground_needed(self):
        self.show_top()

    def _on_new_instance_needed(self, start_arg):
        self.add_instance(start_arg)
        self.show_top()

    def on_config_updated(self, event, **kwargs):
        self.config = self.config.evolve(**kwargs)
        save_config(self.config)
        telemetry.init(self.config)

    def show_window(self, skip_dialogs=False, invitation_link=""):
        try:
            if not self.restoreGeometry(self.config.gui_geometry):
                self.showMaximized()
        except TypeError:
            self.showMaximized()

        QCoreApplication.processEvents()

        # Used with the --diagnose option
        if skip_dialogs:
            return

        # At the very first launch
        if self.config.gui_first_launch:
            r = ask_question(
                self,
                _("TEXT_ERROR_REPORTING_TITLE"),
                _("TEXT_ERROR_REPORTING_INSTRUCTIONS"),
                [_("ACTION_ERROR_REPORTING_ACCEPT"),
                 _("ACTION_NO")],
            )

            # Acknowledge the changes
            self.event_bus.send(
                CoreEvent.GUI_CONFIG_CHANGED,
                gui_first_launch=False,
                gui_last_version=PARSEC_VERSION,
                telemetry_enabled=r == _("ACTION_ERROR_REPORTING_ACCEPT"),
            )

        # For each parsec update
        if self.config.gui_last_version and self.config.gui_last_version != PARSEC_VERSION:

            # Update from parsec `<1.14` to `>=1.14`
            if LooseVersion(self.config.gui_last_version) < "1.14":

                # Revert the acrobat reader workaround
                if (platform.system() == "Windows"
                        and win_registry.is_acrobat_reader_dc_present() and
                        not win_registry.get_acrobat_app_container_enabled()):
                    win_registry.del_acrobat_app_container_enabled()

            # Acknowledge the changes
            self.event_bus.send(CoreEvent.GUI_CONFIG_CHANGED,
                                gui_last_version=PARSEC_VERSION)

        telemetry.init(self.config)

        devices = list_available_devices(self.config.config_dir)
        if not len(devices) and not invitation_link:
            r = ask_question(
                self,
                _("TEXT_KICKSTART_PARSEC_WHAT_TO_DO_TITLE"),
                _("TEXT_KICKSTART_PARSEC_WHAT_TO_DO_INSTRUCTIONS"),
                [
                    _("ACTION_NO_DEVICE_CREATE_ORGANIZATION"),
                    _("ACTION_NO_DEVICE_JOIN_ORGANIZATION"),
                ],
                radio_mode=True,
            )
            if r == _("ACTION_NO_DEVICE_JOIN_ORGANIZATION"):
                self._on_join_org_clicked()
            elif r == _("ACTION_NO_DEVICE_CREATE_ORGANIZATION"):
                self._on_create_org_clicked()

    def show_top(self):
        self.activateWindow()
        self.setWindowState((self.windowState() & ~Qt.WindowMinimized)
                            | Qt.WindowActive)
        self.raise_()
        self.show()

    def on_tab_state_changed(self, tab, state):
        idx = self.tab_center.indexOf(tab)
        if idx == -1:
            return
        if state == "login":
            if self._get_login_tab_index() != -1:
                self.tab_center.removeTab(idx)
            else:
                self.tab_center.setTabToolTip(
                    idx, _("TEXT_TAB_TITLE_LOG_IN_SCREEN"))
                self.tab_center.setTabText(idx,
                                           _("TEXT_TAB_TITLE_LOG_IN_SCREEN"))
        elif state == "logout":
            self.tab_center.removeTab(idx)
            idx = self._get_login_tab_index()
            if idx == -1:
                self.add_instance()
            else:
                tab_widget = self.tab_center.widget(idx)
                log_widget = None if not tab_widget else tab_widget.get_login_widget(
                )
                if log_widget:
                    log_widget.reload_devices()
        elif state == "connected":
            device = tab.current_device
            tab_name = (
                f"{device.organization_id} - {device.short_user_display} - {device.device_display}"
            )
            self.tab_center.setTabToolTip(idx, tab_name)
            self.tab_center.setTabText(
                idx,
                ensure_string_size(tab_name, 150,
                                   self.tab_center.tabBar().font()))
        if self.tab_center.count() == 1:
            self.tab_center.setTabsClosable(False)
        self._toggle_add_tab_button()

    def on_tab_notification(self, widget, event):
        idx = self.tab_center.indexOf(widget)
        if idx == -1 or idx == self.tab_center.currentIndex():
            return
        if event == CoreEvent.SHARING_UPDATED:
            self.tab_center.tabBar().setTabTextColor(
                idx, MainWindow.TAB_NOTIFICATION_COLOR)

    def _get_login_tab_index(self):
        for idx in range(self.tab_center.count()):
            if self.tab_center.tabText(idx) == _(
                    "TEXT_TAB_TITLE_LOG_IN_SCREEN"):
                return idx
        return -1

    def add_new_tab(self):
        tab = InstanceWidget(self.jobs_ctx, self.event_bus, self.config,
                             self.systray_notification)
        tab.join_organization_clicked.connect(self._on_join_org_clicked)
        tab.create_organization_clicked.connect(self._on_create_org_clicked)
        idx = self.tab_center.addTab(tab, "")
        tab.state_changed.connect(self.on_tab_state_changed)
        self.tab_center.setCurrentIndex(idx)
        if self.tab_center.count() > 1:
            self.tab_center.setTabsClosable(True)
        else:
            self.tab_center.setTabsClosable(False)
        return tab

    def switch_to_tab(self, idx):
        if not ParsecApp.has_active_modal():
            self.tab_center.setCurrentIndex(idx)

    def _find_device_from_addr(self, action_addr, display_error=False):
        device = None
        for available_device in list_available_devices(self.config.config_dir):
            if available_device.organization_id == action_addr.organization_id:
                device = available_device
                break
        if device is None:
            show_error(
                self,
                _("TEXT_FILE_LINK_NOT_IN_ORG_organization").format(
                    organization=action_addr.organization_id),
            )
        return device

    def switch_to_login_tab(self, action_addr=None):
        idx = self._get_login_tab_index()
        if idx != -1:
            self.switch_to_tab(idx)
        else:
            tab = self.add_new_tab()
            tab.show_login_widget()
            self.on_tab_state_changed(tab, "login")
            idx = self.tab_center.count() - 1
            self.switch_to_tab(idx)

        if action_addr is not None:
            device = self._find_device_from_addr(action_addr,
                                                 display_error=True)
            instance_widget = self.tab_center.widget(idx)
            instance_widget.set_workspace_path(action_addr)
            login_w = self.tab_center.widget(idx).get_login_widget()
            login_w._on_account_clicked(device)

    def go_to_file_link(self, action_addr):
        found_org = self._find_device_from_addr(action_addr,
                                                display_error=True) is not None
        if not found_org:
            self.switch_to_login_tab()
            return

        for idx in range(self.tab_center.count()):
            if self.tab_center.tabText(idx) == _(
                    "TEXT_TAB_TITLE_LOG_IN_SCREEN"):
                continue
            w = self.tab_center.widget(idx)
            if (not w or not w.core
                    or w.core.device.organization_addr.organization_id !=
                    action_addr.organization_id):
                continue
            user_manifest = w.core.user_fs.get_user_manifest()
            found_workspace = False
            for wk in user_manifest.workspaces:
                if not wk.role:
                    continue
                if wk.id == action_addr.workspace_id:
                    found_workspace = True
                    central_widget = w.get_central_widget()
                    try:
                        central_widget.go_to_file_link(wk.id, action_addr.path)
                        self.switch_to_tab(idx)
                    except AttributeError:
                        logger.exception("Central widget is not available")
                    return
            if not found_workspace:
                show_error(
                    self,
                    _("TEXT_FILE_LINK_WORKSPACE_NOT_FOUND_organization").
                    format(organization=action_addr.organization_id),
                )
                return
        show_info(
            self,
            _("TEXT_FILE_LINK_PLEASE_LOG_IN_organization").format(
                organization=action_addr.organization_id),
        )
        self.switch_to_login_tab(action_addr)

    def show_create_org_widget(self, action_addr):
        self.switch_to_login_tab()
        self._on_create_org_clicked(action_addr)

    def show_claim_user_widget(self, action_addr):
        self.switch_to_login_tab()
        self._on_claim_user_clicked(action_addr)

    def show_claim_device_widget(self, action_addr):
        self.switch_to_login_tab()
        self._on_claim_device_clicked(action_addr)

    def add_instance(self, start_arg: Optional[str] = None):
        action_addr = None
        if start_arg:
            try:
                action_addr = BackendActionAddr.from_url(start_arg)
            except ValueError as exc:
                show_error(self, _("TEXT_INVALID_URL"), exception=exc)

        self.show_top()
        if not action_addr:
            self.switch_to_login_tab()
        elif isinstance(action_addr, BackendOrganizationFileLinkAddr):
            self.go_to_file_link(action_addr)
        elif isinstance(action_addr, BackendOrganizationBootstrapAddr):
            self.show_create_org_widget(action_addr)
        elif (isinstance(action_addr, BackendInvitationAddr)
              and action_addr.invitation_type == InvitationType.USER):
            self.show_claim_user_widget(action_addr)
        elif (isinstance(action_addr, BackendInvitationAddr)
              and action_addr.invitation_type == InvitationType.DEVICE):
            self.show_claim_device_widget(action_addr)
        else:
            show_error(self, _("TEXT_INVALID_URL"))

    def close_current_tab(self, force=False):
        if self.tab_center.count() == 1:
            self.close_app()
        else:
            idx = self.tab_center.currentIndex()
            self.close_tab(idx, force=force)

    def close_app(self, force=False):
        self.show_top()
        self.need_close = True
        self.force_close = force
        self.close()

    def close_all_tabs(self):
        for idx in range(self.tab_center.count()):
            self.close_tab(idx, force=True)

    def close_tab(self, index, force=False):
        tab = self.tab_center.widget(index)
        if not force:
            r = _("ACTION_TAB_CLOSE_CONFIRM")
            if tab and tab.is_logged_in:
                r = ask_question(
                    self,
                    _("TEXT_TAB_CLOSE_TITLE"),
                    _("TEXT_TAB_CLOSE_INSTRUCTIONS_device").format(
                        device=
                        f"{tab.core.device.short_user_display} - {tab.core.device.device_display}"
                    ),
                    [_("ACTION_TAB_CLOSE_CONFIRM"),
                     _("ACTION_CANCEL")],
                )
            if r != _("ACTION_TAB_CLOSE_CONFIRM"):
                return
        self.tab_center.removeTab(index)
        if not tab:
            return
        tab.logout()
        self.reload_login_devices()
        if self.tab_center.count() == 1:
            self.tab_center.setTabsClosable(False)
        self._toggle_add_tab_button()

    def closeEvent(self, event):
        if self.minimize_on_close and not self.need_close:
            self.hide()
            event.ignore()
            if not self.minimize_on_close_notif_already_send:
                self.minimize_on_close_notif_already_send = True
                self.systray_notification.emit(
                    "Parsec", _("TEXT_TRAY_PARSEC_STILL_RUNNING_MESSAGE"),
                    2000)
        else:
            if self.config.gui_confirmation_before_close and not self.force_close:
                result = ask_question(
                    self if self.isVisible() else None,
                    _("TEXT_PARSEC_QUIT_TITLE"),
                    _("TEXT_PARSEC_QUIT_INSTRUCTIONS"),
                    [_("ACTION_PARSEC_QUIT_CONFIRM"),
                     _("ACTION_CANCEL")],
                )
                if result != _("ACTION_PARSEC_QUIT_CONFIRM"):
                    event.ignore()
                    self.force_close = False
                    self.need_close = False
                    return

            state = self.saveGeometry()
            self.event_bus.send(CoreEvent.GUI_CONFIG_CHANGED,
                                gui_geometry=state)
            self.close_all_tabs()
            event.accept()
            QApplication.quit()
Пример #6
0
class ConfigTreeWidget(QTreeView):
    def __init__(self):
        QTreeView.__init__(self)

        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.open_menu)

        self.setSelectionMode(self.SingleSelection)
        # self.setSelectionBehavior(self.SelectItems)

        self.setDragDropMode(QAbstractItemView.DragDrop)
        self.setDefaultDropAction(Qt.MoveAction)
        self.setDragEnabled(True)
        self.setAcceptDrops(True)
        self.setDropIndicatorShown(True)
        self.setAnimated(True)

        self.duplicate_shortcut = QShortcut(QKeySequence('Shift+D'), self)
        self.duplicate_shortcut.activated.connect(
            self.with_selected(self.duplicate))
        self.exclude_shortcut = QShortcut(QKeySequence('Alt+Del'), self)
        self.exclude_shortcut.activated.connect(
            self.with_selected(self.exclude))
        self.remove_shortcut = QShortcut(QKeySequence('Del'), self)
        self.remove_shortcut.activated.connect(self.with_selected(self.remove))
        self.clear_shortcut = QShortcut(QKeySequence('Shift+R'), self)
        self.clear_shortcut.activated.connect(
            self.with_selected(self.reset_item, 'clear_value'))
        self.default_shortcut = QShortcut(QKeySequence('Ctrl+R'), self)
        self.default_shortcut.activated.connect(
            self.with_selected(self.reset_item, 'default'))
        self.reset_shortcut = QShortcut(QKeySequence('Alt+R'), self)
        self.reset_shortcut.activated.connect(
            self.with_selected(self.reset_item, 'all'))
        self.item_shortcut = QShortcut(QKeySequence('Shift+A'), self)
        self.item_shortcut.activated.connect(
            self.with_selected(self.add_item, False))
        self.section_shortcut = QShortcut(QKeySequence('Ctrl+A'), self)
        self.section_shortcut.activated.connect(
            self.with_selected(self.add_item, True))

    def with_selected(self, f, *args, **kwargs):
        def decorated():
            index = self.selectedIndexes()[0]
            return f(index, *args, **kwargs)

        return decorated

    def open_menu(self, point):
        index = self.indexAt(point)
        item = index.internalPointer()

        menu = QMenu()

        duplicate = QAction("Duplicate")
        duplicate.setShortcut(self.duplicate_shortcut.key())
        duplicate.triggered.connect(partial(self.duplicate, index))
        menu.addAction(duplicate)

        exclude = QAction("Toggle exclude")
        exclude.setShortcut(self.exclude_shortcut.key())
        exclude.triggered.connect(partial(self.exclude, index))
        menu.addAction(exclude)

        remove = QAction("Remove from config")
        remove.setShortcut(self.remove_shortcut.key())
        remove.triggered.connect(partial(self.remove, index))
        menu.addAction(remove)

        menu.addSeparator()

        clear = QAction("Clear item value")
        clear.setShortcut(self.clear_shortcut.key())
        clear.triggered.connect(partial(self.reset_item, index, 'clear_value'))
        menu.addAction(clear)

        reset_default = QAction("Reset value to default")
        reset_default.setShortcut(self.default_shortcut.key())
        reset_default.triggered.connect(
            partial(self.reset_item, index, 'default'))
        menu.addAction(reset_default)

        reset_all = QAction("Reset all changes")
        reset_all.setShortcut(self.reset_shortcut.key())
        reset_all.triggered.connect(partial(self.reset_item, index, 'all'))
        menu.addAction(reset_all)

        menu.addSeparator()

        add_option = QAction("Add option")
        add_option.setShortcut(self.item_shortcut.key())
        add_option.triggered.connect(partial(self.add_item, index, False))
        menu.addAction(add_option)

        add_section = QAction("Add section")
        add_section.setShortcut(self.section_shortcut.key())
        add_section.triggered.connect(partial(self.add_item, index, True))
        menu.addAction(add_section)

        if item is None:
            clear.setDisabled(True)
            reset_all.setDisabled(True)
            reset_default.setDisabled(True)

            duplicate.setDisabled(True)
            remove.setDisabled(True)
            exclude.setDisabled(True)
        else:
            if item.type in ('list', 'list_item'):
                add_section.setDisabled(True)

            if item.type == 'list':
                clear.setDisabled(True)  # Temporary, cuz buggg

            # if item.type == 'section':
            #     clear.setDisabled(True)

        menu.exec_(QCursor.pos())

    def duplicate(self, index):
        item = deepcopy(index.internalPointer())
        item.set_state('added')
        ensure_unique_names(item)
        self.model().insertItems(index.row() + 1, [item], index.parent())
        self.expandAll()  # fixes not expanded duplicated section

    def remove(self, index):
        self.model().removeRow(index)

    def exclude(self, index):
        item = self.model().nodeFromIndex(index)
        if item.state == 'deleted':
            self.model().setData(index, item.previous_state, StateRole)
        else:
            self.model().setData(index, 'deleted', StateRole)

    def add_item(self, index, is_section):
        parentItem = self.model().nodeFromIndex(index)

        if parentItem.type in ('list', 'list_item'):
            if is_section:
                return
            item_type = 'list_item'
        else:
            item_type = 'section' if is_section else 'option'

        prompt = 'Enter {} name'.format(item_type.replace('_', ' '))
        text, ok = QInputDialog.getText(self, prompt, prompt)
        if not ok:
            return

        if parentItem.type in (
                'list',
                'section'):  # to append at first index in section or list
            row = 0
            parent = index
        else:
            row = index.row()
            parent = index.parent()
            if row == -1:  # to append at last position e.g. at root
                row = parentItem.childCount()
            else:
                row += 1  # to append under current position

        item = ConfigModelItem((text, None, '', ''),
                               item_type=item_type,
                               state='added')
        self.model().insertItems(row, [item], parent)

        ensure_unique_names(item, include_self=False)
        # parent.internalPointer().set_state('edited')
        self.expandAll()

    def reset_item(self, index, reset_type):
        item = index.internalPointer()
        model = self.model()
        itemdataindex = model.modifyCol(index, 1)

        if reset_type == 'all':
            for i, default in enumerate(item.default_values):
                model.setData(model.modifyCol(index, i), default)

            model.setData(index, item.default_state, role=StateRole)

        elif reset_type == 'default':
            # if item.type == 'list' and \
            #         not isinstance(item.spec_default, (list, tuple)):
            #     self.reset_item(item, 'clear_value')

            model.setData(itemdataindex, item.spec_default)

            if item.default_state == 'unchanged':
                model.setData(index, 'unchanged', role=StateRole)

        elif reset_type == 'clear_value':
            item_type = model.data(itemdataindex, TypeRole)
            if item_type == 'list':
                return
            if item_type != 'section':
                model.setData(itemdataindex, None)

            # if model.data(itemdataindex, TypeRole) == 'list':  # TODO
            #     model.removeRows(0, item.childCount(), index)
            #     model.setData(index, 'option', role=TypeRole)
            #     return

        for child in model.childrenIndexes(index):
            self.reset_item(child, reset_type)
Пример #7
0
class MainWindow(QMainWindow, Ui_MainWindow):  # type: ignore[misc]
    foreground_needed = pyqtSignal()
    new_instance_needed = pyqtSignal(object)
    systray_notification = pyqtSignal(str, str, int)

    TAB_NOTIFICATION_COLOR = QColor(46, 146, 208)
    TAB_NOT_SELECTED_COLOR = QColor(123, 132, 163)
    TAB_SELECTED_COLOR = QColor(12, 65, 159)

    def __init__(
        self,
        jobs_ctx: QtToTrioJobScheduler,
        quit_callback: Callable[[], None],
        event_bus: EventBus,
        config: CoreConfig,
        minimize_on_close: bool = False,
        parent: Optional[QWidget] = None,
    ):
        super().__init__(parent=parent)
        self.setupUi(self)

        self.setMenuBar(None)
        self.jobs_ctx = jobs_ctx
        self.quit_callback = quit_callback
        self.event_bus = event_bus
        self.config = config
        self.minimize_on_close = minimize_on_close
        # Explain only once that the app stays in background
        self.minimize_on_close_notif_already_send = False
        self.force_close = False
        self.need_close = False
        self.event_bus.connect(
            CoreEvent.GUI_CONFIG_CHANGED, cast(EventCallback, self.on_config_updated)
        )
        self.setWindowTitle(_("TEXT_PARSEC_WINDOW_TITLE_version").format(version=PARSEC_VERSION))
        self.foreground_needed.connect(self._on_foreground_needed)
        self.new_instance_needed.connect(self._on_new_instance_needed)
        self.tab_center.tabCloseRequested.connect(self.close_tab)

        self.menu_button = Button()
        self.menu_button.setCursor(Qt.CursorShape.PointingHandCursor)
        self.menu_button.setIcon(QIcon(":/icons/images/material/menu.svg"))
        self.menu_button.setIconSize(QSize(24, 24))
        self.menu_button.setText(_("ACTION_MAIN_MENU_SHOW"))
        self.menu_button.setObjectName("MenuButton")
        self.menu_button.setProperty("color", QColor(0x00, 0x92, 0xFF))
        self.menu_button.setProperty("hover_color", QColor(0x00, 0x70, 0xDD))
        self.menu_button.setStyleSheet(
            "#MenuButton {background: none; border: none; color: #0092FF;}"
            "#MenuButton:hover {color: #0070DD;}"
        )
        self.menu_button.apply_style()
        self.menu_button.clicked.connect(self._show_menu)
        self.tab_center.setCornerWidget(self.menu_button, Qt.Corner.TopRightCorner)

        self.add_tab_button = Button()
        self.add_tab_button.setCursor(Qt.CursorShape.PointingHandCursor)
        self.add_tab_button.setIcon(QIcon(":/icons/images/material/add.svg"))
        self.add_tab_button.setIconSize(QSize(24, 24))
        self.add_tab_button.setProperty("color", QColor(0x00, 0x92, 0xFF))
        self.add_tab_button.setProperty("hover_color", QColor(0x00, 0x70, 0xDD))
        self.add_tab_button.setStyleSheet("background: none; border: none;")
        self.add_tab_button.apply_style()
        self.add_tab_button.clicked.connect(self._on_add_instance_clicked)
        self.tab_center.setCornerWidget(self.add_tab_button, Qt.Corner.TopLeftCorner)

        self.tab_center.currentChanged.connect(self.on_current_tab_changed)
        self._define_shortcuts()
        self.ensurePolished()

        if sys.platform == "darwin":
            # Native menu bar on MacOS
            self._create_mac_menu_bar()

    def _define_shortcuts(self) -> None:
        self.shortcut_close = QShortcut(QKeySequence(QKeySequence.Close), self)
        self.shortcut_close.activated.connect(self._shortcut_proxy(self.close_current_tab))
        self.shortcut_new_tab = QShortcut(QKeySequence(QKeySequence.AddTab), self)
        self.shortcut_new_tab.activated.connect(self._shortcut_proxy(self._on_add_instance_clicked))
        self.shortcut_settings = QShortcut(QKeySequence(_("Ctrl+K")), self)
        self.shortcut_settings.activated.connect(self._shortcut_proxy(self._show_settings))
        self.shortcut_recovery = QShortcut(QKeySequence(_("Ctrl+I")), self)
        self.shortcut_recovery.activated.connect(self._shortcut_proxy(self._on_manage_keys))
        self.shortcut_menu = QShortcut(QKeySequence(_("Alt+E")), self)
        self.shortcut_menu.activated.connect(self._shortcut_proxy(self._show_menu))
        self.shortcut_help = QShortcut(QKeySequence(QKeySequence.HelpContents), self)
        self.shortcut_help.activated.connect(self._shortcut_proxy(self._on_show_doc_clicked))
        self.shortcut_quit = QShortcut(QKeySequence(QKeySequence.Quit), self)
        self.shortcut_quit.activated.connect(self._shortcut_proxy(self.close_app))
        self.shortcut_create_org = QShortcut(QKeySequence(QKeySequence.New), self)
        self.shortcut_create_org.activated.connect(
            self._shortcut_proxy(self._on_create_org_clicked)
        )
        self.shortcut_join_org = QShortcut(QKeySequence(QKeySequence.Open), self)
        self.shortcut_join_org.activated.connect(self._shortcut_proxy(self._on_join_org_clicked))
        shortcut = QShortcut(QKeySequence(QKeySequence.NextChild), self)
        shortcut.activated.connect(self._shortcut_proxy(self._cycle_tabs(1)))
        shortcut = QShortcut(QKeySequence(QKeySequence.PreviousChild), self)
        shortcut.activated.connect(self._shortcut_proxy(self._cycle_tabs(-1)))

    def _shortcut_proxy(self, funct: Callable[[], None]) -> Callable[[], None]:
        def _inner_proxy() -> None:
            if ParsecApp.has_active_modal():
                return
            funct()

        return _inner_proxy

    def _cycle_tabs(self, offset: int) -> Callable[[], None]:
        def _inner_cycle_tabs() -> None:
            idx = self.tab_center.currentIndex()
            idx += offset
            if idx >= self.tab_center.count():
                idx = 0
            if idx < 0:
                idx = self.tab_center.count() - 1
            self.switch_to_tab(idx)

        return _inner_cycle_tabs

    def _toggle_add_tab_button(self) -> None:
        if self._get_login_tab_index() == -1:
            self.add_tab_button.setDisabled(False)
        else:
            self.add_tab_button.setDisabled(True)

    def resizeEvent(self, event: QResizeEvent) -> None:
        super().resizeEvent(event)
        for win in self.children():
            if isinstance(win, GreyedDialog):
                win.resize(event.size())
                win.move(0, 0)

    def _create_mac_menu_bar(self) -> None:
        menuBar = QMenuBar()

        fileMenu = QMenu(_("TEXT_MENU_FILE"), self)
        menuBar.addMenu(fileMenu)

        # 'settings' and 'about' are key words processed by Qt to make standard
        # MacOS submenus associated with standard key bindings. 'quit' links
        # cmd+Q to the close confirmation widget, and leaves the red X to its
        # standard behaviour depending on the `minimize_on_close` option.

        action = fileMenu.addAction("about")
        action.triggered.connect(self._show_about)
        action = fileMenu.addAction("settings")
        action.triggered.connect(self._show_settings)
        action = fileMenu.addAction("quit")
        action.triggered.connect(self.close_app)

        action = fileMenu.addAction(_("ACTION_MAIN_MENU_CREATE_ORGANIZATION"))
        action.triggered.connect(self._on_create_org_clicked)
        action.setShortcut(self.shortcut_create_org.key())
        action = fileMenu.addAction(_("ACTION_MAIN_MENU_JOIN_ORGANIZATION"))
        action.triggered.connect(self._on_join_org_clicked)
        action.setShortcut(self.shortcut_join_org.key())

        deviceMenu = QMenu(_("TEXT_MENU_DEVICE"), self)
        menuBar.addMenu(deviceMenu)

        action = deviceMenu.addAction(_("ACTION_MAIN_MENU_MANAGE_KEYS"))
        action.triggered.connect(self._on_manage_keys)
        action.setShortcut(self.shortcut_recovery.key())

        helpMenu = QMenu(_("TEXT_MENU_HELP"), self)
        menuBar.addMenu(helpMenu)

        action = helpMenu.addAction(_("ACTION_MAIN_MENU_OPEN_DOCUMENTATION"))
        action.triggered.connect(self._on_show_doc_clicked)
        action = helpMenu.addAction(_("ACTION_MAIN_MENU_CHANGELOG"))
        action.triggered.connect(self._show_changelog)

        helpMenu.addSeparator()

        action = helpMenu.addAction(_("ACTION_MAIN_MENU_LICENSE"))
        action.triggered.connect(self._show_license)

        helpMenu.addSeparator()

        action = helpMenu.addAction(_("ACTION_MAIN_MENU_FEEDBACK_SEND"))
        action.triggered.connect(self._on_send_feedback_clicked)

        self.setMenuBar(menuBar)

    def _show_menu(self) -> None:
        menu = QMenu(self)
        menu.setObjectName("MainMenu")
        action = None

        idx = self._get_login_tab_index()
        action = menu.addAction(_("ACTION_MAIN_MENU_ADD_INSTANCE"))
        action.triggered.connect(self._on_add_instance_clicked)
        action.setShortcut(self.shortcut_new_tab.key())
        action.setShortcutVisibleInContextMenu(True)

        if idx != -1:
            action.setDisabled(True)

        action = menu.addAction(_("ACTION_MAIN_MENU_CREATE_ORGANIZATION"))
        action.triggered.connect(self._on_create_org_clicked)
        action.setShortcut(self.shortcut_create_org.key())
        action.setShortcutVisibleInContextMenu(True)

        action = menu.addAction(_("ACTION_MAIN_MENU_JOIN_ORGANIZATION"))
        action.triggered.connect(self._on_join_org_clicked)
        action.setShortcut(self.shortcut_join_org.key())
        action.setShortcutVisibleInContextMenu(True)

        action = menu.addAction(_("ACTION_MAIN_MENU_MANAGE_KEYS"))
        action.triggered.connect(self._on_manage_keys)
        action.setShortcut(self.shortcut_recovery.key())
        action.setShortcutVisibleInContextMenu(True)

        menu.addSeparator()

        action = menu.addAction(_("ACTION_MAIN_MENU_SETTINGS"))
        action.triggered.connect(self._show_settings)
        action.setShortcut(self.shortcut_settings.key())
        action.setShortcutVisibleInContextMenu(True)

        action = menu.addAction(_("ACTION_MAIN_MENU_OPEN_DOCUMENTATION"))
        action.triggered.connect(self._on_show_doc_clicked)
        action.setShortcut(self.shortcut_help.key())
        action.setShortcutVisibleInContextMenu(True)

        action = menu.addAction(_("ACTION_MAIN_MENU_ABOUT"))
        action.triggered.connect(self._show_about)
        action = menu.addAction(_("ACTION_MAIN_MENU_CHANGELOG"))
        action.triggered.connect(self._show_changelog)
        action = menu.addAction(_("ACTION_MAIN_MENU_LICENSE"))
        action.triggered.connect(self._show_license)
        action = menu.addAction(_("ACTION_MAIN_MENU_FEEDBACK_SEND"))
        action.triggered.connect(self._on_send_feedback_clicked)
        menu.addSeparator()
        action = menu.addAction(_("ACTION_MAIN_MENU_QUIT_PARSEC"))
        action.triggered.connect(self.close_app)
        action.setShortcut(self.shortcut_quit.key())
        action.setShortcutVisibleInContextMenu(True)

        pos = self.menu_button.pos()
        pos.setY(pos.y() + self.menu_button.size().height())
        pos = self.mapToGlobal(pos)
        menu.exec_(pos)
        menu.setParent(None)

    def _show_about(self) -> None:
        w = AboutWidget()
        d = GreyedDialog(w, title="", parent=self, width=1000)
        d.exec_()

    def _show_license(self) -> None:
        w = LicenseWidget()
        d = GreyedDialog(w, title=_("TEXT_LICENSE_TITLE"), parent=self, width=1000)
        d.exec_()

    def _show_changelog(self) -> None:
        w = ChangelogWidget()
        d = GreyedDialog(w, title=_("TEXT_CHANGELOG_TITLE"), parent=self, width=1000)
        d.exec_()

    def _show_settings(self) -> None:
        w = SettingsWidget(self.config, self.jobs_ctx, self.event_bus)
        d = GreyedDialog(w, title=_("TEXT_SETTINGS_TITLE"), parent=self, width=1000)
        d.exec_()

    def _on_manage_keys(self) -> None:
        devices = [device for device in list_available_devices(self.config.config_dir)]
        options = [_("ACTION_CANCEL"), _("ACTION_RECOVER_DEVICE")]
        if len(devices):
            options.append(_("ACTION_CREATE_RECOVERY_DEVICE"))
        result = ask_question(
            self, _("TEXT_DEVICE_RECOVERY_TITLE"), _("TEXT_DEVICE_RECOVERY_QUESTION"), options
        )
        if result == _("ACTION_RECOVER_DEVICE"):
            DeviceRecoveryImportWidget.show_modal(
                self.config, self.jobs_ctx, parent=self, on_finished=self.reload_login_devices
            )
        elif result == _("ACTION_CREATE_RECOVERY_DEVICE"):
            DeviceRecoveryExportWidget.show_modal(self.config, self.jobs_ctx, devices, parent=self)

    def _on_show_doc_clicked(self) -> None:
        desktop.open_doc_link()

    def _on_send_feedback_clicked(self) -> None:
        desktop.open_feedback_link()

    def _on_add_instance_clicked(self) -> None:
        self.add_instance()

    def _bind_async_callback(
        self, callback: Callable[[], Awaitable[None]]
    ) -> Callable[[], Awaitable[None]]:
        """Async callbacks need to be bound to the MainWindow instance
        in order to be able to be scheduled in the job context.
        """

        async def wrapper(instance: "MainWindow") -> None:
            return await callback()

        return wrapper.__get__(self)  # type: ignore[attr-defined]

    def _on_create_org_clicked(
        self, addr: Optional[BackendOrganizationBootstrapAddr] = None
    ) -> None:
        widget: CreateOrgWidget

        @self._bind_async_callback
        async def _on_finished() -> None:
            nonlocal widget
            # It's safe to access the widget status here since this does not perform a Qt call.
            # But the underlying C++ widget might already be deleted so we should make sure not
            # not do anything Qt related with this widget.
            if widget.status is None:
                return
            self.reload_login_devices()
            device, auth_method, password = widget.status
            await self.try_login(device, auth_method, password)
            answer = ask_question(
                self,
                _("TEXT_BOOTSTRAP_ORG_SUCCESS_TITLE"),
                _("TEXT_BOOTSTRAP_ORG_SUCCESS_organization").format(
                    organization=device.organization_id
                ),
                [_("ACTION_CREATE_RECOVERY_DEVICE"), _("ACTION_NO")],
                oriented_question=True,
            )
            if answer == _("ACTION_CREATE_RECOVERY_DEVICE"):
                DeviceRecoveryExportWidget.show_modal(
                    self.config, self.jobs_ctx, [device], parent=self
                )

        widget = CreateOrgWidget.show_modal(
            self.jobs_ctx, self.config, self, on_finished=_on_finished, start_addr=addr
        )

    def _on_join_org_clicked(self) -> None:
        url = get_text_input(
            parent=self,
            title=_("TEXT_JOIN_ORG_URL_TITLE"),
            message=_("TEXT_JOIN_ORG_URL_INSTRUCTIONS"),
            placeholder=_("TEXT_JOIN_ORG_URL_PLACEHOLDER"),
            validator=validators.BackendActionAddrValidator(),
        )
        if url is None:
            return
        elif url == "":
            show_error(self, _("TEXT_JOIN_ORG_INVALID_URL"))
            return

        action_addr = None
        try:
            action_addr = BackendActionAddr.from_url(url, allow_http_redirection=True)
        except ValueError as exc:
            show_error(self, _("TEXT_INVALID_URL"), exception=exc)
            return

        if isinstance(action_addr, BackendOrganizationBootstrapAddr):
            self._on_create_org_clicked(action_addr)
        elif isinstance(action_addr, BackendInvitationAddr):
            if action_addr.invitation_type == InvitationType.USER:
                self._on_claim_user_clicked(action_addr)
            elif action_addr.invitation_type == InvitationType.DEVICE:
                self._on_claim_device_clicked(action_addr)
        elif isinstance(action_addr, BackendPkiEnrollmentAddr):
            if not is_pki_enrollment_available():
                show_error(self, _("TEXT_PKI_ENROLLMENT_NOT_AVAILABLE"))
                return
            self._on_claim_pki_clicked(action_addr)
        else:
            show_error(self, _("TEXT_INVALID_URL"))
            return

    def _on_recover_device_clicked(self) -> None:
        DeviceRecoveryImportWidget.show_modal(
            self.config, self.jobs_ctx, parent=self, on_finished=self.reload_login_devices
        )

    def _on_claim_pki_clicked(self, action_addr: BackendPkiEnrollmentAddr) -> None:
        widget: EnrollmentQueryWidget

        def _on_finished() -> None:
            nonlocal widget
            # It's safe to access the widget status here since this does not perform a Qt call.
            # But the underlying C++ widget might already be deleted so we should make sure not
            # not do anything Qt related with this widget.
            if not widget.status:
                return
            show_info(self, _("TEXT_ENROLLMENT_QUERY_SUCCEEDED"))
            self.reload_login_devices()

        widget = EnrollmentQueryWidget.show_modal(
            jobs_ctx=self.jobs_ctx,
            config=self.config,
            addr=action_addr,
            parent=self,
            on_finished=_on_finished,
        )

    def _on_claim_user_clicked(self, action_addr: BackendInvitationAddr) -> None:
        widget: ClaimUserWidget

        @self._bind_async_callback
        async def _on_finished() -> None:
            nonlocal widget
            # It's safe to access the widget status here since this does not perform a Qt call.
            # But the underlying C++ widget might already be deleted so we should make sure not
            # not do anything Qt related with this widget.
            if not widget.status:
                return
            device, auth_method, password = widget.status
            self.reload_login_devices()
            await self.try_login(device, auth_method, password)
            answer = ask_question(
                self,
                _("TEXT_CLAIM_USER_SUCCESSFUL_TITLE"),
                _("TEXT_CLAIM_USER_SUCCESSFUL"),
                [_("ACTION_CREATE_RECOVERY_DEVICE"), _("ACTION_NO")],
                oriented_question=True,
            )
            if answer == _("ACTION_CREATE_RECOVERY_DEVICE"):
                DeviceRecoveryExportWidget.show_modal(self.config, self.jobs_ctx, [device], self)

        widget = ClaimUserWidget.show_modal(
            jobs_ctx=self.jobs_ctx,
            config=self.config,
            addr=action_addr,
            parent=self,
            on_finished=_on_finished,
        )

    def _on_claim_device_clicked(self, action_addr: BackendInvitationAddr) -> None:
        widget: ClaimDeviceWidget

        @self._bind_async_callback
        async def _on_finished() -> None:
            nonlocal widget
            # It's safe to access the widget status here since this does not perform a Qt call.
            # But the underlying C++ widget might already be deleted so we should make sure not
            # not do anything Qt related with this widget.
            if not widget.status:
                return
            device, auth_method, password = widget.status
            self.reload_login_devices()
            await self.try_login(device, auth_method, password)

        widget = ClaimDeviceWidget.show_modal(
            jobs_ctx=self.jobs_ctx,
            config=self.config,
            addr=action_addr,
            parent=self,
            on_finished=_on_finished,
        )

    async def try_login(
        self, device: LocalDevice, auth_method: DeviceFileType, password: str
    ) -> None:
        idx = self._get_login_tab_index()
        if idx == -1:
            tab = self.add_new_tab()
        else:
            tab = self.tab_center.widget(idx)
        kf = get_key_file(self.config.config_dir, device)
        if auth_method == DeviceFileType.PASSWORD:
            tab.login_with_password(kf, password)
        elif auth_method == DeviceFileType.SMARTCARD:
            await tab.login_with_smartcard(kf)

    def reload_login_devices(self) -> None:
        idx = self._get_login_tab_index()
        if idx == -1:
            return
        w = self.tab_center.widget(idx)
        if not w:
            return
        w.show_login_widget()

    def on_current_tab_changed(self, index: int) -> None:
        for i in range(self.tab_center.tabBar().count()):
            if i != index:
                if self.tab_center.tabBar().tabTextColor(i) != MainWindow.TAB_NOTIFICATION_COLOR:
                    self.tab_center.tabBar().setTabTextColor(i, MainWindow.TAB_NOT_SELECTED_COLOR)
            else:
                self.tab_center.tabBar().setTabTextColor(i, MainWindow.TAB_SELECTED_COLOR)

    def _on_foreground_needed(self) -> None:
        self.show_top()

    def _on_new_instance_needed(self, start_arg: Optional[str]) -> None:
        self.add_instance(start_arg)
        self.show_top()

    def on_config_updated(self, event: CoreEvent, **kwargs: object) -> None:
        self.config = self.config.evolve(**kwargs)
        save_config(self.config)
        telemetry.init(self.config)

    def show_window(self, skip_dialogs: bool = False) -> None:
        try:
            if not self.restoreGeometry(self.config.gui_geometry):
                self.showMaximized()
        except TypeError:
            self.showMaximized()

        QCoreApplication.processEvents()

        # Used with the --diagnose option
        if skip_dialogs:
            return

        # At the very first launch
        if self.config.gui_first_launch:
            r = ask_question(
                self,
                _("TEXT_ENABLE_TELEMETRY_TITLE"),
                _("TEXT_ENABLE_TELEMETRY_INSTRUCTIONS"),
                [_("ACTION_ENABLE_TELEMETRY_ACCEPT"), _("ACTION_ENABLE_TELEMETRY_REFUSE")],
                oriented_question=True,
            )

            # Acknowledge the changes
            self.event_bus.send(
                CoreEvent.GUI_CONFIG_CHANGED,
                gui_first_launch=False,
                gui_last_version=PARSEC_VERSION,
                telemetry_enabled=r == _("ACTION_ENABLE_TELEMETRY_ACCEPT"),
            )

        # For each parsec update
        if self.config.gui_last_version and self.config.gui_last_version != PARSEC_VERSION:

            # Update from parsec `<1.14` to `>=1.14`
            if LooseVersion(self.config.gui_last_version) < "1.14":

                # Revert the acrobat reader workaround
                if (
                    sys.platform == "win32"
                    and win_registry.is_acrobat_reader_dc_present()
                    and not win_registry.get_acrobat_app_container_enabled()
                ):
                    win_registry.del_acrobat_app_container_enabled()

            # Acknowledge the changes
            self.event_bus.send(CoreEvent.GUI_CONFIG_CHANGED, gui_last_version=PARSEC_VERSION)

        telemetry.init(self.config)

    def show_top(self) -> None:
        self.activateWindow()
        state: Qt.WindowState = (
            self.windowState() & ~Qt.WindowState.WindowMinimized
        ) | Qt.WindowState.WindowActive
        self.setWindowState(state)
        self.raise_()
        self.show()

    def on_tab_state_changed(self, tab: InstanceWidget, state: str) -> None:
        idx = self.tab_center.indexOf(tab)
        if idx == -1:
            if state == "logout":
                self.reload_login_devices()
            return
        if state == "login":
            if self._get_login_tab_index() != -1:
                self.tab_center.removeTab(idx)
            else:
                self.tab_center.setTabToolTip(idx, _("TEXT_TAB_TITLE_LOG_IN_SCREEN"))
                self.tab_center.setTabText(idx, _("TEXT_TAB_TITLE_LOG_IN_SCREEN"))
        elif state == "logout":
            self.tab_center.removeTab(idx)
            idx = self._get_login_tab_index()
            if idx == -1:
                self.add_instance()
            else:
                tab_widget = self.tab_center.widget(idx)
                log_widget = None if not tab_widget else tab_widget.get_login_widget()
                if log_widget:
                    log_widget.reload_devices()
        elif state == "connected":
            device = tab.current_device
            tab_name = (
                f"{device.organization_id} - {device.short_user_display} - {device.device_display}"
            )
            self.tab_center.setTabToolTip(idx, tab_name)
            self.tab_center.setTabText(
                idx, ensure_string_size(tab_name, 150, self.tab_center.tabBar().font())
            )
        if self.tab_center.count() == 1:
            self.tab_center.setTabsClosable(False)
        self._toggle_add_tab_button()

    def on_tab_notification(self, tab: InstanceWidget, event: CoreEvent) -> None:
        idx = self.tab_center.indexOf(tab)
        if idx == -1 or idx == self.tab_center.currentIndex():
            return
        if event == CoreEvent.SHARING_UPDATED:
            self.tab_center.tabBar().setTabTextColor(idx, MainWindow.TAB_NOTIFICATION_COLOR)

    def _get_login_tab_index(self) -> int:
        for idx in range(self.tab_center.count()):
            if self.tab_center.tabText(idx) == _("TEXT_TAB_TITLE_LOG_IN_SCREEN"):
                return idx
        return -1

    def add_new_tab(self) -> InstanceWidget:
        tab = InstanceWidget(self.jobs_ctx, self.event_bus, self.config, self.systray_notification)
        tab.join_organization_clicked.connect(self._on_join_org_clicked)
        tab.create_organization_clicked.connect(self._on_create_org_clicked)
        tab.recover_device_clicked.connect(self._on_recover_device_clicked)
        idx = self.tab_center.addTab(tab, "")
        tab.state_changed.connect(self.on_tab_state_changed)
        self.tab_center.setCurrentIndex(idx)
        if self.tab_center.count() > 1:
            self.tab_center.setTabsClosable(True)
        else:
            self.tab_center.setTabsClosable(False)
        return tab

    def switch_to_tab(self, idx: int) -> None:
        if not ParsecApp.has_active_modal():
            self.tab_center.setCurrentIndex(idx)

    def switch_to_login_tab(
        self, file_link_addr: Optional[BackendOrganizationFileLinkAddr] = None
    ) -> None:
        # Retrieve the login tab
        idx = self._get_login_tab_index()
        if idx != -1:
            self.switch_to_tab(idx)
        else:
            # No loging tab, create one
            tab = self.add_new_tab()
            tab.show_login_widget()
            self.on_tab_state_changed(tab, "login")
            idx = self.tab_center.count() - 1
            self.switch_to_tab(idx)

        if not file_link_addr:
            # We're done here
            return

        # Find the device corresponding to the organization in the link
        for available_device in list_available_devices(self.config.config_dir):
            if available_device.organization_id == file_link_addr.organization_id:
                break

        else:
            # Cannot reach this organization with our available devices
            show_error(
                self,
                _("TEXT_FILE_LINK_NOT_IN_ORG_organization").format(
                    organization=file_link_addr.organization_id
                ),
            )
            return

        # Pre-select the corresponding device
        login_w = self.tab_center.widget(idx).get_login_widget()
        login_w._on_account_clicked(available_device)

        # Set the path
        instance_widget = self.tab_center.widget(idx)
        instance_widget.set_workspace_path(file_link_addr)

        # Prompt the user for the need to log in first
        SnackbarManager.inform(
            _("TEXT_FILE_LINK_PLEASE_LOG_IN_organization").format(
                organization=file_link_addr.organization_id
            )
        )

    def go_to_file_link(self, addr: BackendOrganizationFileLinkAddr) -> None:
        # Try to use the file link on the already logged in cores
        for idx in range(self.tab_center.count()):
            if self.tab_center.tabText(idx) == _("TEXT_TAB_TITLE_LOG_IN_SCREEN"):
                continue

            w = self.tab_center.widget(idx)
            if (
                not w
                or not w.core
                or w.core.device.organization_addr.organization_id != addr.organization_id
            ):
                continue

            central_widget = w.get_central_widget()
            if not central_widget:
                continue

            try:
                central_widget.go_to_file_link(addr)

            except GoToFileLinkBadOrganizationIDError:
                continue
            except GoToFileLinkBadWorkspaceIDError:
                # Switch tab so user understand where the error comes from
                self.switch_to_tab(idx)
                show_error(
                    self,
                    _("TEXT_FILE_LINK_WORKSPACE_NOT_FOUND_organization").format(
                        organization=addr.organization_id
                    ),
                )
                return
            except GoToFileLinkPathDecryptionError:
                # Switch tab so user understand where the error comes from
                self.switch_to_tab(idx)
                show_error(self, _("TEXT_INVALID_URL"))
                return
            else:
                self.switch_to_tab(idx)
                return

        # The file link is from an organization we'r not currently logged in
        # or we don't have any device related to
        self.switch_to_login_tab(addr)

    def show_create_org_widget(self, action_addr: BackendOrganizationBootstrapAddr) -> None:
        self.switch_to_login_tab()
        self._on_create_org_clicked(action_addr)

    def show_claim_user_widget(self, action_addr: BackendInvitationAddr) -> None:
        self.switch_to_login_tab()
        self._on_claim_user_clicked(action_addr)

    def show_claim_device_widget(self, action_addr: BackendInvitationAddr) -> None:
        self.switch_to_login_tab()
        self._on_claim_device_clicked(action_addr)

    def show_claim_pki_widget(self, action_addr: BackendPkiEnrollmentAddr) -> None:
        self.switch_to_login_tab()
        self._on_claim_pki_clicked(action_addr)

    def add_instance(self, start_arg: Optional[str] = None) -> None:
        action_addr = None
        if start_arg:
            try:
                action_addr = BackendActionAddr.from_url(start_arg, allow_http_redirection=True)
            except ValueError as exc:
                show_error(self, _("TEXT_INVALID_URL"), exception=exc)

        self.show_top()
        if not action_addr:
            self.switch_to_login_tab()
        elif isinstance(action_addr, BackendOrganizationFileLinkAddr):
            self.go_to_file_link(action_addr)
        elif isinstance(action_addr, BackendOrganizationBootstrapAddr):
            self.show_create_org_widget(action_addr)
        elif (
            isinstance(action_addr, BackendInvitationAddr)
            and action_addr.invitation_type == InvitationType.USER
        ):
            self.show_claim_user_widget(action_addr)
        elif (
            isinstance(action_addr, BackendInvitationAddr)
            and action_addr.invitation_type == InvitationType.DEVICE
        ):
            self.show_claim_device_widget(action_addr)
        elif isinstance(action_addr, BackendPkiEnrollmentAddr):
            self.show_claim_pki_widget(action_addr)
        else:
            show_error(self, _("TEXT_INVALID_URL"))

    def close_current_tab(self, force: bool = False) -> None:
        if self.tab_center.count() == 1:
            self.close_app()
        else:
            idx = self.tab_center.currentIndex()
            self.close_tab(idx, force=force)

    def close_app(self, force: bool = False) -> None:
        self.show_top()
        self.need_close = True
        self.force_close = force
        self.close()

    def close_all_tabs(self) -> None:
        for idx in range(self.tab_center.count()):
            self.close_tab(idx, force=True)

    def close_tab(self, index: int, force: bool = False) -> None:
        tab = self.tab_center.widget(index)
        if not force:
            r = _("ACTION_TAB_CLOSE_CONFIRM")
            if tab and tab.is_logged_in:
                r = ask_question(
                    self,
                    _("TEXT_TAB_CLOSE_TITLE"),
                    _("TEXT_TAB_CLOSE_INSTRUCTIONS_device").format(
                        device=f"{tab.core.device.short_user_display} - {tab.core.device.device_display}"
                    ),
                    [_("ACTION_TAB_CLOSE_CONFIRM"), _("ACTION_CANCEL")],
                )
            if r != _("ACTION_TAB_CLOSE_CONFIRM"):
                return
        self.tab_center.removeTab(index)
        if not tab:
            return
        tab.logout()
        self.reload_login_devices()
        if self.tab_center.count() == 1:
            self.tab_center.setTabsClosable(False)
        self._toggle_add_tab_button()

    def closeEvent(self, event: QCloseEvent) -> None:
        if self.minimize_on_close and not self.need_close:
            self.hide()
            event.ignore()

            # This notification is disabled on Mac since minimizing apps on
            # close is the standard behaviour on this OS.
            if not self.minimize_on_close_notif_already_send and sys.platform != "darwin":
                self.minimize_on_close_notif_already_send = True
                self.systray_notification.emit(
                    "Parsec", _("TEXT_TRAY_PARSEC_STILL_RUNNING_MESSAGE"), 2000
                )
        else:
            if self.config.gui_confirmation_before_close and not self.force_close:
                result = ask_question(
                    self if self.isVisible() else None,
                    _("TEXT_PARSEC_QUIT_TITLE"),
                    _("TEXT_PARSEC_QUIT_INSTRUCTIONS"),
                    [_("ACTION_PARSEC_QUIT_CONFIRM"), _("ACTION_CANCEL")],
                )
                if result != _("ACTION_PARSEC_QUIT_CONFIRM"):
                    event.ignore()
                    self.force_close = False
                    self.need_close = False
                    return

            state = self.saveGeometry()
            self.event_bus.send(CoreEvent.GUI_CONFIG_CHANGED, gui_geometry=state)
            self.close_all_tabs()
            self.quit_callback()
            event.ignore()
Пример #8
0
class PlaylistView(QTableView):
    def __init__(self, listplayer, status_bar: QStatusBar, play_ctrls, parent=None):
        super().__init__(parent=parent)
        self.player = listplayer
        self.status_bar = status_bar
        self.play_ctrls = play_ctrls

        self.setSelectionBehavior(self.SelectRows)
        self.setDragDropMode(self.InternalMove)
        self.setDragDropOverwriteMode(False)
        self.setEditTriggers(QTableView.NoEditTriggers)
        self.setAlternatingRowColors(True)
        self.setDropIndicatorShown(True)
        self.setHorizontalHeader(PlaylistViewHeader(parent=self))
        self.setContextMenuPolicy(Qt.CustomContextMenu)

        # Create shortcuts
        self.rem_selected_items_shortcut = QShortcut(self)
        self.rem_selected_items_shortcut.setKey(QKeySequence.Delete)
        self.rem_selected_items_shortcut.setContext(Qt.WidgetWithChildrenShortcut)
        self.rem_selected_items_shortcut.activated.connect(self.remove_selected_items)

        self.play_selected_item_shortcut = QShortcut(self)
        self.play_selected_item_shortcut.setKey(Qt.Key_Return)
        self.play_selected_item_shortcut.setContext(Qt.WidgetWithChildrenShortcut)
        self.play_selected_item_shortcut.activated.connect(self.play_selected_item)

        self.setModel(PlaylistModel())

        # Setup signals
        self.customContextMenuRequested.connect(self.show_context_menu)
        self.doubleClicked.connect(self.on_doubleClicked)
        self.selectionModel().selectionChanged.connect(self.on_selectionChanged)

    def showEvent(self, e):
        if self.model().rowCount():
            if not self.selectionModel().hasSelection():
                self.setCurrentIndex(self.model().item(0).index())
        self.setFocus()

    @pyqtSlot()
    def play_selected_item(self):
        self.player.load_media(index=self.currentIndex())
        self.player.mp.play()

    def mousePressEvent(self, e):
        """Clear both row and current index selections when clicking away from items."""
        clicked_index = self.indexAt(e.pos())
        if clicked_index.isValid():
            item = self.model().item(clicked_index.row())
            status_tip = item.data(role=Qt.DisplayRole)
            self.status_bar.showMessage(status_tip)
        else:
            self.selectionModel().clear()
        return super().mousePressEvent(e)

    def on_selectionChanged(self, selected, deselected):
        if not selected:
            self.selectionModel().clear()

    @pyqtSlot(QModelIndex)
    def on_doubleClicked(self, index):
        self.player.load_media(index=index)
        self.player.mp.play()

    def dropEvent(self, e):
        dragged_index = self.currentIndex()
        dropped_index = self.indexAt(e.pos())

        log.debug(
            f"dragged_index={dragged_index.row(), dragged_index.column()} dropped_index={dropped_index.row(), dropped_index.column()} action={e.dropAction()} source={e.source()}"
        )

        if dropped_index.row() == -1:
            return None

        model = self.model()
        item = model.takeRow(dragged_index.row())
        model.insertRow(dropped_index.row(), item)
        self.setCurrentIndex(dropped_index)
        e.ignore()

    @pyqtSlot(int)
    def on_model_rowCountChanged(self, count):
        """Enable/disable GUI elements when media is added or removed"""
        if count:
            self.play_ctrls.setEnabled(True)
        else:
            self.play_ctrls.setEnabled(False)

    def setModel(self, model):
        # Disconnect previous model
        if self.model():
            self.model().rowCountChanged.disconnect()
        # Connect this model
        model.rowCountChanged.connect(self.on_model_rowCountChanged)
        model.rowCountChanged.connect(self.player.on_playlist_rowCountChanged)
        super().setModel(model)

    def selected_items(self):
        items = []
        for i in self.selectionModel().selectedRows():
            items.append(self.model().itemFromIndex(i))
        return items

    def show_context_menu(self, pos: QPoint):
        selected_items = self.selected_items()
        if len(selected_items) <= 1:
            rem_selected_text = f"Remove '{selected_items[0].data(Qt.DisplayRole)}'"
        else:
            rem_selected_text = f"Remove {len(selected_items)} items"
        menu = QMenu(self)
        menu.addAction(
            icons.get("file_remove"),
            rem_selected_text,
            self.remove_selected_items,
            self.rem_selected_items_shortcut.key(),
        )
        menu.exec_(self.mapToGlobal(pos))

    @pyqtSlot()
    def remove_selected_items(self):
        indexes = self.selectionModel().selectedRows()
        items = [self.model().itemFromIndex(i) for i in indexes]
        self.remove_items(items)

    def remove_items(self, items):
        # Create a status message
        if len(items) == 1:
            status_msg = f"Removed '{items[0].data(Qt.DisplayRole)}'"
        else:
            status_msg = f"Removed {len(items)} items"

        # Unload from player
        self.player.unload_media(items=items)

        # Remove from model
        start_row = self.model().indexFromItem(items[0]).row()
        num_rows = len(items)
        self.model().removeRows(start_row, num_rows)

        # Push status message
        self.status_bar.showMessage(status_msg)