class MainWindow(QMainWindow): def __init__(self, gui): # noqa: max-complexity super().__init__() self.gui = gui self.gateways = [] self.welcome_dialog = None self.recovery_key_exporter = None self.setWindowTitle(APP_NAME) self.setMinimumSize(QSize(600, 400)) self.setUnifiedTitleAndToolBarOnMac(True) self.setContextMenuPolicy(Qt.NoContextMenu) if sys.platform == "darwin": # To disable the broken/buggy "full screen" mode on macOS. # See https://github.com/gridsync/gridsync/issues/241 self.setWindowFlags(Qt.Dialog) grid_invites_enabled = True invites_enabled = True multiple_grids_enabled = True features_settings = settings.get("features") if features_settings: grid_invites = features_settings.get("grid_invites") if grid_invites and grid_invites.lower() == "false": grid_invites_enabled = False invites = features_settings.get("invites") if invites and invites.lower() == "false": invites_enabled = False multiple_grids = features_settings.get("multiple_grids") if multiple_grids and multiple_grids.lower() == "false": multiple_grids_enabled = False if multiple_grids_enabled: self.shortcut_new = QShortcut(QKeySequence.New, self) self.shortcut_new.activated.connect(self.show_welcome_dialog) self.shortcut_open = QShortcut(QKeySequence.Open, self) self.shortcut_open.activated.connect(self.select_folder) self.shortcut_preferences = QShortcut(QKeySequence.Preferences, self) self.shortcut_preferences.activated.connect( self.gui.show_preferences_window) self.shortcut_close = QShortcut(QKeySequence.Close, self) self.shortcut_close.activated.connect(self.close) self.shortcut_quit = QShortcut(QKeySequence.Quit, self) self.shortcut_quit.activated.connect(self.confirm_quit) self.central_widget = CentralWidget(self.gui) self.setCentralWidget(self.central_widget) font = Font(8) folder_icon_default = QFileIconProvider().icon(QFileInfo(config_dir)) folder_icon_composite = CompositePixmap( folder_icon_default.pixmap(256, 256), resource("green-plus.png")) folder_icon = QIcon(folder_icon_composite) folder_action = QAction(folder_icon, "Add Folder", self) folder_action.setToolTip("Add a Folder...") folder_action.setFont(font) folder_action.triggered.connect(self.select_folder) if grid_invites_enabled: invites_action = QAction(QIcon(resource("invite.png")), "Invites", self) invites_action.setToolTip("Enter or Create an Invite Code") invites_action.setFont(font) enter_invite_action = QAction(QIcon(), "Enter Invite Code...", self) enter_invite_action.setToolTip("Enter an Invite Code...") enter_invite_action.triggered.connect(self.open_invite_receiver) create_invite_action = QAction(QIcon(), "Create Invite Code...", self) create_invite_action.setToolTip("Create on Invite Code...") create_invite_action.triggered.connect( self.open_invite_sender_dialog) invites_menu = QMenu(self) invites_menu.addAction(enter_invite_action) invites_menu.addAction(create_invite_action) invites_button = QToolButton(self) invites_button.setDefaultAction(invites_action) invites_button.setMenu(invites_menu) invites_button.setPopupMode(2) invites_button.setStyleSheet( "QToolButton::menu-indicator { image: none }") invites_button.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) elif invites_enabled: invite_action = QAction(QIcon(resource("invite.png")), "Enter Code", self) invite_action.setToolTip("Enter an Invite Code...") invite_action.setFont(font) invite_action.triggered.connect(self.open_invite_receiver) spacer_left = QWidget() spacer_left.setSizePolicy(QSizePolicy.Expanding, 0) self.combo_box = ComboBox() self.combo_box.currentIndexChanged.connect(self.on_grid_selected) if not multiple_grids_enabled: self.combo_box.hide() spacer_right = QWidget() spacer_right.setSizePolicy(QSizePolicy.Expanding, 0) history_action = QAction(QIcon(resource("time.png")), "History", self) history_action.setToolTip("Show/Hide History") history_action.setFont(font) history_action.triggered.connect(self.on_history_button_clicked) self.history_button = QToolButton(self) self.history_button.setDefaultAction(history_action) self.history_button.setCheckable(True) self.history_button.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) recovery_action = QAction(QIcon(resource("key.png")), "Recovery", self) recovery_action.setToolTip("Import or Export a Recovery Key") recovery_action.setFont(font) import_action = QAction(QIcon(), "Import Recovery Key...", self) import_action.setToolTip("Import Recovery Key...") import_action.triggered.connect(self.import_recovery_key) export_action = QAction(QIcon(), "Export Recovery Key...", self) export_action.setToolTip("Export Recovery Key...") export_action.setShortcut(QKeySequence.Save) export_action.triggered.connect(self.export_recovery_key) recovery_menu = QMenu(self) recovery_menu.addAction(import_action) recovery_menu.addAction(export_action) recovery_button = QToolButton(self) recovery_button.setDefaultAction(recovery_action) recovery_button.setMenu(recovery_menu) recovery_button.setPopupMode(2) recovery_button.setStyleSheet( "QToolButton::menu-indicator { image: none }") recovery_button.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) self.toolbar = self.addToolBar("") p = self.palette() dimmer_grey = BlendedColor(p.windowText().color(), p.window().color(), 0.7).name() if sys.platform != "darwin": self.toolbar.setStyleSheet(""" QToolBar {{ border: 0px }} QToolButton {{ color: {} }} """.format(dimmer_grey)) else: self.toolbar.setStyleSheet( "QToolButton {{ color: {} }}".format(dimmer_grey)) self.toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) self.toolbar.setIconSize(QSize(24, 24)) self.toolbar.setMovable(False) self.toolbar.addAction(folder_action) if grid_invites_enabled: self.toolbar.addWidget(invites_button) elif invites_enabled: self.toolbar.addAction(invite_action) self.toolbar.addWidget(spacer_left) self.toolbar.addWidget(self.combo_box) self.toolbar.addWidget(spacer_right) self.toolbar.addWidget(self.history_button) self.toolbar.addWidget(recovery_button) if sys.platform != "win32": # Text is getting clipped on Windows 10 for action in self.toolbar.actions(): widget = self.toolbar.widgetForAction(action) if isinstance(widget, QToolButton): widget.setMaximumWidth(68) self.active_invite_sender_dialogs = [] self.active_invite_receiver_dialogs = [] self.pending_news_message = () def populate(self, gateways): for gateway in gateways: if gateway not in self.gateways: self.central_widget.add_folders_view(gateway) self.central_widget.add_history_view(gateway) self.combo_box.add_gateway(gateway) self.gateways.append(gateway) gateway.newscap_checker.message_received.connect( self.on_message_received) gateway.newscap_checker.upgrade_required.connect( self.on_upgrade_required) def show_news_message(self, gateway, title, message): msgbox = QMessageBox(self) msgbox.setWindowModality(Qt.WindowModal) icon_filepath = os.path.join(gateway.nodedir, "icon") if os.path.exists(icon_filepath): msgbox.setIconPixmap(QIcon(icon_filepath).pixmap(64, 64)) elif os.path.exists(resource("tahoe-lafs.png")): msgbox.setIconPixmap( QIcon(resource("tahoe-lafs.png")).pixmap(64, 64)) else: msgbox.setIcon(QMessageBox.Information) if sys.platform == "darwin": msgbox.setText(title) msgbox.setInformativeText(message) else: msgbox.setWindowTitle(title) msgbox.setText(message) msgbox.show() try: self.gui.unread_messages.remove((gateway, title, message)) except ValueError: return self.gui.systray.update() def _maybe_show_news_message(self, gateway, title, message): self.gui.unread_messages.append((gateway, title, message)) self.gui.systray.update() if self.isVisible(): self.show_news_message(gateway, title, message) else: self.pending_news_message = (gateway, title, message) def on_message_received(self, gateway, message): title = "New message from {}".format(gateway.name) self.gui.show_message(title, strip_html_tags(message.replace("<p>", "\n\n"))) self._maybe_show_news_message(gateway, title, message) def on_upgrade_required(self, gateway): title = "Upgrade required" message = ( "A message was received from {} in an unsupported format. This " "suggests that you are running an out-of-date version of {}.\n\n" "To avoid seeing this warning, please upgrade to the latest " "version.".format(gateway.name, APP_NAME)) self._maybe_show_news_message(gateway, title, message) def current_view(self): try: w = self.central_widget.folders_views[self.combo_box.currentData()] except KeyError: return None return w.layout().itemAt(0).widget() def select_folder(self): self.show_folders_view() view = self.current_view() if view: view.select_folder() def set_current_grid_status(self): current_view = self.current_view() if not current_view: return self.gui.systray.update() def show_folders_view(self): try: self.central_widget.setCurrentWidget( self.central_widget.folders_views[ self.combo_box.currentData()]) except KeyError: pass self.set_current_grid_status() def show_history_view(self): try: self.central_widget.setCurrentWidget( self.central_widget.history_views[ self.combo_box.currentData()]) except KeyError: pass self.set_current_grid_status() def show_welcome_dialog(self): if self.welcome_dialog: self.welcome_dialog.close() self.welcome_dialog = WelcomeDialog(self.gui, self.gateways) self.welcome_dialog.show() self.welcome_dialog.raise_() def on_grid_selected(self, index): if index == self.combo_box.count() - 1: self.show_welcome_dialog() if not self.combo_box.currentData(): return if self.history_button.isChecked(): self.show_history_view() else: self.show_folders_view() self.setWindowTitle("{} - {}".format( APP_NAME, self.combo_box.currentData().name)) def confirm_export(self, path): if os.path.isfile(path): logging.info("Recovery Key successfully exported") info( self, "Export successful", "Recovery Key successfully exported to {}".format(path), ) else: logging.error("Error exporting Recovery Key; file not found.") error( self, "Error exporting Recovery Key", "Destination file not found after export: {}".format(path), ) def export_recovery_key(self, gateway=None): self.show_folders_view() if not gateway: gateway = self.combo_box.currentData() self.recovery_key_exporter = RecoveryKeyExporter(self) self.recovery_key_exporter.done.connect(self.confirm_export) self.recovery_key_exporter.do_export(gateway) def import_recovery_key(self): # XXX Quick hack for user-testing; change later self.welcome_dialog = WelcomeDialog(self.gui, self.gateways) self.welcome_dialog.on_restore_link_activated() def on_history_button_clicked(self): if not self.history_button.isChecked(): self.history_button.setChecked(True) self.show_history_view() else: self.history_button.setChecked(False) self.show_folders_view() def on_invite_received(self, gateway): self.populate([gateway]) for view in self.central_widget.views: view.model().monitor.scan_rootcap("star.png") def on_invite_closed(self, obj): try: self.active_invite_receiver_dialogs.remove(obj) except ValueError: pass def open_invite_receiver(self): invite_receiver_dialog = InviteReceiverDialog(self.gateways) invite_receiver_dialog.done.connect(self.on_invite_received) invite_receiver_dialog.closed.connect(self.on_invite_closed) invite_receiver_dialog.show() self.active_invite_receiver_dialogs.append(invite_receiver_dialog) def open_invite_sender_dialog(self): gateway = self.combo_box.currentData() if gateway: view = self.current_view() if view: invite_sender_dialog = InviteSenderDialog( gateway, self.gui, view.get_selected_folders()) else: invite_sender_dialog = InviteSenderDialog(gateway, self.gui) invite_sender_dialog.closed.connect( self.active_invite_sender_dialogs.remove) invite_sender_dialog.show() self.active_invite_sender_dialogs.append(invite_sender_dialog) def confirm_quit(self): folder_loading = False folder_syncing = False for model in [view.model() for view in self.central_widget.views]: for row in range(model.rowCount()): status = model.item(row, 1).data(Qt.UserRole) mtime = model.item(row, 2).data(Qt.UserRole) if not status and not mtime: # "Loading..." and not yet synced folder_loading = True break if status == 1: # "Syncing" folder_syncing = True break msg = QMessageBox(self) if folder_loading: msg.setIcon(QMessageBox.Warning) informative_text = ( "One or more folders have not finished loading. If these " "folders were recently added, you may need to add them again.") elif folder_syncing: msg.setIcon(QMessageBox.Warning) informative_text = ( "One or more folders are currently syncing. If you quit, any " "pending upload or download operations will be cancelled " "until you launch {} again.".format(APP_NAME)) else: msg.setIcon(QMessageBox.Question) informative_text = ( "If you quit, {} will stop synchronizing your folders until " "you launch it again.".format(APP_NAME)) if sys.platform == "darwin": msg.setText("Are you sure you wish to quit?") msg.setInformativeText(informative_text) else: msg.setWindowTitle("Exit {}?".format(APP_NAME)) msg.setText( "Are you sure you wish to quit? {}".format(informative_text)) msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) msg.setDefaultButton(QMessageBox.No) if msg.exec_() == QMessageBox.Yes: if sys.platform == "win32": self.gui.systray.hide() reactor.stop() def keyPressEvent(self, event): key = event.key() if key in (Qt.Key_Backspace, Qt.Key_Delete): view = self.current_view() selected = view.selectedIndexes() if view else None if selected: view.confirm_stop_syncing(view.get_selected_folders()) if key == Qt.Key_Escape: view = self.current_view() selected = view.selectedIndexes() if view else None if selected: for index in selected: view.selectionModel().select(index, QItemSelectionModel.Deselect) elif self.gui.systray.isSystemTrayAvailable(): self.hide() def closeEvent(self, event): if self.gui.systray.isSystemTrayAvailable(): event.accept() else: event.ignore() self.confirm_quit() def showEvent(self, _): if self.pending_news_message: gateway, title, message = self.pending_news_message self.pending_news_message = () QTimer.singleShot( 0, lambda: self.show_news_message(gateway, title, message))
class MainWindow(QMainWindow): def __init__(self, gui): super(MainWindow, self).__init__() self.gui = gui self.gateways = [] self.welcome_dialog = None self.recovery_key_exporter = None self.setWindowTitle(APP_NAME) self.setMinimumSize(QSize(600, 400)) self.setUnifiedTitleAndToolBarOnMac(True) self.shortcut_new = QShortcut(QKeySequence.New, self) self.shortcut_new.activated.connect(self.show_welcome_dialog) self.shortcut_open = QShortcut(QKeySequence.Open, self) self.shortcut_open.activated.connect(self.select_folder) self.shortcut_close = QShortcut(QKeySequence.Close, self) self.shortcut_close.activated.connect(self.close) self.shortcut_quit = QShortcut(QKeySequence.Quit, self) self.shortcut_quit.activated.connect(self.confirm_quit) self.central_widget = CentralWidget(self.gui) self.setCentralWidget(self.central_widget) font = QFont() if sys.platform == 'darwin': font.setPointSize(11) else: font.setPointSize(8) folder_icon_default = QFileIconProvider().icon(QFileInfo(config_dir)) folder_icon_composite = CompositePixmap( folder_icon_default.pixmap(256, 256), resource('green-plus.png')) folder_icon = QIcon(folder_icon_composite) folder_action = QAction(folder_icon, "Add folder", self) folder_action.setToolTip("Add a folder...") folder_action.setFont(font) folder_action.triggered.connect(self.select_folder) invite_action = QAction( QIcon(resource('invite.png')), "Enter Code", self) invite_action.setToolTip("Enter an Invite Code...") invite_action.setFont(font) invite_action.triggered.connect(self.open_invite_receiver) history_action = QAction( QIcon(resource('time.png')), 'History', self) history_action.setToolTip("View history") history_action.setFont(font) history_action.triggered.connect(self.on_history_button_clicked) self.history_button = QToolButton(self) self.history_button.setDefaultAction(history_action) self.history_button.setCheckable(True) self.history_button.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) spacer_left = QWidget() spacer_left.setSizePolicy(QSizePolicy.Expanding, 0) self.combo_box = ComboBox() self.combo_box.currentIndexChanged.connect(self.on_grid_selected) spacer_right = QWidget() spacer_right.setSizePolicy(QSizePolicy.Expanding, 0) share_action = QAction(QIcon(resource('share.png')), "Share", self) share_action.setToolTip("Share...") share_action.setFont(font) share_action.triggered.connect(self.open_invite_sender_dialog) recovery_action = QAction( QIcon(resource('key.png')), "Recovery", self) recovery_action.setToolTip("Import/Export Recovery Key...") recovery_action.setFont(font) import_action = QAction(QIcon(), "Import Recovery Key...", self) import_action.setToolTip("Import Recovery Key...") import_action.triggered.connect(self.import_recovery_key) export_action = QAction(QIcon(), "Export Recovery Key...", self) export_action.setToolTip("Export Recovery Key...") export_action.setShortcut(QKeySequence.Save) export_action.triggered.connect(self.export_recovery_key) recovery_menu = QMenu(self) recovery_menu.addAction(import_action) recovery_menu.addAction(export_action) recovery_button = QToolButton(self) recovery_button.setDefaultAction(recovery_action) recovery_button.setMenu(recovery_menu) recovery_button.setPopupMode(2) recovery_button.setStyleSheet( 'QToolButton::menu-indicator { image: none }') recovery_button.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) preferences_action = QAction( QIcon(resource('preferences.png')), "Preferences", self) preferences_action.setStatusTip("Preferences") preferences_action.setToolTip("Preferences") preferences_action.setFont(font) preferences_action.setShortcut(QKeySequence.Preferences) preferences_action.triggered.connect(self.gui.show_preferences_window) self.toolbar = self.addToolBar('') if sys.platform != 'darwin': self.toolbar.setStyleSheet(""" QToolBar { border: 0px } QToolButton { color: rgb(50, 50, 50) } """) else: self.toolbar.setStyleSheet( "QToolButton { color: rgb(50, 50, 50) }") self.toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) self.toolbar.setIconSize(QSize(24, 24)) self.toolbar.setMovable(False) self.toolbar.addAction(folder_action) self.toolbar.addAction(invite_action) self.toolbar.addWidget(self.history_button) self.toolbar.addWidget(spacer_left) self.toolbar.addWidget(self.combo_box) self.toolbar.addWidget(spacer_right) self.toolbar.addAction(share_action) self.toolbar.addWidget(recovery_button) self.toolbar.addAction(preferences_action) if sys.platform != 'win32': # Text is getting clipped on Windows 10 for action in self.toolbar.actions(): widget = self.toolbar.widgetForAction(action) if isinstance(widget, QToolButton): widget.setMaximumWidth(68) self.active_invite_sender_dialogs = [] self.active_invite_receiver_dialogs = [] def populate(self, gateways): for gateway in gateways: if gateway not in self.gateways: self.central_widget.add_folders_view(gateway) self.central_widget.add_history_view(gateway) self.combo_box.add_gateway(gateway) self.gateways.append(gateway) self.gui.systray.menu.populate() def current_view(self): try: w = self.central_widget.folders_views[self.combo_box.currentData()] except KeyError: return None return w.layout().itemAt(0).widget() def select_folder(self): self.show_folders_view() view = self.current_view() if view: view.select_folder() def set_current_grid_status(self): current_view = self.current_view() if not current_view: return self.gui.systray.update() def show_folders_view(self): try: self.central_widget.setCurrentWidget( self.central_widget.folders_views[self.combo_box.currentData()] ) except KeyError: pass self.set_current_grid_status() def show_history_view(self): try: self.central_widget.setCurrentWidget( self.central_widget.history_views[self.combo_box.currentData()] ) except KeyError: pass self.set_current_grid_status() def show_welcome_dialog(self): if self.welcome_dialog: self.welcome_dialog.close() self.welcome_dialog = WelcomeDialog(self.gui, self.gateways) self.welcome_dialog.show() self.welcome_dialog.raise_() def on_grid_selected(self, index): if index == self.combo_box.count() - 1: self.show_welcome_dialog() if not self.combo_box.currentData(): return if self.history_button.isChecked(): self.show_history_view() else: self.show_folders_view() self.setWindowTitle( "{} - {}".format(APP_NAME, self.combo_box.currentData().name) ) def confirm_export(self, path): if os.path.isfile(path): info( self, "Export successful", "Recovery Key successfully exported to {}".format(path)) else: error( self, "Error exporting Recovery Key", "Destination file not found after export: {}".format(path)) def export_recovery_key(self, gateway=None): self.show_folders_view() if not gateway: gateway = self.combo_box.currentData() self.recovery_key_exporter = RecoveryKeyExporter(self) self.recovery_key_exporter.done.connect(self.confirm_export) self.recovery_key_exporter.do_export(gateway) def import_recovery_key(self): # XXX Quick hack for user-testing; change later self.welcome_dialog = WelcomeDialog(self.gui, self.gateways) self.welcome_dialog.on_restore_link_activated() def on_history_button_clicked(self): if not self.history_button.isChecked(): self.history_button.setChecked(True) self.show_history_view() else: self.history_button.setChecked(False) self.show_folders_view() def on_invite_received(self, gateway): self.populate([gateway]) for view in self.central_widget.views: view.model().monitor.scan_rootcap('star.png') def on_invite_closed(self, obj): try: self.active_invite_receiver_dialogs.remove(obj) except ValueError: pass def open_invite_receiver(self): invite_receiver_dialog = InviteReceiverDialog(self.gateways) invite_receiver_dialog.done.connect(self.on_invite_received) invite_receiver_dialog.closed.connect(self.on_invite_closed) invite_receiver_dialog.show() self.active_invite_receiver_dialogs.append(invite_receiver_dialog) def open_invite_sender_dialog(self): gateway = self.combo_box.currentData() if gateway: view = self.current_view() if view: invite_sender_dialog = InviteSenderDialog( gateway, self.gui, view.get_selected_folders()) else: invite_sender_dialog = InviteSenderDialog(gateway, self.gui) invite_sender_dialog.closed.connect( self.active_invite_sender_dialogs.remove) invite_sender_dialog.show() self.active_invite_sender_dialogs.append(invite_sender_dialog) def confirm_quit(self): folder_loading = False folder_syncing = False for model in [view.model() for view in self.central_widget.views]: for row in range(model.rowCount()): status = model.item(row, 1).data(Qt.UserRole) mtime = model.item(row, 2).data(Qt.UserRole) if not status and not mtime: # "Loading..." and not yet synced folder_loading = True break elif status == 1: # "Syncing" folder_syncing = True break msg = QMessageBox(self) if folder_loading: msg.setIcon(QMessageBox.Warning) informative_text = ( "One or more folders have not finished loading. If these " "folders were recently added, you may need to add them again.") elif folder_syncing: msg.setIcon(QMessageBox.Warning) informative_text = ( "One or more folders are currently syncing. If you quit, any " "pending upload or download operations will be cancelled " "until you launch {} again.".format(APP_NAME)) else: msg.setIcon(QMessageBox.Question) informative_text = ( "If you quit, {} will stop synchronizing your folders until " "you launch it again.".format(APP_NAME)) if sys.platform == 'darwin': msg.setText("Are you sure you wish to quit?") msg.setInformativeText(informative_text) else: msg.setWindowTitle("Exit {}?".format(APP_NAME)) msg.setText( "Are you sure you wish to quit? {}".format(informative_text)) msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) msg.setDefaultButton(QMessageBox.No) if msg.exec_() == QMessageBox.Yes: if sys.platform == 'win32': self.gui.systray.hide() reactor.stop() def keyPressEvent(self, event): key = event.key() if key in (Qt.Key_Backspace, Qt.Key_Delete): view = self.current_view() selected = (view.selectedIndexes() if view else None) if selected: view.confirm_remove(view.get_selected_folders()) if key == Qt.Key_Escape: view = self.current_view() selected = (view.selectedIndexes() if view else None) if selected: for index in selected: view.selectionModel().select( index, QItemSelectionModel.Deselect) elif self.gui.systray.isSystemTrayAvailable(): self.hide() def closeEvent(self, event): if self.gui.systray.isSystemTrayAvailable(): event.accept() else: event.ignore() self.confirm_quit()