client/securedrop_client/gui/main.py (115 lines of code) (raw):

""" Contains the core UI class for the application. All interactions with the UI go through an instance of this class. Copyright (C) 2018 The Freedom of the Press Foundation. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. """ import logging from gettext import gettext as _ from PyQt5.QtCore import Qt from PyQt5.QtGui import QClipboard, QGuiApplication, QIcon from PyQt5.QtWidgets import QAction, QApplication, QHBoxLayout, QMainWindow, QVBoxLayout, QWidget from securedrop_client import __version__, state from securedrop_client.db import Source, User from securedrop_client.gui.auth import LoginDialog from securedrop_client.gui.shortcuts import Shortcuts from securedrop_client.gui.widgets import BottomPane, LeftPane, MainView from securedrop_client.logic import Controller from securedrop_client.resources import load_all_fonts, load_css, load_icon logger = logging.getLogger(__name__) class Window(QMainWindow): """ Represents the application's main window that will contain the UI widgets. All interactions with the UI go through the object created by this class. """ icon = "icon.png" def __init__( self, app_state: state.State | None = None, ) -> None: """ Create the default start state. The window contains a root widget into which is placed: * A main-view widget, itself containing a list view for sources and a place for details / message contents / forms. * A status bar widget at the bottom, containing network and error status information. """ super().__init__() load_all_fonts() self.setStyleSheet(load_css("sdclient.css")) self.setWindowTitle(_("SecureDrop Client {}").format(__version__)) self.setWindowIcon(load_icon(self.icon)) # Bottom Pane to display activity and error messages self.bottom_pane = BottomPane() # Main Pane to display everything else self.main_pane = QWidget() self.setObjectName("MainWindow") layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self.main_pane.setLayout(layout) self.left_pane = LeftPane() self.main_view = MainView(self.main_pane, app_state) layout.addWidget(self.left_pane) layout.addWidget(self.main_view) # Set the main window's central widget to show Main Pane and Bottom Pane self.central_widget = QWidget() central_widget_layout = QVBoxLayout() central_widget_layout.setContentsMargins(0, 0, 0, 0) central_widget_layout.setSpacing(0) self.central_widget.setLayout(central_widget_layout) self.setCentralWidget(self.central_widget) central_widget_layout.addWidget(self.main_pane) central_widget_layout.addWidget(self.bottom_pane) # Dialogs self.login_dialog: LoginDialog | None = None # Actions quit = QAction(_("Quit"), self) quit.setIcon(QIcon.fromTheme("application-exit")) quit.setShortcut(Shortcuts.QUIT.value) quit.triggered.connect(self.close) self.addAction(quit) def setup(self, controller: Controller) -> None: """ Create references to the controller logic and instantiate the various views used in the UI. """ self.controller = controller self.bottom_pane.setup(self.controller) self.left_pane.setup(self, self.controller) self.main_view.setup(self.controller) # Listen for changes to the selected sources in sourcelist self.main_view.source_list.selected_sources.connect( self.controller.on_receive_selected_sources ) self.show_login() def show_main_window(self, db_user: User | None = None) -> None: """ Show main application window. """ if self.isHidden(): if not self.controller.qubes: self.showMaximized() else: self.setWindowState(Qt.WindowFullScreen) self.show() if db_user: self.set_logged_in_as(db_user) def show_login(self, error: str = "") -> None: """ Show the login form. """ self.login_dialog = LoginDialog(self) # Always display the login dialog centered in the screen. screen_size = QGuiApplication.primaryScreen().availableGeometry() login_dialog_size = self.login_dialog.geometry() x_center = int((screen_size.width() - login_dialog_size.width()) / 2) y_center = int((screen_size.height() - login_dialog_size.height()) / 2) self.login_dialog.move(x_center, y_center) self.login_dialog.setup(self.controller) self.login_dialog.reset() if error: self.login_dialog.error(error) self.login_dialog.show() def show_login_error(self, error: str) -> None: """ Display an error in the login dialog. """ if self.login_dialog and error: self.login_dialog.error(error) def hide_login(self) -> None: """ Kill the login dialog. """ if self.login_dialog is not None: self.login_dialog.accept() self.login_dialog = None def refresh_current_source_conversation(self) -> None: """ Update the current conversation if the source collection has changed. """ self.main_view.refresh_source_conversations() def show_sources(self, sources: list[Source]) -> None: """ Update the left hand sources list in the UI with the passed in list of sources. """ self.main_view.show_sources(sources) def show_last_sync(self, updated_on): # type: ignore[no-untyped-def] """ Display a message indicating the time of last sync with the server. """ if updated_on: self.update_sync_status(_("Last Refresh: {}").format(updated_on.humanize())) else: self.update_sync_status(_("Last Refresh: never")) def set_logged_in_as(self, db_user: User) -> None: """ Update the UI to show user logged in with username. """ self.left_pane.set_logged_in_as(db_user) self.bottom_pane.set_logged_in() self.main_view.set_logged_in() def logout(self) -> None: """ Update the UI to show the user is logged out. """ self.left_pane.set_logged_out() self.bottom_pane.set_logged_out() self.main_view.set_logged_out() def update_sync_status(self, message: str, duration: int = 0) -> None: """ Display an activity status message to the user. Optionally, supply a duration (in milliseconds), the default will continuously show the message. """ self.bottom_pane.update_sync_status(message, duration) def update_activity_status(self, message: str, duration: int = 0) -> None: """ Display an activity status message to the user. Optionally, supply a duration (in milliseconds), the default will continuously show the message. """ self.bottom_pane.update_activity_status(message, duration) def update_error_status(self, message: str, duration: int = 10000) -> None: """ Display an error status message to the user. Optionally, supply a duration (in milliseconds), the default will continuously show the message. """ self.bottom_pane.update_error_status(message, duration) def clear_error_status(self) -> None: """ Clear any message currently in the error status bar. """ self.bottom_pane.clear_error_status() def clear_clipboard(self) -> None: """ Purge any clipboard contents. """ cb = QApplication.clipboard() cb.clear() cb.clear(QClipboard.Selection)