diff --git a/bin/shutter b/bin/shutter index 613f0db3..77647245 100755 --- a/bin/shutter +++ b/bin/shutter @@ -221,6 +221,7 @@ require Shutter::App::Notification; require Shutter::App::ShutterNotification; require Shutter::App::Toolbar; require Shutter::App::Optional::Exif; +require Shutter::App::StatusNotifier; #region require Shutter::Geometry::Region; @@ -370,6 +371,12 @@ my $session_desc; my $settings_dialog; my $settings_xml; my $sm; +my $sni_status = "Active"; +my $sni_initialized = 0; +#SNI tray menu entry variables +my ($sni_tray_show_window, $sni_tray_redoshot, $sni_tray_select, + $sni_tray_desktop, $sni_tray_window, $sni_tray_menu, $sni_tray_tooltip, + $sni_tray_web, $sni_tray_preferences, $sni_tray_about, $sni_tray_quit, $sni_tray_separator); my $sp; my $st; my $status; @@ -3827,6 +3834,8 @@ modes are currently experimental, your desktop environment might fall back to an last; } } + # SNI tray + $sni_tray_redoshot->set_enabled(1) if defined $sni_tray_redoshot; #TOOLBAR #-------------------------------------- @@ -4209,56 +4218,216 @@ modes are currently experimental, your desktop environment might fall back to an #functions #-------------------------------------- - sub fct_try_init_tray { - if ($x11_supported) { - $tray_legacy = Gtk3::StatusIcon->new(); - $tray_legacy->set_from_icon_name("shutter-panel"); - $tray_legacy->set_visible(1); - } else { - $tray_legacy = undef; - } + sub fct_sni_create_tray_sep { + return Shutter::App::StatusNotifier::create_item( type => 'separator' ); + } + sub fct_sni_create_tray_items { + return if $sni_tray_show_window; - fct_update_gui(); + # Funktion aus dem Modul referenzieren + my $create_item = \&Shutter::App::StatusNotifier::create_item; - if ($tray_legacy && $tray_legacy->is_embedded) { + $sni_tray_show_window = $create_item->( + label => $d->get('S_how main window') || 'Show main window', + onclick => sub { $is_hidden = TRUE; fct_control_main_window('show'); } + ); - if ($tray_libappindicator) { - $tray_libappindicator->set_status('passive'); - $tray_libappindicator = undef; - } - $tray = $tray_legacy; - $tray->{'hid'} = $tray->signal_connect( - 'popup-menu' => sub { evt_show_systray_statusicon(@_); }, - $tray - ); - $tray->{'hid2'} = $tray->signal_connect( - 'activate' => sub { - evt_activate_systray_statusicon(@_); - $tray; - }, - $tray + $sni_tray_redoshot = $create_item->( + label => $d->get('_Redo last screenshot') || 'Redo last screenshot', + icon => 'view-refresh', + enabled => 0, + onclick => sub { evt_take_screenshot(undef, 'redoshot'); } + ); + + $sni_tray_select = $create_item->( + label => $d->get('_Selection') || 'Selection', + icon => 'applications-accessories', + onclick => sub { evt_take_screenshot(undef, 'tray_select'); } + ); + + $sni_tray_desktop = $create_item->( + label => $d->get('_Desktop') || 'Desktop', + icon => 'user-desktop', + onclick => sub { evt_take_screenshot(undef, 'tray_full'); } + ); + + $sni_tray_window = $create_item->( + label => $d->get('Window _under Cursor') || 'Window under Cursor', + icon => 'preferences-system-windows', + onclick => sub { evt_take_screenshot(undef, 'tray_window'); } + ); + + $sni_tray_menu = $create_item->( + label => $d->get('_Menu') || 'Menu', + icon => 'application-menu', + onclick => sub { evt_take_screenshot(undef, 'tray_menu'); } + ); + + $sni_tray_tooltip = $create_item->( + label => $d->get('_Tooltip') || 'Tooltip', + icon => 'dialog-information', + onclick => sub { evt_take_screenshot(undef, 'tray_tooltip'); } + ); + + $sni_tray_web = $create_item->( + label => $d->get('_Web') || 'Web', + icon => 'applications-internet', + onclick => sub { evt_take_screenshot(undef, 'tray_web'); } + ); + + $sni_tray_preferences = $create_item->( + label => $d->get('Preferences') || 'Preferences', + icon => 'preferences-system', + onclick => sub { evt_show_settings(undef); } + ); + + $sni_tray_about = $create_item->( + label => $d->get('About') || 'About', + icon => 'help-about', + onclick => sub { evt_about(undef); } + ); + + $sni_tray_quit = $create_item->( + label => $d->get('_Quit') || 'Quit', + icon => 'application-exit', + onclick => sub { evt_delete_window('', 'quit'); } + ); + + if (!$x11_supported) { + $sni_tray_menu->set_enabled(0); + $sni_tray_tooltip->set_enabled(0); + } + if (!$gnome_web_photo) { + $sni_tray_web->set_enabled(0); + } + } + + sub fct_sni_build_menu { + my @menu_items = ( + $sni_tray_show_window, + fct_sni_create_tray_sep, + $sni_tray_redoshot, + fct_sni_create_tray_sep, + $sni_tray_select, + $sni_tray_desktop, + $sni_tray_window, + $sni_tray_menu, + $sni_tray_tooltip, + $sni_tray_web, + fct_sni_create_tray_sep, + $sni_tray_preferences, + $sni_tray_about, + fct_sni_create_tray_sep, + $sni_tray_quit + ); + + Shutter::App::StatusNotifier::set_menu(\@menu_items); + } + + sub fct_sni_init_gdbus { + return 1 if $sni_initialized; + + # initialize menu items + fct_sni_create_tray_items(); + + my $res = Shutter::App::StatusNotifier->init( + $app, + icon_name => 'shutter-panel', + icon_theme_path => "$shutter_root/share/icons/hicolor/scalable/apps", + title => SHUTTER_NAME, + tooltip_text => SHUTTER_NAME . " " . SHUTTER_VERSION, + on_activate => sub { + eval { + if ($is_hidden) { + fct_control_main_window('show'); + } else { + fct_control_main_window('hide'); + } + }; + } ); - } else { - if ($tray_legacy) { - $tray_legacy->set_visible(0); - $tray_legacy = undef; - } - if ($appindicator && !$tray_libappindicator) { - # Fallback to AppIndicator. This one doesn't allow left-click signal, but it seems to be the only option for Gnome on Ubuntu 21.04... - $tray_libappindicator = AppIndicator::Indicator->new("Shutter", "shutter-panel", 'application-status'); - $tray_libappindicator->set_menu($tray_menu); - $tray_libappindicator->set_status('active'); - } + if ($res) { + $sni_initialized = 1; + } + return $res; + } - if ($tray_libappindicator) { - $tray = $tray_libappindicator; - } else { - $tray = undef; - } - } - } + sub fct_sni_set_status { + my ($status) = @_; + $sni_status = $status; + eval { + my $conn = $app->get_dbus_connection(); + if ($conn) { + $conn->emit_signal( + undef, + '/StatusNotifierItem', + 'org.kde.StatusNotifierItem', + 'NewStatus', + Glib::Variant->new('(s)', [$sni_status]) + ); + } + }; + } + + sub fct_try_init_tray { + if ($x11_supported) { + $tray_legacy = Gtk3::StatusIcon->new(); + $tray_legacy->set_from_icon_name("shutter-panel"); + $tray_legacy->set_visible(1); + } else { + $tray_legacy = undef; + } + + fct_update_gui(); + + if ($tray_legacy && $tray_legacy->is_embedded) { + + if ($tray_libappindicator) { + $tray_libappindicator->set_status('passive'); + $tray_libappindicator = undef; + } + $tray = $tray_legacy; + + $tray->{'hid'} = $tray->signal_connect( + 'popup-menu' => sub { evt_show_systray_statusicon(@_); }, + $tray + ); + $tray->{'hid2'} = $tray->signal_connect( + 'activate' => sub { + evt_activate_systray_statusicon(@_); + $tray; + }, + $tray + ); + } else { + if ($tray_legacy) { + $tray_legacy->set_visible(0); + $tray_legacy = undef; + } + + # Try starting SNI via D-Bus first (KDE/Wayland with left-click support) + if (fct_sni_init_gdbus()) { + + # --- D-Bus menu setup --- + fct_sni_build_menu(); + # --------------------------- + + fct_sni_set_status('Active'); + $tray = 'SNI'; + } + # Fallback to AppIndicator if SNI fails + elsif ($appindicator && !$tray_libappindicator) { + $tray_libappindicator = AppIndicator::Indicator->new("Shutter", "shutter-panel", 'application-status'); + $tray_libappindicator->set_menu($tray_menu); + $tray_libappindicator->set_status('active'); + $tray = $tray_libappindicator; + } else { + $tray = $tray_libappindicator || undef; + } + } + } sub fct_create_session_notebook { @@ -6258,7 +6427,7 @@ modes are currently experimental, your desktop environment might fall back to an sub fct_take_screenshot { my ($widget, $data, $folder_from_config, $extra) = @_; - print "\n$data was emitted by widget $widget\n" + print "\n$data was emitted by widget " . ($widget // 'SNI/DBus (tray menu)') . "\n" if $sc->get_debug; #quality @@ -7446,12 +7615,16 @@ modes are currently experimental, your desktop environment might fall back to an #TRAY TOOLTIP #-------------------------------------- if ($combobox_settings_profiles) { - if ($tray && $tray->isa('Gtk3::StatusIcon')) { - if ($combobox_settings_profiles->get_active_text) { - $tray->set_tooltip_text($d->get("Current profile") . ": " . $combobox_settings_profiles->get_active_text); - } else { - $tray->set_tooltip_text(SHUTTER_NAME . " " . SHUTTER_VERSION); - } + my $active_text = $combobox_settings_profiles->get_active_text; + my $tooltip_text = (defined $active_text && length $active_text) + ? $d->get("Current profile") . ": " . $active_text + : SHUTTER_NAME . " " . SHUTTER_VERSION; + + if ($tray && ref($tray) && $tray->isa('Gtk3::StatusIcon')) { + $tray->set_tooltip_text($tooltip_text); + } + elsif ($sni_initialized) { + Shutter::App::StatusNotifier->set_tooltip_text(SHUTTER_NAME, $tooltip_text); } } diff --git a/share/shutter/resources/modules/Shutter/App/StatusNotifier.pm b/share/shutter/resources/modules/Shutter/App/StatusNotifier.pm new file mode 100644 index 00000000..c1724443 --- /dev/null +++ b/share/shutter/resources/modules/Shutter/App/StatusNotifier.pm @@ -0,0 +1,600 @@ +package Shutter::App::StatusNotifier; + +use strict; +use warnings; +use Glib; +use Scalar::Util qw(blessed); +use Exporter 'import'; + +our @EXPORT_OK = qw(create_item); + +my $active = 0; +my $object_id_sni; +my $object_id_menu; +my $dbus_conn; +my $current_menu_path; +my $menu_id_counter = 1; + +# Tooltip state storage +my $tooltip_icon = 'shutter'; +my $tooltip_title = 'Shutter'; +my $tooltip_text = 'Screenshot Tool'; + +# Menu state storage +my $menu_revision = 1; +my %MENU_ITEMS; # ID => MenuItem Object/HashRef +my %MENU_CHILDREN; # ID => ArrayRef of child IDs (0 acts as the root) + +my $sni_xml = <<"XML"; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +XML + +my $dbusmenu_xml = <<"XML"; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +XML + +# ========================================================================= +# FACTORY METHOD +# ========================================================================= + +sub create_item { + my (%props) = @_; + my $id = $menu_id_counter++; + return Shutter::App::StatusNotifier::MenuItem->new($id, %props); +} + +# ========================================================================= +# PUBLIC API +# ========================================================================= + +sub set_menu { + my ($items_array_ref) = @_; + + # Reset internal storage + %MENU_ITEMS = (); + %MENU_CHILDREN = ( 0 => [] ); + + _add_items_to_tree(0, $items_array_ref); + _trigger_layout_update(0) if $active; +} + +sub update_submenu { + my ($parent_id, $items_array_ref) = @_; + + return unless exists $MENU_CHILDREN{$parent_id}; + + if (my $old_children = $MENU_CHILDREN{$parent_id}) { + delete $MENU_ITEMS{$_} for @$old_children; + } + + $MENU_CHILDREN{$parent_id} = []; + _add_items_to_tree($parent_id, $items_array_ref); + _trigger_layout_update($parent_id) if $active; +} + +sub set_tooltip_text { + my ($class_or_self, $arg1, $arg2) = @_; + + if (defined $arg2) { + $tooltip_title = $arg1; + $tooltip_text = $arg2; + } else { + $tooltip_text = $arg1 // ''; + } + + _trigger_tooltip_update(); + return 1; +} + +# ========================================================================= +# INTERNAL DBUS HELPERS +# ========================================================================= + +sub _add_items_to_tree { + my ($parent_id, $items) = @_; + + for my $item (@$items) { + # Support both MenuItem objects and plain hashes + my $is_obj = blessed($item) && $item->isa('Shutter::App::StatusNotifier::MenuItem'); + + my $id = $is_obj ? $item->id : $item->{id}; + my $children = $is_obj ? $item->children_items : $item->{children_items}; + + $MENU_ITEMS{$id} = $item; + push @{$MENU_CHILDREN{$parent_id}}, $id; + + if ($children && @$children) { + $MENU_CHILDREN{$id} = []; + _add_items_to_tree($id, $children); + } + } +} + +sub _get_item_props_hash { + my ($id) = @_; + my $item = $MENU_ITEMS{$id}; + my %props; + + # Mark as submenu if it's the root node or contains children + if ($id == 0 || (exists $MENU_CHILDREN{$id} && @{$MENU_CHILDREN{$id}})) { + $props{'children-display'} = Glib::Variant->new_string('submenu'); + } + + return \%props if $id == 0 || !$item; + + my $is_obj = blessed($item) && $item->isa('Shutter::App::StatusNotifier::MenuItem'); + + my $label = $is_obj ? $item->label : $item->{label}; + my $type = $is_obj ? $item->type : $item->{type}; + my $icon = $is_obj ? $item->icon : $item->{icon}; + my $enabled = $is_obj ? $item->is_enabled : ($item->{enabled} // 1); + my $visible = $is_obj ? $item->is_visible : ($item->{visible} // 1); + + $props{'label'} = Glib::Variant->new_string($label) if defined $label; + $props{'type'} = Glib::Variant->new_string($type) if defined $type; + + $enabled = ($enabled && $enabled ne 'false' && $enabled ne '0') ? 1 : 0; + $props{'enabled'} = Glib::Variant->new_boolean($enabled); + + $visible = ($visible && $visible ne 'false' && $visible ne '0') ? 1 : 0; + $props{'visible'} = Glib::Variant->new_boolean($visible); + + $props{'icon-name'} = Glib::Variant->new_string($icon) if defined $icon && $icon ne ''; + + return \%props; +} + +sub _process_menu_event { + my ($id, $event_id) = @_; + + # Hilfreicher Debug-Output für die Konsole: + # warn "[DBUS-DEBUG] Event received: '$event_id' for item ID $id\n"; + + if ($event_id eq 'clicked' && exists $MENU_ITEMS{$id}) { + my $item = $MENU_ITEMS{$id}; + my $cb = (blessed($item) && $item->isa('Shutter::App::StatusNotifier::MenuItem')) + ? $item->callback + : $item->{onclick}; + + if ($cb) { + Glib::Idle->add(sub { $cb->($id); return 0; }); + } + } +} + +sub _trigger_layout_update { + my ($parent_id) = @_; + $parent_id //= 0; + $menu_revision++; + + return unless $active && $dbus_conn && $current_menu_path; + + eval { + my $sig_params = Glib::Variant->new_tuple([ + Glib::Variant->new_uint32($menu_revision), + Glib::Variant->new_int32($parent_id) + ]); + $dbus_conn->emit_signal( + undef, $current_menu_path, 'com.canonical.dbusmenu', 'LayoutUpdated', $sig_params + ); + }; + warn "[DBUS-ERROR] LayoutUpdated failed: $@\n" if $@; +} + +sub _trigger_icon_update { + return unless $active && $dbus_conn; + + eval { + $dbus_conn->emit_signal( + undef, '/StatusNotifierItem', 'org.kde.StatusNotifierItem', 'NewIcon', undef + ); + }; + warn "[DBUS-ERROR] NewIcon signal failed: $@\n" if $@; +} + +sub _trigger_item_property_update { + my ($id, $prop_name, $prop_variant) = @_; + + return unless $active && $dbus_conn && $current_menu_path; + + eval { + # Construct update array using native Perl structures. + # Signature 'a(ia{sv})': Array of Structs (ID, Dict(String => Variant)) + my $updated_array = Glib::Variant->new('a(ia{sv})', [ + [ int($id), { $prop_name => $prop_variant } ] + ]); + + # Array for removed properties, usually empty. Signature 'a(ia(s))'. + my $removed_array = Glib::Variant->new('a(ia(s))', []); + + my $sig_params = Glib::Variant->new_tuple([$updated_array, $removed_array]); + + $dbus_conn->emit_signal( + undef, $current_menu_path, 'com.canonical.dbusmenu', 'ItemsPropertiesUpdated', $sig_params + ); + }; + warn "[DBUS-ERROR] Property update signal failed: $@\n" if $@; +} + +sub _trigger_tooltip_update { + return unless $active && $dbus_conn; + + eval { + $dbus_conn->emit_signal( + undef, '/StatusNotifierItem', 'org.kde.StatusNotifierItem', 'NewToolTip', undef + ); + }; + warn "[DBUS-ERROR] NewToolTip signal failed: $@\n" if $@; +} + +sub _make_item_props { + my ($id) = @_; + return Glib::Variant->new('a{sv}', _get_item_props_hash($id)); +} + +sub _make_item_struct { + my ($id, $depth) = @_; + my $props_var = _make_item_props($id); + my @children_vars = (); + + if ($depth != 0 && exists $MENU_CHILDREN{$id}) { + my $next_depth = $depth > 0 ? $depth - 1 : -1; + for my $child_id (@{$MENU_CHILDREN{$id}}) { + push @children_vars, _make_item_struct($child_id, $next_depth); + } + } + + return Glib::Variant->new_tuple([ + Glib::Variant->new_int32($id), + $props_var, + Glib::Variant->new('av', \@children_vars) + ]); +} + +sub init { + my $class_or_app = shift; + my $app = (ref($class_or_app) && $class_or_app->isa('Shutter::App')) ? $class_or_app : shift; + my %args = @_; + + return 1 if $active; + + eval { + my $icon_name = $args{icon_name} || 'shutter'; + my $icon_theme_path = $args{icon_theme_path} || ''; + my $on_activate = $args{on_activate}; + + $tooltip_title = $args{title} || $args{tooltip_title} || 'Shutter'; + $tooltip_text = $args{tooltip} || $args{tooltip_text} || 'Screenshot Tool'; + $tooltip_icon = $icon_name; + + $dbus_conn = $app->get_dbus_connection(); + die "No D-Bus connection!" unless $dbus_conn; + + # 1. Register SNI Object + my $sni_node = Glib::IO::DBusNodeInfo->new_for_xml($sni_xml); + my $sni_iface = $sni_node->lookup_interface('org.kde.StatusNotifierItem'); + + my $rnd_path = int(rand(10000)); + $current_menu_path = "/AppMenu_$rnd_path"; + + $object_id_sni = $dbus_conn->register_object( + '/StatusNotifierItem', $sni_iface, + sub { + my ($conn, $sender, $obj_path, $iface, $method, $params, $invocation) = @_; + if ($method eq 'Activate') { + Glib::Idle->add(sub { $on_activate->() if $on_activate; return 0; }); + } + eval { $invocation->return_value(Glib::Variant->new_tuple([])); }; + }, + sub { + my ($conn, $sender, $obj_path, $iface, $prop) = @_; + return Glib::Variant->new_string('ApplicationStatus') if $prop eq 'Category'; + return Glib::Variant->new_string('Shutter') if $prop eq 'Id' || $prop eq 'Title'; + return Glib::Variant->new_string('Active') if $prop eq 'Status'; + return Glib::Variant->new_string($icon_name) if $prop eq 'IconName'; + return Glib::Variant->new_string($icon_theme_path) if $prop eq 'IconThemePath'; + return Glib::Variant->new_boolean(0) if $prop eq 'ItemIsMenu'; + return Glib::Variant->new_int32(0) if $prop eq 'WindowId'; + return Glib::Variant->new_object_path($current_menu_path) if $prop eq 'Menu'; + if ($prop eq 'ToolTip') { + return Glib::Variant->new_tuple([ + Glib::Variant->new_string($tooltip_icon), + Glib::Variant->new('a(iiay)', []), + Glib::Variant->new_string($tooltip_title), + Glib::Variant->new_string($tooltip_text) + ]); + } + return undef; + }, + sub { return 0; } + ); + + # 2. Register DBusMenu Object + my $menu_node = Glib::IO::DBusNodeInfo->new_for_xml($dbusmenu_xml); + my $menu_iface = $menu_node->lookup_interface('com.canonical.dbusmenu'); + + $object_id_menu = $dbus_conn->register_object( + $current_menu_path, $menu_iface, + sub { + my ($conn, $sender, $obj_path, $iface, $method, $params, $invocation) = @_; + + if ($method eq 'GetLayout') { + eval { + my $parent_id = $params->get_child_value(0)->get_int32(); + my $depth = $params->get_child_value(1)->get_int32(); + my $layout = _make_item_struct($parent_id, $depth); + $invocation->return_value(Glib::Variant->new_tuple([ + Glib::Variant->new_uint32($menu_revision), + $layout + ])); + }; + warn "[DBUS-ERROR] GetLayout failed: $@\n" if $@; + } + elsif ($method eq 'GetProperty') { + eval { + my $id = $params->get_child_value(0)->get_int32(); + my $name = $params->get_child_value(1)->get_string(); + my $props_hash = _get_item_props_hash($id); + my $val_variant = exists $props_hash->{$name} + ? $props_hash->{$name} + : Glib::Variant->new_string(''); + + $invocation->return_value(Glib::Variant->new_tuple([ + Glib::Variant->new_variant($val_variant) + ])); + }; + warn "[DBUS-ERROR] GetProperty failed: $@\n" if $@; + } + elsif ($method eq 'GetGroupProperties') { + eval { + my $ids_var = $params->get_child_value(0); + my $n = $ids_var->n_children(); + my @props_array; + + for (my $i = 0; $i < $n; $i++) { + my $id = $ids_var->get_child_value($i)->get_int32(); + if (exists $MENU_ITEMS{$id} || $id == 0) { + # WICHTIG: Ein nativ geschachteltes Perl-Array [ integer, hash_ref ] + push @props_array, [ int($id), _get_item_props_hash($id) ]; + } + } + + $invocation->return_value(Glib::Variant->new_tuple([ + Glib::Variant->new('a(ia{sv})', \@props_array) + ])); + }; + warn "[DBUS-ERROR] GetGroupProperties failed: $@\n" if $@; + } + elsif ($method eq 'AboutToShow') { + eval { + my $id = $params->get_child_value(0)->get_int32(); + my $needs_update = 0; + my $item = $MENU_ITEMS{$id}; + + if ($item && blessed($item) && $item->can('on_about_to_show') && $item->on_about_to_show) { + $needs_update = $item->on_about_to_show->($id); + } + + $invocation->return_value(Glib::Variant->new_tuple([ + Glib::Variant->new_boolean($needs_update) + ])); + }; + warn "[DBUS-ERROR] AboutToShow failed: $@\n" if $@; + } + elsif ($method eq 'Event') { + eval { + my $id = $params->get_child_value(0)->get_int32(); + my $event_id = $params->get_child_value(1)->get_string(); + + _process_menu_event($id, $event_id); + + $invocation->return_value(Glib::Variant->new_tuple([])); + }; + warn "[DBUS-ERROR] Event failed: $@\n" if $@; + } + elsif ($method eq 'EventGroup') { + eval { + my $events_var = $params->get_child_value(0); + my $n = $events_var->n_children(); + + for (my $i = 0; $i < $n; $i++) { + my $event_struct = $events_var->get_child_value($i); + my $id = $event_struct->get_child_value(0)->get_int32(); + my $event_id = $event_struct->get_child_value(1)->get_string(); + + _process_menu_event($id, $event_id); + } + + $invocation->return_value(Glib::Variant->new_tuple([ + Glib::Variant->new('a(i)', []) + ])); + }; + warn "[DBUS-ERROR] EventGroup failed: $@\n" if $@; + } + else { + eval { $invocation->return_value(Glib::Variant->new_tuple([])); }; + } + }, + sub { + my ($conn, $sender, $obj_path, $iface, $prop) = @_; + return Glib::Variant->new_uint32(3) if $prop eq 'Version'; + return Glib::Variant->new_string('ltr') if $prop eq 'TextDirection'; + return Glib::Variant->new_string('normal') if $prop eq 'Status'; + return undef; + }, + sub { return 0; } + ); + + # 3. Register at Watcher + $dbus_conn->call( + 'org.kde.StatusNotifierWatcher', '/StatusNotifierWatcher', + 'org.kde.StatusNotifierWatcher', 'RegisterStatusNotifierItem', + Glib::Variant->new('(s)', ['/StatusNotifierItem']), undef, 'none', -1, undef, + sub { + my ($c, $res) = @_; + eval { $c->call_finish($res); }; + } + ); + + $active = 1; + _trigger_icon_update(); + }; + + if ($@) { + warn "[DBUS-ERROR] SNI GDBus initialization error: $@\n"; + $active = 0; + } + return $active; +} + +# ========================================================================= +# CLASS: MenuItem +# ========================================================================= + +package Shutter::App::StatusNotifier::MenuItem; + +sub new { + my ($class, $id, %props) = @_; + + my $self = { + id => $id, + label => $props{label}, + icon => $props{icon}, + enabled => defined $props{enabled} ? $props{enabled} : 1, + visible => defined $props{visible} ? $props{visible} : 1, + type => $props{type}, + onclick => $props{onclick} || $props{on_click}, + children_items => $props{children_items} || [], + }; + + return bless $self, $class; +} + +# --- GETTERS --- +sub id { return shift->{id}; } +sub label { return shift->{label}; } +sub icon { return shift->{icon}; } +sub is_enabled { return shift->{enabled}; } +sub is_visible { return shift->{visible}; } +sub type { return shift->{type}; } +sub callback { return shift->{onclick}; } +sub children_items { return shift->{children_items}; } + +# --- SETTERS --- +# Updates immediately trigger targeted D-Bus property changes. + +sub set_enabled { + my ($self, $value) = @_; + $self->{enabled} = $value; + my $bool_variant = Glib::Variant->new_boolean($value ? 1 : 0); + Shutter::App::StatusNotifier::_trigger_item_property_update($self->{id}, 'enabled', $bool_variant); +} + +sub set_visible { + my ($self, $value) = @_; + $self->{visible} = $value; + my $bool_variant = Glib::Variant->new_boolean($value ? 1 : 0); + Shutter::App::StatusNotifier::_trigger_item_property_update($self->{id}, 'visible', $bool_variant); +} + +sub set_label { + my ($self, $value) = @_; + $self->{label} = $value; + my $str_variant = Glib::Variant->new_string($value // ''); + Shutter::App::StatusNotifier::_trigger_item_property_update($self->{id}, 'label', $str_variant); +} + +sub set_icon { + my ($self, $value) = @_; + $self->{icon} = $value; + my $str_variant = Glib::Variant->new_string($value // ''); + Shutter::App::StatusNotifier::_trigger_item_property_update($self->{id}, 'icon-name', $str_variant); +} + +sub set_children { + my ($self, $children_array_ref) = @_; + $self->{children_items} = $children_array_ref || []; + + # Structural changes require rebuilding the specific submenu branch + Shutter::App::StatusNotifier::update_submenu($self->{id}, $self->{children_items}); +} + +1;