Skip to content
Open
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
78 changes: 26 additions & 52 deletions bin/shutter
Original file line number Diff line number Diff line change
Expand Up @@ -670,14 +670,6 @@ sub STARTUP {
$sm->{_menuitem_tooltip}->signal_connect('activate', \&evt_take_screenshot, 'tooltip');
$sm->{_menuitem_iclipboard}->signal_connect('activate', \&fct_clipboard_import);

unless ($x11_supported) {
# on Wayland selection/window/awindow go through the XDG portal;
# only menu/tooltip capture stay disabled (not supported by the portal)
for my $name ('menu', 'tooltip') {
$sm->{"_menuitem_$name"}->set_sensitive(FALSE);
}
}

#gnome web photo is optional, don't enable it when gnome-web-photo is not in PATH
if ($gnome_web_photo) {
$sm->{_menuitem_web}->set_sensitive(TRUE);
Expand Down Expand Up @@ -969,11 +961,9 @@ sub STARTUP {
}

unless ($x11_supported) {
my $wayland_warning = Gtk3::Label->new;
$wayland_warning->set_line_wrap(TRUE);
$wayland_warning->set_markup($d->get("Wayland support is limited, for more advanced screenshots please switch back to Xorg. Click <a href='https://github.com/shutter-project/shutter/issues/187'>here</a> for details. Selection and window
modes are currently experimental, your desktop environment might fall back to an interactive mode chooser or full screen capture."));
$vbox->pack_start($wayland_warning, FALSE, TRUE, 0);
my $wayland_info = Gtk3::Label->new($d->get("Available capture modes on Wayland depend on your desktop environment. Some modes require manual selection."));
$wayland_info->set_line_wrap(TRUE);
$vbox->pack_start($wayland_info, FALSE, TRUE, 0);
}

$vbox->pack_start($st->create_toolbar, FALSE, TRUE, 0);
Expand Down Expand Up @@ -1028,15 +1018,8 @@ modes are currently experimental, your desktop environment might fall back to an
$st->{_upload}->set_sensitive(FALSE);

unless ($x11_supported) {
# on Wayland _select/_window use the XDG portal; menu/tooltip stay disabled
my $tooltip = $d->get("Can't take screenshots without X11 server");
for my $name ('_menu', '_tooltip') {
$st->{$name}->set_sensitive(FALSE);
$st->{$name}->set_tooltip_text($tooltip);
}
# dropdown menus (workspace list / window list) still need X11 to enumerate
for my $name ('_full', '_window') {
$st->{$name}->set_arrow_tooltip_text($tooltip);
$st->{$name}->set_tooltip_text($d->get("Capture after a delay, then select the menu or tooltip in the frozen screenshot"));
}
}

Expand Down Expand Up @@ -2053,15 +2036,10 @@ modes are currently experimental, your desktop environment might fall back to an
'menu_delay_changed'
);

if ($x11_supported) {
if (defined $settings_xml->{'general'}->{'menu_delay'}) {
$menu_delay->set_value($settings_xml->{'general'}->{'menu_delay'});
} else {
$menu_delay->set_value(10);
}
if (defined $settings_xml->{'general'}->{'menu_delay'} && $settings_xml->{'general'}->{'menu_delay'} > 0) {
$menu_delay->set_value($settings_xml->{'general'}->{'menu_delay'});
} else {
$menu_delay->set_value(0);
$menu_delay->set_sensitive(FALSE);
$menu_delay->set_value(10);
}

$menu_delay->set_tooltip_text($d->get("Capture menu/tooltip after a delay of n seconds"));
Expand Down Expand Up @@ -3257,11 +3235,10 @@ modes are currently experimental, your desktop environment might fall back to an
fct_control_signals('unblock');
return TRUE;
} elsif (!$x11_supported
&& $data !~ /^(tray_)?(full|select|window|awindow)$/
&& $data !~ /^(tray_)?(full|select|window|awindow|menu|tooltip)$/
&& $data !~ /^shutter_monitor_direct/
&& $data !~ /^redoshot/) {
# full/select/window/awindow/monitor/redo are served via the XDG portal or supported on Wayland;
# everything else (menu, tooltip, ...) still needs X11
# Supported captures use the XDG portal; other modes still need X11.
my $sd = Shutter::App::SimpleDialogs->new;
$sd->dlg_error_message($d->get("Can't take screenshots without X11 server"), $d->get("Failed"));
fct_control_signals('unblock');
Expand All @@ -3278,7 +3255,7 @@ modes are currently experimental, your desktop environment might fall back to an
my $scd_text;
if ($data eq "menu" || $data eq "tray_menu") {
$scd_text = $d->get("Please activate the menu you want to capture");
if ($ENV{GDK_SCALE} > 1) {
if ($x11_supported && $ENV{GDK_SCALE} > 1) {
my $sd = Shutter::App::SimpleDialogs->new;
$sd->dlg_info_message("Capturing a cascading menu is known to be broken with HiDPI.\nPlease unset GDK_SCALE variable and restart Shutter.\nPlease follow https://github.com/shutter-project/shutter/issues/326 and send us the patch\nWill attempt capturing it anyway...", undef, 'gtk-ok');
}
Expand Down Expand Up @@ -4543,28 +4520,24 @@ modes are currently experimental, your desktop environment might fall back to an

#enable/disable controls
if ($st->{_select} && $sm->{_menuitem_selection}) {
# if $x11_supported is false, we are on Wayland, and all these buttons are already disabled and should stay disabled
# These capture modes are supported on both X11 and Wayland.

#menu
if ($x11_supported) {
$sm->{_menuitem_selection}->set_sensitive($sensitive);
$sm->{_menuitem_window}->set_sensitive($sensitive);
#$sm->{_menuitem_section}->set_sensitive($sensitive);
$sm->{_menuitem_menu}->set_sensitive($sensitive);
$sm->{_menuitem_tooltip}->set_sensitive($sensitive);
}
$sm->{_menuitem_selection}->set_sensitive($sensitive);
$sm->{_menuitem_window}->set_sensitive($sensitive);
#$sm->{_menuitem_section}->set_sensitive($sensitive);
$sm->{_menuitem_menu}->set_sensitive($sensitive);
$sm->{_menuitem_tooltip}->set_sensitive($sensitive);
$sm->{_menuitem_web}->set_sensitive($sensitive)
if ($gnome_web_photo);
$sm->{_menuitem_iclipboard}->set_sensitive($sensitive);

#toolbar
if ($x11_supported) {
$st->{_select}->set_sensitive($sensitive);
$st->{_window}->set_sensitive($sensitive);
#$st->{_section}->set_sensitive($sensitive);
$st->{_menu}->set_sensitive($sensitive);
$st->{_tooltip}->set_sensitive($sensitive);
}
$st->{_select}->set_sensitive($sensitive);
$st->{_window}->set_sensitive($sensitive);
#$st->{_section}->set_sensitive($sensitive);
$st->{_menu}->set_sensitive($sensitive);
$st->{_tooltip}->set_sensitive($sensitive);
$st->{_web}->set_sensitive($sensitive) if ($gnome_web_photo);

#special case: redoshot (toolbar and menu)
Expand Down Expand Up @@ -6358,7 +6331,8 @@ modes are currently experimental, your desktop environment might fall back to an
my $target; # capture mode sent to xdg-portal
my $interactive = 1; # whether to use the desktop environment's GUI (only for the time till DEs support non-interactive calls)

if ($data eq "full" || $data eq "tray_full") {
my $popup_mode = Shutter::Screenshot::Wayland::popup_mode($data);
if ($data eq "full" || $data eq "tray_full" || defined $popup_mode) {
$target = 1;
} elsif ($data eq "window" || $data eq "tray_window") {
$target = 2;
Expand All @@ -6377,7 +6351,7 @@ modes are currently experimental, your desktop environment might fall back to an
}
}

$screenshooter = Shutter::Screenshot::Wayland->new($sc, $interactive, $target, $monitor);
$screenshooter = Shutter::Screenshot::Wayland->new($sc, $interactive, $target, $monitor, $popup_mode, $menu_delay->get_value);
$screenshot = $screenshooter->xdg_portal();
}
} else {
Expand Down Expand Up @@ -8958,7 +8932,7 @@ modes are currently experimental, your desktop environment might fall back to an

#menu
my $menuitem_window_menu = Gtk3::ImageMenuItem->new_with_mnemonic($d->get('_Menu'));
$menuitem_window_menu->set_sensitive($x11_supported);
$menuitem_window_menu->set_sensitive(TRUE);
if ($traytheme->has_icon('alacarte')) {
$menuitem_window_menu->set_image(Gtk3::Image->new_from_icon_name('alacarte', 'menu'));
} else {
Expand All @@ -8971,7 +8945,7 @@ modes are currently experimental, your desktop environment might fall back to an

#tooltip
my $menuitem_window_tooltip = Gtk3::ImageMenuItem->new_with_mnemonic($d->get('_Tooltip'));
$menuitem_window_tooltip->set_sensitive($x11_supported);
$menuitem_window_tooltip->set_sensitive(TRUE);
if ($traytheme->has_icon('help-faq')) {
$menuitem_window_tooltip->set_image(Gtk3::Image->new_from_icon_name('help-faq', 'menu'));
} else {
Expand Down
72 changes: 72 additions & 0 deletions share/shutter/resources/modules/Shutter/Screenshot/Popup.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package Shutter::Screenshot::Popup;

use utf8;
use strict;
use warnings;
use Gtk3;
use Gtk3::ImageView;

# Coordinates come from ImageView in source-image pixels, even when the
# preview is scaled to fit. Never capture the desktop again after selection.
sub crop_region {
my ($pixbuf, $region) = @_;
return unless defined $region;
my ($x, $y, $w, $h) = @{$region}{qw/x y width height/};
return unless defined $x && defined $y && defined $w && defined $h;
if ($x < 0) { $w += $x; $x = 0; }
if ($y < 0) { $h += $y; $y = 0; }
$w = $pixbuf->get_width - $x if $x + $w > $pixbuf->get_width;
$h = $pixbuf->get_height - $y if $y + $h > $pixbuf->get_height;
return if $w <= 0 || $h <= 0;
return $pixbuf->new_subpixbuf($x, $y, $w, $h)->copy;
}

sub select_region {
my ($sc, $pixbuf) = @_;
my $d = $sc->get_gettext;
my $dialog = Gtk3::Dialog->new(
$d->get("Select the menu or tooltip"), $sc->get_mainwindow,
['modal', 'destroy-with-parent'], 'gtk-cancel' => 'cancel', 'gtk-ok' => 'ok');
$dialog->set_default_size(900, 650);
$dialog->set_default_response('ok');
$dialog->set_response_sensitive('ok', 0);

my $content = $dialog->get_content_area;
my $label = Gtk3::Label->new($d->get("Drag around the menu or tooltip in this frozen screenshot, then confirm."));
$label->set_line_wrap(1);
$content->pack_start($label, 0, 0, 8);
my $view = Gtk3::ImageView->new;
$view->set_tool(Gtk3::ImageView::Tool::Selector->new($view));
$view->set_pixbuf($pixbuf);
# Fit only after GTK allocates the preview; fitting a 1-pixel placeholder
# can request a zoom below ImageView's supported minimum on large screens.
my $fitted;
$view->signal_connect('size-allocate' => sub {
my ($widget, $allocation) = @_;
if (!$fitted && $allocation->{width} > 1 && $allocation->{height} > 1) {
$fitted = 1;
$widget->set_zoom_to_fit(1);
}
});
$content->pack_start($view, 1, 1, 0);
$view->signal_connect('selection-changed' => sub {
my $region = $view->get_selection;
$dialog->set_response_sensitive('ok', defined $region && $region->{width} > 0 && $region->{height} > 0);
});

my $output;
eval {
$dialog->show_all;
$view->grab_focus;
while ($dialog->run eq 'ok') {
$output = crop_region($pixbuf, $view->get_selection);
last if defined $output;
}
};
my $error = $@;
$dialog->destroy;
die $error if $error;
return $output;
}

1;
75 changes: 65 additions & 10 deletions share/shutter/resources/modules/Shutter/Screenshot/Wayland.pm
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ sub new {
_target => shift, # screenshot mode (full, select, window, awindow) to send to xdg-portal
_monitor => shift, # undef captures the whole desktop
};
$self->{_popup_mode} = shift;
$self->{_repeat_delay} = shift;
$self->{_gdk_screen} = Gtk3::Gdk::Screen::get_default();
bless $self, $class;
return $self;
Expand All @@ -26,9 +28,31 @@ sub new {
sub redo_capture {
my $self = shift;
return 3 unless defined $self->{_history};
if (defined $self->{_popup_mode}) {
# The initial request uses the application's menu/tooltip countdown.
# Repeats need the same opportunity to reopen the transient popup.
$self->wait_for_popup;
}
return $self->xdg_portal;
}

# Menus and tooltips cannot be enumerated through the screenshot portal.
# Capture the desktop before presenting any selection UI.
sub popup_mode {
my ($mode) = @_;
return unless defined $mode;
return $1 if $mode =~ /^(?:tray_)?(menu|tooltip)$/;
return;
}

sub wait_for_popup {
my $self = shift;
my $loop = Glib::MainLoop->new;
my $delay = $self->{_repeat_delay} || 10;
Glib::Timeout->add($delay * 1000, sub { $loop->quit; return 0; });
$loop->run;
}

sub get_history {
my $self = shift;
return $self->{_history};
Expand All @@ -53,6 +77,8 @@ sub xdg_portal {

my $pixbuf;
my $portal_error;
my $cancelled;
delete $self->{_error_text};

eval {
my $portal_service = $bus->get_service('org.freedesktop.portal.Desktop');
Expand All @@ -78,6 +104,9 @@ sub xdg_portal {
# only define a target if xdg-portal supports non-interactive calls
$options{target} = Net::DBus::dbus_uint32($self->{_target}) if $self->{_interactive} ne 1;

# An interactive chooser could dismiss the menu or tooltip before capture.
$options{interactive} = Net::DBus::dbus_boolean(0) if defined $self->{_popup_mode};

my $request_path = $portal->Screenshot('', \%options);

if ($request->get_object_path ne $request_path) {
Expand All @@ -92,7 +121,8 @@ sub xdg_portal {
if (!defined $num || $num != 0) {
if (defined $num && $num == 1) {
# portal Response: 1 = user cancelled -> treat as abort (code 5), not error
return 5;
$cancelled = 1;
return;
}
$portal_error = "Response " . (defined $num ? $num : "timeout") . " from XDG portal";
return;
Expand All @@ -107,23 +137,40 @@ sub xdg_portal {

$giofile->delete;
};
if (defined $self->{_monitor}) {
$pixbuf = crop_to_monitor($pixbuf, $self->{_gdk_screen}, $self->{_monitor});
}

#a history marker makes this capture repeatable through redoshot
$self->{_history} = Shutter::Screenshot::History->new($self->{_sc});
if ($@) {
$self->{_error_text} = $@;
# A return inside eval only leaves that block. Handle cancellation and
# errors before accessing the pixbuf or recording a successful capture.
my $exception = $@;
if ($exception) {
$self->{_error_text} = $exception;
return 9;
}
return 5 if $cancelled;
if (defined $portal_error) {
$self->{_error_text} = $portal_error;
return 9;
}

if (defined $self->{_monitor}) {
$pixbuf = crop_to_monitor($pixbuf, $self->{_gdk_screen}, $self->{_monitor});
}

if (defined $self->{_popup_mode}) {
my $selected = eval { $self->select_popup($pixbuf) };
if ($@) {
$self->{_error_text} = $@;
return 9;
}
return 5 unless defined $selected;
$pixbuf = $selected;
}

#a history marker makes this capture repeatable through redoshot
$self->{_history} = Shutter::Screenshot::History->new($self->{_sc});

# get name
if ($self->{_target} eq 1) {
if (defined $self->{_popup_mode}) {
$self->{_action_name} = $self->{_popup_mode} eq 'menu' ? $d->get("Menu") : $d->get("Tooltip");
} elsif ($self->{_target} eq 1) {
if (defined $self->{_monitor}) {
$self->{_action_name} = $self->{_gdk_screen}->get_monitor_plug_name($self->{_monitor});
} else {
Expand All @@ -144,6 +191,14 @@ sub xdg_portal {
return $pixbuf;
}

# Select from the captured pixels, not the live desktop: moving the pointer
# or opening this dialog can no longer dismiss the captured popup.
sub select_popup {
my ($self, $pixbuf) = @_;
require Shutter::Screenshot::Popup;
return Shutter::Screenshot::Popup::select_region($self->{_sc}, $pixbuf);
}

#The XDG portal always returns the whole desktop spanning every monitor. Crop
#that pixbuf down to a single monitor's area so users can capture just one.
sub crop_to_monitor {
Expand Down
Loading