Skip to content

Commit

Permalink
refactor(screengrab): use protocols and similar logic as for capture
Browse files Browse the repository at this point in the history
  • Loading branch information
dynobo committed Jan 20, 2024
1 parent 8e2d689 commit a0af2db
Show file tree
Hide file tree
Showing 29 changed files with 517 additions and 454 deletions.
2 changes: 2 additions & 0 deletions normcap/clipboard/system_info.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import functools
import logging
import os
import re
Expand All @@ -24,6 +25,7 @@ def os_has_awesome_wm() -> bool:
return "awesome" in os.environ.get("XDG_CURRENT_DESKTOP", "").lower()


@functools.cache
def get_gnome_version() -> str:
"""Detect Gnome version of current session.
Expand Down
12 changes: 6 additions & 6 deletions normcap/screengrab/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from normcap.screengrab import main
from normcap.screengrab.main import capture
from normcap.screengrab.main import Handler, capture, get_available_handlers
from normcap.screengrab.permissions import (
dbus_portal_show_request_permission_dialog,
has_screenshot_permission,
Expand All @@ -9,11 +8,12 @@
)

__all__ = [
"dbus_portal_show_request_permission_dialog",
"request_screenshot_permission",
"capture",
"main",
"dbus_portal_show_request_permission_dialog",
"get_available_handlers",
"Handler",
"has_screenshot_permission",
"macos_show_request_permission_dialog",
"macos_reset_screenshot_permission",
"macos_show_request_permission_dialog",
"request_screenshot_permission",
]
18 changes: 0 additions & 18 deletions normcap/screengrab/exceptions.py

This file was deleted.

Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@
import logging
import random
import re
import sys
from typing import Optional
from urllib.parse import urlparse

from PySide6 import QtCore, QtDBus, QtGui

from normcap.screengrab.exceptions import (
from normcap.screengrab import system_info
from normcap.screengrab.post_processing import split_full_desktop_to_screens
from normcap.screengrab.structures import (
ScreenshotPermissionError,
ScreenshotRequestError,
ScreenshotResponseError,
ScreenshotTimeoutError,
)
from normcap.screengrab.utils import split_full_desktop_to_screens

logger = logging.getLogger(__name__)

install_instructions = ""

# Note on Request Timeout:
#
Expand Down Expand Up @@ -170,7 +173,7 @@ def got_signal(self, message: QtDBus.QDBusMessage) -> None:
self.on_result.emit(uri)


def _synchronized_capture(interactive: bool) -> list[QtGui.QImage]:
def _synchronized_capture(interactive: bool) -> QtGui.QImage:
loop = QtCore.QEventLoop()
result = []
exceptions = []
Expand Down Expand Up @@ -200,8 +203,16 @@ def _exception_triggered(uri: Exception) -> None:
raise error

uri = result[0]
full_image = QtGui.QImage(urlparse(uri).path)
return split_full_desktop_to_screens(full_image)
return QtGui.QImage(urlparse(uri).path)


def is_compatible() -> bool:
return sys.platform == "linux"


def is_installed() -> bool:
gnome_version = system_info.get_gnome_version()
return not gnome_version or gnome_version >= "41"


def capture() -> list[QtGui.QImage]:
Expand All @@ -217,11 +228,9 @@ def capture() -> list[QtGui.QImage]:
1. Try none-interactive mode
2. If timeout triggers, retry in interactive mode with a helper window
"""
result: list[QtGui.QImage] = []

try:
result = _synchronized_capture(interactive=False)
image = _synchronized_capture(interactive=False)
except TimeoutError as exc:
raise ScreenshotTimeoutError("Timeout when taking screenshot!") from exc
else:
return result
return split_full_desktop_to_screens(image)
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
except ImportError:
QtDBus = cast(Any, None)

from normcap.screengrab.utils import split_full_desktop_to_screens
from normcap.screengrab.post_processing import split_full_desktop_to_screens

logger = logging.getLogger(__name__)

install_instructions = ""


def _get_screenshot_interface(): # noqa: ANN202
if not QtDBus:
Expand Down Expand Up @@ -49,6 +51,14 @@ def _fullscreen_to_file(filename: Union[os.PathLike, str]) -> None:
logger.error("Invalid dbus interface")


def is_compatible() -> bool:
return True


def is_installed() -> bool:
return True


def capture() -> list[QtGui.QImage]:
"""Capture screenshots for all screens using org.gnome.Shell.Screenshot.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import logging
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

from PySide6 import QtGui

from normcap.screengrab.utils import split_full_desktop_to_screens
from normcap.screengrab.post_processing import split_full_desktop_to_screens

logger = logging.getLogger(__name__)

install_instructions = ""


def is_compatible() -> bool:
return sys.platform == "linux"


def is_installed() -> bool:
return bool(shutil.which("grim"))


def capture() -> list[QtGui.QImage]:
"""Capture screenshot with the grim CLI tool for wayland.
Expand Down
13 changes: 13 additions & 0 deletions normcap/screengrab/qt.py → normcap/screengrab/handlers/qt.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import logging
import sys

from PySide6 import QtGui, QtWidgets

from normcap.screengrab import system_info

logger = logging.getLogger(__name__)

install_instructions = ""


def is_compatible() -> bool:
return sys.platform != "linux" or not system_info.os_has_wayland_display_manager()


def is_installed() -> bool:
return True


def capture() -> list[QtGui.QImage]:
"""Capture screenshot with QT method and Screen object.
Expand Down
103 changes: 76 additions & 27 deletions normcap/screengrab/main.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,94 @@
import logging
import sys
from typing import Protocol

from PySide6 import QtGui

from normcap.screengrab import utils
from normcap.screengrab.handlers import dbus_portal, dbus_shell, grim, qt
from normcap.screengrab.structures import Handler, HandlerProtocol

logger = logging.getLogger(__name__)


class CaptureFunc(Protocol):
def __call__(self) -> list[QtGui.QImage]:
... # pragma: no cover
_capture_handlers: dict[Handler, HandlerProtocol] = {
Handler.QT: qt,
Handler.DBUS_PORTAL: dbus_portal,
Handler.DBUS_SHELL: dbus_shell,
Handler.GRIM: grim,
}


def get_capture_func() -> CaptureFunc:
# fmt: off
if sys.platform != "linux" or not utils.is_wayland_display_manager():
logger.debug("Select capture method QT")
from normcap.screengrab import qt
return qt.capture
def get_available_handlers() -> list[Handler]:
compatible_handlers = [h for h in Handler if _capture_handlers[h].is_compatible()]
logger.debug(
"Compatible capture handlers: %s", [h.name for h in compatible_handlers]
)

if utils.has_grim_support():
logger.debug("Select capture method grim")
from normcap.screengrab import grim
return grim.capture
available_handlers = [
n for n in compatible_handlers if _capture_handlers[n].is_installed()
]
logger.debug("Available capture handlers: %s", [h.name for h in available_handlers])

# TODO: implement gnome-screenshot method
if not compatible_handlers:
logger.error(
"None of the implemented capture handlers is compatible with this system!"
)
return []

if utils.has_dbus_portal_support():
logger.debug("Select capture method DBUS portal")
from normcap.screengrab import dbus_portal
return dbus_portal.capture
if not available_handlers:
logger.error(
"No working capture handler found for your system. "
"The preferred handler on your system would be %s but can't be "
"used due to missing dependencies. %s",
compatible_handlers[0].name,
_capture_handlers[compatible_handlers[0]].install_instructions,
)
return []

if compatible_handlers[0] != available_handlers[0]:
logger.warning(
"The preferred capture handler on your system would be %s but can't be "
"used due to missing dependencies. %s",
compatible_handlers[0].name,
_capture_handlers[compatible_handlers[0]].install_instructions,
)

logger.debug("Select capture method DBUS shell")
from normcap.screengrab import dbus_shell
return dbus_shell.capture
# fmt: on
return available_handlers


def _capture(handler: Handler) -> list[QtGui.QImage]:
capture_handler = _capture_handlers[handler]
if not capture_handler.is_compatible():
logger.warning("%s's capture() called on incompatible system!", handler.name)
try:
images = capture_handler.capture()
except Exception:
logger.exception("%s's capture() failed!", handler.name)
return []
else:
logger.info("Screen captured using %s", handler.name)
return images


def capture_with_handler(handler_name: str) -> list[QtGui.QImage]:
"""Capture screen using a specific handler.
Args:
handler_name: Name of one of the supported capture methods.
Returns:
Single image for every screen.
"""
return _capture(handler=Handler[handler_name.upper()])


def capture() -> list[QtGui.QImage]:
capture_func = get_capture_func()
return capture_func()
"""Capture screen using compatible handlers.
Returns:
Single image for every screen.
"""
for handler in get_available_handlers():
if images := _capture(handler=handler):
return images

logger.error("Unable to capture screen! (Increase log-level for details)")
return []
16 changes: 8 additions & 8 deletions normcap/screengrab/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

from PySide6 import QtGui, QtWidgets

from normcap.screengrab import exceptions, utils
from normcap.screengrab import structures, system_info

try:
from normcap.screengrab import dbus_portal
from normcap.screengrab.handlers import dbus_portal

except ImportError:
dbus_portal = cast(Any, None)
Expand Down Expand Up @@ -199,8 +199,8 @@ def _dbus_portal_has_screenshot_permission() -> bool:
try:
result = dbus_portal.capture()
except (
exceptions.ScreenshotPermissionError,
exceptions.ScreenshotTimeoutError,
structures.ScreenshotPermissionError,
structures.ScreenshotTimeoutError,
) as exc:
logger.warning("Screenshot permissions on Wayland seem missing.", exc_info=exc)
return len(result) > 0
Expand All @@ -225,9 +225,9 @@ def has_screenshot_permission() -> bool:
logger.debug("Checking screenshot permission")
if sys.platform == "darwin":
return _macos_has_screenshot_permission()
if sys.platform == "linux" and not utils.is_wayland_display_manager():
if sys.platform == "linux" and not system_info.os_has_wayland_display_manager():
return True
if sys.platform == "linux" and utils.is_wayland_display_manager():
if sys.platform == "linux" and system_info.os_has_wayland_display_manager():
return _dbus_portal_has_screenshot_permission()
if sys.platform == "win32":
return True
Expand All @@ -245,14 +245,14 @@ def request_screenshot_permission(
)
return

if sys.platform == "linux" and not utils.is_wayland_display_manager():
if sys.platform == "linux" and not system_info.os_has_wayland_display_manager():
logger.debug(
"Not necessary to request screenshot permission on Linux, if the "
"display manager is not Wayland. Skipping."
)
return

if sys.platform == "linux" and utils.is_wayland_display_manager():
if sys.platform == "linux" and system_info.os_has_wayland_display_manager():
logger.debug("Show request permission dialog.")
dbus_portal_show_request_permission_dialog(
title=dialog_title, text=linux_dialog_text
Expand Down
Loading

0 comments on commit a0af2db

Please sign in to comment.