/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- * * Copyright (C) 2011,2012 Canonical Ltd * Copyright (C) 2015-2017 Mike Gabriel * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * Authors: Robert Ancell * Michael Terry * Mike Gabriel */ private class IndicatorMenuItem : Gtk.MenuItem { public unowned Indicator.ObjectEntry entry; private Gtk.Box hbox; public IndicatorMenuItem (Indicator.ObjectEntry entry) { this.entry = entry; this.hbox = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 3); this.add (this.hbox); this.hbox.show (); if (entry.label != null) { entry.label.show.connect (this.visibility_changed_cb); entry.label.hide.connect (this.visibility_changed_cb); hbox.pack_start (entry.label, false, false, 0); } if (entry.image != null) { entry.image.show.connect (visibility_changed_cb); entry.image.hide.connect (visibility_changed_cb); hbox.pack_start (entry.image, false, false, 0); } if (entry.accessible_desc != null) get_accessible ().set_name (entry.accessible_desc); if (entry.menu != null) set_submenu (entry.menu); if (has_visible_child ()) show (); } public bool has_visible_child () { return (entry.image != null && entry.image.get_visible ()) || (entry.label != null && entry.label.get_visible ()); } public void visibility_changed_cb (Gtk.Widget widget) { visible = has_visible_child (); } } public class MenuBar : Gtk.MenuBar { public Background? background { get; construct; default = null; } public bool high_contrast { get; private set; default = false; } public Gtk.Window? keyboard_window { get; private set; default = null; } public Gtk.AccelGroup? accel_group { get; construct; } private const int HEIGHT = 32; public MenuBar (Background bg, Gtk.AccelGroup ag) { Object (background: bg, accel_group: ag); } public override bool draw (Cairo.Context c) { if (background != null) { int x, y; background.translate_coordinates (this, 0, 0, out x, out y); c.save (); c.translate (x, y); background.draw_full (c, Background.DrawFlags.NONE); c.restore (); } c.set_source_rgb (0.1, 0.1, 0.1); c.paint_with_alpha (0.4); foreach (var child in get_children ()) { propagate_draw (child, c); } return false; } /* Due to LP #973922 the keyboard has to be loaded after the main window * is shown and given focus. Therefore we don't enable the active state * until now. */ public void set_keyboard_state () { if (!ArcticaGreeter.singleton.test_mode) onscreen_keyboard_item.set_active (AGSettings.get_boolean (AGSettings.KEY_ONSCREEN_KEYBOARD)); } private string default_theme_name; private List indicator_objects; private Gtk.CheckMenuItem high_contrast_item; private Pid keyboard_pid = 0; private Pid reader_pid = 0; private Gtk.CheckMenuItem onscreen_keyboard_item; construct { Gtk.Settings.get_default ().get ("gtk-theme-name", out default_theme_name); pack_direction = Gtk.PackDirection.RTL; if (AGSettings.get_boolean (AGSettings.KEY_SHOW_HOSTNAME)) { var label = new Gtk.Label (Posix.utsname ().nodename); label.show (); var hostname_item = new Gtk.MenuItem (); hostname_item.add (label); hostname_item.sensitive = false; hostname_item.right_justified = true; hostname_item.show (); append (hostname_item); /* Hack to get a label showing on the menubar */ var fg = label.get_style_context ().get_color (Gtk.StateFlags.NORMAL); label.override_color (Gtk.StateFlags.INSENSITIVE, fg); } /* Prevent dragging the window by the menubar */ try { var style = new Gtk.CssProvider (); style.load_from_data ("* {-GtkWidget-window-dragging: false;}", -1); get_style_context ().add_provider (style, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); } catch (Error e) { debug ("Internal error loading menubar style: %s", e.message); } setup_indicators (); ArcticaGreeter.singleton.starting_session.connect (cleanup); } private void close_pid (ref Pid pid) { if (pid > 0) { Posix.kill (pid, Posix.SIGTERM); int status; Posix.waitpid (pid, out status, 0); pid = 0; } } public void cleanup () { close_pid (ref keyboard_pid); close_pid (ref reader_pid); } public override void get_preferred_height (out int min, out int nat) { min = HEIGHT; nat = HEIGHT; } private void greeter_set_env (string key, string val) { GLib.Environment.set_variable (key, val, true); /* And also set it in the DBus activation environment so that any * indicator services pick it up. */ try { var proxy = new GLib.DBusProxy.for_bus_sync (GLib.BusType.SESSION, GLib.DBusProxyFlags.NONE, null, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", null); var builder = new GLib.VariantBuilder (GLib.VariantType.ARRAY); builder.add ("{ss}", key, val); proxy.call_sync ("UpdateActivationEnvironment", new GLib.Variant ("(a{ss})", builder), GLib.DBusCallFlags.NONE, -1, null); } catch (Error e) { warning ("Could not get set environment for indicators: %s", e.message); return; } } private Gtk.Widget make_a11y_indicator () { var a11y_item = new Gtk.MenuItem (); var hbox = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 3); hbox.show (); a11y_item.add (hbox); var image = new Gtk.Image.from_file (Path.build_filename (Config.PKGDATADIR, "a11y.svg")); image.show (); hbox.add (image); a11y_item.show (); a11y_item.set_submenu (new Gtk.Menu ()); onscreen_keyboard_item = new Gtk.CheckMenuItem.with_label (_("Onscreen keyboard")); onscreen_keyboard_item.toggled.connect (keyboard_toggled_cb); onscreen_keyboard_item.show (); unowned Gtk.Menu submenu = a11y_item.submenu; submenu.append (onscreen_keyboard_item); high_contrast_item = new Gtk.CheckMenuItem.with_label (_("High Contrast")); high_contrast_item.toggled.connect (high_contrast_toggled_cb); high_contrast_item.add_accelerator ("activate", accel_group, Gdk.Key.h, Gdk.ModifierType.CONTROL_MASK, Gtk.AccelFlags.VISIBLE); high_contrast_item.show (); submenu.append (high_contrast_item); high_contrast_item.set_active (AGSettings.get_boolean (AGSettings.KEY_HIGH_CONTRAST)); var item = new Gtk.CheckMenuItem.with_label (_("Screen Reader")); item.toggled.connect (screen_reader_toggled_cb); item.add_accelerator ("activate", accel_group, Gdk.Key.s, Gdk.ModifierType.SUPER_MASK | Gdk.ModifierType.MOD1_MASK, Gtk.AccelFlags.VISIBLE); item.show (); submenu.append (item); item.set_active (AGSettings.get_boolean (AGSettings.KEY_SCREEN_READER)); return a11y_item; } private Indicator.Object? load_indicator_file (string indicator_name) { string dir = Config.INDICATOR_FILE_DIR; string path; Indicator.Object io; /* To stay backwards compatible, use org.ayatana.indicator as the default prefix */ if (indicator_name.index_of_char ('.') < 0) path = @"$dir/org.ayatana.indicator.$indicator_name"; else path = @"$dir/$indicator_name"; try { io = new Indicator.Ng.for_profile (path, "desktop_greeter"); } catch (FileError error) { /* the calling code handles file-not-found; don't warn here */ return null; } catch (Error error) { warning ("unable to load %s: %s", indicator_name, error.message); return null; } return io; } private Indicator.Object? load_indicator_library (string indicator_name) { // Find file, if it exists string[] names_to_try = {"lib" + indicator_name + ".so", indicator_name + ".so", indicator_name}; foreach (var filename in names_to_try) { var full_path = Path.build_filename (Config.INDICATORDIR, filename); var io = new Indicator.Object.from_file (full_path); if (io != null) return io; } return null; } private void load_indicator (string indicator_name) { if (!ArcticaGreeter.singleton.test_mode) { if (indicator_name == "ug-accessibility") { var a11y_item = make_a11y_indicator (); insert (a11y_item, (int) get_children ().length () - 1); } else { var io = load_indicator_file (indicator_name); if (io == null) io = load_indicator_library (indicator_name); if (io != null) { indicator_objects.append (io); io.entry_added.connect (indicator_added_cb); io.entry_removed.connect (indicator_removed_cb); foreach (var entry in io.get_entries ()) indicator_added_cb (io, entry); } } } } private void setup_indicators () { /* Set indicators to run with reduced functionality */ greeter_set_env ("INDICATOR_GREETER_MODE", "1"); /* Don't allow virtual file systems? */ greeter_set_env ("GIO_USE_VFS", "local"); greeter_set_env ("GVFS_DISABLE_FUSE", "1"); /* Hint to have mate-settings-daemon run in greeter mode */ greeter_set_env ("RUNNING_UNDER_GDM", "1"); /* Let indicators know about our unique dbus name */ try { var conn = Bus.get_sync (BusType.SESSION); greeter_set_env ("ARCTICA_GREETER_DBUS_NAME", conn.get_unique_name ()); } catch (IOError e) { debug ("Could not set DBUS_NAME: %s", e.message); } debug ("LANG=%s LANGUAGE=%s", Environment.get_variable ("LANG"), Environment.get_variable ("LANGUAGE")); var indicator_list = AGSettings.get_strv(AGSettings.KEY_INDICATORS); var update_indicator_list = false; for (var i = 0; i < indicator_list.length; i++) { if (indicator_list[i] == "ug-keyboard") { indicator_list[i] = "org.ayatana.indicator.keyboard"; update_indicator_list = true; } } if (update_indicator_list) AGSettings.set_strv(AGSettings.KEY_INDICATORS, indicator_list); foreach (var indicator in indicator_list) load_indicator(indicator); indicator_objects.sort((a, b) => { int pos_a = a.get_position (); int pos_b = b.get_position (); if (pos_a < 0) pos_a = 1000; if (pos_b < 0) pos_b = 1000; return pos_a - pos_b; }); debug ("LANG=%s LANGUAGE=%s", Environment.get_variable ("LANG"), Environment.get_variable ("LANGUAGE")); } private void keyboard_toggled_cb (Gtk.CheckMenuItem item) { /* FIXME: The below would be sufficient if gnome-session were running * to notice and run a screen keyboard in /etc/xdg/autostart... But * since we're not running gnome-session, we hardcode onboard here. */ /* var settings = new Settings ("org.gnome.desktop.a11y.applications");*/ /*settings.set_boolean ("screen-keyboard-enabled", item.active);*/ AGSettings.set_boolean (AGSettings.KEY_ONSCREEN_KEYBOARD, item.active); if (keyboard_window == null) { int id = 0; try { string[] argv; int onboard_stdout_fd; Shell.parse_argv ("onboard --xid", out argv); Process.spawn_async_with_pipes (null, argv, null, SpawnFlags.SEARCH_PATH, null, out keyboard_pid, null, out onboard_stdout_fd, null); var f = FileStream.fdopen (onboard_stdout_fd, "r"); var stdout_text = new char[1024]; if (f.gets (stdout_text) != null) id = int.parse ((string) stdout_text); } catch (Error e) { warning ("Error setting up keyboard: %s", e.message); return; } var keyboard_socket = new Gtk.Socket (); keyboard_socket.show (); keyboard_window = new Gtk.Window (); keyboard_window.accept_focus = false; keyboard_window.focus_on_map = false; keyboard_window.add (keyboard_socket); keyboard_socket.add_id (id); /* Put keyboard at the bottom of the screen */ var display = get_display (); var monitor = display.get_monitor_at_window (get_window ()); Gdk.Rectangle geom; geom = monitor.get_geometry (); keyboard_window.move (geom.x, geom.y + geom.height - 200); keyboard_window.resize (geom.width, 200); } keyboard_window.visible = item.active; } private void high_contrast_toggled_cb (Gtk.CheckMenuItem item) { var settings = Gtk.Settings.get_default (); if (item.active) settings.set ("gtk-theme-name", "HighContrastInverse"); else settings.set ("gtk-theme-name", default_theme_name); high_contrast = item.active; AGSettings.set_boolean (AGSettings.KEY_HIGH_CONTRAST, high_contrast); } private void screen_reader_toggled_cb (Gtk.CheckMenuItem item) { /* FIXME: The below would be sufficient if gnome-session were running * to notice and run a screen reader in /etc/xdg/autostart... But * since we're not running gnome-session, we hardcode orca here. /*var settings = new Settings ("org.gnome.desktop.a11y.applications");*/ /*settings.set_boolean ("screen-reader-enabled", item.active);*/ AGSettings.set_boolean (AGSettings.KEY_SCREEN_READER, item.active); /* Hardcoded orca: */ if (item.active) { try { string[] argv; Shell.parse_argv ("orca --replace --no-setup --disable splash-window,", out argv); Process.spawn_async (null, argv, null, SpawnFlags.SEARCH_PATH, null, out reader_pid); // This is a workaroud for bug https://launchpad.net/bugs/944159 // The problem is that orca seems to not notice that it's in a // password field on startup. We just need to kick orca in the // pants. We do this two ways: a racy way and a non-racy way. // We kick it after a second which is ideal if we win the race, // because the user gets to hear what widget they are in, and // the first character will be masked. Otherwise, if we lose // that race, the first time the user types (see // DashEntry.key_press_event), we will kick orca again. While // this is not racy with orca startup, it is racy with whether // orca will read the first character or not out loud. Hence // why we do both. Ideally this would be fixed in orca itself. ArcticaGreeter.singleton.orca_needs_kick = true; Timeout.add_seconds (1, () => { Signal.emit_by_name ((get_toplevel () as Gtk.Window).get_focus ().get_accessible (), "focus-event", true); return false; }); } catch (Error e) { warning ("Failed to run Orca: %s", e.message); } } else close_pid (ref reader_pid); } private uint get_indicator_index (Indicator.Object object) { uint index = 0; foreach (var io in indicator_objects) { if (io == object) return index; index++; } return index; } private Indicator.Object? get_indicator_object_from_entry (Indicator.ObjectEntry entry) { foreach (var io in indicator_objects) { foreach (var e in io.get_entries ()) { if (e == entry) return io; } } return null; } private void indicator_added_cb (Indicator.Object object, Indicator.ObjectEntry entry) { var index = get_indicator_index (object); var pos = 0; foreach (var child in get_children ()) { if (!(child is IndicatorMenuItem)) break; var menuitem = (IndicatorMenuItem) child; var child_object = get_indicator_object_from_entry (menuitem.entry); var child_index = get_indicator_index (child_object); if (child_index > index) break; pos++; } debug ("Adding indicator object %p at position %d", entry, pos); var menuitem = new IndicatorMenuItem (entry); insert (menuitem, pos); } private void indicator_removed_cb (Indicator.Object object, Indicator.ObjectEntry entry) { debug ("Removing indicator object %p", entry); foreach (var child in get_children ()) { var menuitem = (IndicatorMenuItem) child; if (menuitem.entry == entry) { remove (child); return; } } warning ("Indicator object %p not in menubar", entry); } }