1/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*-
2 *
3 * Copyright (C) 2011,2012 Canonical Ltd
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License version 3 as
7 * published by the Free Software Foundation.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authors: Robert Ancell <robert.ancell@canonical.com>
18 *          Michael Terry <michael.terry@canonical.com>
19 */
20
21public class MainWindow : Gtk.Window
22{
23    public MenuBar menubar;
24
25    private List<Monitor> monitors;
26    private Monitor? primary_monitor;
27    private Monitor active_monitor;
28    private string only_on_monitor;
29    private bool monitor_setting_ok;
30    private Background background;
31    private Gtk.Box login_box;
32    private Gtk.Box hbox;
33    private Gtk.Button back_button;
34    private ShutdownDialog? shutdown_dialog = null;
35    private bool do_resize;
36
37    public ListStack stack;
38
39    // Menubar is smaller, but with shadow, we reserve more space
40    public const int MENUBAR_HEIGHT = 32;
41
42    construct
43    {
44        events |= Gdk.EventMask.POINTER_MOTION_MASK;
45
46        var accel_group = new Gtk.AccelGroup ();
47        add_accel_group (accel_group);
48
49        var bg_color = Gdk.RGBA ();
50        bg_color.parse (UGSettings.get_string (UGSettings.KEY_BACKGROUND_COLOR));
51        override_background_color (Gtk.StateFlags.NORMAL, bg_color);
52        get_accessible ().set_name (_("Login Screen"));
53        has_resize_grip = false;
54        SlickGreeter.add_style_class (this);
55
56        background = new Background ();
57        add (background);
58        SlickGreeter.add_style_class (background);
59
60        login_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
61        login_box.show ();
62        background.add (login_box);
63
64        /* Box for menubar shadow */
65        var menubox = new Gtk.EventBox ();
66        var menualign = new Gtk.Alignment (0.0f, 0.0f, 1.0f, 0.0f);
67        var shadow_path = Path.build_filename (Config.PKGDATADIR,
68                                               "shadow.png", null);
69        var shadow_style = "";
70        if (FileUtils.test (shadow_path, FileTest.EXISTS))
71        {
72            shadow_style = "background-image: url('%s');background-repeat: repeat;".printf(shadow_path);
73        }
74        try
75        {
76            var style = new Gtk.CssProvider ();
77            style.load_from_data ("* {background-color: transparent;%s}".printf(shadow_style), -1);
78            var context = menubox.get_style_context ();
79            context.add_provider (style,
80                                  Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
81        }
82        catch (Error e)
83        {
84            debug ("Internal error loading menubox style: %s", e.message);
85        }
86        menubox.set_size_request (-1, MENUBAR_HEIGHT);
87        menubox.show ();
88        menualign.show ();
89        menubox.add (menualign);
90        login_box.add (menubox);
91        SlickGreeter.add_style_class (menualign);
92        SlickGreeter.add_style_class (menubox);
93
94        menubar = new MenuBar (background, accel_group, this);
95        menubar.show ();
96        menualign.add (menubar);
97        SlickGreeter.add_style_class (menubar);
98
99        hbox = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
100        hbox.expand = true;
101        hbox.show ();
102        login_box.add (hbox);
103
104        var align = new Gtk.Alignment (0.5f, 0.5f, 0.0f, 0.0f);
105        // Hack to avoid gtk 3.20's new allocate logic, which messes us up.
106        align.resize_mode = Gtk.ResizeMode.QUEUE;
107        align.set_size_request (grid_size, -1);
108        align.margin_bottom = MENUBAR_HEIGHT; /* offset for menubar at top */
109        align.show ();
110        hbox.add (align);
111
112        back_button = new FlatButton ();
113        back_button.get_accessible ().set_name (_("Back"));
114        Gtk.button_set_focus_on_click (back_button, false);
115        var image = new Gtk.Image.from_file (Path.build_filename (Config.PKGDATADIR, "arrow_left.png", null));
116        image.show ();
117        back_button.set_size_request (grid_size - GreeterList.BORDER * 2, grid_size - GreeterList.BORDER * 2);
118        back_button.add (image);
119        back_button.clicked.connect (pop_list);
120        align.add (back_button);
121
122        align = new Gtk.Alignment (0.0f, 0.5f, 0.0f, 1.0f);
123        align.show ();
124        hbox.add (align);
125
126        stack = new ListStack ();
127        stack.show ();
128        align.add (stack);
129
130        add_user_list ();
131
132        primary_monitor = null;
133        do_resize = false;
134
135        only_on_monitor = UGSettings.get_string(UGSettings.KEY_ONLY_ON_MONITOR);
136        monitor_setting_ok = only_on_monitor == "auto";
137
138        if (SlickGreeter.singleton.test_mode)
139        {
140            /* Simulate an 800x600 monitor to the left of a 640x480 monitor */
141            monitors = new List<Monitor> ();
142            monitors.append (new Monitor (0, 0, 800, 600));
143            monitors.append (new Monitor (800, 120, 640, 480));
144            background.set_monitors (monitors);
145            move_to_monitor (monitors.nth_data (0));
146            resize (background.width, background.height);
147        }
148        else
149        {
150            var screen = get_screen ();
151            screen.monitors_changed.connect (monitors_changed_cb);
152            monitors_changed_cb (screen);
153        }
154
155        /* Force a call on login_box.show()...
156            This fixes the following issue:
157            When the greeter starts, the login box looks too small, its entry isn't visible and
158            its session button isn't sensitive/clickable.
159            Pressing Escape fixes the box but not the session button..
160            Scrolling up/down fixes both..
161        */
162        if (login_box.sensitive) {
163            login_box.show();
164        }
165    }
166
167    public void push_list (GreeterList widget)
168    {
169        stack.push (widget);
170
171        if (stack.num_children > 1)
172            back_button.show ();
173    }
174
175    public void pop_list ()
176    {
177        if (stack.num_children <= 2)
178            back_button.hide ();
179
180        stack.pop ();
181    }
182
183    public override void size_allocate (Gtk.Allocation allocation)
184    {
185        base.size_allocate (allocation);
186
187        if (hbox != null)
188        {
189            hbox.margin_left = get_grid_offset (get_allocated_width ()) + grid_size;
190            hbox.margin_right = get_grid_offset (get_allocated_width ());
191            hbox.margin_top = get_grid_offset (get_allocated_height ());
192            hbox.margin_bottom = get_grid_offset (get_allocated_height ());
193        }
194    }
195
196    public override void realize ()
197    {
198        base.realize ();
199        background.set_surface (Gdk.cairo_create (get_window ()).get_target ());
200    }
201
202    public void before_session_start()
203    {
204        debug ("Killing orca and onboard");
205        menubar.cleanup();
206    }
207
208    /* Setup the size and position of the window */
209    public void setup_window ()
210    {
211        resize (background.width, background.height);
212        move (0, 0);
213        move_to_monitor (primary_monitor);
214    }
215
216    private void monitors_changed_cb (Gdk.Screen screen)
217    {
218        Gdk.Display display = screen.get_display();
219        Gdk.Monitor primary = display.get_primary_monitor();
220        Gdk.Rectangle geometry;
221
222        monitors = new List<Monitor> ();
223        primary_monitor = null;
224
225        for (var i = 0; i < display.get_n_monitors (); i++)
226        {
227            Gdk.Monitor monitor = display.get_monitor(i);
228            geometry = monitor.get_geometry ();
229            debug ("Monitor %d is %dx%d pixels at %d,%d", i, geometry.width, geometry.height, geometry.x, geometry.y);
230
231            if (monitor_is_unique_position (display, i))
232            {
233                var greeter_monitor = new Monitor (geometry.x, geometry.y, geometry.width, geometry.height);
234                var plug_name = monitor.get_model();
235                monitors.append (greeter_monitor);
236
237                if (plug_name == only_on_monitor)
238                    monitor_setting_ok = true;
239
240                if (plug_name == only_on_monitor || primary_monitor == null || primary == monitor)
241                    primary_monitor = greeter_monitor;
242            }
243        }
244
245        debug ("MainWindow is %dx%d pixels", background.width, background.height);
246
247        background.set_monitors (monitors);
248
249        if(do_resize)
250        {
251            setup_window ();
252        }
253        else
254        {
255            do_resize = true;
256        }
257    }
258
259    /* Check if a monitor has a unique position */
260    private bool monitor_is_unique_position (Gdk.Display display, int n)
261    {
262        Gdk.Rectangle g0;
263        Gdk.Monitor mon0;
264        mon0 = display.get_monitor(n);
265        g0 = mon0.get_geometry ();
266
267        for (var i = n + 1; i < display.get_n_monitors (); i++)
268        {
269            Gdk.Rectangle g1;
270            Gdk.Monitor mon1;
271            mon1 = display.get_monitor(i);
272            g1 = mon1.get_geometry();
273
274            if (g0.x == g1.x && g0.y == g1.y)
275                return false;
276        }
277
278        return true;
279    }
280
281    public override bool motion_notify_event (Gdk.EventMotion event)
282    {
283        if (!monitor_setting_ok || only_on_monitor == "auto")
284        {
285            var x = (int) (event.x + 0.5);
286            var y = (int) (event.y + 0.5);
287
288            /* Get motion event relative to this widget */
289            if (event.window != get_window ())
290            {
291                int w_x, w_y;
292                get_window ().get_origin (out w_x, out w_y);
293                x -= w_x;
294                y -= w_y;
295                event.window.get_origin (out w_x, out w_y);
296                x += w_x;
297                y += w_y;
298            }
299
300            foreach (var m in monitors)
301            {
302                if (x >= m.x && x <= m.x + m.width && y >= m.y && y <= m.y + m.height)
303                {
304                    move_to_monitor (m);
305                    break;
306                }
307            }
308        }
309
310        return false;
311    }
312
313    private void move_to_monitor (Monitor monitor)
314    {
315        active_monitor = monitor;
316        login_box.set_size_request (monitor.width, monitor.height);
317        background.set_active_monitor (monitor);
318        background.move (login_box, monitor.x, monitor.y);
319
320        if (shutdown_dialog != null)
321        {
322            shutdown_dialog.set_active_monitor (monitor);
323            background.move (shutdown_dialog, monitor.x, monitor.y);
324        }
325    }
326
327    private void add_user_list ()
328    {
329        GreeterList greeter_list;
330        greeter_list = new UserList (background, menubar);
331        greeter_list.show ();
332        SlickGreeter.add_style_class (greeter_list);
333        push_list (greeter_list);
334    }
335
336    public override bool key_press_event (Gdk.EventKey event)
337    {
338        var top = stack.top ();
339
340        if (stack.top () is UserList)
341        {
342            var user_list = stack.top () as UserList;
343            if (!user_list.show_hidden_users)
344            {
345                var shift_mask = Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.MOD1_MASK;
346                var control_mask = Gdk.ModifierType.SHIFT_MASK | Gdk.ModifierType.MOD1_MASK;
347                var alt_mask = Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK;
348                if (((event.keyval == Gdk.Key.Shift_L || event.keyval == Gdk.Key.Shift_R) && (event.state & shift_mask) == shift_mask) ||
349                    ((event.keyval == Gdk.Key.Control_L || event.keyval == Gdk.Key.Control_R) && (event.state & control_mask) == control_mask) ||
350                    ((event.keyval == Gdk.Key.Alt_L || event.keyval == Gdk.Key.Alt_R) && (event.state & alt_mask) == alt_mask))
351                {
352                    debug ("Hidden user key combination detected");
353                    user_list.show_hidden_users = true;
354                    return true;
355                }
356            }
357        }
358
359        switch (event.keyval)
360        {
361        case Gdk.Key.Escape:
362            if (login_box.sensitive)
363                top.cancel_authentication ();
364            if (shutdown_dialog != null)
365                shutdown_dialog.cancel ();
366            return true;
367        case Gdk.Key.Page_Up:
368        case Gdk.Key.KP_Page_Up:
369            if (login_box.sensitive)
370                top.scroll (GreeterList.ScrollTarget.START);
371            return true;
372        case Gdk.Key.Page_Down:
373        case Gdk.Key.KP_Page_Down:
374            if (login_box.sensitive)
375                top.scroll (GreeterList.ScrollTarget.END);
376            return true;
377        case Gdk.Key.Up:
378        case Gdk.Key.KP_Up:
379            if (login_box.sensitive)
380                top.scroll (GreeterList.ScrollTarget.UP);
381            return true;
382        case Gdk.Key.Down:
383        case Gdk.Key.KP_Down:
384            if (login_box.sensitive)
385                top.scroll (GreeterList.ScrollTarget.DOWN);
386            return true;
387        case Gdk.Key.Left:
388        case Gdk.Key.KP_Left:
389            if (shutdown_dialog != null)
390                shutdown_dialog.focus_prev ();
391            return true;
392        case Gdk.Key.Right:
393        case Gdk.Key.KP_Right:
394            if (shutdown_dialog != null)
395                shutdown_dialog.focus_next ();
396            return true;
397        case Gdk.Key.F10:
398            if (login_box.sensitive)
399                menubar.select_first (false);
400            return true;
401        case Gdk.Key.PowerOff:
402            show_shutdown_dialog (ShutdownDialogType.SHUTDOWN);
403            return true;
404        case Gdk.Key.Print:
405            debug ("Taking screenshot");
406            var root = Gdk.get_default_root_window ();
407            var screenshot = Gdk.pixbuf_get_from_window (root, 0, 0, root.get_width (), root.get_height ());
408            try
409            {
410                screenshot.save ("Screenshot.png", "png", null);
411            }
412            catch (Error e)
413            {
414                warning ("Failed to save screenshot: %s", e.message);
415            }
416            return true;
417        case Gdk.Key.z:
418            if (SlickGreeter.singleton.test_mode && (event.state & Gdk.ModifierType.MOD1_MASK) != 0)
419            {
420                show_shutdown_dialog (ShutdownDialogType.SHUTDOWN);
421                return true;
422            }
423            break;
424        case Gdk.Key.Z:
425            if (SlickGreeter.singleton.test_mode && (event.state & Gdk.ModifierType.MOD1_MASK) != 0)
426            {
427                show_shutdown_dialog (ShutdownDialogType.RESTART);
428                return true;
429            }
430            break;
431        }
432
433        return base.key_press_event (event);
434    }
435
436    public void set_keyboard_state ()
437    {
438        menubar.set_keyboard_state ();
439    }
440
441    public void show_shutdown_dialog (ShutdownDialogType type)
442    {
443        if (shutdown_dialog != null)
444            shutdown_dialog.destroy ();
445
446        /* Stop input to login box */
447        login_box.sensitive = false;
448
449        shutdown_dialog = new ShutdownDialog (type, background);
450        shutdown_dialog.closed.connect (close_shutdown_dialog);
451        background.add (shutdown_dialog);
452        move_to_monitor (active_monitor);
453        shutdown_dialog.visible = true;
454    }
455
456    public void close_shutdown_dialog ()
457    {
458        if (shutdown_dialog == null)
459            return;
460
461        shutdown_dialog.destroy ();
462        shutdown_dialog = null;
463
464        login_box.sensitive = true;
465    }
466}
467