diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index c25ab2076..6a109da50 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -78,7 +78,8 @@ from electrum import constants from electrum.gui.common_qt.i18n import ElectrumTranslator from electrum.gui.messages import TERMS_OF_USE_LATEST_VERSION -from .util import read_QIcon, ColorScheme, custom_message_box, MessageBoxMixin, WWLabel +from .util import (read_QIcon, ColorScheme, custom_message_box, MessageBoxMixin, WWLabel, + set_windows_os_screenshot_protection_drm_flag) from .main_window import ElectrumWindow from .network_dialog import NetworkDialog from .stylesheet_patcher import patch_qt_stylesheet @@ -105,6 +106,20 @@ class OpenFileEventFilter(QObject): return False +class ScreenshotProtectionEventFilter(QObject): + def __init__(self): + super().__init__() + + def eventFilter(self, obj, event): + if ( + event.type() == QtCore.QEvent.Type.Show + and isinstance(obj, QWidget) + and obj.isWindow() + ): + set_windows_os_screenshot_protection_drm_flag(obj) + return False + + class QElectrumApplication(QApplication): new_window_signal = pyqtSignal(str, object) quit_signal = pyqtSignal() @@ -136,9 +151,12 @@ class ElectrumGui(BaseElectrumGui, Logger): QGuiApplication.setApplicationName("Electrum") self.gui_thread = threading.current_thread() self.windows = [] # type: List[ElectrumWindow] - self.efilter = OpenFileEventFilter(self.windows) + self.open_file_efilter = OpenFileEventFilter(self.windows) self.app = QElectrumApplication(sys.argv) - self.app.installEventFilter(self.efilter) + self.app.installEventFilter(self.open_file_efilter) + self.screenshot_protection_efilter = ScreenshotProtectionEventFilter() + if sys.platform in ['win32', 'windows'] and self.config.GUI_QT_SCREENSHOT_PROTECTION: + self.app.installEventFilter(self.screenshot_protection_efilter) self.app.setWindowIcon(read_QIcon("electrum.png")) self.translator = ElectrumTranslator() self.app.installTranslator(self.translator) @@ -247,8 +265,11 @@ class ElectrumGui(BaseElectrumGui, Logger): return self._cleaned_up = True self.app.new_window_signal.disconnect() - self.app.removeEventFilter(self.efilter) - self.efilter = None + self.app.removeEventFilter(self.open_file_efilter) + self.open_file_efilter = None + # it is save to remove the filter, even if it has not been installed + self.app.removeEventFilter(self.screenshot_protection_efilter) + self.screenshot_protection_efilter = None # If there are still some open windows, try to clean them up. for window in list(self.windows): window.close() diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index beda9ec57..ca7fba8a5 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -24,6 +24,7 @@ # SOFTWARE. import ast +import sys from typing import TYPE_CHECKING, Dict from PyQt6.QtCore import Qt @@ -262,6 +263,20 @@ class SettingsDialog(QDialog, QtEventListener): self.need_restart = True filelogging_cb.stateChanged.connect(on_set_filelogging) + screenshot_protection_cb = checkbox_from_configvar( + self.config.cv.GUI_QT_SCREENSHOT_PROTECTION + ) + screenshot_protection_cb.setChecked(self.config.GUI_QT_SCREENSHOT_PROTECTION) + if sys.platform not in ['windows', 'win32']: + screenshot_protection_cb.setChecked(False) + screenshot_protection_cb.setDisabled(True) + screenshot_protection_cb.setToolTip(_("This option is only available on Windows")) + + def on_set_screenshot_protection(_x): + self.config.GUI_QT_SCREENSHOT_PROTECTION = screenshot_protection_cb.isChecked() + self.need_restart = True + screenshot_protection_cb.stateChanged.connect(on_set_screenshot_protection) + block_explorers = sorted(util.block_explorer_info().keys()) BLOCK_EX_CUSTOM_ITEM = _("Custom URL") if BLOCK_EX_CUSTOM_ITEM in block_explorers: # malicious translation? @@ -386,6 +401,7 @@ class SettingsDialog(QDialog, QtEventListener): misc_widgets = [] misc_widgets.append((updatecheck_cb, None)) misc_widgets.append((filelogging_cb, None)) + misc_widgets.append((screenshot_protection_cb, None)) misc_widgets.append((alias_label, self.alias_e)) misc_widgets.append((qr_label, qr_combo)) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 454cca711..f744d6333 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -6,6 +6,7 @@ import platform import queue import os import webbrowser +import ctypes from functools import partial, lru_cache, wraps from typing import (NamedTuple, Callable, Optional, TYPE_CHECKING, List, Any, Sequence, Tuple, Union) @@ -1532,6 +1533,21 @@ def insert_spaces(text: str, every_chars: int) -> str: return ' '.join(text[i:i+every_chars] for i in range(0, len(text), every_chars)) +def set_windows_os_screenshot_protection_drm_flag(window: QWidget) -> None: + """ + sets the windows WDA_MONITOR flag on the window so windows prevents capturing + screenshots and microsoft recall will not be able to record the window + https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowdisplayaffinity + """ + if sys.platform not in ('win32', 'windows'): + return + try: + window_id = int(window.winId()) + WDA_MONITOR = 0x01 + ctypes.windll.user32.SetWindowDisplayAffinity(window_id, WDA_MONITOR) + except Exception: + _logger.exception(f"failed to set windows screenshot protection flag") + class _ABCQObjectMeta(type(QObject), ABCMeta): pass class _ABCQWidgetMeta(type(QWidget), ABCMeta): pass class AbstractQObject(QObject, ABC, metaclass=_ABCQObjectMeta): pass diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 5daf25e87..63404a5bc 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -800,6 +800,14 @@ Warning: setting this to too low will result in lots of payment failures."""), GUI_QT_SHOW_TAB_CONTACTS = ConfigVar('show_contacts_tab', default=False, type_=bool) GUI_QT_SHOW_TAB_CONSOLE = ConfigVar('show_console_tab', default=False, type_=bool) GUI_QT_SHOW_TAB_NOTES = ConfigVar('show_notes_tab', default=False, type_=bool) + GUI_QT_SCREENSHOT_PROTECTION = ConfigVar( + 'screenshot_protection', default=True, type_=bool, + short_desc=lambda: _("Prevent screenshots"), + # currently this option is Windows only, so the description can be specific to Windows + long_desc=lambda: _( + 'Signals Windows to disallow recordings and screenshots of the application window. ' + 'There is no guarantee Windows will respect this signal.'), + ) GUI_QML_PREFERRED_REQUEST_TYPE = ConfigVar('preferred_request_type', default='bolt11', type_=str) GUI_QML_USER_KNOWS_PRESS_AND_HOLD = ConfigVar('user_knows_press_and_hold', default=False, type_=bool)