From 5d6977b0501c409e089156700c3bacd37c1a9aa0 Mon Sep 17 00:00:00 2001 From: Photon89 Date: Tue, 11 Aug 2026 23:24:59 +0200 Subject: [PATCH 1/5] Ported SelectorAdvanced.pm from GooCanvas2 to DrawingArea --- .../Shutter/Screenshot/SelectorAdvanced.pm | 1515 +++++++++-------- 1 file changed, 807 insertions(+), 708 deletions(-) diff --git a/share/shutter/resources/modules/Shutter/Screenshot/SelectorAdvanced.pm b/share/shutter/resources/modules/Shutter/Screenshot/SelectorAdvanced.pm index 9d6cb2a6..47646d9f 100644 --- a/share/shutter/resources/modules/Shutter/Screenshot/SelectorAdvanced.pm +++ b/share/shutter/resources/modules/Shutter/Screenshot/SelectorAdvanced.pm @@ -61,788 +61,888 @@ sub new { $self->{_init_h} = shift; $self->{_confirmation_necessary} = shift; - $self->{_dpi_scale} = Gtk3::Window->new('toplevel')->get('scale-factor'); - - #view, selector, dragger - $self->{_view} = Gtk3::ImageView->new; - $self->{_selector} = Gtk3::ImageView::Tool::Selector->new($self->{_view}); - #$self->{_dragger} = Gtk3::ImageView::Tool::Dragger->new($self->{_view}); - $self->{_view}->set_interpolation('nearest'); - $self->{_view}->set_tool($self->{_selector}); - $self->{_view}->set('zoom-step', 1.2); - - #WORKAROUND - #upstream bug - #http://trac.bjourne.webfactional.com/ticket/21 - #left => zoom in - #right => zoom out - $self->{_view}->signal_connect( - 'scroll-event', - sub { - my ($view, $ev) = @_; - if ($ev->direction eq 'left') { - $ev->direction('up'); - } elsif ($ev->direction eq 'right') { - $ev->direction('down'); - } - return FALSE; - }); + my $scale = 1; + eval { + $scale = $self->{_select_window}->get_scale_factor if $self->{_select_window}; + }; + $self->{_dpi_scale} = $scale || 1; - bless $self, $class; - return $self; -} + $self->{_zoom_window} = Gtk3::Window->new('popup'); + $self->{_zoom_window}->set_decorated(0); + $self->{_zoom_window}->set_keep_above(1); + $self->{_zoom_window}->set_modal(0); -#~ sub DESTROY { -#~ my $self = shift; -#~ print "$self dying at\n"; -#~ } + $self->{_select_window} = Gtk3::Window->new('popup'); + $self->{_select_window}->set_decorated(0); + $self->{_select_window}->set_keep_above(1); + $self->{_select_window}->set_modal(1); -sub select_advanced { - my $self = shift; + my $zoom_vbox = Gtk3::VBox->new(0, 4); + $self->{_zoom_window}->add($zoom_vbox); - #return value - my $output = 5; + my $scwin = Gtk3::ScrolledWindow->new; + $scwin->set_policy('never', 'never'); + $zoom_vbox->pack_start($scwin, 1, 1, 0); + + $self->{_x_label} = Gtk3::Label->new("X: 0"); + $self->{_y_label} = Gtk3::Label->new("Y: 0"); + $self->{_size_label} = Gtk3::Label->new("0 x 0"); + + $zoom_vbox->pack_start($self->{_x_label}, 0, 0, 0); + $zoom_vbox->pack_start($self->{_y_label}, 0, 0, 0); + $zoom_vbox->pack_start($self->{_size_label}, 0, 0, 0); - my $d = $self->{_sc}->get_gettext; - #create pixbuf (root window) - my $clean_pixbuf = Gtk3::Gdk::pixbuf_get_from_window($self->{_root}, 0, 0, $self->{_root}->{w}, $self->{_root}->{h}); - - $self->{_view}->set_pixbuf($clean_pixbuf); - - #show help text (do not show help text if predefined selection area is enabled)? - if ($self->{_init_w} < 1 || $self->{_init_h} < 1) { - if ($self->{_show_help}) { - - Glib::Idle->add( - sub { - - #we display the tip only on the current monitor - #if we would use the root window we would display the next - #right in the middle of both screens, this is pretty ugly - my $mon1 = $self->get_current_monitor; - - print "Using monitor: " . $mon1->{x} . " - " . $mon1->{y} . " - " . $mon1->{width} . " - " . $mon1->{height} . "\n" - if $self->{_sc}->get_debug; - - #obtain current colors and font_desc from the main window - my $style = $self->{_sc}->get_mainwindow->get_style_context; - my $sel_bg = Gtk3::Gdk::RGBA::parse('#131313'); - my $font_fam = $style->get_font('normal')->get_family; - my $font_size = $style->get_font('normal')->get_size * $self->{_dpi_scale} / Pango::SCALE; - - #create cairo context und layout - my $surface = Cairo::ImageSurface->create('argb32', $self->{_root}->{w}*$self->{_dpi_scale}, $self->{_root}->{h}*$self->{_dpi_scale}); - my $cr = Cairo::Context->create($surface); - - #set_source_pixbuf - Gtk3::Gdk::cairo_set_source_pixbuf($cr, $clean_pixbuf, 0, 0); - $cr->paint; - - my $layout = Pango::Cairo::create_layout($cr); - $layout->set_width(int($mon1->{width} * $self->{_dpi_scale} / 2) * Pango::SCALE); - $layout->set_alignment('left'); - $layout->set_wrap('word'); - - #determine font-size - my $size1 = int($font_size * 2.0); - my $size2 = int($font_size * 1.5); - my $size3 = int($font_size * 1.0); - - my $text1 = $d->get("Draw a rectangular area using the mouse."); - - my $text2 = $d->get("To take a screenshot, double-click or press the Enter key.\nPress Esc to abort."); - - my $text3 = - $d->get("shift/right-click → selection dialog on/off") . "\n" - . $d->get("scrollwheel → zoom in/out") . "\n" - . $d->get("space → zoom window on/off") . "\n" - . $d->get("cursor keys → move cursor") . "\n" - . $d->get("cursor keys + alt → move selection") . "\n" - . $d->get("cursor keys + ctrl → resize selection"); - - #use this one for white font-color - $layout->set_markup( -"$text1\n$text2\n\n$text3" - ); - - #draw the rectangle - $cr->set_source_rgba($sel_bg->red, $sel_bg->green, $sel_bg->blue, 0.85); - - my ($lw, $lh) = $layout->get_pixel_size; - - my $w = $lw + $size1 * 2; - my $h = $lh + $size1 * 2; - my $x = int(($mon1->{width}*$self->{_dpi_scale} - $w) / 2) + $mon1->{x}; - my $y = int(($mon1->{height}*$self->{_dpi_scale} - $h) / 2) + $mon1->{y}; - my $r = 20*$self->{_dpi_scale}; - - $cr->move_to($x + $r, $y); - $cr->line_to($x + $w - $r, $y); - $cr->curve_to($x + $w, $y, $x + $w, $y, $x + $w, $y + $r); - $cr->line_to($x + $w, $y + $h - $r); - $cr->curve_to($x + $w, $y + $h, $x + $w, $y + $h, $x + $w - $r, $y + $h); - $cr->line_to($x + $r, $y + $h); - $cr->curve_to($x, $y + $h, $x, $y + $h, $x, $y + $h - $r); - $cr->line_to($x, $y + $r); - $cr->curve_to($x, $y, $x, $y, $x + $r, $y); - $cr->fill; - - $cr->move_to($x + $size1, $y + $size1); - - #draw the pango layout - Pango::Cairo::show_layout($cr, $layout); - - #write surface to pixbuf - my $loader = Gtk3::Gdk::PixbufLoader->new; - $surface->write_to_png_stream( - sub { - my ($closure, $data) = @_; - $loader->write([map ord, split //, $data]); - return TRUE; - }); - $loader->close; - - #set pixbuf - $self->{_view}->set_pixbuf($loader->get_pixbuf); - - return FALSE; - }); + $self->{_zoom_area} = Gtk3::DrawingArea->new; + $self->{_zoom_area}->set_size_request(100, 100); + $scwin->add($self->{_zoom_area}); + $self->{_canvas} = Gtk3::DrawingArea->new; + + my $overlay = Gtk3::Overlay->new(); + $overlay->add($self->{_canvas}); + + $self->{_prop_window} = $self->select_dialog(); + $overlay->add_overlay($self->{_prop_window}); + + $self->{_prop_window}->set_halign('end'); + $self->{_prop_window}->set_valign('end'); + + $self->{_prop_window}->set_margin_right(20); + $self->{_prop_window}->set_margin_bottom(20); + + $self->{_prop_window}->hide(); + $self->{_prop_active} = 0; + + $self->{_select_window}->add($overlay); + + $self->{_selector_handler} = $self->{_canvas}->signal_connect(draw => sub { + my ($widget, $cr) = @_; + + if (defined $self->{_screenshot_pixbuf}) { + Gtk3::Gdk::cairo_set_source_pixbuf($cr, $self->{_screenshot_pixbuf}, 0, 0); + $cr->paint; } - } - #define zoom window - $self->{_zoom_window} = Gtk3::Window->new('popup'); - $self->{_zoom_window}->set_decorated(FALSE); - $self->{_zoom_window}->set_skip_taskbar_hint(TRUE); - $self->{_zoom_window}->set_skip_pager_hint(TRUE); - $self->{_zoom_window}->set_keep_above(TRUE); - $self->{_zoom_window}->set_accept_focus(FALSE); + if (defined $self->{_state} && defined $self->{_state}->{sel}) { + my $s = $self->{_state}->{sel}; + + my ($r, $g, $b) = (0.5, 0.5, 0.5); + + eval { + my $context = $widget->get_style_context(); + + $context->save(); + $context->add_class('rubberband'); + + my $rgba = $context->get_background_color('normal'); + + if (defined $rgba) { + $r = $rgba->red; + $g = $rgba->green; + $b = $rgba->blue; + } + $context->restore(); + }; - #pack canvas to a scrolled window - my $scwin = Gtk3::ScrolledWindow->new(); - $scwin->set_policy('never', 'never'); + $cr->set_source_rgba($r, $g, $b, 1.0); + $cr->set_line_width(2.0); + $cr->rectangle($s->{x}, $s->{y}, $s->{width}, $s->{height}); + $cr->stroke; - #define and setup the canvas - my $canvas = GooCanvas2::Canvas->new(); - $canvas->set_size_request(105, 105); - $canvas->modify_bg('normal', Gtk3::Gdk::RGBA::parse('#00000000')); - $canvas->set_bounds(-10*$self->{_dpi_scale}, -10*$self->{_dpi_scale}, ($self->{_root}->{w}+10)*$self->{_dpi_scale}, ($self->{_root}->{h}+10)*$self->{_dpi_scale}); - $canvas->set_scale(5); + $cr->set_source_rgba($r, $g, $b, 0.15); + $cr->rectangle($s->{x}, $s->{y}, $s->{width}, $s->{height}); + $cr->fill; + } - my $canvas_root = $canvas->get_root_item(); - $scwin->add($canvas); + return 0; + }); - my $xlabel = Gtk3::Label->new("X: "); - my $ylabel = Gtk3::Label->new("Y: "); - my $rlabel = Gtk3::Label->new("0 x 0"); + return $self; +} - $ylabel->set_max_width_chars(10); - $xlabel->set_max_width_chars(10); - $rlabel->set_max_width_chars(10); - my $zoom_vbox = Gtk3::VBox->new; - $zoom_vbox->pack_start($scwin, TRUE, TRUE, 0); - $zoom_vbox->pack_start($xlabel, TRUE, TRUE, 0); - $zoom_vbox->pack_start($ylabel, TRUE, TRUE, 0); - $zoom_vbox->pack_start($rlabel, TRUE, TRUE, 0); +#~ sub DESTROY { +#~ my $self = shift; +#~ print "$self dying at\n"; +#~ } - #do some packing - $self->{_zoom_window}->add($zoom_vbox); - $self->{_zoom_window}->move($self->{_root}->{x}, $self->{_root}->{y}); +sub select_advanced { + my $self = shift; - #define shutter cursor (frame) - my $shutter_cursor_pixbuf_frame = Gtk3::Gdk::Pixbuf->new_from_file($self->{_sc}->get_root . "/share/shutter/resources/icons/shutter_cursor_frame.png"); + my $output = 5; + my $d = $self->{_sc}->get_gettext; - #create root... - my $root_item = GooCanvas2::CanvasImage->new( - parent => $canvas_root, - x => 0, - y => 0, - pixbuf => $clean_pixbuf + my $clean_pixbuf = Gtk3::Gdk::pixbuf_get_from_window( + $self->{_root}, 0, 0, $self->{_root}->{w}, $self->{_root}->{h} ); - GooCanvas2::CairoTypes::cairoize_pattern($root_item->get('pattern'))->set_filter('nearest'); - - #...and cursor icon - my $cursor_item = GooCanvas2::CanvasImage->new( - parent => $canvas_root, - x => 0, - y => 0, - pixbuf => $shutter_cursor_pixbuf_frame, - ); - GooCanvas2::CairoTypes::cairoize_pattern($cursor_item->get('pattern'))->set_filter('nearest'); - #starting point - my ($window_at_pointer, $xinit, $yinit, $mask) = $self->{_root}->get_pointer; + $self->{_screenshot_pixbuf} = $clean_pixbuf; - #move cursor on the canvas... - $cursor_item->set( - x => $xinit - 10, - y => $yinit - 10, - ); + $self->{_state} = { + pixbuf => $clean_pixbuf, + zoom => 5, + cursor_x => 0, + cursor_y => 0, + sel => undef, + dclick => undef, + }; + my $state = $self->{_state}; - #scroll region - #$canvas->set_scroll_region($xinit - 9, $yinit - 9, $xinit + 10, $yinit + 10); - $canvas->scroll_to($xinit - 10, $yinit - 10); - #window to manipulate the selection - $self->{_prop_window} = $self->select_dialog(); - $self->{_prop_active} = FALSE; + my ($window_at_pointer, $xinit, $yinit, $mask) = $self->{_root}->get_pointer; + $state->{cursor_x} = $xinit; + $state->{cursor_y} = $yinit; + + my $scwin = Gtk3::ScrolledWindow->new(); + $scwin->set_policy('never', 'never'); + + my $canvas = $self->{_canvas}; + $canvas->set_can_focus(TRUE); + $canvas->add_events([ + qw( + button-press-mask + button-release-mask + pointer-motion-mask + key-press-mask + scroll-mask + ) + ]); + + + # zoom preview + my $xlabel = $self->{_x_label}; + my $ylabel = $self->{_y_label}; + my $rlabel = $self->{_size_label}; + + my $zoom_vbox = Gtk3::VBox->new(FALSE, 0); + + $self->{_zoom_window}->set_type_hint('splashscreen'); + $self->{_zoom_window}->set_can_focus(TRUE); + $self->{_zoom_window}->set_accept_focus(TRUE); + $self->{_zoom_window}->set_skip_taskbar_hint(TRUE); + $self->{_zoom_window}->set_skip_pager_hint(TRUE); + $self->{_zoom_window}->set_keep_above(TRUE); + $self->{_zoom_window}->move($self->{_root}->{x}, $self->{_root}->{y}); - #window that contains the imageview widget - $self->{_select_window} = Gtk3::Window->new('popup'); $self->{_select_window}->set_type_hint('splashscreen'); $self->{_select_window}->set_can_focus(TRUE); $self->{_select_window}->set_accept_focus(TRUE); $self->{_select_window}->set_modal(TRUE); - $self->{_select_window}->set_decorated(FALSE); $self->{_select_window}->set_skip_taskbar_hint(TRUE); $self->{_select_window}->set_skip_pager_hint(TRUE); $self->{_select_window}->set_keep_above(TRUE); - $self->{_select_window}->add($self->{_view}); $self->{_select_window}->set_default_size($self->{_root}->{w}, $self->{_root}->{h}); $self->{_select_window}->resize($self->{_root}->{w}, $self->{_root}->{h}); $self->{_select_window}->move($self->{_root}->{x}, $self->{_root}->{y}); - $self->{_select_window}->show_all; - $self->{_select_window}->present; - - #init state flags - if ($self->{_show_help}) { - $self->{_selector_init} = TRUE; - } else { - $self->{_selector_init} = FALSE; + + if (defined $self->{_prop_window}) { + $self->{_prop_window}->hide; + $self->{_prop_active} = 0; } - $self->{_selector_init_zoom} = 0; - - #hide help text when selector is invoked - $self->{_selector_handler} = $self->{_selector}->signal_connect( - 'selection-changed' => sub { - #hide initial text - if ($self->{_selector_init}) { - $self->{_view}->set_pixbuf($clean_pixbuf, FALSE); - $self->{_selector_init} = FALSE; - $self->{_selector_init_zoom}++; + $self->{_canvas}->set_can_focus(TRUE); + $self->{_canvas}->add_events([ + qw( + button-press-mask + button-release-mask + pointer-motion-mask + key-press-mask + scroll-mask + ) + ]); + + # usage hint + $self->{_help_label} = undef; + + if (($self->{_init_w} < 1 || $self->{_init_h} < 1) && $self->{_show_help}) { + my $mon1 = $self->get_current_monitor; + + my $text1 = $d->get("Draw a rectangular area using the mouse."); + my $text2 = $d->get("To take a screenshot, double-click or press the Enter key.\nPress Esc to abort."); + my $text3 = + $d->get("shift/right-click → selection dialog on/off") . "\n" + . $d->get("scrollwheel → zoom in/out") . "\n" + . $d->get("space → zoom window on/off") . "\n" + . $d->get("cursor keys → move cursor") . "\n" + . $d->get("cursor keys + alt → move selection") . "\n" + . $d->get("cursor keys + ctrl → resize selection"); + + my $help_lbl = Gtk3::Label->new(); + $help_lbl->set_use_markup(TRUE); + $help_lbl->set_justify('left'); + $help_lbl->set_line_wrap(TRUE); + $help_lbl->set_max_width_chars(60); + $help_lbl->set_markup( + "$text1\n" . + "$text2\n\n" . + "$text3" + ); + + my $help_frame = Gtk3::Frame->new(); + $help_frame->set_shadow_type('none'); # Alten Standard-Rahmen abschalten + $help_frame->add($help_lbl); + + my $help_box = Gtk3::EventBox->new(); + $help_box->set_visible_window(TRUE); + $help_box->add($help_frame); + + my $context = $help_frame->get_style_context(); + $context->add_class('shutter-help-frame'); + + my $css_provider = Gtk3::CssProvider->new(); + $css_provider->load_from_data( + ".shutter-help-frame { " . + " background-color: rgba(19, 19, 19, 0.85); " . + " border-radius: 20px; " . + " padding: 30px; " . + "}" + ); + $context->add_provider($css_provider, 600); + + my $box_context = $help_box->get_style_context(); + $box_context->add_class('shutter-transparent-box'); + my $box_css = Gtk3::CssProvider->new(); + $box_css->load_from_data(".shutter-transparent-box { background-color: transparent; }"); + $box_context->add_provider($box_css, 600); + + $help_box->set_halign('center'); + $help_box->set_valign('center'); + + if (defined $self->{_canvas}) { + my $overlay_parent = $self->{_canvas}->get_parent(); + if (defined $overlay_parent && $overlay_parent->isa('Gtk3::Overlay')) { + $overlay_parent->add_overlay($help_box); } + } - #update prop dialog values - $self->adjust_prop_values(); + $self->{_help_label} = $help_box; + } - }); + $self->{_select_window}->show_all; + + $self->{_prop_window}->hide if defined $self->{_prop_window}; + $self->{_prop_active} = 0; - #handle zoom events - #ignore zoom values smaller 1 - $self->{_view_zoom_handler} = $self->{_view}->signal_connect( - 'zoom-changed' => sub { - my ($view, $zoom) = @_; - if ($zoom >= 1) { - $view->set_interpolation('nearest'); - $view->set_zoom(10) if $zoom > 10; - } else { - $view->set_interpolation('bilinear'); - $view->set_zoom(1); - } - if ($self->{_zoom_active}) { - if ($zoom > 1) { - $self->{_zoom_window}->hide; - } else { - $self->{_zoom_window}->show_all; - $self->zoom_check_pos(); - } - } + $self->{_select_window}->present; - #hide help text when zoomed - if ($self->{_selector_init_zoom} == 1) { - $view->set_pixbuf($clean_pixbuf, FALSE); - $self->{_selector_init} = FALSE; - } else { - $self->{_selector_init_zoom}++; - } + if (defined $self->{_canvas}->get_window()) { + my $gdk_win = $self->{_canvas}->get_window(); + my $cur = Gtk3::Gdk::Cursor->new_from_name($gdk_win->get_display(), 'crosshair'); + $gdk_win->set_cursor($cur) if defined $cur; + } - }); + # helper for redraws + my $queue_redraw = sub { + if (defined $self->{_canvas}) { + $self->{_canvas}->queue_draw; + } + if (defined $self->{_zoom_area}) { + $self->{_zoom_area}->queue_draw; + } + }; - #set initial size - Glib::Idle->add( + $self->{_zoom_area}->signal_connect( + 'draw', sub { - if ($self->{_init_w} && $self->{_init_h}) { - $self->{_selector}->set_selection({x=>$self->{_init_x}, y=>$self->{_init_y}, width=>$self->{_init_w}, height=>$self->{_init_h}}); - } - return FALSE; - }); + my ($widget, $cr) = @_; - #event-handling - #we simulate a 2button-press here - $self->{_view_button_handler} = $self->{_view}->signal_connect( - 'button-press-event' => sub { - my ($view, $event) = @_; - return FALSE unless defined $event; + my $pixbuf = $self->{_screenshot_pixbuf}; + return FALSE unless $pixbuf; - my $s = $self->{_selector}->get_selection; + my $zoom = $self->{_state}->{zoom} || 4; + my $cx = $self->{_state}->{cursor_x} // 0; + my $cy = $self->{_state}->{cursor_y} // 0; - if ($event->button == 1) { + my $allocated_w = $widget->get_allocated_width; + my $allocated_h = $widget->get_allocated_height; - unless (defined $self->{_dclick}) { + my $crop_w = int($allocated_w / $zoom); + my $crop_h = int($allocated_h / $zoom); - $self->{_dclick} = $event->time; - return FALSE; + my $src_x = int($cx - $crop_w / 2); + my $src_y = int($cy - $crop_h / 2); - } else { + $src_x = 0 if $src_x < 0; + $src_y = 0 if $src_y < 0; - if ($event->time - $self->{_dclick} <= 500) { + my $max_x = $pixbuf->get_width - $crop_w; + my $max_y = $pixbuf->get_height - $crop_h; - $self->{_select_window}->hide; - $self->{_zoom_window}->hide; - $self->{_prop_window}->hide; + $src_x = $max_x if $src_x > $max_x; + $src_y = $max_y if $src_y > $max_y; - #A short timeout to give the server a chance to - #redraw the area - Glib::Timeout->add( - $self->{_hide_time}, - sub { - Gtk3->main_quit; - return FALSE; - }); - Gtk3->main(); + my $crop = $pixbuf->new_subpixbuf($src_x, $src_y, $crop_w, $crop_h); - $output = $self->take_screenshot($s, $clean_pixbuf); - $self->quit; + $cr->save; + $cr->scale($zoom, $zoom); + Gtk3::Gdk::cairo_set_source_pixbuf($cr, $crop, 0, 0); + $cr->paint; + $cr->restore; - } else { - - $self->{_dclick} = $event->time; - return FALSE; - - } - } + my $mid_x = int($allocated_w / 2); + my $mid_y = int($allocated_h / 2); + if (($zoom % 2) != 0) { + $mid_x += 0.5; + $mid_y += 0.5; } - }); - #event-handling - #all other events - $self->{_view_event_handler} = $self->{_view}->signal_connect( - 'event' => sub { - my ($window, $event) = @_; - return FALSE unless defined $event; + $cr->set_source_rgba(1.0, 0.0, 0.0, 0.8); + $cr->set_line_width($zoom); - my $s = $self->{_selector}->get_selection; + my $half_pixel = $zoom / 2; - #~ print $event->type, "\n"; + $cr->move_to(0, $mid_y); + $cr->line_to($mid_x - $half_pixel, $mid_y); + + $cr->move_to($mid_x + $half_pixel, $mid_y); + $cr->line_to($allocated_w, $mid_y); - #handle button-release event - if ($event->type eq 'button-release') { + $cr->move_to($mid_x, 0); + $cr->line_to($mid_x, $mid_y - $half_pixel); + + $cr->move_to($mid_x, $mid_y + $half_pixel); + $cr->line_to($mid_x, $allocated_h); - if ($event->button == 3) { - if ($self->{_prop_active}) { - Gtk3::Gdk::keyboard_ungrab(Gtk3::get_current_event_time()); - $self->{_prop_window}->hide; - $self->{_prop_active} = FALSE; - Gtk3::Gdk::keyboard_grab($self->{_select_window}->get_window, 0, Gtk3::get_current_event_time()); - } else { - Gtk3::Gdk::keyboard_ungrab(Gtk3::get_current_event_time()); - my ($window_at_pointer, $x, $y, $mask) = $self->{_root}->get_pointer; - $self->{_prop_window}->move($x, $y); - $self->{_prop_window}->show_all; - $self->{_prop_active} = TRUE; - Gtk3::Gdk::keyboard_grab($self->{_prop_window}->get_window, 0, Gtk3::get_current_event_time()); - } - } elsif ($event->button == 1) { - if (not $self->{_confirmation_necessary}) { - $self->{_select_window}->hide; - $self->{_zoom_window}->hide; - $self->{_prop_window}->hide; - - #A short timeout to give the server a chance to - #redraw the area - Glib::Timeout->add( - $self->{_hide_time}, - sub { - Gtk3->main_quit; - return FALSE; - }); - Gtk3->main(); - - $output = $self->take_screenshot($s, $clean_pixbuf); - $self->quit; - } - } - - #handle motion-notify - } elsif ($event->type eq 'motion-notify') { - - #update zoom window - if ($self->{_zoom_active} && $self->{_view}->get_zoom == 1) { - - my $s = $self->{_selector}->get_selection; - my $v = $self->{_view}->get_viewport; - - my ($window_at_pointer, $x, $y, $mask) = $self->{_root}->get_pointer; - - #event coordinates - my $zoom = $self->{_view}->get_zoom; - my $ev_x = int($v->{x} / $zoom + $x * $self->{_dpi_scale} / $zoom); - my $ev_y = int($v->{y} / $zoom + $y * $self->{_dpi_scale} / $zoom); - - #sync cursor with selection - if (0 && defined $s) { - my $cursor = $self->{_selector}->cursor_at_point($x, $y)->get_cursor_type; - print Dumper($cursor); - - my $sx = $s->{x}; - my $sy = $s->{y}; - my $sw = $s->{width}; - my $sh = $s->{height}; - - if ($cursor eq 'bottom-right-corner') { - - $ev_x = $sx + $sw - 1; - $ev_y = $sy + $sh - 1; - - } elsif ($cursor eq 'right-side') { - - $ev_x = $sx + $sw - 1; - - } elsif ($cursor eq 'top-right-corner') { - - $ev_x = $sx + $sw - 1; - $ev_y = $sy; + $cr->stroke; - } elsif ($cursor eq 'top-side') { - - $ev_y = $sy; + return FALSE; + } + ); - } elsif ($cursor eq 'top-left-corner') { + my $set_cursor_text = sub { + my ($x, $y) = @_; + $xlabel->set_text("X: " . (int($x) + 1)); + $ylabel->set_text("Y: " . (int($y) + 1)); + }; - $ev_x = $sx; - $ev_y = $sy; + my $update_size_text = sub { + if (defined $self->{_state}) { + my $s = $self->{_state}->{sel}; + if (defined $s) { + my $w = int($s->{width} // 0); + my $h = int($s->{height} // 0); + + $rlabel->set_text($w . " x " . $h) if defined $rlabel; + return; + } + } + + $rlabel->set_text("0 x 0") if defined $rlabel; + }; - } elsif ($cursor eq 'left-side') { + my $finish_capture = sub { + my $s = $state->{sel}; - $ev_x = $sx; + $self->{_select_window}->hide if defined $self->{_select_window}; + $self->{_zoom_window}->hide if defined $self->{_zoom_window}; + $self->{_prop_window}->hide if defined $self->{_prop_window}; - } elsif ($cursor eq 'bottom-left-corner') { + Glib::Timeout->add($self->{_hide_time}, sub { + Gtk3->main_quit; + return FALSE; + }); - $ev_x = $sx; - $ev_y = $sy + $sh - 1; + Gtk3->main(); + $output = $self->take_screenshot($s, $clean_pixbuf); + $self->quit; + }; - } elsif ($cursor eq 'bottom-side') { - $ev_y = $sy + $sh - 1; + # initial help-state + $self->{_selector_init} = $self->{_show_help} ? TRUE : FALSE; + $self->{_selector_init_zoom} = 0; - } + my $is_dragging = 0; + my $is_moving_rect = 0; + my $is_resizing = ''; # directions can be as follows: 'n', 's', 'w', 'e', 'nw', 'ne', 'sw', 'se' + + my ($start_x, $start_y) = (0, 0); + my ($offset_x, $offset_y) = (0, 0); + + my $handle_size = 6 * ($self->{_dpi_scale} // 1); + + # helper to get which edge or corner is targeted by the mouse pointer + my $get_resize_edge = sub { + my ($mx, $my) = @_; + my $s = $self->{_state}->{sel}; + return '' unless (defined $s && $s->{width} > 0 && $s->{height} > 0); + + my $x1 = $s->{x}; + my $y1 = $s->{y}; + my $x2 = $s->{x} + $s->{width}; + my $y2 = $s->{y} + $s->{height}; + + return '' if ($mx < $x1 - $handle_size || $mx > $x2 + $handle_size || + $my < $y1 - $handle_size || $my > $y2 + $handle_size); + + my $near_n = (abs($my - $y1) <= $handle_size); + my $near_s = (abs($my - $y2) <= $handle_size); + my $near_w = (abs($mx - $x1) <= $handle_size); + my $near_e = (abs($mx - $x2) <= $handle_size); + + return 'nw' if ($near_n && $near_w); + return 'ne' if ($near_n && $near_e); + return 'sw' if ($near_s && $near_w); + return 'se' if ($near_s && $near_e); + return 'n' if ($near_n && $mx >= $x1 && $mx <= $x2); + return 's' if ($near_s && $mx >= $x1 && $mx <= $x2); + return 'w' if ($near_w && $my >= $y1 && $my <= $y2); + return 'e' if ($near_e && $my >= $y1 && $my <= $y2); + + return ''; + }; - } + $self->{_view_button_handler} = $self->{_canvas}->signal_connect('button-press-event' => sub { + my ($widget, $event) = @_; + return FALSE unless defined $event; + + if ($event->button == 1) { + my $mx = int($event->x); + my $my = int($event->y); + my $s = $self->{_state}->{sel}; + + my $edge = $get_resize_edge->($mx, $my); + + if ($edge ne '') { + $is_resizing = $edge; + $start_x = $mx; + $start_y = $my; + } elsif (defined $s && $s->{width} > 0 && $s->{height} > 0 && + $mx >= $s->{x} && $mx <= ($s->{x} + $s->{width}) && + $my >= $s->{y} && $my <= ($s->{y} + $s->{height})) { + + $is_moving_rect = 1; + $offset_x = $mx - $s->{x}; + $offset_y = $my - $s->{y}; + } else { + $is_dragging = 1; + $start_x = $mx; + $start_y = $my; - #update label in zoom_window - $xlabel->set_text("X: " . ($ev_x + 1)); - $ylabel->set_text("Y: " . ($ev_y + 1)); + if (defined $self->{_help_label}) { + $self->{_help_label}->hide; + } - #check pos and geometry of the zoom window and move it if needed - $self->zoom_check_pos(); + $self->{_state}->{sel} = { x => $start_x, y => $start_y, width => 0, height => 0 }; + } + $queue_redraw->(); + } + return TRUE; + }); - #move cursor on the canvas... - $cursor_item->set( - x => $ev_x - 10, - y => $ev_y - 10, - ); + $self->{_view_event_handler} = $self->{_canvas}->signal_connect('motion-notify-event' => sub { + my ($widget, $event) = @_; + return FALSE unless defined $event; - #update scroll region - #this is significantly faster than - #scroll_to - #$canvas->set_scroll_region($ev_x - 9, $ev_y - 9, $ev_x + 10, $ev_y + 10); - $canvas->scroll_to($ev_x - 10, $ev_y - 10); + my $mx = int($event->x); + my $my = int($event->y); - #update zoom_window text - if (defined $s) { - $rlabel->set_text($s->{width} . " x " . $s->{height}); - } else { - $rlabel->set_text("0 x 0"); - } + $self->{_state}->{cursor_x} = $mx; + $self->{_state}->{cursor_y} = $my; + $set_cursor_text->($mx, $my) if defined $set_cursor_text; - } #zoom active + if (defined $self->{_zoom_window} && $self->{_zoom_window}->get_visible && $self->can('zoom_check_pos')) { + $self->zoom_check_pos(); + } - #handle key-press + if (defined $widget->get_window()) { + my $gdk_window = $widget->get_window(); + my $display = $gdk_window->get_display(); + my $s = $self->{_state}->{sel}; + + my $cursor_type = 'crosshair'; + + my $active_edge = $is_resizing ne '' ? $is_resizing : $get_resize_edge->($mx, $my); + + if ($active_edge ne '') { + my %cursors = ( + n => 'n-resize', s => 's-resize', w => 'w-resize', e => 'e-resize', + nw => 'nw-resize', ne => 'ne-resize', sw => 'sw-resize', se => 'se-resize' + ); + $cursor_type = $cursors{$active_edge}; + } elsif ($is_moving_rect) { + $cursor_type = 'grabbing'; + } elsif (defined $s && $s->{width} > 0 && $s->{height} > 0) { + if ($mx >= $s->{x} && $mx <= ($s->{x} + $s->{width}) && + $my >= $s->{y} && $my <= ($s->{y} + $s->{height})) { + $cursor_type = 'grab'; + } } - }); - - $self->{_key_handler} = $self->{_select_window}->signal_connect( - 'key-press-event' => sub { - my ($window, $event) = @_; - return FALSE unless defined $event; - my $s = $self->{_selector}->get_selection; - #where is the pointer currently? - my ($window_at_pointer, $x, $y, $mask) = $self->{_root}->get_pointer; + my $new_cursor = Gtk3::Gdk::Cursor->new_from_name($display, $cursor_type); + $gdk_window->set_cursor($new_cursor) if defined $new_cursor; + } - #toggle zoom window - if ($event->keyval == Gtk3::Gdk::keyval_from_name('space')) { + if ($is_dragging) { + my $x = $mx < $start_x ? $mx : $start_x; + my $y = $my < $start_y ? $my : $start_y; + my $w = abs($mx - $start_x); + my $h = abs($my - $start_y); + $self->{_state}->{sel} = { x => $x, y => $y, width => $w, height => $h }; + $update_size_text->() if defined $update_size_text; + + } elsif ($is_moving_rect) { + my $s = $self->{_state}->{sel}; + if (defined $s) { + my $new_x = $mx - $offset_x; + my $new_y = $my - $offset_y; + + $new_x = 0 if $new_x < 0; + $new_y = 0 if $new_y < 0; + my $max_x = $self->{_root}->{w} - $s->{width}; + my $max_y = $self->{_root}->{h} - $s->{height}; + $new_x = $max_x if $new_x > $max_x; + $new_y = $max_y if $new_y > $max_y; + + $s->{x} = $new_x; $s->{y} = $new_y; + } + } elsif ($is_resizing ne '') { + my $s = $self->{_state}->{sel}; + if (defined $s) { + my $x1 = $s->{x}; my $y1 = $s->{y}; + my $x2 = $s->{x} + $s->{width}; my $y2 = $s->{y} + $s->{height}; + + if ($is_resizing =~ /w/ && $mx < $x2) { $x1 = $mx; } + if ($is_resizing =~ /e/ && $mx > $x1) { $x2 = $mx; } + if ($is_resizing =~ /n/ && $my < $y2) { $y1 = $my; } + if ($is_resizing =~ /s/ && $my > $y1) { $y2 = $my; } + + $s->{x} = $x1; + $s->{y} = $y1; + $s->{width} = $x2 - $x1; + $s->{height} = $y2 - $y1; + $update_size_text->() if defined $update_size_text; + } + } - if ($self->{_zoom_active}) { - $self->{_zoom_window}->hide; - $self->{_zoom_active} = FALSE; - } elsif ($self->{_view}->get_zoom == 1) { - $self->zoom_check_pos(); - $self->{_zoom_active} = TRUE; - } + if (($is_dragging || $is_moving_rect || $is_resizing ne '') && $self->{_prop_active}) { + my $s = $self->{_state}->{sel}; + if (defined $s) { + $self->{_x_spin_w}->signal_handler_block($self->{_x_spin_w_handler}) if defined $self->{_x_spin_w_handler}; + $self->{_y_spin_w}->signal_handler_block($self->{_y_spin_w_handler}) if defined $self->{_y_spin_w_handler}; + $self->{_width_spin_w}->signal_handler_block($self->{_width_spin_w_handler}) if defined $self->{_width_spin_w_handler}; + $self->{_height_spin_w}->signal_handler_block($self->{_height_spin_w_handler}) if defined $self->{_height_spin_w_handler}; + + $self->{_x_spin_w}->set_value(int($s->{x})) if defined $self->{_x_spin_w}; + $self->{_y_spin_w}->set_value(int($s->{y})) if defined $self->{_y_spin_w}; + $self->{_width_spin_w}->set_value(int($s->{width})) if defined $self->{_width_spin_w}; + $self->{_height_spin_w}->set_value(int($s->{height})) if defined $self->{_height_spin_w}; + + $self->{_x_spin_w}->signal_handler_unblock($self->{_x_spin_w_handler}) if defined $self->{_x_spin_w_handler}; + $self->{_y_spin_w}->signal_handler_unblock($self->{_y_spin_w_handler}) if defined $self->{_y_spin_w_handler}; + $self->{_width_spin_w}->signal_handler_unblock($self->{_width_spin_w_handler}) if defined $self->{_width_spin_w_handler}; + $self->{_height_spin_w}->signal_handler_unblock($self->{_height_spin_w_handler}) if defined $self->{_height_spin_w_handler}; + } + } - #toggle prop dialog - } elsif ($event->keyval == Gtk3::Gdk::keyval_from_name('Shift_L') || $event->keyval == Gtk3::Gdk::keyval_from_name('Shift_R')) { + $queue_redraw->(); + return TRUE; + }); - if ($self->{_prop_active}) { - Gtk3::Gdk::keyboard_ungrab(Gtk3::get_current_event_time()); - $self->{_prop_window}->hide; - $self->{_prop_active} = FALSE; - Gtk3::Gdk::keyboard_grab($self->{_select_window}->get_window, 0, Gtk3::get_current_event_time()); + $self->{_view_release_handler} = $self->{_canvas}->signal_connect('button-release-event' => sub { + my ($widget, $event) = @_; + return FALSE unless defined $event; + if ($event->button == 3) { + if (defined $self->{_prop_window}) { + if ($self->{_prop_active}) { + $self->{_prop_window}->hide; + $self->{_prop_active} = 0; + } else { + my $mx = int($event->x); + my $my = int($event->y); + + if (defined $self->{_state} && defined $self->{_state}->{sel}) { + my $s = $self->{_state}->{sel}; + $self->{_x_spin_w}->set_value(int($s->{x})) if defined $self->{_x_spin_w}; + $self->{_y_spin_w}->set_value(int($s->{y})) if defined $self->{_y_spin_w}; + $self->{_width_spin_w}->set_value(int($s->{width})) if defined $self->{_width_spin_w}; + $self->{_height_spin_w}->set_value(int($s->{height})) if defined $self->{_height_spin_w}; } else { - Gtk3::Gdk::keyboard_ungrab(Gtk3::get_current_event_time()); - my ($window_at_pointer, $x, $y, $mask) = $self->{_root}->get_pointer; - $self->{_prop_window}->move($x, $y); - $self->{_prop_window}->show_all; - $self->{_prop_active} = TRUE; - Gtk3::Gdk::keyboard_grab($self->{_prop_window}->get_window, 0, Gtk3::get_current_event_time()); + $self->{_x_spin_w}->set_value($mx) if defined $self->{_x_spin_w}; + $self->{_y_spin_w}->set_value($my) if defined $self->{_y_spin_w}; + $self->{_width_spin_w}->set_value(0) if defined $self->{_width_spin_w}; + $self->{_height_spin_w}->set_value(0) if defined $self->{_height_spin_w}; } + + $self->{_prop_window}->show_all; + $self->{_prop_active} = 1; + + if (defined $self->{_x_spin_w}) { + $self->{_x_spin_w}->grab_focus; + } + } + } + } elsif ($event->button == 1) { + my $was_modifying = ($is_moving_rect || $is_resizing ne ''); + $is_dragging = 0; + $is_moving_rect = 0; + $is_resizing = ''; + + if (!$self->{_confirmation_necessary} && !$was_modifying) { + $finish_capture->(); + } + } - #abort screenshot - } elsif ($event->keyval == Gtk3::Gdk::keyval_from_name('Escape')) { - - $self->quit; + return TRUE; + }); - #move / resize selector - } elsif ($event->keyval == Gtk3::Gdk::keyval_from_name('Up')) { - if ($event->state >= 'control-mask' && $s) { - $s->{height} -= 1; - $self->{_selector}->set_selection($s); - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{width} + $s->{x}, $s->{height} + $s->{y}); - } elsif ($event->state >= 'mod1-mask' && $s) { - $s->{y} -= 1; - $self->{_selector}->set_selection($s); - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{x}, $s->{y}); - } else { - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x, $y - 1); - } - } elsif ($event->keyval == Gtk3::Gdk::keyval_from_name('Down')) { - - if ($event->state >= 'control-mask' && $s) { - $s->{height} += 1; - $self->{_selector}->set_selection($s); - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{width} + $s->{x}, $s->{height} + $s->{y}); - } elsif ($event->state >= 'control-mask') { - $self->{_selector}->set_selection({x=>$x, y=>$y, width=>1, height=>2}); - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x + 1, $y + 2); - } elsif ($event->state >= 'mod1-mask' && $s) { - $s->{y} += 1; - $self->{_selector}->set_selection($s); - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{x}, $s->{y}); - } else { - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x, $y + 1); - } + # --- keyboard --- + $self->{_key_handler} = $self->{_select_window}->signal_connect('key-press-event' => sub { + my ($window, $event) = @_; + return FALSE unless defined $event; - } elsif ($event->keyval == Gtk3::Gdk::keyval_from_name('Left')) { + my $state = $self->{_state}; + my $s = $state->{sel}; + my ($window_at_pointer, $x, $y, $mask) = $self->{_root}->get_pointer; - if ($event->state >= 'control-mask' && $s) { - $s->{width} -= 1; - $self->{_selector}->set_selection($s); - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{width} + $s->{x}, $s->{height} + $s->{y}); - } elsif ($event->state >= 'mod1-mask' && $s) { - $s->{x} -= 1; - $self->{_selector}->set_selection($s); - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{x}, $s->{y}); - } else { - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x - 1, $y); - } + my $keyname = Gtk3::Gdk::keyval_name($event->keyval); - } elsif ($event->keyval == Gtk3::Gdk::keyval_from_name('Right')) { - - if ($event->state >= 'control-mask' && $s) { - $s->{width} += 1; - $self->{_selector}->set_selection($s); - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{width} + $s->{x}, $s->{height} + $s->{y}); - } elsif ($event->state >= 'control-mask') { - $self->{_selector}->set_selection({x=>$x, y=>$y, width=>2, height=>1}); - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x + 2, $y + 1); - } elsif ($event->state >= 'mod1-mask' && $s) { - $s->{x} += 1; - $self->{_selector}->set_selection($s); - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{x}, $s->{y}); - } else { - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x + 1, $y); + if ($keyname eq 'Shift_L' || $keyname eq 'Shift_R') { + if (defined $self->{_prop_window}) { + if ($self->{_prop_active}) { + $self->{_prop_window}->hide; + $self->{_prop_active} = 0; + } else { + if (defined $s) { + $self->{_x_spin_w}->set_value(int($s->{x})) if defined $self->{_x_spin_w}; + $self->{_y_spin_w}->set_value(int($s->{y})) if defined $self->{_y_spin_w}; + $self->{_width_spin_w}->set_value(int($s->{width})) if defined $self->{_width_spin_w}; + $self->{_height_spin_w}->set_value(int($s->{height})) if defined $self->{_height_spin_w}; } - - #zoom in - } elsif ($event->keyval == Gtk3::Gdk::keyval_from_name('KP_Add') - || $event->keyval == Gtk3::Gdk::keyval_from_name('plus') - || $event->keyval == Gtk3::Gdk::keyval_from_name('equal')) - { - - if ($event->state >= 'control-mask') { - $self->{_view}->zoom_in; + $self->{_prop_window}->show_all; + $self->{_prop_active} = 1; + + if (defined $self->{_x_spin_w}) { + $self->{_x_spin_w}->grab_focus; } + } + $queue_redraw->() if defined $queue_redraw; + return TRUE; + } + } - #zoom out - } elsif ($event->keyval == Gtk3::Gdk::keyval_from_name('KP_Subtract') - || $event->keyval == Gtk3::Gdk::keyval_from_name('minus')) - { + my $has_ctrl = $event->state & 'control-mask'; + my $has_alt = $event->state & 'mod1-mask'; + + my $kv_space = Gtk3::Gdk::keyval_from_name('space'); + my $kv_escape = Gtk3::Gdk::keyval_from_name('Escape'); + my $kv_up = Gtk3::Gdk::keyval_from_name('Up'); + my $kv_down = Gtk3::Gdk::keyval_from_name('Down'); + my $kv_left = Gtk3::Gdk::keyval_from_name('Left'); + my $kv_right = Gtk3::Gdk::keyval_from_name('Right'); + my $kv_add = Gtk3::Gdk::keyval_from_name('KP_Add'); + my $kv_plus = Gtk3::Gdk::keyval_from_name('plus'); + my $kv_equal = Gtk3::Gdk::keyval_from_name('equal'); + my $kv_sub = Gtk3::Gdk::keyval_from_name('KP_Subtract'); + my $kv_minus = Gtk3::Gdk::keyval_from_name('minus'); + my $kv_zero = Gtk3::Gdk::keyval_from_name('0'); + my $kv_return = Gtk3::Gdk::keyval_from_name('Return'); + my $kv_kp_enter = Gtk3::Gdk::keyval_from_name('KP_Enter'); + + if ($event->keyval == $kv_space) { + if (defined $self->{_zoom_window} && $self->{_zoom_window}->get_visible) { + $self->{_zoom_window}->hide; + $self->{_zoom_active} = FALSE; + } else { + $self->{_zoom_active} = TRUE; + $self->zoom_check_pos() if $self->can('zoom_check_pos'); + $self->{_zoom_window}->show_all if defined $self->{_zoom_window}; + } + $queue_redraw->() if defined $queue_redraw; + return TRUE; + + } elsif ($event->keyval == $kv_escape) { + $self->quit; + + } elsif ($event->keyval == $kv_up) { + if ($has_ctrl && $s) { + $s->{height} -= 1; + $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{width} + $s->{x}, $s->{height} + $s->{y}); + } elsif ($has_alt && $s) { + $s->{y} -= 1; + $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{x}, $s->{y}); + } else { + $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x, $y - 1); + } - if ($event->state >= 'control-mask') { - $self->{_view}->zoom_out; - } + } elsif ($event->keyval == $kv_down) { + if ($has_ctrl && $s) { + $s->{height} += 1; + $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{width} + $s->{x}, $s->{height} + $s->{y}); + } elsif ($has_ctrl) { + $state->{sel} = { x => $x, y => $y, width => 1, height => 2 }; + $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x + 1, $y + 2); + } elsif ($has_alt && $s) { + $s->{y} += 1; + $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{x}, $s->{y}); + } else { + $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x, $y + 1); + } - #zoom normal - } elsif ($event->keyval == Gtk3::Gdk::keyval_from_name('0')) { + } elsif ($event->keyval == $kv_left) { + if ($has_ctrl && $s) { + $s->{width} -= 1; + $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{width} + $s->{x}, $s->{height} + $s->{y}); + } elsif ($has_alt && $s) { + $s->{x} -= 1; + $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{x}, $s->{y}); + } else { + $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x - 1, $y); + } - if ($event->state >= 'control-mask') { - $self->{_view}->set_zoom(1); - } + } elsif ($event->keyval == $kv_right) { + if ($has_ctrl && $s) { + $s->{width} += 1; + $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{width} + $s->{x}, $s->{height} + $s->{y}); + } elsif ($has_ctrl) { + $state->{sel} = { x => $x, y => $y, width => 2, height => 1 }; + $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x + 2, $y + 1); + } elsif ($has_alt && $s) { + $s->{x} += 1; + $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{x}, $s->{y}); + } else { + $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x + 1, $y); + } - #take screenshot - } elsif ($event->keyval == Gtk3::Gdk::keyval_from_name('Return') || $event->keyval == Gtk3::Gdk::keyval_from_name('KP_Enter')) { + } elsif ($event->keyval == $kv_add || $event->keyval == $kv_plus || $event->keyval == $kv_equal) { + $state->{zoom}++ if $has_ctrl; - $self->{_select_window}->hide; - $self->{_zoom_window}->hide; - $self->{_prop_window}->hide; + } elsif ($event->keyval == $kv_sub || $event->keyval == $kv_minus) { + $state->{zoom}-- if $has_ctrl; + $state->{zoom} = 1 if $state->{zoom} < 1; - #A short timeout to give the server a chance to - #redraw the area - Glib::Timeout->add( - $self->{_hide_time}, - sub { - Gtk3->main_quit; - return FALSE; - }); - Gtk3->main(); + } elsif ($event->keyval == $kv_zero) { + $state->{zoom} = 1 if $has_ctrl; - $output = $self->take_screenshot($s, $clean_pixbuf); - $self->quit; + } elsif ($event->keyval == $kv_return || $event->keyval == $kv_kp_enter) { + if (defined $s) { + $finish_capture->(); + } + } - } - }); + $queue_redraw->() if defined $queue_redraw; + return TRUE; + }); + + + # initial selection + Glib::Idle->add(sub { + if ($self->{_init_w} && $self->{_init_h}) { + $state->{sel} = { + x => $self->{_init_x}, + y => $self->{_init_y}, + width => $self->{_init_w}, + height => $self->{_init_h}, + }; + $queue_redraw->(); + } + return FALSE; + }); + # grab keyboard my $status = Gtk3::Gdk::keyboard_grab($self->{_select_window}->get_window, 0, Gtk3::get_current_event_time()); - #~ if($status eq 'success'){ if ($self->{_zoom_active}) { $self->{_zoom_window}->show_all; $self->{_zoom_window}->get_window->set_override_redirect(TRUE); $self->zoom_check_pos(); $self->{_zoom_window}->get_window->raise; } - Gtk3->main(); - - #~ }else{ - #~ $output = 1; - #~ $self->clean; - #~ } + Gtk3->main(); return $output; } + sub zoom_check_pos { my $self = shift; - my $s = $self->{_selector}->get_selection; - my $v = $self->{_view}->get_viewport; + return FALSE unless defined $self->{_zoom_window}; + return FALSE unless $self->{_zoom_window}->get_visible; - return FALSE unless defined $v; - - my ($window_at_pointer, $x, $y, $mask) = $self->{_root}->get_pointer; - - #event coordinates - my $zoom = $self->{_view}->get_zoom; - my $ev_x = int($v->{x} / $zoom + $x * $self->{_dpi_scale} / $zoom); - my $ev_y = int($v->{y} / $zoom + $y * $self->{_dpi_scale} / $zoom); + my ($window_at_pointer, $ev_x, $ev_y, $mask) = $self->{_root}->get_pointer; my ($zw, $zh) = $self->{_zoom_window}->get_size; my ($zx, $zy) = $self->{_zoom_window}->get_position; - my $distance = 50 * $self->{_dpi_scale}; - my $zzw = $zw * $self->{_dpi_scale} + $distance; - my $zzh = $zh * $self->{_dpi_scale} + $distance; - - my $sregion = undef; - if (defined $s) { - $sregion = Cairo::Region->create({x=>$s->{x}, y=>$s->{y}, width=>$s->{width} + $distance, height=>$s->{height} + $distance}); - } else { - $sregion = Cairo::Region->create({x=>$ev_x, y=>$ev_y, width=>$distance, height=>$distance}); - } - - my $otype = $sregion->contains_rectangle({x=>$zx, y=>$zy, width=>$zzw, height=>$zzh}); - if ($otype eq 'in' || $otype eq 'part' || !$self->{_zoom_window}->get_visible) { - - my $moved = FALSE; + my $distance = 50 * ($self->{_dpi_scale} // 1); + + my $box_x1 = $zx - $distance; + my $box_y1 = $zy - $distance; + my $box_x2 = $zx + $zw + $distance; + my $box_y2 = $zy + $zh + $distance; - #possible positions if we need to move the zoom window + if ($ev_x >= $box_x1 && $ev_x <= $box_x2 && $ev_y >= $box_y1 && $ev_y <= $box_y2) { + my @pos = ( - {x=>$self->{_root}->{x}, y=>$self->{_root}->{y}, }, - {x=>$self->{_root}->{x}, y=>$self->{_root}->{h} - $zh}, - {x=>$self->{_root}->{w} - $zw, y=>$self->{_root}->{y}, }, - {x=>$self->{_root}->{w} - $zw, y=>$self->{_root}->{h} - $zh}); - - foreach (@pos) { - my $otypet = $sregion->contains_rectangle({x=>$_->{x}*$self->{_dpi_scale}, y=>$_->{y}*$self->{_dpi_scale}, width=>$zzw, height=>$zzh}); - if ($otypet eq 'out') { - $self->{_zoom_window}->move($_->{x}, $_->{y}); - $self->{_zoom_window}->show_all; - $moved = TRUE; - last; + {x => $self->{_root}->{x}, y => $self->{_root}->{y}}, # 1. Oben links + {x => $self->{_root}->{x}, y => $self->{_root}->{h} - $zh}, # 2. Unten links + {x => $self->{_root}->{w} - $zw, y => $self->{_root}->{h} - $zh}, # 3. Unten rechts + {x => $self->{_root}->{w} - $zw, y => $self->{_root}->{y}}, # 4. Oben rechts + ); + + foreach my $p (@pos) { + my $p_box_x1 = $p->{x} - $distance; + my $p_box_y1 = $p->{y} - $distance; + my $p_box_x2 = $p->{x} + $zw + $distance; + my $p_box_y2 = $p->{y} + $zh + $distance; + + if (!($ev_x >= $p_box_x1 && $ev_x <= $p_box_x2 && $ev_y >= $p_box_y1 && $ev_y <= $p_box_y2)) { + $self->{_zoom_window}->move($p->{x}, $p->{y}); + $self->{_zoom_window}->queue_draw; + return TRUE; } - - } - - #if window could not be moved without covering the selection area - unless ($moved) { - $moved = FALSE; - $self->{_zoom_window}->hide; } } + return TRUE; } + sub adjust_prop_values { my $self = shift; - #block 'value-change' handlers for widgets - #so we do not apply the changes twice - $self->{_x_spin_w}->signal_handler_block($self->{_x_spin_w_handler}); - $self->{_y_spin_w}->signal_handler_block($self->{_y_spin_w_handler}); - $self->{_width_spin_w}->signal_handler_block($self->{_width_spin_w_handler}); - $self->{_height_spin_w}->signal_handler_block($self->{_height_spin_w_handler}); - - my $s = $self->{_selector}->get_selection; + return unless defined $self->{_state}; - if ($s) { - $self->{_x_spin_w}->set_value($s->{x}); - $self->{_x_spin_w}->set_range(0, $self->{_root}->{w} - $s->{width}); + my $s = $self->{_state}->{sel}; - $self->{_y_spin_w}->set_value($s->{y}); - $self->{_y_spin_w}->set_range(0, $self->{_root}->{h} - $s->{height}); + if (defined $s) { + $self->{_x_spin_w}->signal_handler_block($self->{_x_spin_w_handler}) if defined $self->{_x_spin_w_handler}; + $self->{_y_spin_w}->signal_handler_block($self->{_y_spin_w_handler}) if defined $self->{_y_spin_w_handler}; + $self->{_width_spin_w}->signal_handler_block($self->{_width_spin_w_handler}) if defined $self->{_width_spin_w_handler}; + $self->{_height_spin_w}->signal_handler_block($self->{_height_spin_w_handler}) if defined $self->{_height_spin_w_handler}; - $self->{_width_spin_w}->set_value($s->{width}); - $self->{_width_spin_w}->set_range(0, $self->{_root}->{w} - $s->{x}); + $self->{_x_spin_w}->set_value(int($s->{x})) if defined $self->{_x_spin_w}; + $self->{_x_spin_w}->set_range(0, int($self->{_root}->{w} - $s->{width})) if defined $self->{_x_spin_w}; - $self->{_height_spin_w}->set_value($s->{height}); - $self->{_height_spin_w}->set_range(0, $self->{_root}->{h} - $s->{y}); - } + $self->{_y_spin_w}->set_value(int($s->{y})) if defined $self->{_y_spin_w}; + $self->{_y_spin_w}->set_range(0, int($self->{_root}->{h} - $s->{height})) if defined $self->{_y_spin_w}; - #unblock 'value-change' handlers for widgets - $self->{_x_spin_w}->signal_handler_unblock($self->{_x_spin_w_handler}); - $self->{_y_spin_w}->signal_handler_unblock($self->{_y_spin_w_handler}); - $self->{_width_spin_w}->signal_handler_unblock($self->{_width_spin_w_handler}); - $self->{_height_spin_w}->signal_handler_unblock($self->{_height_spin_w_handler}); + $self->{_width_spin_w}->set_value(int($s->{width})) if defined $self->{_width_spin_w}; + $self->{_width_spin_w}->set_range(0, int($self->{_root}->{w} - $s->{x})) if defined $self->{_width_spin_w}; - return TRUE; + $self->{_height_spin_w}->set_value(int($s->{height})) if defined $self->{_height_spin_w}; + $self->{_height_spin_w}->set_range(0, int($self->{_root}->{h} - $s->{y})) if defined $self->{_height_spin_w}; + $self->{_x_spin_w}->signal_handler_unblock($self->{_x_spin_w_handler}) if defined $self->{_x_spin_w_handler}; + $self->{_y_spin_w}->signal_handler_unblock($self->{_y_spin_w_handler}) if defined $self->{_y_spin_w_handler}; + $self->{_width_spin_w}->signal_handler_unblock($self->{_width_spin_w_handler}) if defined $self->{_width_spin_w_handler}; + $self->{_height_spin_w}->signal_handler_unblock($self->{_height_spin_w_handler}) if defined $self->{_height_spin_w_handler}; + } } + sub select_dialog { my $self = shift; my $d = $self->{_sc}->get_gettext; - #current selection - my $s = $self->{_selector}->get_selection; + my $state = $self->{_state}; + my $s = defined $state ? $state->{sel} : undef; my $sx = 0; my $sy = 0; @@ -857,11 +957,19 @@ sub select_dialog { } my $value_callback = sub { - $self->{_selector} - ->set_selection({x=>$self->{_x_spin_w}->get_value, y=>$self->{_y_spin_w}->get_value, width=>$self->{_width_spin_w}->get_value, height=>$self->{_height_spin_w}->get_value}); + if (defined $self->{_state}) { + $self->{_state}->{sel} = { + x => int($self->{_x_spin_w}->get_value), + y => int($self->{_y_spin_w}->get_value), + width => int($self->{_width_spin_w}->get_value), + height => int($self->{_height_spin_w}->get_value), + }; + $self->{_canvas}->queue_draw if defined $self->{_canvas}; + $self->{_zoom_area}->queue_draw if defined $self->{_zoom_area}; + } }; - #X + # x coordinate my $xw_label = Gtk3::Label->new($d->get("X") . ":"); $self->{_x_spin_w} = Gtk3::SpinButton->new_with_range(0, $self->{_root}->{w}, 1); $self->{_x_spin_w}->set_value($sx); @@ -872,7 +980,7 @@ sub select_dialog { $xw_hbox->pack_start($xw_label, FALSE, FALSE, 5); $xw_hbox->pack_start($self->{_x_spin_w}, FALSE, FALSE, 5); - #y + # y coordinate my $yw_label = Gtk3::Label->new($d->get("Y") . ":"); $self->{_y_spin_w} = Gtk3::SpinButton->new_with_range(0, $self->{_root}->{h}, 1); $self->{_y_spin_w}->set_value($sy); @@ -883,10 +991,11 @@ sub select_dialog { $yw_hbox->pack_start($yw_label, FALSE, FALSE, 5); $yw_hbox->pack_start($self->{_y_spin_w}, FALSE, FALSE, 5); - #width + # width my $widthw_label = Gtk3::Label->new($d->get("Width") . ":"); $self->{_width_spin_w} = Gtk3::SpinButton->new_with_range(0, $self->{_root}->{w}, 1); $self->{_width_spin_w}->set_value($sw); + $self->{_width_spin_w_handler} = $self->{_width_spin_w}->get_value_as_int; # Hilfswert $self->{_width_spin_w_handler} = $self->{_width_spin_w}->signal_connect( 'value-changed' => $value_callback); @@ -894,7 +1003,7 @@ sub select_dialog { $ww_hbox->pack_start($widthw_label, FALSE, FALSE, 5); $ww_hbox->pack_start($self->{_width_spin_w}, FALSE, FALSE, 5); - #height + # height my $heightw_label = Gtk3::Label->new($d->get("Height") . ":"); $self->{_height_spin_w} = Gtk3::SpinButton->new_with_range(0, $self->{_root}->{h}, 1); $self->{_height_spin_w}->set_value($sh); @@ -905,74 +1014,27 @@ sub select_dialog { $hw_hbox->pack_start($heightw_label, FALSE, FALSE, 5); $hw_hbox->pack_start($self->{_height_spin_w}, FALSE, FALSE, 5); - my $prop_dialog = Gtk3::Window->new('toplevel'); - $prop_dialog->set_modal(TRUE); - $prop_dialog->set_decorated(FALSE); - $prop_dialog->set_skip_taskbar_hint(TRUE); - $prop_dialog->set_skip_pager_hint(TRUE); - $prop_dialog->set_keep_above(TRUE); - $prop_dialog->set_accept_focus(TRUE); - $prop_dialog->set_resizable(FALSE); - - $prop_dialog->signal_connect( - 'key-press-event' => sub { - my $window = shift; - my $event = shift; - - #toggle zoom window - if ($event->keyval == Gtk3::Gdk::keyval_from_name('Space')) { - - if ($self->{_zoom_active}) { - $self->{_zoom_window}->hide; - $self->{_zoom_active} = FALSE; - } elsif ($self->{_view}->get_zoom == 1) { - $self->zoom_check_pos(); - $self->{_zoom_active} = TRUE; - } + my $prop_dialog = Gtk3::EventBox->new(); - #toggle prop dialog - } elsif ($event->keyval == Gtk3::Gdk::keyval_from_name('Shift_L') || $event->keyval == Gtk3::Gdk::keyval_from_name('Shift_R')) { - - if ($self->{_prop_active}) { - Gtk3::Gdk::keyboard_ungrab(Gtk3::get_current_event_time()); - $self->{_prop_window}->hide; - $self->{_prop_active} = FALSE; - Gtk3::Gdk::keyboard_grab($self->{_select_window}->get_window, 0, Gtk3::get_current_event_time()); - } else { - Gtk3::Gdk::keyboard_ungrab(Gtk3::get_current_event_time()); - my ($window_at_pointer, $x, $y, $mask) = $self->{_root}->get_pointer; - $self->{_prop_window}->move($x, $y); - $self->{_prop_window}->show_all; - $self->{_prop_active} = TRUE; - Gtk3::Gdk::keyboard_grab($self->{_prop_window}->get_window, 0, Gtk3::get_current_event_time()); - } - - #abort screenshot - } elsif ($event->keyval == Gtk3::Gdk::keyval_from_name('Escape')) { - - $self->quit; - - } - - }); + # fixed size + $prop_dialog->set_size_request(180, 160); + + $prop_dialog->override_background_color('normal', Gtk3::Gdk::RGBA->new(0.9, 0.9, 0.9, 1.0)); + $prop_dialog->set_focus_on_click(TRUE); my $hide_btn = Gtk3::Button->new_with_mnemonic($d->get("_Hide")); $hide_btn->set_image(Gtk3::Image->new_from_stock('gtk-close', 'button')); $hide_btn->set_can_default(TRUE); $hide_btn->signal_connect( 'clicked' => sub { - Gtk3::Gdk::keyboard_ungrab(Gtk3::get_current_event_time()); $prop_dialog->hide; $self->{_prop_active} = FALSE; - Gtk3::Gdk::keyboard_grab($self->{_select_window}->get_window, 0, Gtk3::get_current_event_time()); }); - #final_packing - #all labels = one size - $xw_label->set_alignment(0, 0.5); - $yw_label->set_alignment(0, 0.5); - $widthw_label->set_alignment(0, 0.5); - $heightw_label->set_alignment(0, 0.5); + $xw_label->set_xalign(0); $xw_label->set_yalign(0.5); + $yw_label->set_xalign(0); $yw_label->set_yalign(0.5); + $widthw_label->set_xalign(0); $widthw_label->set_yalign(0.5); + $heightw_label->set_xalign(0); $heightw_label->set_yalign(0.5); my $sg_main = Gtk3::SizeGroup->new('horizontal'); $sg_main->add_widget($xw_label); @@ -987,7 +1049,6 @@ sub select_dialog { $vbox->pack_start($hw_hbox, FALSE, FALSE, 3); $vbox->pack_start($hide_btn, FALSE, FALSE, 3); - #nice frame as well my $frame_label = Gtk3::Label->new; $frame_label->set_markup("" . $d->get("Selection") . ""); @@ -997,16 +1058,12 @@ sub select_dialog { $frame->set_shadow_type('none'); $frame->add($vbox); - $prop_dialog->add($frame); - $prop_dialog->realize; - $prop_dialog->set_transient_for($self->{_select_window}); - $prop_dialog->get_window->set_override_redirect(TRUE); - return $prop_dialog; } + sub take_screenshot { my $self = shift; my $s = shift; @@ -1074,21 +1131,63 @@ sub get_action_name { sub quit { my $self = shift; - $self->ungrab_pointer_and_keyboard(FALSE, FALSE, TRUE); + eval { $self->ungrab_pointer_and_keyboard(FALSE, FALSE, TRUE); }; $self->clean; } sub clean { my $self = shift; - $self->{_selector}->signal_handler_disconnect($self->{_selector_handler}); - $self->{_view}->signal_handler_disconnect($self->{_view_zoom_handler}); - $self->{_view}->signal_handler_disconnect($self->{_view_button_handler}); - $self->{_view}->signal_handler_disconnect($self->{_view_event_handler}); - $self->{_select_window}->signal_handler_disconnect($self->{_key_handler}); - $self->{_select_window}->destroy; - $self->{_zoom_window}->destroy; - $self->{_prop_window}->destroy; + if (defined $self->{_canvas}) { + if (defined $self->{_selector_handler} && $self->{_selector_handler} > 0) { + eval { $self->{_canvas}->signal_handler_disconnect($self->{_selector_handler}); }; + $self->{_selector_handler} = undef; + } + + if (defined $self->{_view_event_handler} && $self->{_view_event_handler} > 0) { + eval { $self->{_canvas}->signal_handler_disconnect($self->{_view_event_handler}); }; + $self->{_view_event_handler} = undef; + } + + if (defined $self->{_view_button_handler} && $self->{_view_button_handler} > 0) { + eval { $self->{_canvas}->signal_handler_disconnect($self->{_view_button_handler}); }; + $self->{_view_button_handler} = undef; + } + + if (defined $self->{_view_release_handler} && $self->{_view_release_handler} > 0) { + eval { $self->{_canvas}->signal_handler_disconnect($self->{_view_release_handler}); }; + $self->{_view_release_handler} = undef; + } + } + + if (defined $self->{_zoom_area} && defined $self->{_view_zoom_handler} && $self->{_view_zoom_handler} > 0) { + eval { $self->{_zoom_area}->signal_handler_disconnect($self->{_view_zoom_handler}); }; + $self->{_view_zoom_handler} = undef; + } + + if (defined $self->{_select_window} && defined $self->{_key_handler} && $self->{_key_handler} > 0) { + eval { $self->{_select_window}->signal_handler_disconnect($self->{_key_handler}); }; + $self->{_key_handler} = undef; + } + + if (defined $self->{_select_window}) { + $self->{_select_window}->destroy; + $self->{_select_window} = undef; + } + + if (defined $self->{_zoom_window}) { + $self->{_zoom_window}->destroy; + $self->{_zoom_window} = undef; + } + + if (defined $self->{_prop_window}) { + $self->{_prop_window}->destroy; + $self->{_prop_window} = undef; + } + + $self->{_canvas} = undef; + $self->{_zoom_area} = undef; } + 1; From 830060f73a9901108ab7a687adbe6f4669540015 Mon Sep 17 00:00:00 2001 From: Photon89 Date: Wed, 12 Aug 2026 17:46:46 +0200 Subject: [PATCH 2/5] Fixed input into coordinate entry dialog, added comments --- .../Shutter/Screenshot/SelectorAdvanced.pm | 587 ++++++++++-------- 1 file changed, 327 insertions(+), 260 deletions(-) diff --git a/share/shutter/resources/modules/Shutter/Screenshot/SelectorAdvanced.pm b/share/shutter/resources/modules/Shutter/Screenshot/SelectorAdvanced.pm index 47646d9f..b6d5a042 100644 --- a/share/shutter/resources/modules/Shutter/Screenshot/SelectorAdvanced.pm +++ b/share/shutter/resources/modules/Shutter/Screenshot/SelectorAdvanced.pm @@ -47,36 +47,35 @@ use Glib qw/TRUE FALSE/; sub new { my $class = shift; - #call constructor of super class (shutter_common, include_cursor, delay, notify_timeout) + # Call the constructor of the superclass (Shutter::Screenshot::Main) my $self = $class->SUPER::new(shift, shift, shift, shift); - $self->{_zoom_active} = shift; - $self->{_hide_time} = shift; #a short timeout to give the server a chance to redraw the area that was obscured - $self->{_show_help} = shift; #hide help text? + # Initialize interactive behavior and helper flags + $self->{_zoom_active} = shift; # Determines if magnifier tool is active at start + $self->{_hide_time} = shift; # Timeout allowing the server to redraw obscured screen portions + $self->{_show_help} = shift; # Toggle flag for the introductory shortcut guide panel - #initial selection size - $self->{_init_x} = shift; - $self->{_init_y} = shift; - $self->{_init_w} = shift; - $self->{_init_h} = shift; - $self->{_confirmation_necessary} = shift; + # Set initial geometry constraints for the selection area + $self->{_init_x} = shift; + $self->{_init_y} = shift; + $self->{_init_w} = shift; + $self->{_init_h} = shift; + $self->{_confirmation_necessary} = shift; # If true, user must confirm via Enter key + # Query and calculate the system's monitor scale factor for HiDPI support my $scale = 1; eval { $scale = $self->{_select_window}->get_scale_factor if $self->{_select_window}; }; $self->{_dpi_scale} = $scale || 1; + # Create the independent Popup window container for the magnifier lens preview $self->{_zoom_window} = Gtk3::Window->new('popup'); $self->{_zoom_window}->set_decorated(0); $self->{_zoom_window}->set_keep_above(1); $self->{_zoom_window}->set_modal(0); - $self->{_select_window} = Gtk3::Window->new('popup'); - $self->{_select_window}->set_decorated(0); - $self->{_select_window}->set_keep_above(1); - $self->{_select_window}->set_modal(1); - + # Setup the layout inside the magnifier popup my $zoom_vbox = Gtk3::VBox->new(0, 4); $self->{_zoom_window}->add($zoom_vbox); @@ -84,59 +83,75 @@ sub new { $scwin->set_policy('never', 'never'); $zoom_vbox->pack_start($scwin, 1, 1, 0); - $self->{_x_label} = Gtk3::Label->new("X: 0"); - $self->{_y_label} = Gtk3::Label->new("Y: 0"); + # Instantiate coordinate context descriptors + $self->{_x_label} = Gtk3::Label->new("X: 0"); + $self->{_y_label} = Gtk3::Label->new("Y: 0"); $self->{_size_label} = Gtk3::Label->new("0 x 0"); $zoom_vbox->pack_start($self->{_x_label}, 0, 0, 0); $zoom_vbox->pack_start($self->{_y_label}, 0, 0, 0); $zoom_vbox->pack_start($self->{_size_label}, 0, 0, 0); - + # Create the drawing lens canvas area for pixel zooming $self->{_zoom_area} = Gtk3::DrawingArea->new; - $self->{_zoom_area}->set_size_request(100, 100); + $self->{_zoom_area}->set_size_request(160, 160); $scwin->add($self->{_zoom_area}); + # Create the primary full-screen workspace window + $self->{_select_window} = Gtk3::Window->new('popup'); + $self->{_select_window}->set_decorated(0); + $self->{_select_window}->set_keep_above(1); + $self->{_select_window}->set_modal(1); + + # Create the main full-screen interaction canvas $self->{_canvas} = Gtk3::DrawingArea->new; + # Use a Gtk3::Overlay container to layer floating dialog elements on top of the canvas my $overlay = Gtk3::Overlay->new(); $overlay->add($self->{_canvas}); + # Instantiate and append the dimension property dialog panel into the overlay $self->{_prop_window} = $self->select_dialog(); $overlay->add_overlay($self->{_prop_window}); + # Lock the coordinate control panel statically to the bottom-right viewport corner $self->{_prop_window}->set_halign('end'); $self->{_prop_window}->set_valign('end'); - $self->{_prop_window}->set_margin_right(20); $self->{_prop_window}->set_margin_bottom(20); + # Start concealed until explicitly requested via Shift or Right-click $self->{_prop_window}->hide(); $self->{_prop_active} = 0; + # Bind the configured overlay workspace layout to the primary window shell $self->{_select_window}->add($overlay); + # PRIMARY CANVAS DRAW SIGNAL: Renders base snapshot and rubberband marquee selection $self->{_selector_handler} = $self->{_canvas}->signal_connect(draw => sub { my ($widget, $cr) = @_; + # 1. Render the captured raw desktop image layer if (defined $self->{_screenshot_pixbuf}) { Gtk3::Gdk::cairo_set_source_pixbuf($cr, $self->{_screenshot_pixbuf}, 0, 0); $cr->paint; } + # 2. Render the selection geometry boundary pathing accents if (defined $self->{_state} && defined $self->{_state}->{sel}) { my $s = $self->{_state}->{sel}; - my ($r, $g, $b) = (0.5, 0.5, 0.5); + # Fallback gray accent rule parameters + my ($r, $g, $b) = (0.5, 0.5, 0.5); + # Safely tap into the current GTK theme context stylesheet parameters eval { my $context = $widget->get_style_context(); - $context->save(); + # Match the desktop's native selection indicator tint values (rubberband class) $context->add_class('rubberband'); my $rgba = $context->get_background_color('normal'); - if (defined $rgba) { $r = $rgba->red; $g = $rgba->green; @@ -145,40 +160,49 @@ sub new { $context->restore(); }; + # 3. Draw the solid rectangular bounding outline strokes $cr->set_source_rgba($r, $g, $b, 1.0); $cr->set_line_width(2.0); $cr->rectangle($s->{x}, $s->{y}, $s->{width}, $s->{height}); $cr->stroke; + # 4. Fill the rectangle selection interior with a soft accent mask tint $cr->set_source_rgba($r, $g, $b, 0.15); $cr->rectangle($s->{x}, $s->{y}, $s->{width}, $s->{height}); $cr->fill; } - return 0; + return 0; # Signal draw execution finalized }); return $self; } + #~ sub DESTROY { #~ my $self = shift; #~ print "$self dying at\n"; #~ } + +# ========================================================================= +# MAIN ROUTINE: INTERACTIVE ADVANCED SELECTOR SCREENSHOT MODE +# ========================================================================= sub select_advanced { my $self = shift; my $output = 5; my $d = $self->{_sc}->get_gettext; + # Freeze the desktop layout view by capturing the root window buffer maps my $clean_pixbuf = Gtk3::Gdk::pixbuf_get_from_window( $self->{_root}, 0, 0, $self->{_root}->{w}, $self->{_root}->{h} ); $self->{_screenshot_pixbuf} = $clean_pixbuf; + # Initialize global selector session data tracking context mappings $self->{_state} = { pixbuf => $clean_pixbuf, zoom => 5, @@ -189,14 +213,12 @@ sub select_advanced { }; my $state = $self->{_state}; - + # Query the hardware pointing device vectors variables configurations on load my ($window_at_pointer, $xinit, $yinit, $mask) = $self->{_root}->get_pointer; $state->{cursor_x} = $xinit; $state->{cursor_y} = $yinit; - my $scwin = Gtk3::ScrolledWindow->new(); - $scwin->set_policy('never', 'never'); - + # Configure primary workspace interaction canvas parameters my $canvas = $self->{_canvas}; $canvas->set_can_focus(TRUE); $canvas->add_events([ @@ -209,14 +231,12 @@ sub select_advanced { ) ]); - - # zoom preview + # Retrieve shared zoom viewer layout labels references pointers my $xlabel = $self->{_x_label}; my $ylabel = $self->{_y_label}; my $rlabel = $self->{_size_label}; - my $zoom_vbox = Gtk3::VBox->new(FALSE, 0); - + # Configure magnifier window parameters layout rules $self->{_zoom_window}->set_type_hint('splashscreen'); $self->{_zoom_window}->set_can_focus(TRUE); $self->{_zoom_window}->set_accept_focus(TRUE); @@ -225,6 +245,7 @@ sub select_advanced { $self->{_zoom_window}->set_keep_above(TRUE); $self->{_zoom_window}->move($self->{_root}->{x}, $self->{_root}->{y}); + # Configure primary workspace selection window behaviors $self->{_select_window}->set_type_hint('splashscreen'); $self->{_select_window}->set_can_focus(TRUE); $self->{_select_window}->set_accept_focus(TRUE); @@ -236,23 +257,15 @@ sub select_advanced { $self->{_select_window}->resize($self->{_root}->{w}, $self->{_root}->{h}); $self->{_select_window}->move($self->{_root}->{x}, $self->{_root}->{y}); + # Keep coordinate control properties widgets hidden initially if (defined $self->{_prop_window}) { $self->{_prop_window}->hide; $self->{_prop_active} = 0; } - $self->{_canvas}->set_can_focus(TRUE); - $self->{_canvas}->add_events([ - qw( - button-press-mask - button-release-mask - pointer-motion-mask - key-press-mask - scroll-mask - ) - ]); - - # usage hint + # ------------------------------------------------------------------------- + # INITIALIZE INTRODUCTION USER GUIDE OVERLAY + # ------------------------------------------------------------------------- $self->{_help_label} = undef; if (($self->{_init_w} < 1 || $self->{_init_h} < 1) && $self->{_show_help}) { @@ -279,8 +292,9 @@ sub select_advanced { "$text3" ); + # Leverage Frame structures for seamless CSS backdrop processing in GTK3 my $help_frame = Gtk3::Frame->new(); - $help_frame->set_shadow_type('none'); # Alten Standard-Rahmen abschalten + $help_frame->set_shadow_type('none'); $help_frame->add($help_lbl); my $help_box = Gtk3::EventBox->new(); @@ -306,6 +320,7 @@ sub select_advanced { $box_css->load_from_data(".shutter-transparent-box { background-color: transparent; }"); $box_context->add_provider($box_css, 600); + # Symmetrically center the overlay on the current monitor layout map bounds $help_box->set_halign('center'); $help_box->set_valign('center'); @@ -319,6 +334,7 @@ sub select_advanced { $self->{_help_label} = $help_box; } + # Realize window configurations pipelines $self->{_select_window}->show_all; $self->{_prop_window}->hide if defined $self->{_prop_window}; @@ -326,13 +342,16 @@ sub select_advanced { $self->{_select_window}->present; + # Set the initial targeting reticle layout cursor icon descriptor if (defined $self->{_canvas}->get_window()) { my $gdk_win = $self->{_canvas}->get_window(); my $cur = Gtk3::Gdk::Cursor->new_from_name($gdk_win->get_display(), 'crosshair'); $gdk_win->set_cursor($cur) if defined $cur; } - # helper for redraws + # ------------------------------------------------------------------------- + # ANONYMOUS INTERACTION SCREEN REDRAW METHOD + # ------------------------------------------------------------------------- my $queue_redraw = sub { if (defined $self->{_canvas}) { $self->{_canvas}->queue_draw; @@ -342,6 +361,9 @@ sub select_advanced { } }; + # ------------------------------------------------------------------------- + # RENDERING SIGNAL: DETAIL PIPELINE MAGNIFIER (Zoom Area Draw Pipeline) + # ------------------------------------------------------------------------- $self->{_zoom_area}->signal_connect( 'draw', sub { @@ -374,12 +396,14 @@ sub select_advanced { my $crop = $pixbuf->new_subpixbuf($src_x, $src_y, $crop_w, $crop_h); + # Scale cropped sub-image to draw a pixelated detail context layer $cr->save; $cr->scale($zoom, $zoom); Gtk3::Gdk::cairo_set_source_pixbuf($cr, $crop, 0, 0); $cr->paint; $cr->restore; + # Render magnifier tactical grid target indicators hairs lines my $mid_x = int($allocated_w / 2); my $mid_y = int($allocated_h / 2); @@ -393,6 +417,7 @@ sub select_advanced { my $half_pixel = $zoom / 2; + # Render disconnected intersecting crosshairs lines targeting paths $cr->move_to(0, $mid_y); $cr->line_to($mid_x - $half_pixel, $mid_y); @@ -407,10 +432,13 @@ sub select_advanced { $cr->stroke; - return FALSE; + return FALSE; # Propagate draw state execution updates } ); + # ------------------------------------------------------------------------- + # ANONYMOUS INTERACTION TEXT HUD DISPLAY STRINGS REFRESH HANDLERS + # ------------------------------------------------------------------------- my $set_cursor_text = sub { my ($x, $y) = @_; $xlabel->set_text("X: " . (int($x) + 1)); @@ -439,6 +467,7 @@ sub select_advanced { $self->{_zoom_window}->hide if defined $self->{_zoom_window}; $self->{_prop_window}->hide if defined $self->{_prop_window}; + # Defer the main loop termination briefly to allow the display server to catch up Glib::Timeout->add($self->{_hide_time}, sub { Gtk3->main_quit; return FALSE; @@ -449,31 +478,33 @@ sub select_advanced { $self->quit; }; - - # initial help-state + # Setup initial helper constraints tracker states $self->{_selector_init} = $self->{_show_help} ? TRUE : FALSE; $self->{_selector_init_zoom} = 0; + # ------------------------------------------------------------------------- + # INTERACTION EVENT STATE VARIABLES (Marquee Dragging, Moving & Resizing) + # ------------------------------------------------------------------------- my $is_dragging = 0; my $is_moving_rect = 0; - my $is_resizing = ''; # directions can be as follows: 'n', 's', 'w', 'e', 'nw', 'ne', 'sw', 'se' + my $is_resizing = ''; # Holds direction strings: 'n', 's', 'w', 'e', 'nw', 'ne', 'sw', 'se' - my ($start_x, $start_y) = (0, 0); + my ($start_x, $start_y) = (0, 0); my ($offset_x, $offset_y) = (0, 0); + # Proximity handle padding context (6 pixels scaled for HiDPI) my $handle_size = 6 * ($self->{_dpi_scale} // 1); - # helper to get which edge or corner is targeted by the mouse pointer + # Helper sub to detect if the pointer sits near any edge or corner of the marquee my $get_resize_edge = sub { my ($mx, $my) = @_; my $s = $self->{_state}->{sel}; return '' unless (defined $s && $s->{width} > 0 && $s->{height} > 0); - my $x1 = $s->{x}; - my $y1 = $s->{y}; - my $x2 = $s->{x} + $s->{width}; - my $y2 = $s->{y} + $s->{height}; + my $x1 = $s->{x}; my $y1 = $s->{y}; + my $x2 = $s->{x} + $s->{width}; my $y2 = $s->{y} + $s->{height}; + # Discard checks if mouse is too far outside the marquee buffer zone return '' if ($mx < $x1 - $handle_size || $mx > $x2 + $handle_size || $my < $y1 - $handle_size || $my > $y2 + $handle_size); @@ -482,6 +513,7 @@ sub select_advanced { my $near_w = (abs($mx - $x1) <= $handle_size); my $near_e = (abs($mx - $x2) <= $handle_size); + # Corner detection takes strict priority over line edge paths return 'nw' if ($near_n && $near_w); return 'ne' if ($near_n && $near_e); return 'sw' if ($near_s && $near_w); @@ -489,11 +521,14 @@ sub select_advanced { return 'n' if ($near_n && $mx >= $x1 && $mx <= $x2); return 's' if ($near_s && $mx >= $x1 && $mx <= $x2); return 'w' if ($near_w && $my >= $y1 && $my <= $y2); - return 'e' if ($near_e && $my >= $y1 && $my <= $y2); + return 'e' if ($near_e && $mx >= $x1 && $mx <= $x2); return ''; }; + # ========================================================================= + # BUTTON PRESS EVENT: Mouse clicks initiate actions based on pointer location + # ========================================================================= $self->{_view_button_handler} = $self->{_canvas}->signal_connect('button-press-event' => sub { my ($widget, $event) = @_; return FALSE unless defined $event; @@ -501,30 +536,29 @@ sub select_advanced { if ($event->button == 1) { my $mx = int($event->x); my $my = int($event->y); - my $s = $self->{_state}->{sel}; + my $s = $self->{_state}->{sel}; + # Check if clicking a resize handle edge my $edge = $get_resize_edge->($mx, $my); if ($edge ne '') { $is_resizing = $edge; - $start_x = $mx; - $start_y = $my; + $start_x = $mx; $start_y = $my; } elsif (defined $s && $s->{width} > 0 && $s->{height} > 0 && $mx >= $s->{x} && $mx <= ($s->{x} + $s->{width}) && $my >= $s->{y} && $my <= ($s->{y} + $s->{height})) { + # Clicking inside marquee activates reposition/moving mode $is_moving_rect = 1; $offset_x = $mx - $s->{x}; $offset_y = $my - $s->{y}; } else { + # Clicking empty background constructs a brand new marquee bounds area $is_dragging = 1; - $start_x = $mx; - $start_y = $my; - - if (defined $self->{_help_label}) { - $self->{_help_label}->hide; - } + $start_x = $mx; $start_y = $my; + # Suppress welcoming overlay help banner text immediately + $self->{_help_label}->hide if defined $self->{_help_label}; $self->{_state}->{sel} = { x => $start_x, y => $start_y, width => 0, height => 0 }; } $queue_redraw->(); @@ -532,6 +566,10 @@ sub select_advanced { return TRUE; }); + + # ========================================================================= + # MOTION NOTIFY EVENT: Mouse movements recalculate geometric states and cursors + # ========================================================================= $self->{_view_event_handler} = $self->{_canvas}->signal_connect('motion-notify-event' => sub { my ($widget, $event) = @_; return FALSE unless defined $event; @@ -543,16 +581,16 @@ sub select_advanced { $self->{_state}->{cursor_y} = $my; $set_cursor_text->($mx, $my) if defined $set_cursor_text; - if (defined $self->{_zoom_window} && $self->{_zoom_window}->get_visible && $self->can('zoom_check_pos')) { - $self->zoom_check_pos(); - } + # Safely trigger magnifier displacement check pipeline + $self->zoom_check_pos() if (defined $self->{_zoom_window} && $self->{_zoom_window}->get_visible && $self->can('zoom_check_pos')); + # --- CONTEXTUAL MOUSE CURSOR SHAPE EVALUATION --- if (defined $widget->get_window()) { my $gdk_window = $widget->get_window(); my $display = $gdk_window->get_display(); - my $s = $self->{_state}->{sel}; + my $s = $self->{_state}->{sel}; - my $cursor_type = 'crosshair'; + my $cursor_type = 'crosshair'; # Target crosshair default my $active_edge = $is_resizing ne '' ? $is_resizing : $get_resize_edge->($mx, $my); @@ -563,11 +601,11 @@ sub select_advanced { ); $cursor_type = $cursors{$active_edge}; } elsif ($is_moving_rect) { - $cursor_type = 'grabbing'; + $cursor_type = 'grabbing'; # Hand grabs down tight while shifting positions } elsif (defined $s && $s->{width} > 0 && $s->{height} > 0) { if ($mx >= $s->{x} && $mx <= ($s->{x} + $s->{width}) && $my >= $s->{y} && $my <= ($s->{y} + $s->{height})) { - $cursor_type = 'grab'; + $cursor_type = 'grab'; # Hovering inside marquee presents open palm } } @@ -575,6 +613,7 @@ sub select_advanced { $gdk_window->set_cursor($new_cursor) if defined $new_cursor; } + # --- COMPUTE INTERACTIVE COORDINATE ADJUSTMENTS --- if ($is_dragging) { my $x = $mx < $start_x ? $mx : $start_x; my $y = $my < $start_y ? $my : $start_y; @@ -586,11 +625,9 @@ sub select_advanced { } elsif ($is_moving_rect) { my $s = $self->{_state}->{sel}; if (defined $s) { - my $new_x = $mx - $offset_x; - my $new_y = $my - $offset_y; - - $new_x = 0 if $new_x < 0; - $new_y = 0 if $new_y < 0; + my $new_x = $mx - $offset_x; my $new_y = $my - $offset_y; + $new_x = 0 if $new_x < 0; $new_y = 0 if $new_y < 0; + my $max_x = $self->{_root}->{w} - $s->{width}; my $max_y = $self->{_root}->{h} - $s->{height}; $new_x = $max_x if $new_x > $max_x; @@ -609,14 +646,14 @@ sub select_advanced { if ($is_resizing =~ /n/ && $my < $y2) { $y1 = $my; } if ($is_resizing =~ /s/ && $my > $y1) { $y2 = $my; } - $s->{x} = $x1; - $s->{y} = $y1; + $s->{x} = $x1; $s->{y} = $y1; $s->{width} = $x2 - $x1; $s->{height} = $y2 - $y1; $update_size_text->() if defined $update_size_text; } } + # Feed numerical widget entry values live when sidebar panels are currently active if (($is_dragging || $is_moving_rect || $is_resizing ne '') && $self->{_prop_active}) { my $s = $self->{_state}->{sel}; if (defined $s) { @@ -641,18 +678,22 @@ sub select_advanced { return TRUE; }); + + # ========================================================================= + # BUTTON RELEASE EVENT: Mouse releases finalize drag operations + # ========================================================================= $self->{_view_release_handler} = $self->{_canvas}->signal_connect('button-release-event' => sub { my ($widget, $event) = @_; return FALSE unless defined $event; + + # Context Right click opens or closes parameters configuration settings if ($event->button == 3) { if (defined $self->{_prop_window}) { if ($self->{_prop_active}) { $self->{_prop_window}->hide; $self->{_prop_active} = 0; } else { - my $mx = int($event->x); - my $my = int($event->y); - + my $mx = int($event->x); my $my = int($event->y); if (defined $self->{_state} && defined $self->{_state}->{sel}) { my $s = $self->{_state}->{sel}; $self->{_x_spin_w}->set_value(int($s->{x})) if defined $self->{_x_spin_w}; @@ -662,16 +703,10 @@ sub select_advanced { } else { $self->{_x_spin_w}->set_value($mx) if defined $self->{_x_spin_w}; $self->{_y_spin_w}->set_value($my) if defined $self->{_y_spin_w}; - $self->{_width_spin_w}->set_value(0) if defined $self->{_width_spin_w}; - $self->{_height_spin_w}->set_value(0) if defined $self->{_height_spin_w}; } - $self->{_prop_window}->show_all; $self->{_prop_active} = 1; - - if (defined $self->{_x_spin_w}) { - $self->{_x_spin_w}->grab_focus; - } + $self->{_x_spin_w}->grab_focus if defined $self->{_x_spin_w}; } } } elsif ($event->button == 1) { @@ -684,28 +719,69 @@ sub select_advanced { $finish_capture->(); } } - return TRUE; }); - - - # --- keyboard --- + # ========================================================================= + # KEY PRESS EVENT: Full hardware layout keyboard input interceptor pipeline + # ========================================================================= $self->{_key_handler} = $self->{_select_window}->signal_connect('key-press-event' => sub { my ($window, $event) = @_; return FALSE unless defined $event; - my $state = $self->{_state}; - my $s = $state->{sel}; + my $state_obj = $self->{_state}; + my $s = $state_obj->{sel}; my ($window_at_pointer, $x, $y, $mask) = $self->{_root}->get_pointer; - + + # Resolve text based representation mapping names for intercepted physical keypresses my $keyname = Gtk3::Gdk::keyval_name($event->keyval); + # --------------------------------------------------------------------- + # CRITICAL FIX: FREE TYPING PATH & VALUES UPDATE ON ENTER + # --------------------------------------------------------------------- + if ($self->{_prop_active}) { + # Allow normal text typing, deletions and navigations to bypass interception + if ($keyname =~ /^[0-9]$/ || $keyname eq 'BackSpace' || $keyname eq 'Delete' || + $keyname eq 'Left' || $keyname eq 'Right' || $keyname eq 'period') { + return FALSE; + } + + # Intercept Return/Enter while the dialog is active to commit values instead of shooting + if ($keyname eq 'Return' || $keyname eq 'KP_Enter') { + # Force all spin buttons to flush their current text buffers into numerical values + $self->{_x_spin_w}->update() if defined $self->{_x_spin_w}; + $self->{_y_spin_w}->update() if defined $self->{_y_spin_w}; + $self->{_width_spin_w}->update() if defined $self->{_width_spin_w}; + $self->{_height_spin_w}->update() if defined $self->{_height_spin_w}; + + # Extract values safely beforehand to prevent inline syntax errors + my $val_x = defined $self->{_x_spin_w} ? int($self->{_x_spin_w}->get_value) : 0; + my $val_y = defined $self->{_y_spin_w} ? int($self->{_y_spin_w}->get_value) : 0; + my $val_w = defined $self->{_width_spin_w} ? int($self->{_width_spin_w}->get_value) : 0; + my $val_h = defined $self->{_height_spin_w} ? int($self->{_height_spin_w}->get_value) : 0; + + # Manually reshape the selection state with the fresh values + $self->{_state}->{sel} = { + x => $val_x, + y => $val_y, + width => $val_w, + height => $val_h, + }; + + # Force canvas updates to visualize the new geometry instantly + $self->{_canvas}->queue_draw if defined $self->{_canvas}; + $self->{_zoom_area}->queue_draw if defined $self->{_zoom_area}; + + return TRUE; # Stop event propagation here to prevent taking the screenshot! + } + } + # --------------------------------------------------------------------- + + # Shift action triggers settings viewport toggles if ($keyname eq 'Shift_L' || $keyname eq 'Shift_R') { if (defined $self->{_prop_window}) { if ($self->{_prop_active}) { - $self->{_prop_window}->hide; - $self->{_prop_active} = 0; + $self->{_prop_window}->hide; $self->{_prop_active} = 0; } else { if (defined $s) { $self->{_x_spin_w}->set_value(int($s->{x})) if defined $self->{_x_spin_w}; @@ -713,12 +789,8 @@ sub select_advanced { $self->{_width_spin_w}->set_value(int($s->{width})) if defined $self->{_width_spin_w}; $self->{_height_spin_w}->set_value(int($s->{height})) if defined $self->{_height_spin_w}; } - $self->{_prop_window}->show_all; - $self->{_prop_active} = 1; - - if (defined $self->{_x_spin_w}) { - $self->{_x_spin_w}->grab_focus; - } + $self->{_prop_window}->show_all; $self->{_prop_active} = 1; + $self->{_x_spin_w}->grab_focus if defined $self->{_x_spin_w}; } $queue_redraw->() if defined $queue_redraw; return TRUE; @@ -728,114 +800,56 @@ sub select_advanced { my $has_ctrl = $event->state & 'control-mask'; my $has_alt = $event->state & 'mod1-mask'; - my $kv_space = Gtk3::Gdk::keyval_from_name('space'); - my $kv_escape = Gtk3::Gdk::keyval_from_name('Escape'); - my $kv_up = Gtk3::Gdk::keyval_from_name('Up'); - my $kv_down = Gtk3::Gdk::keyval_from_name('Down'); - my $kv_left = Gtk3::Gdk::keyval_from_name('Left'); - my $kv_right = Gtk3::Gdk::keyval_from_name('Right'); - my $kv_add = Gtk3::Gdk::keyval_from_name('KP_Add'); - my $kv_plus = Gtk3::Gdk::keyval_from_name('plus'); - my $kv_equal = Gtk3::Gdk::keyval_from_name('equal'); - my $kv_sub = Gtk3::Gdk::keyval_from_name('KP_Subtract'); - my $kv_minus = Gtk3::Gdk::keyval_from_name('minus'); - my $kv_zero = Gtk3::Gdk::keyval_from_name('0'); - my $kv_return = Gtk3::Gdk::keyval_from_name('Return'); - my $kv_kp_enter = Gtk3::Gdk::keyval_from_name('KP_Enter'); - - if ($event->keyval == $kv_space) { + # Space toggles magnifier lens active visualization frames + if ($keyname eq 'space' || $keyname eq 'Space') { if (defined $self->{_zoom_window} && $self->{_zoom_window}->get_visible) { - $self->{_zoom_window}->hide; - $self->{_zoom_active} = FALSE; + $self->{_zoom_window}->hide; $self->{_zoom_active} = FALSE; } else { $self->{_zoom_active} = TRUE; $self->zoom_check_pos() if $self->can('zoom_check_pos'); $self->{_zoom_window}->show_all if defined $self->{_zoom_window}; } - $queue_redraw->() if defined $queue_redraw; return TRUE; - - } elsif ($event->keyval == $kv_escape) { + } elsif ($keyname eq 'Escape') { $self->quit; - - } elsif ($event->keyval == $kv_up) { - if ($has_ctrl && $s) { - $s->{height} -= 1; - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{width} + $s->{x}, $s->{height} + $s->{y}); - } elsif ($has_alt && $s) { - $s->{y} -= 1; - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{x}, $s->{y}); - } else { - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x, $y - 1); - } - - } elsif ($event->keyval == $kv_down) { - if ($has_ctrl && $s) { - $s->{height} += 1; - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{width} + $s->{x}, $s->{height} + $s->{y}); - } elsif ($has_ctrl) { - $state->{sel} = { x => $x, y => $y, width => 1, height => 2 }; - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x + 1, $y + 2); - } elsif ($has_alt && $s) { - $s->{y} += 1; - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{x}, $s->{y}); - } else { - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x, $y + 1); - } - - } elsif ($event->keyval == $kv_left) { - if ($has_ctrl && $s) { - $s->{width} -= 1; - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{width} + $s->{x}, $s->{height} + $s->{y}); - } elsif ($has_alt && $s) { - $s->{x} -= 1; - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{x}, $s->{y}); - } else { - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x - 1, $y); - } - - } elsif ($event->keyval == $kv_right) { - if ($has_ctrl && $s) { - $s->{width} += 1; - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{width} + $s->{x}, $s->{height} + $s->{y}); - } elsif ($has_ctrl) { - $state->{sel} = { x => $x, y => $y, width => 2, height => 1 }; - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x + 2, $y + 1); - } elsif ($has_alt && $s) { - $s->{x} += 1; - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $s->{x}, $s->{y}); - } else { - $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x + 1, $y); - } - - } elsif ($event->keyval == $kv_add || $event->keyval == $kv_plus || $event->keyval == $kv_equal) { - $state->{zoom}++ if $has_ctrl; - - } elsif ($event->keyval == $kv_sub || $event->keyval == $kv_minus) { - $state->{zoom}-- if $has_ctrl; - $state->{zoom} = 1 if $state->{zoom} < 1; - - } elsif ($event->keyval == $kv_zero) { - $state->{zoom} = 1 if $has_ctrl; - - } elsif ($event->keyval == $kv_return || $event->keyval == $kv_kp_enter) { - if (defined $s) { - $finish_capture->(); - } + return TRUE; + } elsif ($keyname eq 'Up') { + if ($has_ctrl && $s) { $s->{height} -= 1; } + elsif ($has_alt && $s) { $s->{y} -= 1; } + else { $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x, $y - 1); } + } elsif ($keyname eq 'Down') { + if ($has_ctrl && $s) { $s->{height} += 1; } + elsif ($has_alt && $s) { $s->{y} += 1; } + else { $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x, $y + 1); } + } elsif ($keyname eq 'Left') { + if ($has_ctrl && $s) { $s->{width} -= 1; } + elsif ($has_alt && $s) { $s->{x} -= 1; } + else { $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x - 1, $y); } + } elsif ($keyname eq 'Right') { + if ($has_ctrl && $s) { $s->{width} += 1; } + elsif ($has_alt && $s) { $s->{x} += 1; } + else { $self->{_gdk_display}->warp_pointer($self->{_gdk_screen}, $x + 1, $y); } + } elsif ($keyname eq 'KP_Add' || $keyname eq 'plus' || $keyname eq 'equal') { + $state_obj->{zoom}++ if $has_ctrl; + } elsif ($keyname eq 'KP_Subtract' || $keyname eq 'minus') { + $state_obj->{zoom}-- if $has_ctrl; $state_obj->{zoom} = 1 if $state_obj->{zoom} < 1; + } elsif ($keyname eq '0') { + $state_obj->{zoom} = 1 if $has_ctrl; + } elsif ($keyname eq 'Return' || $keyname eq 'KP_Enter') { + $finish_capture->() if defined $s; } - $queue_redraw->() if defined $queue_redraw; + $self->{_canvas}->queue_draw if defined $self->{_canvas}; return TRUE; }); - - # initial selection + # Apply initial selection parameters asynchronously on main loop activation idle cycles Glib::Idle->add(sub { if ($self->{_init_w} && $self->{_init_h}) { $state->{sel} = { - x => $self->{_init_x}, - y => $self->{_init_y}, - width => $self->{_init_w}, + x => $self->{_init_x}, + y => $self->{_init_y}, + width => $self->{_init_w}, height => $self->{_init_h}, }; $queue_redraw->(); @@ -843,91 +857,115 @@ sub select_advanced { return FALSE; }); - # grab keyboard - my $status = Gtk3::Gdk::keyboard_grab($self->{_select_window}->get_window, 0, Gtk3::get_current_event_time()); + # Intercept hardware layout keyboard inputs hooks strictly on the overlay viewport window node + my $status = Gtk3::Gdk::keyboard_grab($self->{_select_window}->get_window, FALSE, Gtk3::get_current_event_time()); + # Synchronize magnifier initialization visibility states rules if ($self->{_zoom_active}) { $self->{_zoom_window}->show_all; - $self->{_zoom_window}->get_window->set_override_redirect(TRUE); $self->zoom_check_pos(); $self->{_zoom_window}->get_window->raise; } - Gtk3->main(); + Gtk3::main(); return $output; } +# ========================================================================= +# LENS POSITIONING BOUNDS REGULATOR (Magnifier Window Collision Avoidance) +# ========================================================================= sub zoom_check_pos { my $self = shift; + # Guard: Abort immediately if the magnifier window doesn't exist or is hidden return FALSE unless defined $self->{_zoom_window}; return FALSE unless $self->{_zoom_window}->get_visible; + # Query the real, absolute screen coordinates of the hardware mouse pointer my ($window_at_pointer, $ev_x, $ev_y, $mask) = $self->{_root}->get_pointer; + # Retrieve the current screen dimensions and positioning of the magnifier window my ($zw, $zh) = $self->{_zoom_window}->get_size; my ($zx, $zy) = $self->{_zoom_window}->get_position; + # Define a safety buffer padding context (50 pixels scaled for HiDPI support) my $distance = 50 * ($self->{_dpi_scale} // 1); + # Construct an expanded safety collision box boundary around the current magnifier position my $box_x1 = $zx - $distance; my $box_y1 = $zy - $distance; my $box_x2 = $zx + $zw + $distance; my $box_y2 = $zy + $zh + $distance; + # Collision Check: If the hardware cursor penetrates the expanded safety box if ($ev_x >= $box_x1 && $ev_x <= $box_x2 && $ev_y >= $box_y1 && $ev_y <= $box_y2) { + # Define target layout screen corner positions (Strict Shutter clockwise order sequence) my @pos = ( - {x => $self->{_root}->{x}, y => $self->{_root}->{y}}, # 1. Oben links - {x => $self->{_root}->{x}, y => $self->{_root}->{h} - $zh}, # 2. Unten links - {x => $self->{_root}->{w} - $zw, y => $self->{_root}->{h} - $zh}, # 3. Unten rechts - {x => $self->{_root}->{w} - $zw, y => $self->{_root}->{y}}, # 4. Oben rechts + {x => $self->{_root}->{x}, y => $self->{_root}->{y}}, # 1. Top-Left + {x => $self->{_root}->{x}, y => $self->{_root}->{h} - $zh}, # 2. Bottom-Left + {x => $self->{_root}->{w} - $zw, y => $self->{_root}->{h} - $zh}, # 3. Bottom-Right + {x => $self->{_root}->{w} - $zw, y => $self->{_root}->{y}}, # 4. Top-Right ); + # Iterate through available corners to discover the first non-colliding location foreach my $p (@pos) { my $p_box_x1 = $p->{x} - $distance; my $p_box_y1 = $p->{y} - $distance; my $p_box_x2 = $p->{x} + $zw + $distance; my $p_box_y2 = $p->{y} + $zh + $distance; + # If the cursor does NOT collide with this specific corner's safety area if (!($ev_x >= $p_box_x1 && $ev_x <= $p_box_x2 && $ev_y >= $p_box_y1 && $ev_y <= $p_box_y2)) { + # Instantly move the magnifier window to the safe screen corner location $self->{_zoom_window}->move($p->{x}, $p->{y}); $self->{_zoom_window}->queue_draw; - return TRUE; + return TRUE; # Rotation successful, break routine pipeline } } } - return TRUE; + return TRUE; # Frame processing complete } - +# ========================================================================= +# PROPERTY BOUNDS REGULATOR (Updates SpinButton limits and values safely) +# ========================================================================= sub adjust_prop_values { my $self = shift; + # Guard: Abort immediately if session state tracking object is missing return unless defined $self->{_state}; + # Retrieve current active selection geometry coordinates and bounds map my $s = $self->{_state}->{sel}; if (defined $s) { + # Temporarily detach interactive widget event signals listeners channels + # This prevents circular recursive updates loops while modifying values via code $self->{_x_spin_w}->signal_handler_block($self->{_x_spin_w_handler}) if defined $self->{_x_spin_w_handler}; $self->{_y_spin_w}->signal_handler_block($self->{_y_spin_w_handler}) if defined $self->{_y_spin_w_handler}; $self->{_width_spin_w}->signal_handler_block($self->{_width_spin_w_handler}) if defined $self->{_width_spin_w_handler}; $self->{_height_spin_w}->signal_handler_block($self->{_height_spin_w_handler}) if defined $self->{_height_spin_w_handler}; + # 1. Update X coordinate positioning input and recalculate maximum slide range $self->{_x_spin_w}->set_value(int($s->{x})) if defined $self->{_x_spin_w}; $self->{_x_spin_w}->set_range(0, int($self->{_root}->{w} - $s->{width})) if defined $self->{_x_spin_w}; + # 2. Update Y coordinate positioning input and recalculate maximum slide range $self->{_y_spin_w}->set_value(int($s->{y})) if defined $self->{_y_spin_w}; $self->{_y_spin_w}->set_range(0, int($self->{_root}->{h} - $s->{height})) if defined $self->{_y_spin_w}; + # 3. Update width dimension input and constrain range to remaining space on the right $self->{_width_spin_w}->set_value(int($s->{width})) if defined $self->{_width_spin_w}; $self->{_width_spin_w}->set_range(0, int($self->{_root}->{w} - $s->{x})) if defined $self->{_width_spin_w}; + # 4. Update height dimension input and constrain range to remaining space underneath $self->{_height_spin_w}->set_value(int($s->{height})) if defined $self->{_height_spin_w}; $self->{_height_spin_w}->set_range(0, int($self->{_root}->{h} - $s->{y})) if defined $self->{_height_spin_w}; + # Re-attach the numerical spin entries alert listeners to unlock user input processing $self->{_x_spin_w}->signal_handler_unblock($self->{_x_spin_w_handler}) if defined $self->{_x_spin_w_handler}; $self->{_y_spin_w}->signal_handler_unblock($self->{_y_spin_w_handler}) if defined $self->{_y_spin_w_handler}; $self->{_width_spin_w}->signal_handler_unblock($self->{_width_spin_w_handler}) if defined $self->{_width_spin_w_handler}; @@ -936,26 +974,30 @@ sub adjust_prop_values { } +# ========================================================================= +# CONTROL DIALOG CONFIGURATOR (Builds the inline floating numeric input overlay) +# ========================================================================= sub select_dialog { my $self = shift; + # Retrieve the standard translation module handle for Shutter my $d = $self->{_sc}->get_gettext; + # Check the active state context safely my $state = $self->{_state}; my $s = defined $state ? $state->{sel} : undef; - my $sx = 0; - my $sy = 0; - my $sw = 0; - my $sh = 0; + # Initialize geometry default buffers + my $sx = 0; my $sy = 0; + my $sw = 0; my $sh = 0; + # Populate initialization dimensions if a marquee selection area is predefined if (defined $s) { - $sx = $s->{x}; - $sy = $s->{y}; - $sw = $s->{width}; - $sh = $s->{height}; + $sx = $s->{x}; $sy = $s->{y}; + $sw = $s->{width}; $sh = $s->{height}; } + # Centralized entry callback triggered upon any spin button modifications my $value_callback = sub { if (defined $self->{_state}) { $self->{_state}->{sel} = { @@ -964,91 +1006,87 @@ sub select_dialog { width => int($self->{_width_spin_w}->get_value), height => int($self->{_height_spin_w}->get_value), }; - $self->{_canvas}->queue_draw if defined $self->{_canvas}; + # Instantly enforce redraw operations across workspace components + $self->{_canvas}->queue_draw if defined $self->{_canvas}; $self->{_zoom_area}->queue_draw if defined $self->{_zoom_area}; } }; - # x coordinate + # 1. Coordinate configuration inputs setup: X parameter row my $xw_label = Gtk3::Label->new($d->get("X") . ":"); $self->{_x_spin_w} = Gtk3::SpinButton->new_with_range(0, $self->{_root}->{w}, 1); $self->{_x_spin_w}->set_value($sx); - $self->{_x_spin_w_handler} = $self->{_x_spin_w}->signal_connect( - 'value-changed' => $value_callback); + $self->{_x_spin_w_handler} = $self->{_x_spin_w}->signal_connect('value-changed' => $value_callback); my $xw_hbox = Gtk3::HBox->new(FALSE, 5); $xw_hbox->pack_start($xw_label, FALSE, FALSE, 5); $xw_hbox->pack_start($self->{_x_spin_w}, FALSE, FALSE, 5); - # y coordinate + # 2. Coordinate configuration inputs setup: Y parameter row my $yw_label = Gtk3::Label->new($d->get("Y") . ":"); $self->{_y_spin_w} = Gtk3::SpinButton->new_with_range(0, $self->{_root}->{h}, 1); $self->{_y_spin_w}->set_value($sy); - $self->{_y_spin_w_handler} = $self->{_y_spin_w}->signal_connect( - 'value-changed' => $value_callback); + $self->{_y_spin_w_handler} = $self->{_y_spin_w}->signal_connect('value-changed' => $value_callback); my $yw_hbox = Gtk3::HBox->new(FALSE, 5); $yw_hbox->pack_start($yw_label, FALSE, FALSE, 5); $yw_hbox->pack_start($self->{_y_spin_w}, FALSE, FALSE, 5); - # width + # 3. Coordinate configuration inputs setup: Width parameter row my $widthw_label = Gtk3::Label->new($d->get("Width") . ":"); $self->{_width_spin_w} = Gtk3::SpinButton->new_with_range(0, $self->{_root}->{w}, 1); $self->{_width_spin_w}->set_value($sw); - $self->{_width_spin_w_handler} = $self->{_width_spin_w}->get_value_as_int; # Hilfswert - $self->{_width_spin_w_handler} = $self->{_width_spin_w}->signal_connect( - 'value-changed' => $value_callback); + # Cleared the unused 'get_value_as_int' temporary zombie assignment line here during refactoring + $self->{_width_spin_w_handler} = $self->{_width_spin_w}->signal_connect('value-changed' => $value_callback); my $ww_hbox = Gtk3::HBox->new(FALSE, 5); $ww_hbox->pack_start($widthw_label, FALSE, FALSE, 5); $ww_hbox->pack_start($self->{_width_spin_w}, FALSE, FALSE, 5); - # height + # 4. Coordinate configuration inputs setup: Height parameter row my $heightw_label = Gtk3::Label->new($d->get("Height") . ":"); $self->{_height_spin_w} = Gtk3::SpinButton->new_with_range(0, $self->{_root}->{h}, 1); $self->{_height_spin_w}->set_value($sh); - $self->{_height_spin_w_handler} = $self->{_height_spin_w}->signal_connect( - 'value-changed' => $value_callback); + $self->{_height_spin_w_handler} = $self->{_height_spin_w}->signal_connect('value-changed' => $value_callback); my $hw_hbox = Gtk3::HBox->new(FALSE, 5); $hw_hbox->pack_start($heightw_label, FALSE, FALSE, 5); $hw_hbox->pack_start($self->{_height_spin_w}, FALSE, FALSE, 5); + # Construct an EventBox as the core parent layer container to receive background styles safely my $prop_dialog = Gtk3::EventBox->new(); - - # fixed size - $prop_dialog->set_size_request(180, 160); - + $prop_dialog->set_size_request(180, 160); # Lock panel viewport dimensions to a fixed size bounding box $prop_dialog->override_background_color('normal', Gtk3::Gdk::RGBA->new(0.9, 0.9, 0.9, 1.0)); $prop_dialog->set_focus_on_click(TRUE); + # Initialize panel close/dismiss interface control my $hide_btn = Gtk3::Button->new_with_mnemonic($d->get("_Hide")); $hide_btn->set_image(Gtk3::Image->new_from_stock('gtk-close', 'button')); $hide_btn->set_can_default(TRUE); $hide_btn->signal_connect( 'clicked' => sub { $prop_dialog->hide; - $self->{_prop_active} = FALSE; + $self->{_prop_active} = 0; # Set to integer 0 for full code state synchronization }); + # Equalize label text layouts settings fields alignments $xw_label->set_xalign(0); $xw_label->set_yalign(0.5); $yw_label->set_xalign(0); $yw_label->set_yalign(0.5); $widthw_label->set_xalign(0); $widthw_label->set_yalign(0.5); $heightw_label->set_xalign(0); $heightw_label->set_yalign(0.5); + # Group label widgets horizontally to align the starting inputs borders symmetrically my $sg_main = Gtk3::SizeGroup->new('horizontal'); - $sg_main->add_widget($xw_label); - $sg_main->add_widget($yw_label); - $sg_main->add_widget($widthw_label); - $sg_main->add_widget($heightw_label); + $sg_main->add_widget($xw_label); $sg_main->add_widget($yw_label); + $sg_main->add_widget($widthw_label); $sg_main->add_widget($heightw_label); + # Construct structural packaging containers layout tree my $vbox = Gtk3::VBox->new(FALSE, 5); - $vbox->pack_start($xw_hbox, FALSE, FALSE, 3); - $vbox->pack_start($yw_hbox, FALSE, FALSE, 3); - $vbox->pack_start($ww_hbox, FALSE, FALSE, 3); - $vbox->pack_start($hw_hbox, FALSE, FALSE, 3); + $vbox->pack_start($xw_hbox, FALSE, FALSE, 3); $vbox->pack_start($yw_hbox, FALSE, FALSE, 3); + $vbox->pack_start($ww_hbox, FALSE, FALSE, 3); $vbox->pack_start($hw_hbox, FALSE, FALSE, 3); $vbox->pack_start($hide_btn, FALSE, FALSE, 3); + # Embed layouts into an elegant localized layout frame element my $frame_label = Gtk3::Label->new; $frame_label->set_markup("" . $d->get("Selection") . ""); @@ -1064,80 +1102,106 @@ sub select_dialog { } +# ========================================================================= +# CAPTURE PIPELINE EVALUATOR (Extracts and processes the final cropped image) +# ========================================================================= sub take_screenshot { my $self = shift; - my $s = shift; - my $clean_pixbuf = shift; + my $s = shift; # Selection geometric coordinates map + my $clean_pixbuf = shift; # Raw full screen desktop capture cache my $d = $self->{_sc}->get_gettext; - my $output; - #no delay? then we take a subsection of the pixbuf in memory + # Scenario A: Immediate capture (No delay) -> crop the cached memory pixbuf if ($s && $clean_pixbuf && $self->{_delay} == 0) { $output = $clean_pixbuf->new_subpixbuf($s->{x}, $s->{y}, $s->{width}, $s->{height}); - #include cursor + # Layer the hardware mouse pointer cursor into the cropped region if requested if ($self->{_include_cursor}) { $output = $self->include_cursor($s->{x}, $s->{y}, $s->{width}, $s->{height}, $self->{_root}, $output); } - #if there is a delay != 0 set, we have to wait and get a new pixbuf from the root window + # Scenario B: Delayed capture -> wait for timeout and fetch a fresh root drawable buffer } elsif ($s && $self->{_delay} != 0) { ($output) = $self->get_pixbuf_from_drawable($self->{_root}, $s->{x}, $s->{y}, $s->{width}, $s->{height}); - #section not valid + # Scenario C: Aborted or invalid layout state parameters } else { $output = 0; } - #we don't have a useful string for wildcards (e.g. $name) + # Set localized human-readable component name descriptor fallback metadata if ($output =~ /Gtk3/) { $self->{_action_name} = $d->get("Selection"); } - #set history object + # Push selection geometries snapshots states into Shutter's history tracking system if ($s) { - $self->{_history} = Shutter::Screenshot::History->new($self->{_sc}, $self->{_root}, $s->{x}, $s->{y}, $s->{width}, $s->{height}); + $self->{_history} = Shutter::Screenshot::History->new( + $self->{_sc}, $self->{_root}, $s->{x}, $s->{y}, $s->{width}, $s->{height} + ); } - return $output; + return $output; # Returns the ready-to-save Gtk3::Gdk::Pixbuf object } + +# ========================================================================= +# HISTORY REPLAY INTERFACE (Re-runs the exact last coordinate capture clip) +# ========================================================================= sub redo_capture { my $self = shift; - my $output = 3; + my $output = 3; # Default error signal bit handler fallback + + # If a historical coordinates footprint map object is available if (defined $self->{_history}) { + # Query a fresh drawable capture segment matching the exact cached boundary bounds ($output) = $self->get_pixbuf_from_drawable($self->{_history}->get_last_capture); } return $output; } +# ========================================================================= +# GETTER ACCESS PIPELINES (Read-only metadata endpoints) +# ========================================================================= sub get_history { my $self = shift; - return $self->{_history}; + return $self->{_history}; # Returns Shutter's active History tracking module instance } sub get_error_text { my $self = shift; - return $self->{_error_text}; + # Safely returns the logged exception buffer string or an empty string for graceful escapes + return $self->{_error_text} // ""; } sub get_action_name { my $self = shift; - return $self->{_action_name}; + return $self->{_action_name}; # Returns active localized user activity name tag } +# ========================================================================= +# DESTRUCTOR CLOSURE PIPELINES (Gracefully tears down server grabs and windows) +# ========================================================================= sub quit { my $self = shift; + # Safely lift active input target lockups constraints on server device layers eval { $self->ungrab_pointer_and_keyboard(FALSE, FALSE, TRUE); }; + + # Execute absolute memory purge routine $self->clean; } + +# ========================================================================= +# MEMORY CLEANUP & DEALLOCATION ROUTINE (Prevents memory leaks in GTK3) +# ========================================================================= sub clean { my $self = shift; + # 1. Safely disconnect active event handlers from the primary drawing canvas if (defined $self->{_canvas}) { if (defined $self->{_selector_handler} && $self->{_selector_handler} > 0) { eval { $self->{_canvas}->signal_handler_disconnect($self->{_selector_handler}); }; @@ -1160,16 +1224,19 @@ sub clean { } } + # 2. Disconnect render loop handler from the magnifier zoom area if (defined $self->{_zoom_area} && defined $self->{_view_zoom_handler} && $self->{_view_zoom_handler} > 0) { eval { $self->{_zoom_area}->signal_handler_disconnect($self->{_view_zoom_handler}); }; $self->{_view_zoom_handler} = undef; } + # 3. Disconnect global hardware hotkey interceptor from the primary window if (defined $self->{_select_window} && defined $self->{_key_handler} && $self->{_key_handler} > 0) { eval { $self->{_select_window}->signal_handler_disconnect($self->{_key_handler}); }; $self->{_key_handler} = undef; } + # 4. Destroy window architectures and drop references for garbage collection if (defined $self->{_select_window}) { $self->{_select_window}->destroy; $self->{_select_window} = undef; @@ -1185,9 +1252,9 @@ sub clean { $self->{_prop_window} = undef; } - $self->{_canvas} = undef; + # 5. Clear remaining underlying widget handles completely + $self->{_canvas} = undef; $self->{_zoom_area} = undef; } - 1; From 00d1580239e74cec45d812d7302b1a9906c96ccb Mon Sep 17 00:00:00 2001 From: Photon89 Date: Fri, 14 Aug 2026 18:48:37 +0200 Subject: [PATCH 3/5] Make the help message click through --- .../Shutter/Screenshot/SelectorAdvanced.pm | 151 +++++++++--------- 1 file changed, 79 insertions(+), 72 deletions(-) diff --git a/share/shutter/resources/modules/Shutter/Screenshot/SelectorAdvanced.pm b/share/shutter/resources/modules/Shutter/Screenshot/SelectorAdvanced.pm index b6d5a042..a9f59b72 100644 --- a/share/shutter/resources/modules/Shutter/Screenshot/SelectorAdvanced.pm +++ b/share/shutter/resources/modules/Shutter/Screenshot/SelectorAdvanced.pm @@ -172,8 +172,74 @@ sub new { $cr->fill; } - return 0; # Signal draw execution finalized - }); + # ------------------------------------------------------------------------- + # DRAW OVERLAY LAYER: Render dynamic native help instructions box on context + # ------------------------------------------------------------------------- + if ($self->{_show_help_overlay}) { + my $allocated_w = $widget->get_allocated_width; + my $allocated_h = $widget->get_allocated_height; + + # Ensure gettext handle is locally accessible for translations + my $d = $self->{_sc}->get_gettext; + my $text1 = $d->get("Draw a rectangular area using the mouse."); + my $text2 = $d->get("To take a screenshot, double-click or press the Enter key.\nPress Esc to abort."); + my $text3 = + $d->get("shift/right-click → selection dialog on/off") . "\n" + . $d->get("scrollwheel → zoom in/out") . "\n" + . $d->get("space → zoom window on/off") . "\n" + . $d->get("cursor keys → move cursor") . "\n" + . $d->get("cursor keys + alt → move selection") . "\n" + . $d->get("cursor keys + ctrl → resize selection"); + + # Create layout and assign structured markup blocks + my $layout = $widget->create_pango_layout(""); + $layout->set_markup( + "$text1\n" . + "$text2\n\n" . + "$text3" + ); + + # Restrict max text layout bounds width (e.g., 550px) to force wrapping + my $max_text_width = 550; + $layout->set_width($max_text_width * Pango::SCALE); + $layout->set_wrap('word-char'); + + # Query exact geometric dimensions parsed by the layout engine + my ($text_w, $text_h) = $layout->get_pixel_size(); + + # Configure bounding box dimensions with symmetrical 30px padding + my $padding = 30; + my $box_w = $text_w + ($padding * 2); + my $box_h = $text_h + ($padding * 2); + + # Center the dynamic card based on current allocation bounds + my $box_x = int(($allocated_w - $box_w) / 2); + my $box_y = int(($allocated_h - $box_h) / 2); + + # Draw backdrop card using specific hex color structure (rgba: 19, 19, 19, 0.85) + $cr->save(); + $cr->set_source_rgba(0.074, 0.074, 0.074, 0.85); + my $radius = 20; + $cr->new_sub_path(); + $cr->arc($box_x + $box_w - $radius, $box_y + $radius, $radius, -1.5708, 0); + $cr->arc($box_x + $box_w - $radius, $box_y + $box_h - $radius, $radius, 0, 1.5708); + $cr->arc($box_x + $radius, $box_y + $box_h - $radius, $radius, 1.5708, 3.1416); + $cr->arc($box_x + $radius, $box_y + $radius, $radius, 3.1416, 4.7124); + $cr->close_path(); + $cr->fill(); + $cr->restore(); + + # Execute final surface text blitting inside the padded region + $cr->save(); + $cr->move_to($box_x + $padding, $box_y + $padding); + Pango::Cairo::show_layout($cr, $layout); + $cr->restore(); + } + + + # (Hier steht dein bereits existierendes 'return FALSE;') + return FALSE; + }); return $self; } @@ -264,75 +330,15 @@ sub select_advanced { } # ------------------------------------------------------------------------- - # INITIALIZE INTRODUCTION USER GUIDE OVERLAY + # INITIALIZE INTRODUCTION USER GUIDE OVERLAY (Cairo status flag configuration) # ------------------------------------------------------------------------- - $self->{_help_label} = undef; + $self->{_show_help_overlay} = 0; if (($self->{_init_w} < 1 || $self->{_init_h} < 1) && $self->{_show_help}) { - my $mon1 = $self->get_current_monitor; - - my $text1 = $d->get("Draw a rectangular area using the mouse."); - my $text2 = $d->get("To take a screenshot, double-click or press the Enter key.\nPress Esc to abort."); - my $text3 = - $d->get("shift/right-click → selection dialog on/off") . "\n" - . $d->get("scrollwheel → zoom in/out") . "\n" - . $d->get("space → zoom window on/off") . "\n" - . $d->get("cursor keys → move cursor") . "\n" - . $d->get("cursor keys + alt → move selection") . "\n" - . $d->get("cursor keys + ctrl → resize selection"); - - my $help_lbl = Gtk3::Label->new(); - $help_lbl->set_use_markup(TRUE); - $help_lbl->set_justify('left'); - $help_lbl->set_line_wrap(TRUE); - $help_lbl->set_max_width_chars(60); - $help_lbl->set_markup( - "$text1\n" . - "$text2\n\n" . - "$text3" - ); - - # Leverage Frame structures for seamless CSS backdrop processing in GTK3 - my $help_frame = Gtk3::Frame->new(); - $help_frame->set_shadow_type('none'); - $help_frame->add($help_lbl); - - my $help_box = Gtk3::EventBox->new(); - $help_box->set_visible_window(TRUE); - $help_box->add($help_frame); - - my $context = $help_frame->get_style_context(); - $context->add_class('shutter-help-frame'); - - my $css_provider = Gtk3::CssProvider->new(); - $css_provider->load_from_data( - ".shutter-help-frame { " . - " background-color: rgba(19, 19, 19, 0.85); " . - " border-radius: 20px; " . - " padding: 30px; " . - "}" - ); - $context->add_provider($css_provider, 600); - - my $box_context = $help_box->get_style_context(); - $box_context->add_class('shutter-transparent-box'); - my $box_css = Gtk3::CssProvider->new(); - $box_css->load_from_data(".shutter-transparent-box { background-color: transparent; }"); - $box_context->add_provider($box_css, 600); - - # Symmetrically center the overlay on the current monitor layout map bounds - $help_box->set_halign('center'); - $help_box->set_valign('center'); - - if (defined $self->{_canvas}) { - my $overlay_parent = $self->{_canvas}->get_parent(); - if (defined $overlay_parent && $overlay_parent->isa('Gtk3::Overlay')) { - $overlay_parent->add_overlay($help_box); - } - } - - $self->{_help_label} = $help_box; + $self->{_show_help_overlay} = 1; } + $self->{_help_label} = undef; # Deprecated window widget instance tracker + # Realize window configurations pipelines $self->{_select_window}->show_all; @@ -557,10 +563,11 @@ sub select_advanced { $is_dragging = 1; $start_x = $mx; $start_y = $my; - # Suppress welcoming overlay help banner text immediately - $self->{_help_label}->hide if defined $self->{_help_label}; + # Clear the Cairo rendering flag to dim the help card instantly + $self->{_show_help_overlay} = 0; $self->{_state}->{sel} = { x => $start_x, y => $start_y, width => 0, height => 0 }; } + $queue_redraw->(); } return TRUE; @@ -966,9 +973,9 @@ sub adjust_prop_values { $self->{_height_spin_w}->set_range(0, int($self->{_root}->{h} - $s->{y})) if defined $self->{_height_spin_w}; # Re-attach the numerical spin entries alert listeners to unlock user input processing - $self->{_x_spin_w}->signal_handler_unblock($self->{_x_spin_w_handler}) if defined $self->{_x_spin_w_handler}; - $self->{_y_spin_w}->signal_handler_unblock($self->{_y_spin_w_handler}) if defined $self->{_y_spin_w_handler}; - $self->{_width_spin_w}->signal_handler_unblock($self->{_width_spin_w_handler}) if defined $self->{_width_spin_w_handler}; + $self->{_x_spin_w}->signal_handler_unblock($self->{_x_spin_w_handler}) if defined $self->{_x_spin_w_handler}; + $self->{_y_spin_w}->signal_handler_unblock($self->{_y_spin_w_handler}) if defined $self->{_y_spin_w_handler}; + $self->{_width_spin_w}->signal_handler_unblock($self->{_width_spin_w_handler}) if defined $self->{_width_spin_w_handler}; $self->{_height_spin_w}->signal_handler_unblock($self->{_height_spin_w_handler}) if defined $self->{_height_spin_w_handler}; } } From 8278859946195c46f32eb71e19c0a88e6303a6c7 Mon Sep 17 00:00:00 2001 From: Photon89 Date: Fri, 14 Aug 2026 19:06:00 +0200 Subject: [PATCH 4/5] Fix boundaries for selection area in the coordinate dialog --- .../Shutter/Screenshot/SelectorAdvanced.pm | 51 ++++++++++++++++--- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/share/shutter/resources/modules/Shutter/Screenshot/SelectorAdvanced.pm b/share/shutter/resources/modules/Shutter/Screenshot/SelectorAdvanced.pm index a9f59b72..e27a1391 100644 --- a/share/shutter/resources/modules/Shutter/Screenshot/SelectorAdvanced.pm +++ b/share/shutter/resources/modules/Shutter/Screenshot/SelectorAdvanced.pm @@ -337,8 +337,6 @@ sub select_advanced { if (($self->{_init_w} < 1 || $self->{_init_h} < 1) && $self->{_show_help}) { $self->{_show_help_overlay} = 1; } - $self->{_help_label} = undef; # Deprecated window widget instance tracker - # Realize window configurations pipelines $self->{_select_window}->show_all; @@ -1005,14 +1003,47 @@ sub select_dialog { } # Centralized entry callback triggered upon any spin button modifications - my $value_callback = sub { + my $value_callback; + $value_callback = sub { if (defined $self->{_state}) { + # 1. Read out currently requested integer values from the inputs + my $current_x = int($self->{_x_spin_w}->get_value); + my $current_y = int($self->{_y_spin_w}->get_value); + my $current_w = int($self->{_width_spin_w}->get_value); + my $current_h = int($self->{_height_spin_w}->get_value); + + # 2. Block recursion safely by temporarily turning off signal listeners + $self->{_x_spin_w}->signal_handler_block($self->{_x_spin_w_handler}); + $self->{_y_spin_w}->signal_handler_block($self->{_y_spin_w_handler}); + $self->{_width_spin_w}->signal_handler_block($self->{_width_spin_w_handler}); + $self->{_height_spin_w}->signal_handler_block($self->{_height_spin_w_handler}); + + # 3. Mathematically adjust maximum thresholds dynamically (Clamping) + my $max_w = $self->{_root}->{w} - $current_x; + my $max_h = $self->{_root}->{h} - $current_y; + my $max_x = $self->{_root}->{w} - $current_w; + my $max_y = $self->{_root}->{h} - $current_h; + + # 4. Enforce new ranges on the spin buttons on-the-fly + $self->{_x_spin_w}->set_range(0, $max_x > 0 ? $max_x : 0); + $self->{_y_spin_w}->set_range(0, $max_y > 0 ? $max_y : 0); + $self->{_width_spin_w}->set_range(0, $max_w > 0 ? $max_w : 0); + $self->{_height_spin_w}->set_range(0, $max_h > 0 ? $max_h : 0); + + # 5. Restore safe boundary configurations back inside internal state tracker $self->{_state}->{sel} = { x => int($self->{_x_spin_w}->get_value), y => int($self->{_y_spin_w}->get_value), width => int($self->{_width_spin_w}->get_value), height => int($self->{_height_spin_w}->get_value), }; + + # 6. Reactivate signal listeners to catch next modifications + $self->{_x_spin_w}->signal_handler_unblock($self->{_x_spin_w_handler}); + $self->{_y_spin_w}->signal_handler_unblock($self->{_y_spin_w_handler}); + $self->{_width_spin_w}->signal_handler_unblock($self->{_width_spin_w_handler}); + $self->{_height_spin_w}->signal_handler_unblock($self->{_height_spin_w_handler}); + # Instantly enforce redraw operations across workspace components $self->{_canvas}->queue_draw if defined $self->{_canvas}; $self->{_zoom_area}->queue_draw if defined $self->{_zoom_area}; @@ -1021,7 +1052,9 @@ sub select_dialog { # 1. Coordinate configuration inputs setup: X parameter row my $xw_label = Gtk3::Label->new($d->get("X") . ":"); - $self->{_x_spin_w} = Gtk3::SpinButton->new_with_range(0, $self->{_root}->{w}, 1); + # Initialize with a safe dynamic ceiling right away + my $init_max_x = $self->{_root}->{w} - $sw; + $self->{_x_spin_w} = Gtk3::SpinButton->new_with_range(0, $init_max_x > 0 ? $init_max_x : $self->{_root}->{w}, 1); $self->{_x_spin_w}->set_value($sx); $self->{_x_spin_w_handler} = $self->{_x_spin_w}->signal_connect('value-changed' => $value_callback); @@ -1031,7 +1064,8 @@ sub select_dialog { # 2. Coordinate configuration inputs setup: Y parameter row my $yw_label = Gtk3::Label->new($d->get("Y") . ":"); - $self->{_y_spin_w} = Gtk3::SpinButton->new_with_range(0, $self->{_root}->{h}, 1); + my $init_max_y = $self->{_root}->{h} - $sh; + $self->{_y_spin_w} = Gtk3::SpinButton->new_with_range(0, $init_max_y > 0 ? $init_max_y : $self->{_root}->{h}, 1); $self->{_y_spin_w}->set_value($sy); $self->{_y_spin_w_handler} = $self->{_y_spin_w}->signal_connect('value-changed' => $value_callback); @@ -1041,9 +1075,9 @@ sub select_dialog { # 3. Coordinate configuration inputs setup: Width parameter row my $widthw_label = Gtk3::Label->new($d->get("Width") . ":"); - $self->{_width_spin_w} = Gtk3::SpinButton->new_with_range(0, $self->{_root}->{w}, 1); + my $init_max_w = $self->{_root}->{w} - $sx; + $self->{_width_spin_w} = Gtk3::SpinButton->new_with_range(0, $init_max_w > 0 ? $init_max_w : $self->{_root}->{w}, 1); $self->{_width_spin_w}->set_value($sw); - # Cleared the unused 'get_value_as_int' temporary zombie assignment line here during refactoring $self->{_width_spin_w_handler} = $self->{_width_spin_w}->signal_connect('value-changed' => $value_callback); my $ww_hbox = Gtk3::HBox->new(FALSE, 5); @@ -1052,7 +1086,8 @@ sub select_dialog { # 4. Coordinate configuration inputs setup: Height parameter row my $heightw_label = Gtk3::Label->new($d->get("Height") . ":"); - $self->{_height_spin_w} = Gtk3::SpinButton->new_with_range(0, $self->{_root}->{h}, 1); + my $init_max_h = $self->{_root}->{h} - $sy; + $self->{_height_spin_w} = Gtk3::SpinButton->new_with_range(0, $init_max_h > 0 ? $init_max_h : $self->{_root}->{h}, 1); $self->{_height_spin_w}->set_value($sh); $self->{_height_spin_w_handler} = $self->{_height_spin_w}->signal_connect('value-changed' => $value_callback); From 8605d7f89d85e440575679582d8837168c1cfbd0 Mon Sep 17 00:00:00 2001 From: Photon89 Date: Fri, 14 Aug 2026 21:17:27 +0200 Subject: [PATCH 5/5] Coordinate select dialog now appears on mouse pointer position --- .../Shutter/Screenshot/SelectorAdvanced.pm | 71 ++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/share/shutter/resources/modules/Shutter/Screenshot/SelectorAdvanced.pm b/share/shutter/resources/modules/Shutter/Screenshot/SelectorAdvanced.pm index e27a1391..51235987 100644 --- a/share/shutter/resources/modules/Shutter/Screenshot/SelectorAdvanced.pm +++ b/share/shutter/resources/modules/Shutter/Screenshot/SelectorAdvanced.pm @@ -1050,12 +1050,22 @@ sub select_dialog { } }; + # ------------------------------------------------------------------------- + # DYNAMIC SYMMETRY: Compute max character width from screen real estate + # ------------------------------------------------------------------------- + # Dynamically determine the maximum character length based on screen resolution + my $max_dimension = $self->{_root}->{w} > $self->{_root}->{h} ? $self->{_root}->{w} : $self->{_root}->{h}; + my $char_width = length(int($max_dimension)); + + # Enforce a sensible minimum width of 4 characters just to be visually uniform + $char_width = 4 if $char_width < 4; + # 1. Coordinate configuration inputs setup: X parameter row my $xw_label = Gtk3::Label->new($d->get("X") . ":"); - # Initialize with a safe dynamic ceiling right away my $init_max_x = $self->{_root}->{w} - $sw; $self->{_x_spin_w} = Gtk3::SpinButton->new_with_range(0, $init_max_x > 0 ? $init_max_x : $self->{_root}->{w}, 1); $self->{_x_spin_w}->set_value($sx); + $self->{_x_spin_w}->set_width_chars($char_width); # Force uniform alignment length $self->{_x_spin_w_handler} = $self->{_x_spin_w}->signal_connect('value-changed' => $value_callback); my $xw_hbox = Gtk3::HBox->new(FALSE, 5); @@ -1067,6 +1077,7 @@ sub select_dialog { my $init_max_y = $self->{_root}->{h} - $sh; $self->{_y_spin_w} = Gtk3::SpinButton->new_with_range(0, $init_max_y > 0 ? $init_max_y : $self->{_root}->{h}, 1); $self->{_y_spin_w}->set_value($sy); + $self->{_y_spin_w}->set_width_chars($char_width); # Force uniform alignment length $self->{_y_spin_w_handler} = $self->{_y_spin_w}->signal_connect('value-changed' => $value_callback); my $yw_hbox = Gtk3::HBox->new(FALSE, 5); @@ -1078,6 +1089,7 @@ sub select_dialog { my $init_max_w = $self->{_root}->{w} - $sx; $self->{_width_spin_w} = Gtk3::SpinButton->new_with_range(0, $init_max_w > 0 ? $init_max_w : $self->{_root}->{w}, 1); $self->{_width_spin_w}->set_value($sw); + $self->{_width_spin_w}->set_width_chars($char_width); # Force uniform alignment length $self->{_width_spin_w_handler} = $self->{_width_spin_w}->signal_connect('value-changed' => $value_callback); my $ww_hbox = Gtk3::HBox->new(FALSE, 5); @@ -1089,6 +1101,7 @@ sub select_dialog { my $init_max_h = $self->{_root}->{h} - $sy; $self->{_height_spin_w} = Gtk3::SpinButton->new_with_range(0, $init_max_h > 0 ? $init_max_h : $self->{_root}->{h}, 1); $self->{_height_spin_w}->set_value($sh); + $self->{_height_spin_w}->set_width_chars($char_width); # Force uniform alignment length $self->{_height_spin_w_handler} = $self->{_height_spin_w}->signal_connect('value-changed' => $value_callback); my $hw_hbox = Gtk3::HBox->new(FALSE, 5); @@ -1139,11 +1152,67 @@ sub select_dialog { $frame->add($vbox); $prop_dialog->add($frame); + # ------------------------------------------------------------------------- + # INTERFACE LAYOUT ALIGNMENT: Dynamic mouse positioning via safe margin shifts + # ------------------------------------------------------------------------- + # Static cache variables to freeze the very first pristine size measurement + my ($cached_w, $cached_h); + + # Connect to the show signal to position the box at the exact moment it is toggled + $prop_dialog->signal_connect('show' => sub { + my $widget = shift; + + # CRITICAL GTK3 FIX: Force the alignment to 'start' so margins act as real coordinates + $widget->set_halign('start'); + $widget->set_valign('start'); + + # Safe Fallback Coordinates + my $mx = 100; + my $my = 100; + + # Retrieve the verified precise cursor vectors directly from Shutter's active session state + if (defined $self->{_state}) { + $mx = $self->{_state}->{cursor_x} // 100; + $my = $self->{_state}->{cursor_y} // 100; + } + + # PROGRAMMATIC METRICS: Query the current theme engine size + my ($min_req, $nat_req) = $widget->get_preferred_size(); + my $raw_w = (defined $nat_req && $nat_req->width > 0) ? $nat_req->width : 200; + my $raw_h = (defined $nat_req && $nat_req->height > 0) ? $nat_req->height : 250; + + # Lock the size parameters on the very first execution before margins bloat the layout + if (!defined $cached_w || !defined $cached_h) { + $cached_w = $raw_w; + $cached_h = $raw_h; + } + + # Use the safely cached dimensions for boundary calculations + my $w = $cached_w; + my $h = $cached_h; + my $pad = 15; # Safe padding in pixels to prevent edge clipping + # Calculate coordinates with a safety margin when flipping at the edges + my $target_x = ($mx + $w > $self->{_root}->{w}) ? ($mx - $w - $pad) : $mx; + my $target_y = ($my + $h > $self->{_root}->{h}) ? ($my - $h - $pad) : $my; + + # Enforce absolute safety boundaries using standard clamping thresholds + $target_x = 0 if $target_x < 0; + $target_y = 0 if $target_y < 0; + $target_x = 32760 if $target_x > 32760; + $target_y = 32760 if $target_y > 32760; + + # Safely apply coordinates as padding margins inside the overlay container + $widget->set_margin_left($target_x); + $widget->set_margin_top($target_y); + }); + + # Return the original dialogue box widget directly so Shutter's references match perfectly return $prop_dialog; } + # ========================================================================= # CAPTURE PIPELINE EVALUATOR (Extracts and processes the final cropped image) # =========================================================================