import enum import logging import os import platform import signal import sys import typing from typing import List, Optional import click import colorama # FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details. if typing.TYPE_CHECKING: from PySide2 import QtCore, QtGui, QtWidgets else: try: from PySide6 import QtCore, QtGui, QtWidgets except ImportError: from PySide2 import QtCore, QtGui, QtWidgets from .. import args, errors from ..document import Document from ..isolation_provider.container import Container from ..isolation_provider.dummy import Dummy from ..isolation_provider.qubes import Qubes, is_qubes_native_conversion from ..util import get_resource_path, get_version from .logic import DangerzoneGui from .main_window import MainWindow from .updater import UpdaterThread log = logging.getLogger(__name__) class OSColorMode(enum.Enum): """ Operating system color mode, e.g. Light or Dark Mode on macOS 10.14+ or Windows 10+. The enum values are used as the names of Qt properties that will be selected by QSS property selectors to set color-mode-specific style rules. """ LIGHT = "light" DARK = "dark" class Application(QtWidgets.QApplication): document_selected = QtCore.Signal(list) application_activated = QtCore.Signal() def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: super(Application, self).__init__(*args, **kwargs) self.setQuitOnLastWindowClosed(False) with open(get_resource_path("dangerzone.css"), "r") as f: style = f.read() self.setStyleSheet(style) # Needed under certain windowing systems to match the application to the # desktop entry in order to display the correct application name and icon # and to allow identifying windows that belong to the application (e.g. # under Wayland it sets the correct app ID). The value is the name of the # Dangerzone .desktop file. self.setDesktopFileName("press.freedom.dangerzone") # In some combinations of window managers and OSes, if we don't set an # application name, then the window manager may report it as `python3` or # `__init__.py`. Always set this to `dangerzone`, which corresponds to the # executable name as well. # See: https://github.com/freedomofpress/dangerzone/issues/402 self.setApplicationName("dangerzone") self.original_event = self.event def monkeypatch_event(arg__1: QtCore.QEvent) -> bool: event = arg__1 # oddly Qt calls internally event by "arg__1" # In macOS, handle the file open event if isinstance(event, QtGui.QFileOpenEvent): # Skip file open events in dev mode if not hasattr(sys, "dangerzone_dev"): self.document_selected.emit([event.file()]) return True elif event.type() == QtCore.QEvent.ApplicationActivate: self.application_activated.emit() return True return self.original_event(event) self.event = monkeypatch_event # type: ignore [method-assign] self.os_color_mode = self.infer_os_color_mode() log.debug(f"Inferred system color scheme as {self.os_color_mode}") def infer_os_color_mode(self) -> OSColorMode: """ Qt 6.5+ explicitly provides the OS color scheme via QStyleHints.colorScheme(), but we still need to support PySide2/Qt 5, so instead we infer the OS color scheme from the default palette. """ text_color, window_color = ( self.palette().color(role) for role in (QtGui.QPalette.WindowText, QtGui.QPalette.Window) ) if text_color.lightness() > window_color.lightness(): return OSColorMode.DARK return OSColorMode.LIGHT @click.command() @click.option( "--unsafe-dummy-conversion", "dummy_conversion", flag_value=True, hidden=True ) @click.argument( "filenames", required=False, nargs=-1, type=click.UNPROCESSED, callback=args.validate_input_filenames, ) @click.version_option(version=get_version(), message="%(version)s") @errors.handle_document_errors def gui_main(dummy_conversion: bool, filenames: Optional[List[str]]) -> bool: setup_logging() if platform.system() == "Darwin": # Required for macOS Big Sur: https://stackoverflow.com/a/64878899 os.environ["QT_MAC_WANTS_LAYER"] = "1" # Make sure /usr/local/bin is in the path os.environ["PATH"] = "/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin" # Don't show ANSI colors from stdout output, to prevent terminal # colors from breaking the macOS GUI app colorama.deinit() # Create the Qt app app = Application() # Common objects if getattr(sys, "dangerzone_dev", False) and dummy_conversion: dummy = Dummy() dangerzone = DangerzoneGui(app, isolation_provider=dummy) elif is_qubes_native_conversion(): qubes = Qubes() dangerzone = DangerzoneGui(app, isolation_provider=qubes) else: container = Container() dangerzone = DangerzoneGui(app, isolation_provider=container) # Allow Ctrl-C to smoothly quit the program instead of throwing an exception signal.signal(signal.SIGINT, signal.SIG_DFL) def open_files(filenames: List[str] = []) -> None: documents = [Document(filename) for filename in filenames] window.content_widget.doc_selection_widget.documents_selected.emit(documents) window = MainWindow(dangerzone) # Check for updates log.debug("Setting up Dangerzone updater") updater = UpdaterThread(dangerzone) window.register_update_handler(updater.finished) log.debug("Consulting updater settings before checking for updates") if updater.should_check_for_updates(): log.debug("Checking for updates") updater.start() else: log.debug("Will not check for updates, based on updater settings") # Ensure the status of the toggle updates checkbox is updated, after the user is # prompted to enable updates. window.toggle_updates_action.setChecked(bool(updater.check)) if filenames: open_files(filenames) # MacOS: Open a new window, if all windows are closed def application_activated() -> None: window.show() # If we get a file open event, open it app.document_selected.connect(open_files) # If the application is activated and all windows are closed, open a new one app.application_activated.connect(application_activated) # Launch the GUI ret = app.exec_() sys.exit(ret) def setup_logging() -> None: logging.basicConfig(level=logging.DEBUG, format="[%(levelname)s] %(message)s") args.override_parser_and_check_suspicious_options(gui_main)