1/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*-
2 *
3 * Copyright (C) 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: Michael Terry <michael.terry@canonical.com>
18 */
19
20/* Vala's vapi for gtk3 is broken for lookup_color (it forgets the out keyword) */
21[CCode (cheader_filename = "gtk/gtk.h")]
22extern bool gtk_style_context_lookup_color (Gtk.StyleContext ctx, string color_name, out Gdk.RGBA color);
23
24public class DashEntry : Gtk.Entry, Fadable
25{
26    public static string font = "Ubuntu 14";
27    public signal void respond ();
28
29    public string constant_placeholder_text { get; set; }
30    public bool can_respond { get; set; default = true; }
31
32    private bool _did_respond;
33    public bool did_respond
34    {
35        get
36        {
37            return _did_respond;
38        }
39        set
40        {
41            _did_respond = value;
42            if (value)
43                set_state_flags (Gtk.StateFlags.ACTIVE, false);
44            else
45                unset_state_flags (Gtk.StateFlags.ACTIVE);
46            queue_draw ();
47        }
48    }
49
50    protected FadeTracker fade_tracker { get; protected set; }
51    private Gdk.Window arrow_win;
52    private static Gdk.Pixbuf arrow_pixbuf;
53
54    construct
55    {
56        fade_tracker = new FadeTracker (this);
57
58        notify["can-respond"].connect (queue_draw);
59        button_press_event.connect (button_press_event_cb);
60
61        if (arrow_pixbuf == null)
62        {
63            var filename = Path.build_filename (Config.PKGDATADIR, "arrow_right.png");
64            try
65            {
66                arrow_pixbuf = new Gdk.Pixbuf.from_file (filename);
67            }
68            catch (Error e)
69            {
70                debug ("Internal error loading arrow icon: %s", e.message);
71            }
72        }
73
74        override_font (Pango.FontDescription.from_string (font));
75
76        var style_ctx = get_style_context ();
77
78        try
79        {
80            var padding_provider = new Gtk.CssProvider ();
81            var css = "* {padding-right: %dpx;}".printf (get_arrow_size ());
82            padding_provider.load_from_data (css, -1);
83            style_ctx.add_provider (padding_provider,
84                                    Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
85        }
86        catch (Error e)
87        {
88            debug ("Internal error loading padding style: %s", e.message);
89        }
90    }
91
92    public override bool draw (Cairo.Context c)
93    {
94        var style_ctx = get_style_context ();
95
96        // See construct method for explanation of why we remove classes
97        style_ctx.save ();
98        c.save ();
99        c.push_group ();
100        base.draw (c);
101        c.pop_group_to_source ();
102        c.paint_with_alpha (fade_tracker.alpha);
103        c.restore ();
104        style_ctx.restore ();
105
106        /* Now draw the prompt text */
107        if (get_text_length () == 0 && constant_placeholder_text.length > 0)
108            draw_prompt_text (c);
109
110        /* Draw activity spinner if we need to */
111        if (did_respond)
112            draw_spinner (c);
113        else if (can_respond && get_text_length () > 0)
114            draw_arrow (c);
115
116        return false;
117    }
118
119    private void draw_spinner (Cairo.Context c)
120    {
121        c.save ();
122
123        var style_ctx = get_style_context ();
124        var arrow_size = get_arrow_size ();
125        Gtk.cairo_transform_to_window (c, this, arrow_win);
126        style_ctx.render_activity (c, 0, 0, arrow_size, arrow_size);
127
128        c.restore ();
129    }
130
131    private void draw_arrow (Cairo.Context c)
132    {
133        if (arrow_pixbuf == null)
134            return;
135
136        c.save ();
137
138        var arrow_size = get_arrow_size ();
139        Gtk.cairo_transform_to_window (c, this, arrow_win);
140        c.translate (arrow_size - arrow_pixbuf.get_width () - 1, 0); // right align
141        Gdk.cairo_set_source_pixbuf (c, arrow_pixbuf, 0, 0);
142
143        c.paint ();
144        c.restore ();
145    }
146
147    private void draw_prompt_text (Cairo.Context c)
148    {
149        c.save ();
150
151        /* Position text */
152        int layout_width, layout_height;
153        Gdk.Rectangle rect;
154
155        var layout = create_pango_layout (constant_placeholder_text);
156        layout.set_font_description (Pango.FontDescription.from_string ("Ubuntu 13"));
157        layout.get_pixel_size (out layout_width, out layout_height);
158
159        get_text_area (out rect);
160
161        c.move_to (rect.x, rect.y + (rect.height / 2) - (layout_height / 2));
162
163        /* Set foreground color */
164        var fg = Gdk.RGBA ();
165        var context = get_style_context ();
166        if (!gtk_style_context_lookup_color (context, "placeholder_text_color", out fg))
167            fg.parse ("#888");
168        c.set_source_rgba (fg.red, fg.green, fg.blue, fg.alpha);
169
170        /* Draw text */
171        Pango.cairo_show_layout (c, layout);
172
173        c.restore ();
174    }
175
176    public override void activate ()
177    {
178        base.activate ();
179        if (can_respond)
180        {
181            did_respond = true;
182            respond ();
183        }
184        else
185        {
186            get_toplevel ().child_focus (Gtk.DirectionType.TAB_FORWARD);
187        }
188    }
189
190    public bool button_press_event_cb (Gdk.EventButton event)
191    {
192        if (event.window == arrow_win && get_text_length () > 0)
193        {
194            activate ();
195            return true;
196        }
197        else
198            return false;
199    }
200
201    private int get_arrow_size ()
202    {
203        // height is larger than width for the arrow, so we measure using that
204        if (arrow_pixbuf != null)
205            return arrow_pixbuf.get_height ();
206        else
207            return 20; // Shouldn't happen
208    }
209
210    private void get_arrow_location (out int x, out int y)
211    {
212        var arrow_size = get_arrow_size ();
213
214        Gtk.Allocation allocation;
215        get_allocation (out allocation);
216
217        // height is larger than width for the arrow, so we measure using that
218        var margin = (allocation.height - arrow_size) / 2;
219
220        x = allocation.x + allocation.width - margin - arrow_size;
221        y = allocation.y + margin;
222    }
223
224    public override void size_allocate (Gtk.Allocation allocation)
225    {
226        base.size_allocate (allocation);
227
228        if (arrow_win == null)
229            return;
230
231        int arrow_x, arrow_y;
232        get_arrow_location (out arrow_x, out arrow_y);
233        var arrow_size = get_arrow_size ();
234
235        arrow_win.move_resize (arrow_x, arrow_y, arrow_size, arrow_size);
236    }
237
238    public override void realize ()
239    {
240        base.realize ();
241
242        var cursor = new Gdk.Cursor.for_display (get_display (), Gdk.CursorType.LEFT_PTR);
243        var attrs = Gdk.WindowAttr ();
244        attrs.x = 0;
245        attrs.y = 0;
246        attrs.width = 1;
247        attrs.height = 1;
248        attrs.cursor = cursor;
249        attrs.wclass = Gdk.WindowWindowClass.INPUT_ONLY;
250        attrs.window_type = Gdk.WindowType.CHILD;
251        attrs.event_mask = get_events () |
252                           Gdk.EventMask.BUTTON_PRESS_MASK;
253
254        arrow_win = new Gdk.Window (get_window (), attrs,
255                                    Gdk.WindowAttributesType.X |
256                                    Gdk.WindowAttributesType.Y |
257                                    Gdk.WindowAttributesType.CURSOR);
258        arrow_win.ref ();
259        arrow_win.set_user_data (this);
260    }
261
262    public override void unrealize ()
263    {
264        if (arrow_win != null)
265        {
266            arrow_win.destroy ();
267            arrow_win = null;
268        }
269        base.unrealize ();
270    }
271
272    public override void map ()
273    {
274        base.map ();
275        if (arrow_win != null)
276            arrow_win.show ();
277    }
278
279    public override void unmap ()
280    {
281        if (arrow_win != null)
282            arrow_win.hide ();
283        base.unmap ();
284    }
285
286    public override bool key_press_event (Gdk.EventKey event)
287    {
288        // This is a workaroud for bug https://launchpad.net/bugs/944159
289        // The problem is that orca seems to not notice that it's in a password
290        // field on startup.  We just need to kick orca in the pants.
291        if (SlickGreeter.singleton.orca_needs_kick)
292        {
293            Signal.emit_by_name (get_accessible (), "focus-event", true);
294            SlickGreeter.singleton.orca_needs_kick = false;
295        }
296
297        return base.key_press_event (event);
298    }
299}
300