diff --git a/bin/shutter b/bin/shutter index 19733fb9..b41bdfff 100755 --- a/bin/shutter +++ b/bin/shutter @@ -969,6 +969,9 @@ sub STARTUP { #init menus if ($x11_supported){ $st->{_full}->set_menu(fct_ret_workspace_menu(TRUE)); + } elsif (Gtk3::Gdk::Screen::get_default->get_n_monitors > 1) { + #Wayland: offer per-monitor capture through the "full" dropdown + $st->{_full}->set_menu(fct_ret_workspace_menu(TRUE)); } $st->{_window}->set_menu(fct_ret_window_menu()); @@ -3133,7 +3136,7 @@ sub STARTUP { #unblock signal handler fct_control_signals('unblock'); return TRUE; - } elsif (!$x11_supported && $data ne "full" && $data ne "tray_full") { + } elsif (!$x11_supported && $data ne "full" && $data ne "tray_full" && $data !~ /^shutter_monitor_direct/ && $data !~ /^redoshot/) { 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'); @@ -6189,7 +6192,11 @@ sub STARTUP { } #fullscreen screenshot - if ($data eq "full" || $data eq "tray_full") { + if ($data eq "full" || $data eq "tray_full" || $data =~ /^shutter_monitor_direct(\d+)/) { + + #on Wayland a single monitor can be requested via the "full" dropdown + my $monitor = ($data =~ /^shutter_monitor_direct(\d+)/) ? $1 : undef; + $data = "full" if defined $monitor; if ($x11_supported) { $screenshooter = Shutter::Screenshot::Workspace->new( @@ -6201,7 +6208,8 @@ sub STARTUP { $screenshot = $screenshooter->workspace(); } else { # TODO: support kwin directly, because it has more features than the xdg portal - $screenshot = Shutter::Screenshot::Wayland::xdg_portal($screenshooter); + $screenshooter = Shutter::Screenshot::Wayland->new($sc, $monitor); + $screenshot = $screenshooter->capture; } #window @@ -8447,6 +8455,24 @@ sub STARTUP { my $menu_wrksp = Gtk3::Menu->new; unless ($x11_supported) { + + #On Wayland we can't enumerate workspaces via libwnck, but we can still + #offer to capture a single monitor by cropping the portal screenshot. + my $gdk_screen = Gtk3::Gdk::Screen::get_default; + my $n_mons = $gdk_screen->get_n_monitors; + if ($n_mons > 1) { + for (my $i = 0 ; $i < $n_mons ; $i++) { + my $geo = $gdk_screen->get_monitor_geometry($i); + my $plug = eval { $gdk_screen->get_monitor_plug_name($i) }; + my $label = $plug + ? sprintf("%s (%dx%d)", $plug, $geo->{width}, $geo->{height}) + : sprintf($d->get("Monitor %d (%dx%d)"), $i + 1, $geo->{width}, $geo->{height}); + my $mon_item = Gtk3::MenuItem->new_with_label($label); + $mon_item->signal_connect('activate' => \&evt_take_screenshot, "shutter_monitor_direct$i"); + $menu_wrksp->append($mon_item); + } + $menu_wrksp->show_all; + } return $menu_wrksp; } diff --git a/share/shutter/resources/modules/Shutter/Screenshot/Wayland.pm b/share/shutter/resources/modules/Shutter/Screenshot/Wayland.pm index e6c5e16e..bbecd8e0 100644 --- a/share/shutter/resources/modules/Shutter/Screenshot/Wayland.pm +++ b/share/shutter/resources/modules/Shutter/Screenshot/Wayland.pm @@ -6,6 +6,52 @@ use Net::DBus::Reactor; package Shutter::Screenshot::Wayland; +use Shutter::Screenshot::History; + +#Object wrapper around the portal so full/monitor captures can be repeated +#(e.g. via the redoshot/F5 shortcut) just like the X11 screenshooters. +sub new { + my $class = shift; + my $self = { + _sc => shift, + _monitor => shift, #undef captures the whole desktop + }; + $self->{_gdk_screen} = Gtk3::Gdk::Screen::get_default(); + bless $self, $class; + return $self; +} + +sub capture { + my $self = shift; + + my $pixbuf = xdg_portal($self); + return $pixbuf unless ref($pixbuf) && $pixbuf->isa('Gtk3::Gdk::Pixbuf'); + + 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}); + return $pixbuf; +} + +sub redo_capture { + my $self = shift; + return 3 unless defined $self->{_history}; + return $self->capture; +} + +sub get_history { + my $self = shift; + return $self->{_history}; +} + +sub get_error_text { + my $self = shift; + return $self->{_error_text}; +} + sub xdg_portal { my $screenshooter = shift; my $reactor = Net::DBus::Reactor->main; @@ -15,6 +61,7 @@ sub xdg_portal { $me =~ s/^://g; my $pixbuf; + my $portal_error; eval { my $portal_service = $bus->get_service('org.freedesktop.portal.Desktop'); @@ -39,9 +86,16 @@ sub xdg_portal { } $reactor->run; $request->disconnect_from_signal(Response => $conn); - if ($num != 0) { - $screenshooter->{_error_text} = "Response $num from XDG portal"; - return 9; + + #a "return" here would only exit the eval and leave the caller with an + #undef pixbuf, so record the failure and bail out after the eval instead + if (!defined $num || $num != 0) { + $portal_error = "Response " . (defined $num ? $num : "timeout") . " from XDG portal"; + return; + } + unless (defined $output && defined $output->{uri}) { + $portal_error = "XDG portal returned no screenshot URI"; + return; } my $giofile = Glib::IO::File::new_for_uri($output->{uri}); print "xdg portal: got file ".$giofile->get_path."\n"; @@ -51,9 +105,47 @@ sub xdg_portal { if ($@) { $screenshooter->{_error_text} = $@; return 9; - }; + } + if (defined $portal_error) { + $screenshooter->{_error_text} = $portal_error; + return 9; + } return $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 { + my ($pixbuf, $gdk_screen, $monitor) = @_; + + return $pixbuf unless defined $pixbuf && defined $gdk_screen && defined $monitor; + + my $geo = $gdk_screen->get_monitor_geometry($monitor); + return $pixbuf unless $geo; + + #Monitor geometry is in logical pixels while the portal captures device + #pixels; derive the scale from the full desktop size to stay correct on HiDPI. + my $screen_w = $gdk_screen->get_width || $pixbuf->get_width; + my $screen_h = $gdk_screen->get_height || $pixbuf->get_height; + my $scale_x = $pixbuf->get_width / $screen_w; + my $scale_y = $pixbuf->get_height / $screen_h; + + my $x = int($geo->{x} * $scale_x); + my $y = int($geo->{y} * $scale_y); + my $w = int($geo->{width} * $scale_x); + my $h = int($geo->{height} * $scale_y); + + #clamp to the captured area + $x = 0 if $x < 0; + $y = 0 if $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 $pixbuf if $w <= 0 || $h <= 0; + + my $cropped = Gtk3::Gdk::Pixbuf->new('rgb', $pixbuf->get_has_alpha, 8, $w, $h); + $pixbuf->copy_area($x, $y, $w, $h, $cropped, 0, 0); + return $cropped; +} + 1;