Skip to content

Commit

Permalink
Merge pull request #183 from thenaterhood/dev
Browse files Browse the repository at this point in the history
* wayland gets custom cursors (#182)

* wayland gets custom cursors

* lint

* lint

* tests, lint

* lint

* fix custom cursor loop

* cancel cursor locate on keypress

* lint

* specfile and readme

* fix saving to nontransparent formats
  • Loading branch information
thenaterhood authored Mar 17, 2024
2 parents 609c280 + 1498c6b commit de7890e
Show file tree
Hide file tree
Showing 14 changed files with 275 additions and 46 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,16 +235,19 @@ At this point, you can install gscreenshot itself by running
* PIL/python-pillow + slop + python-xlib
* Scrot only (any version) (cursor capture will not work in some scenarios, region selection may be glitchy due to scrot issues)

Feel free to mix and match, though some combinations may work better than others.
As of gscreenshot 3.5.0 python-xlib is _optional_ for full cursor capture functionality but will require an extra click.

## For full functionality on Wayland, the recommended packages are:

* grim (for screenshots)
* xdg-desktop-portal for your environment (for screenshots)
* slurp (for region selection)
* xdg-open (for opening screenshots in your image viewer)
* wl-clipboard (for copy to clipboard)

## For alternate Wayland configurations, choose from one of the following combinations:

* xdg-desktop-portal + slurp + python-dbus
* grim + slurp + python-dbus

You can install X11 and Wayland package configurations in parallel - gscreenshot will detect if your
session is Wayland or X11.
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ def compile_manpage():
'gscreenshot',
'gscreenshot.frontend',
'gscreenshot.frontend.gtk',
'gscreenshot.cursor_locator',
'gscreenshot.screenshooter',
'gscreenshot.screenshot',
'gscreenshot.screenshot.effects',
Expand Down
4 changes: 2 additions & 2 deletions specs/gscreenshot.spec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
%define name gscreenshot
%define version 3.4.3
%define unmangled_version 3.4.3
%define version 3.5.0
%define unmangled_version 3.5.0
%define release 1

Summary: A simple screenshot tool
Expand Down
33 changes: 27 additions & 6 deletions src/gscreenshot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,27 @@ def get_available_cursors(self) -> typing.Dict[str, typing.Optional[Image.Image]
)
}

if session_is_wayland():
del available['theme']

available.update(self._stamps)
return available

def get_cursor_by_name(self, name: typing.Optional[str]):
'''
Get the cursor glyph that goes by the given name. This is
safe to call with None or bad names and will return a default
value.
'''
cursors = self.get_available_cursors()
#pylint: disable=consider-iterating-dictionary
default = list(cursors.keys())[0]

if name and name in cursors.keys():
return cursors[name]

return cursors[default]

def show_screenshot_notification(self) -> bool:
'''
Show a notification that a screenshot was taken.
Expand Down Expand Up @@ -209,7 +227,8 @@ def get_screenshooter_name(self) -> str:

#pylint: disable=too-many-arguments
def screenshot_full_display(self, delay: int=0, capture_cursor: bool=False,
cursor_name: str='theme', overwrite: bool=False, count: int=1
cursor_name: typing.Optional[str]=None,
overwrite: bool=False, count: int=1
) -> typing.Optional[Image.Image]:
"""
Takes a screenshot of the full display with a
Expand All @@ -224,7 +243,7 @@ def screenshot_full_display(self, delay: int=0, capture_cursor: bool=False,
if not capture_cursor:
use_cursor = None
else:
use_cursor = self.get_available_cursors()[cursor_name]
use_cursor = self.get_cursor_by_name(cursor_name)

for _ in range(0, count):
self.screenshooter.grab_fullscreen_(
Expand All @@ -245,7 +264,8 @@ def screenshot_full_display(self, delay: int=0, capture_cursor: bool=False,

#pylint: disable=too-many-arguments
def screenshot_selected(self, delay: int=0, capture_cursor: bool=False,
cursor_name: str='theme', overwrite: bool=False, count: int=1,
cursor_name: typing.Optional[str]=None,
overwrite: bool=False, count: int=1,
region: typing.Optional[typing.Tuple[int, int, int, int]]=None
) -> typing.Optional[Image.Image]:
"""
Expand All @@ -261,7 +281,7 @@ def screenshot_selected(self, delay: int=0, capture_cursor: bool=False,
if not capture_cursor:
use_cursor = None
else:
use_cursor = self.get_available_cursors()[cursor_name]
use_cursor = self.get_cursor_by_name(cursor_name)

for _ in range(0, count):
self.screenshooter.grab_selection_(
Expand All @@ -283,7 +303,8 @@ def screenshot_selected(self, delay: int=0, capture_cursor: bool=False,

#pylint: disable=too-many-arguments
def screenshot_window(self, delay: int=0, capture_cursor: bool=False,
cursor_name: str='theme', overwrite: bool=False, count: int=1
cursor_name: typing.Optional[str]=None,
overwrite: bool=False, count: int=1
) -> typing.Optional[Image.Image]:
"""
Interactively takes a screenshot of a selected window
Expand All @@ -298,7 +319,7 @@ def screenshot_window(self, delay: int=0, capture_cursor: bool=False,
if not capture_cursor:
use_cursor = None
else:
use_cursor = self.get_available_cursors()[cursor_name]
use_cursor = self.get_cursor_by_name(cursor_name)

for _ in range(0, count):
self.screenshooter.grab_window_(
Expand Down
24 changes: 24 additions & 0 deletions src/gscreenshot/cursor_locator/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'''
Interface class for integrating cursor locators
'''
import typing


class CursorLocator():
'''Parent class for cursor locator strategies'''

__utilityname__: str = "default"

def __init__(self):
"""constructor"""

def get_cursor_position(self) -> typing.Optional[typing.Tuple[int, int]]:
'''Return the cursor position as a tuple of (x, y)'''
raise NotImplementedError()

@staticmethod
def can_run() -> bool:
"""
Whether this cursor locator can run
"""
return True
49 changes: 49 additions & 0 deletions src/gscreenshot/cursor_locator/factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'''
Utilities for selecting a cursor locator utility
'''

import typing
from gscreenshot.cursor_locator import CursorLocator
from gscreenshot.cursor_locator.gtk_cursor_locator import GtkCursorLocator
from gscreenshot.cursor_locator.x11_cursor_locator import X11CursorLocator
from gscreenshot.util import session_is_wayland


class NoSupportedCursorLocatorError(Exception):
"""NoSupportedCursorLocatorError"""


class CursorLocatorFactory(object):
'''Selects and instantiates a usable cursor finder'''

def __init__(self, cursor_locator=None):
self.cursor_locator:typing.Optional[CursorLocator] = cursor_locator
self.xorg_locators = [
X11CursorLocator,
GtkCursorLocator
]

self.wayland_locators = [
GtkCursorLocator
]

self.locators:list = []

if session_is_wayland():
self.locators = self.wayland_locators
else:
self.locators = self.xorg_locators

def create(self) -> CursorLocator:
'''Returns a locator instance'''
if self.cursor_locator is not None:
return self.cursor_locator

for locator in self.locators:
if locator.can_run():
return locator()

raise NoSupportedCursorLocatorError(
"No supported cursor locator available",
[x.__utilityname__ for x in self.locators if x.__utilityname__ is not None]
)
86 changes: 86 additions & 0 deletions src/gscreenshot/cursor_locator/gtk_cursor_locator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'''
Classes for capturing the cursor position using Gtk
'''
#pylint: disable=wrong-import-order
#pylint: disable=wrong-import-position
#pylint: disable=ungrouped-imports
import typing
import pygtkcompat
pygtkcompat.enable()
pygtkcompat.enable_gtk(version='3.0')

from gi.repository import Gtk, Gdk
from gscreenshot.cursor_locator import CursorLocator


class GtkCursorLocator(CursorLocator):
'''
Interactive cursor locator driving class
'''

__utilityname__: str = "gscreenshot"

def get_cursor_position(self) -> typing.Optional[typing.Tuple[int, int]]:
"""
Gets the current position of the mouse cursor, if able.
Returns (x, y) or None.
"""
locator = GtkCursorLocatorWindow()
locator.show_all()
Gtk.main()
while Gtk.events_pending():
Gtk.main_iteration()

return locator.position
@staticmethod
def can_run() -> bool:
return True


class GtkCursorLocatorWindow(Gtk.Window):
'''
GTK window for capturing the cursor position
'''
def __init__(self):
'''constructor'''
self.position = None
super().__init__()
self.set_title("gscreenshot")
self.set_position(Gtk.WindowPosition.CENTER)
self.fullscreen()
self.set_opacity(.65)
self.screen = self.get_screen()

box: Gtk.Grid = Gtk.Grid(
vexpand=False,
halign = Gtk.Align.CENTER,
valign = Gtk.Align.CENTER
)

help_text = Gtk.Label()
help_text.set_text(
"Move your cursor to the desired position then click to capture"
)
help_subtext = Gtk.Label()
help_subtext.set_text(
"This extra step is required on Wayland. On X11, install Xlib to skip this."
)
box.attach(help_text, 0, 0, 1, 1)
box.attach(help_subtext, 0, 1, 1, 1)
self.add(box)
self.connect("button_press_event", self.on_button_press)
self.connect("key-press-event", self.on_keypress)

self.set_events(Gdk.POINTER_MOTION_MASK
| Gdk.BUTTON_PRESS_MASK)

def on_button_press(self, _widget, event):
'''handle button press'''
self.position = (int(event.x), int(event.y))
self.destroy()
Gtk.main_quit()

def on_keypress(self, _widget, _event):
'''handle keypress'''
self.destroy()
Gtk.main_quit()
43 changes: 43 additions & 0 deletions src/gscreenshot/cursor_locator/x11_cursor_locator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""
Xlib cursor locator classes
"""
import typing
try:
from Xlib import display
except ImportError:
display = None

from gscreenshot.cursor_locator import CursorLocator


class X11CursorLocator(CursorLocator):
'''Xlib-based cursor locator'''

__utilityname__: str = "python-xlib"

def get_cursor_position(self) -> typing.Optional[typing.Tuple[int, int]]:
"""
Gets the current position of the mouse cursor, if able.
Returns (x, y) or None.
"""
if display is None:
return None

try:
# This is a ctype
# pylint: disable=protected-access
mouse_data = display.Display().screen().root.query_pointer()._data
if 'root_x' not in mouse_data or 'root_y' not in mouse_data:
return None
# pylint: disable=bare-except
except:
# We don't really care about the specific error here. If we can't
# get the pointer, then just move on.
return None

return (mouse_data["root_x"], mouse_data["root_y"])

@staticmethod
def can_run() -> bool:
'''can_run'''
return display is not None
5 changes: 3 additions & 2 deletions src/gscreenshot/frontend/gtk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,20 @@ def __init__(self, application: Gscreenshot, view: View):
self._hide = True
self._capture_cursor = False
self._show_preview()
self._view.show_cursor_options(self._capture_cursor)
self._keymappings = {}
self._overwrite_mode = True

cursors = self._app.get_available_cursors()
cursors[i18n("custom")] = None

self._cursor_selection = 'theme'
self._cursor_selection = list(cursors.keys())[0]

self._view.update_available_cursors(
cursors
)

self._view.show_cursor_options(self._capture_cursor)

def _begin_take_screenshot(self, app_method, **args):
app_method(delay=self._delay,
capture_cursor=self._capture_cursor,
Expand Down
6 changes: 4 additions & 2 deletions src/gscreenshot/frontend/gtk/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ def show_cursor_options(self, show: bool):
if show and GSCapabilities.ALTERNATE_CURSOR in self._capabilities:
self._enable_and_show(self._cursor_selection_dropdown)
self._enable_and_show(self._cursor_selection_label)
if self._cursor_selection_dropdown.get_active() < 0:
self._cursor_selection_dropdown.set_active(0)
else:
self._disable_and_hide(self._cursor_selection_dropdown)
self._disable_and_hide(self._cursor_selection_label)
Expand Down Expand Up @@ -302,11 +304,11 @@ def update_available_cursors(self, cursors: dict, selected: typing.Optional[str]

if selected is not None and selected in cursors:
self._cursor_selection_dropdown.set_active(
selected_idx
selected_idx-1
)
elif cursor_name == "theme":
self._cursor_selection_dropdown.set_active(
len(self._cursor_selection_items)-1
0
)

def run(self):
Expand Down
Loading

0 comments on commit de7890e

Please sign in to comment.