Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions data/org.cinnamon.gschema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,7 @@
<child name="desklets" schema="org.cinnamon.desklets" />
<child name="sounds" schema="org.cinnamon.sounds" />
<child name="launcher" schema="org.cinnamon.launcher" />
<child name="screenshot" schema="org.cinnamon.screenshot" />

<key name="enable-vfade" type="b">
<default>true</default>
Expand Down Expand Up @@ -1020,4 +1021,51 @@

</schema>

<enum id="org.cinnamon.screenshot.file-types">
<value nick="png" value="0"/>
<value nick="jpg" value="1"/>
<value nick="bmp" value="2"/>
<value nick="tiff" value="3"/>
</enum>

<schema id="org.cinnamon.screenshot" path="/org/cinnamon/screenshot/">

<key name="delay" type="i">
<default>0</default>
<summary>Screenshot delay</summary>
<description>The number of seconds to wait before taking the screenshot.</description>
</key>

<key name="save-directory" type="s">
<default>''</default>
<summary>Default save folder</summary>
<description>URI of the folder where screenshots are saved by default. If empty, the user's Pictures folder is used.</description>
</key>

<key name="include-pointer" type="b">
<default>false</default>
<summary>Include pointer</summary>
<description>Include the mouse pointer in the screenshot.</description>
</key>

<key name="default-file-type" enum="org.cinnamon.screenshot.file-types">
<default>'png'</default>
<summary>Default file type</summary>
<description>The default file type extension for saved screenshots.</description>
</key>

<key name="include-shadow" type="b">
<default>true</default>
<summary>Include shadow</summary>
<description>Include the shadow when taking a window screenshot.</description>
</key>

<key name="launch-file-manager-after-save" type="b">
<default>false</default>
<summary>Launch file manager after saving</summary>
<description>When enabled, opens the file manager with the saved screenshot pre-selected after a successful save.</description>
</key>

</schema>

</schemalist>
5 changes: 5 additions & 0 deletions files/usr/bin/cinnamon-screenshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/python3
import sys
sys.path.insert(0, '/usr/share/cinnamon/cinnamon-screenshot')
import application
sys.exit(application.main())
24 changes: 24 additions & 0 deletions files/usr/share/applications/cinnamon-screenshot.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[Desktop Entry]
Exec=cinnamon-screenshot --interactive
Terminal=false
Type=Application
Icon=applets-screenshooter
StartupNotify=true
Categories=GTK;Utility;
OnlyShowIn=X-Cinnamon;
Actions=screen-shot;window-shot;area-shot;
Name=Screenshot
Comment=Save images of your screen or individual windows
Keywords=snapshot;capture;print;screenshot;

[Desktop Action screen-shot]
Name=Take a Screenshot of the Whole Screen
Exec=cinnamon-screenshot

[Desktop Action window-shot]
Name=Take a Screenshot of the Current Window
Exec=cinnamon-screenshot -w

[Desktop Action area-shot]
Name=Take a Screenshot of an Area
Exec=cinnamon-screenshot -a
181 changes: 181 additions & 0 deletions files/usr/share/cinnamon/cinnamon-screenshot/application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import argparse
import gettext
import sys

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gio, GLib, Gtk

import screenshot_backend
import prefs
import util

gettext.install('cinnamon', '/usr/share/locale')


class ScreenshotApplication(Gtk.Application):
def __init__(self, args):
super().__init__(
application_id='org.cinnamon.Screenshot',
flags=Gio.ApplicationFlags.NON_UNIQUE,
)
self.args = args
self.backend = screenshot_backend.Backend()
self._exit_code = 0

def do_activate(self):
args = self.args
if args.clipboard:
self._run_clipboard()
elif args.file and not args.interactive:
self._run_save_to_file(args.file)
else:
self._run_window()

def _resolve_mode(self):
if self.args.window:
return 'window'
if self.args.area:
return 'area'
if self.args.monitor is not None:
return 'monitor'
return 'screen'

def capture(self, mode, include_pointer, include_shadow, delay, on_done, area_rect=None):
# Monitor mode and area mode both end up calling screenshot_area,
# which both grabs and flashes the captured rect on the server side.
# Area mode without a rect runs the selector first to get one.
def do_capture():
if area_rect is not None:
x, y, w, h = area_rect
self.backend.screenshot_area(x, y, w, h, include_pointer, on_done)
elif mode == 'window':
self.backend.screenshot_window(include_pointer, include_shadow, on_done)
else:
self.backend.screenshot(include_pointer, on_done)
return GLib.SOURCE_REMOVE

def schedule(fn):
if delay > 0:
GLib.timeout_add_seconds(delay, fn)
else:
GLib.idle_add(fn)

if mode == 'area' and area_rect is None:
def after_select(rect):
nonlocal area_rect
if rect is None:
on_done(None)
return
area_rect = rect
schedule(do_capture)
self.backend.select_area(after_select)
else:
schedule(do_capture)

def _run_window(self):
from main_window import MainWindow
win = MainWindow(self)
win.run()

def _run_clipboard(self):
self.hold()
mode = self._resolve_mode()
area_rect = None
if mode == 'monitor':
area_rect = util.monitor_rect(self.args.monitor)
if area_rect is None:
mode = 'screen'
include_pointer = self.args.include_pointer or prefs.get_include_pointer()
include_shadow = self.args.include_shadow or prefs.get_include_shadow()
delay = self.args.delay if self.args.delay is not None else 0

def done(pixbuf):
if pixbuf is not None:
util.copy_pixbuf_to_clipboard(pixbuf)
GLib.idle_add(self._finish_clipboard)
else:
self._exit_code = 1
self.release()
self.quit()

self.capture(mode, include_pointer, include_shadow, delay, done, area_rect=area_rect)

def _finish_clipboard(self):
self.release()
self.quit()
return GLib.SOURCE_REMOVE

def _run_save_to_file(self, path):
self.hold()
mode = self._resolve_mode()
area_rect = None
if mode == 'monitor':
area_rect = util.monitor_rect(self.args.monitor)
if area_rect is None:
mode = 'screen'
include_pointer = self.args.include_pointer or prefs.get_include_pointer()
include_shadow = self.args.include_shadow or prefs.get_include_shadow()
delay = self.args.delay if self.args.delay is not None else 0

def done(pixbuf):
if pixbuf is not None:
try:
util.save_pixbuf(pixbuf, path)
except Exception as exc:
print(f'cinnamon-screenshot: save failed: {exc}', file=sys.stderr)
self._exit_code = 1
else:
self._exit_code = 1
self.release()
self.quit()

self.capture(mode, include_pointer, include_shadow, delay, done, area_rect=area_rect)

@property
def exit_code(self):
return self._exit_code


def _build_arg_parser():
parser = argparse.ArgumentParser(
prog='cinnamon-screenshot',
description='Take screenshots of your screen, windows, or selected areas',
add_help=True,
)
parser.add_argument('-c', '--clipboard', action='store_true',
help='Send the grab directly to the clipboard')
parser.add_argument('-w', '--window', action='store_true',
help='Grab the active window instead of the entire screen')
parser.add_argument('-a', '--area', action='store_true',
help='Grab a selected area of the screen')
parser.add_argument('-m', '--monitor', type=int, default=None, metavar='INDEX',
help='Grab a specific monitor by 0-based index')
parser.add_argument('-p', '--include-pointer', action='store_true',
help='Include the pointer in the screenshot')
parser.add_argument('-s', '--include-shadow', action='store_true',
help='Include the window shadow when grabbing a window')
parser.add_argument('-d', '--delay', type=int, default=None, metavar='SECONDS',
help='Take the screenshot after a delay')
parser.add_argument('-i', '--interactive', action='store_true',
help='Interactively set options before taking the screenshot')
parser.add_argument('-f', '--file', metavar='PATH',
help='Save the screenshot directly to PATH')
return parser

def main():
parser = _build_arg_parser()
args = parser.parse_args()

if args.window and args.area:
parser.error('cannot combine --window and --area')
if args.monitor is not None and (args.window or args.area):
parser.error('--monitor cannot be combined with --window or --area')
if args.include_shadow and not args.window:
parser.error('--include-shadow requires --window')
if args.interactive and (args.delay is not None or args.include_pointer or args.include_shadow):
parser.error('--interactive cannot be combined with --delay, --include-pointer, or --include-shadow')

app = ScreenshotApplication(args)
app.run([])
return app.exit_code
Empty file.
18 changes: 18 additions & 0 deletions files/usr/share/cinnamon/cinnamon-screenshot/backends/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class Backend:
"""Backends deliver results via on_done(result) rather than returning
them, so DBus-backed implementations can run without blocking the UI."""

def screenshot(self, include_pointer, on_done):
on_done(None)

def screenshot_window(self, include_pointer, include_shadow, on_done):
on_done(None)

def screenshot_area(self, x, y, w, h, include_pointer, on_done):
on_done(None)

def flash_area(self, x, y, w, h):
pass

def select_area(self, on_done):
on_done(None)
94 changes: 94 additions & 0 deletions files/usr/share/cinnamon/cinnamon-screenshot/backends/cinnamon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import os
import sys

import gi
gi.require_version('XApp', '1.0')
from gi.repository import GdkPixbuf, Gio, GLib, GObject, XApp

from backends.base import Backend


BUS_NAME = 'org.cinnamon.Screenshot'
OBJECT_PATH = '/org/cinnamon/Screenshot'
INTERFACE = 'org.cinnamon.Screenshot'


class CinnamonBackend(Backend, GObject.Object):
__gsignals__ = {
'online-changed': (GObject.SignalFlags.RUN_LAST, None, (bool,)),
}

def __init__(self):
GObject.Object.__init__(self)
self._proxy = Gio.DBusProxy.new_for_bus_sync(
Gio.BusType.SESSION,
Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES,
None,
BUS_NAME, OBJECT_PATH, INTERFACE,
None,
)
self._proxy.connect('notify::g-name-owner', self._on_name_owner_changed)

def _on_name_owner_changed(self, *_args):
self.emit('online-changed', self.is_available())

def is_available(self):
return self._proxy.get_name_owner() is not None

def _tempfile(self):
return os.path.join(XApp.get_tmp_dir(), f'cinnamon-screenshot-{os.getpid()}.png')

def _call(self, method, params, on_result):
"""Invoke a DBus method asynchronously; deliver the unpacked
result tuple (or None on failure) to on_result."""
def cb(proxy, res):
try:
result = proxy.call_finish(res).unpack()
except GLib.Error as exc:
print(f'cinnamon-screenshot: DBus {method} failed: {exc.message}', file=sys.stderr)
result = None
on_result(result)
self._proxy.call(method, params, Gio.DBusCallFlags.NONE, -1, None, cb)

def _load_and_unlink(self, path):
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
except GLib.Error:
pixbuf = None
try:
os.unlink(path)
except OSError:
pass
return pixbuf

def _deliver_image(self, path, result, on_done):
if not result or not result[0]:
on_done(None)
return
on_done(self._load_and_unlink(result[1] or path))

def screenshot(self, include_pointer, on_done):
path = self._tempfile()
self._call('Screenshot',
GLib.Variant('(bs)', (include_pointer, path)),
lambda result: self._deliver_image(path, result, on_done))

def screenshot_window(self, include_pointer, include_shadow, on_done):
path = self._tempfile()
self._call('ScreenshotWindow',
GLib.Variant('(bbs)', (include_shadow, include_pointer, path)),
lambda result: self._deliver_image(path, result, on_done))

def screenshot_area(self, x, y, w, h, include_pointer, on_done):
path = self._tempfile()
self._call('ScreenshotArea',
GLib.Variant('(iiiibs)', (x, y, w, h, include_pointer, path)),
lambda result: self._deliver_image(path, result, on_done))

def flash_area(self, x, y, w, h):
self._proxy.call('FlashArea', GLib.Variant('(iiii)', (x, y, w, h)),
Gio.DBusCallFlags.NONE, -1, None, None)

def select_area(self, on_done):
self._call('SelectArea', None,
lambda result: on_done(tuple(result) if result else None))
Loading
Loading