1/*
2 * Copyright 2018 Michael Gratton <mike@vee.net>
3 *
4 * This software is licensed under the GNU Lesser General Public License
5 * (version 2.1 or later).  See the COPYING file in this distribution.
6 */
7
8
9internal class Accounts.EditorRow<PaneType> : Gtk.ListBoxRow {
10
11    private const string DND_ATOM = "geary-editor-row";
12    private const Gtk.TargetEntry[] DRAG_ENTRIES = {
13        { DND_ATOM, Gtk.TargetFlags.SAME_APP, 0 }
14    };
15
16
17    protected Gtk.Grid layout { get; private set; default = new Gtk.Grid(); }
18
19    private Gtk.Container drag_handle;
20    private bool drag_picked_up = false;
21    private bool drag_entered = false;
22
23
24    public signal void move_to(int new_position);
25    public signal void dropped(EditorRow target);
26
27
28    public EditorRow() {
29        get_style_context().add_class("geary-settings");
30        get_style_context().add_class("geary-labelled-row");
31
32        this.layout.orientation = Gtk.Orientation.HORIZONTAL;
33        this.layout.show();
34        add(this.layout);
35
36        // We'd like to add the drag handle only when needed, but
37        // GNOME/gtk#1495 prevents us from doing so.
38        Gtk.EventBox drag_box = new Gtk.EventBox();
39        drag_box.add(
40            new Gtk.Image.from_icon_name(
41                "list-drag-handle-symbolic", Gtk.IconSize.BUTTON
42            )
43        );
44        this.drag_handle = new Gtk.Grid();
45        this.drag_handle.valign = Gtk.Align.CENTER;
46        this.drag_handle.add(drag_box);
47        this.drag_handle.show_all();
48        this.drag_handle.hide();
49        // Translators: Tooltip for dragging list items
50        this.drag_handle.set_tooltip_text(_("Drag to move this item"));
51        this.layout.add(drag_handle);
52
53        this.show();
54    }
55
56    public virtual void activated(PaneType pane) {
57        // No-op by default
58    }
59
60    public override bool key_press_event(Gdk.EventKey event) {
61        bool ret = Gdk.EVENT_PROPAGATE;
62
63        if (event.state == Gdk.ModifierType.CONTROL_MASK) {
64            int index = get_index();
65            if (event.keyval == Gdk.Key.Up) {
66                index -= 1;
67                if (index >= 0) {
68                    move_to(index);
69                    ret = Gdk.EVENT_STOP;
70                }
71            } else if (event.keyval == Gdk.Key.Down) {
72                index += 1;
73                Gtk.ListBox? parent = get_parent() as Gtk.ListBox;
74                if (parent != null &&
75                    index < parent.get_children().length() &&
76                    !(parent.get_row_at_index(index) is AddRow)) {
77                    move_to(index);
78                    ret = Gdk.EVENT_STOP;
79                }
80            }
81        }
82
83        if (ret != Gdk.EVENT_STOP) {
84            ret = base.key_press_event(event);
85        }
86
87        return ret;
88    }
89
90    /** Adds a drag handle to the row and enables drag signals. */
91    protected void enable_drag() {
92        Gtk.drag_source_set(
93            this.drag_handle,
94            Gdk.ModifierType.BUTTON1_MASK,
95            DRAG_ENTRIES,
96            Gdk.DragAction.MOVE
97        );
98
99        Gtk.drag_dest_set(
100            this,
101            // No highlight, we'll take care of that ourselves so we
102            // can avoid highlighting the row that was picked up
103            Gtk.DestDefaults.MOTION | Gtk.DestDefaults.DROP,
104            DRAG_ENTRIES,
105            Gdk.DragAction.MOVE
106        );
107
108        this.drag_handle.drag_begin.connect(on_drag_begin);
109        this.drag_handle.drag_end.connect(on_drag_end);
110        this.drag_handle.drag_data_get.connect(on_drag_data_get);
111
112        this.drag_motion.connect(on_drag_motion);
113        this.drag_leave.connect(on_drag_leave);
114        this.drag_data_received.connect(on_drag_data_received);
115
116        this.drag_handle.get_style_context().add_class("geary-drag-handle");
117        this.drag_handle.show();
118
119        get_style_context().add_class("geary-draggable");
120    }
121
122
123    private void on_drag_begin(Gdk.DragContext context) {
124        // Draw a nice drag icon
125        Gtk.Allocation alloc = Gtk.Allocation();
126        this.get_allocation(out alloc);
127
128        Cairo.ImageSurface surface = new Cairo.ImageSurface(
129            Cairo.Format.ARGB32, alloc.width, alloc.height
130        );
131        Cairo.Context paint = new Cairo.Context(surface);
132
133
134        Gtk.StyleContext style = get_style_context();
135        style.add_class("geary-drag-icon");
136        draw(paint);
137        style.remove_class("geary-drag-icon");
138
139        int x, y;
140        this.drag_handle.translate_coordinates(this, 0, 0, out x, out y);
141        surface.set_device_offset(-x, -y);
142        Gtk.drag_set_icon_surface(context, surface);
143
144        // Set a visual hint that the row is being dragged
145        style.add_class("geary-drag-source");
146        this.drag_picked_up = true;
147    }
148
149    private void on_drag_end(Gdk.DragContext context) {
150        get_style_context().remove_class("geary-drag-source");
151        this.drag_picked_up = false;
152    }
153
154    private bool on_drag_motion(Gdk.DragContext context,
155                                int x, int y,
156                                uint time_) {
157        if (!this.drag_entered) {
158            this.drag_entered = true;
159
160            // Don't highlight the same row that was picked up
161            if (!this.drag_picked_up) {
162                Gtk.ListBox? parent = get_parent() as Gtk.ListBox;
163                if (parent != null) {
164                    parent.drag_highlight_row(this);
165                }
166            }
167        }
168
169        return true;
170    }
171
172    private void on_drag_leave(Gdk.DragContext context,
173                               uint time_) {
174        if (!this.drag_picked_up) {
175            Gtk.ListBox? parent = get_parent() as Gtk.ListBox;
176            if (parent != null) {
177                parent.drag_unhighlight_row();
178            }
179        }
180        this.drag_entered = false;
181    }
182
183    private void on_drag_data_get(Gdk.DragContext context,
184                                  Gtk.SelectionData selection_data,
185                                  uint info, uint time_) {
186        selection_data.set(
187            Gdk.Atom.intern_static_string(DND_ATOM), 8,
188            get_index().to_string().data
189        );
190    }
191
192    private void on_drag_data_received(Gdk.DragContext context,
193                                       int x, int y,
194                                       Gtk.SelectionData selection_data,
195                                       uint info, uint time_) {
196        int drag_index = int.parse((string) selection_data.get_data());
197        Gtk.ListBox? parent = this.get_parent() as Gtk.ListBox;
198        if (parent != null) {
199            EditorRow? drag_row = parent.get_row_at_index(drag_index) as EditorRow;
200            if (drag_row != null && drag_row != this) {
201                drag_row.dropped(this);
202            }
203        }
204    }
205
206}
207
208
209internal class Accounts.LabelledEditorRow<PaneType,V> : EditorRow<PaneType> {
210
211
212    public Gtk.Label label { get; private set; default = new Gtk.Label(""); }
213    public V value { get; private set; }
214
215
216    public LabelledEditorRow(string label, V value) {
217        this.label.halign = Gtk.Align.START;
218        this.label.valign = Gtk.Align.CENTER;
219        this.label.set_text(label);
220        this.label.show();
221        this.layout.add(this.label);
222
223        bool expand_label = true;
224        this.value = value;
225        Gtk.Widget? widget = value as Gtk.Widget;
226        if (widget != null) {
227            Gtk.Entry? entry = value as Gtk.Entry;
228            if (entry != null) {
229                expand_label = false;
230                entry.xalign = 1;
231                entry.hexpand = true;
232            }
233
234            widget.valign = Gtk.Align.CENTER;
235            widget.show();
236            this.layout.add(widget);
237        }
238
239        this.label.hexpand = expand_label;
240    }
241
242    public void set_dim_label(bool is_dim) {
243        if (is_dim) {
244            this.label.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL);
245        } else {
246            this.label.get_style_context().remove_class(Gtk.STYLE_CLASS_DIM_LABEL);
247        }
248    }
249
250}
251
252
253internal class Accounts.AddRow<PaneType> : EditorRow<PaneType> {
254
255
256    public AddRow() {
257        get_style_context().add_class("geary-add-row");
258        Gtk.Image add_icon = new Gtk.Image.from_icon_name(
259            "list-add-symbolic", Gtk.IconSize.BUTTON
260        );
261        add_icon.set_hexpand(true);
262        add_icon.show();
263
264        this.layout.add(add_icon);
265    }
266
267}
268
269
270internal class Accounts.ServiceProviderRow<PaneType> :
271    LabelledEditorRow<PaneType,Gtk.Label> {
272
273
274    public ServiceProviderRow(Geary.ServiceProvider provider,
275                              string other_type_label) {
276        string? label = null;
277        switch (provider) {
278        case Geary.ServiceProvider.GMAIL:
279            label = _("Gmail");
280            break;
281
282        case Geary.ServiceProvider.OUTLOOK:
283            label = _("Outlook.com");
284            break;
285
286        case Geary.ServiceProvider.YAHOO:
287            label = _("Yahoo");
288            break;
289
290        case Geary.ServiceProvider.OTHER:
291            label = other_type_label;
292            break;
293        }
294
295        base(
296            // Translators: Label describes the service provider
297            // hosting the email account, e.g. Gmail, Yahoo, or some
298            // other generic IMAP service.
299            _("Service provider"),
300            new Gtk.Label(label)
301        );
302
303        // Can't change this, so deactivate and dim out
304        set_activatable(false);
305        this.value.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL);
306    }
307
308}
309
310
311internal abstract class Accounts.AccountRow<PaneType,V> :
312    LabelledEditorRow<PaneType,V> {
313
314
315    internal Geary.AccountInformation account { get; private set; }
316
317
318    protected AccountRow(Geary.AccountInformation account, string label, V value) {
319        base(label, value);
320        this.account = account;
321        this.account.changed.connect(on_account_changed);
322
323        set_dim_label(true);
324    }
325
326    ~AccountRow() {
327        this.account.changed.disconnect(on_account_changed);
328    }
329
330    public abstract void update();
331
332    private void on_account_changed() {
333        update();
334    }
335
336}
337
338
339private abstract class Accounts.ServiceRow<PaneType,V> : AccountRow<PaneType,V> {
340
341
342    internal Geary.ServiceInformation service { get; private set; }
343
344    protected virtual bool is_value_editable {
345        get {
346            return (
347                this.account.service_provider == Geary.ServiceProvider.OTHER &&
348                !this.is_goa_account
349            );
350        }
351    }
352
353    // XXX convenience method until we get a better way of doing this.
354    protected bool is_goa_account {
355        get { return (this.account.mediator is GoaMediator); }
356    }
357
358
359    protected ServiceRow(Geary.AccountInformation account,
360                         Geary.ServiceInformation service,
361                         string label,
362                         V value) {
363        base(account, label, value);
364        this.service = service;
365        this.service.notify.connect_after(on_notify);
366
367        bool is_editable = this.is_value_editable;
368        set_activatable(is_editable);
369
370        Gtk.Widget? widget = value as Gtk.Widget;
371        if (widget != null && !is_editable) {
372            if (widget is Gtk.Label) {
373                widget.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL);
374            } else {
375                widget.set_sensitive(false);
376            }
377        }
378    }
379
380    ~ServiceRow() {
381        this.service.notify.disconnect(on_notify);
382    }
383
384    private void on_notify() {
385        update();
386    }
387
388}
389
390
391/** Interface for rows that use a validator for editable values. */
392internal interface Accounts.ValidatingRow : EditorRow {
393
394
395    /** The row's validator */
396    public abstract Components.Validator validator { get; protected set; }
397
398    /** Determines if the row's value has actually changed. */
399    public abstract bool has_changed { get; }
400
401    /** Fired when validated and the value has actually changed. */
402    public signal void changed();
403
404    /** Fired when validated and the value has actually changed. */
405    public signal void committed();
406
407    /**
408     * Hooks up signals to the validator.
409     *
410     * Implementing classes should call this in their constructor
411     * after having constructed a validator
412     */
413    protected void setup_validator() {
414        this.validator.changed.connect(on_validator_changed);
415        this.validator.activated.connect(on_validator_check_commit);
416        this.validator.focus_lost.connect(on_validator_check_commit);
417    }
418
419    /**
420     * Called when the row's value should be stored.
421     *
422     * This is only called when the row's value has changed, is
423     * valid, and the user has activated or changed to a different
424     * row.
425     */
426    protected virtual void commit() {
427        // noop
428    }
429
430    private void on_validator_changed() {
431        if (this.has_changed) {
432            changed();
433        }
434    }
435
436    private void on_validator_check_commit() {
437        if (this.has_changed) {
438            commit();
439            committed();
440        }
441    }
442
443}
444
445
446internal class Accounts.TlsComboBox : Gtk.ComboBox {
447
448    private const string INSECURE_ICON = "channel-insecure-symbolic";
449    private const string SECURE_ICON = "channel-secure-symbolic";
450
451
452    public string label { get; private set; default = ""; }
453
454
455    public Geary.TlsNegotiationMethod method {
456        get {
457            try {
458                return Geary.TlsNegotiationMethod.for_value(this.active_id);
459            } catch {
460                return Geary.TlsNegotiationMethod.TRANSPORT;
461            }
462        }
463        set {
464            this.active_id = value.to_value();
465        }
466    }
467
468
469    public TlsComboBox() {
470        // Translators: This label describes what form of transport
471        // security (TLS, StartTLS, etc) used by an account's IMAP or SMTP
472        // service.
473        this.label = _("Connection security");
474
475        Gtk.ListStore store = new Gtk.ListStore(
476            3, typeof(string), typeof(string), typeof(string)
477        );
478        Gtk.TreeIter iter;
479        store.append(out iter);
480        store.set(
481            iter,
482            0, Geary.TlsNegotiationMethod.NONE.to_value(),
483            1, INSECURE_ICON,
484            2, _("None")
485        );
486        store.append(out iter);
487        store.set(
488            iter,
489            0, Geary.TlsNegotiationMethod.START_TLS.to_value(),
490            1, SECURE_ICON,
491            2, _("StartTLS")
492        );
493        store.append(out iter);
494        store.set(
495            iter,
496            0, Geary.TlsNegotiationMethod.TRANSPORT.to_value(),
497            1, SECURE_ICON,
498            2, _("TLS")
499        );
500
501        this.model = store;
502        set_id_column(0);
503
504        Gtk.CellRendererText text_renderer = new Gtk.CellRendererText();
505        pack_start(text_renderer, true);
506        add_attribute(text_renderer, "text", 2);
507
508        Gtk.CellRendererPixbuf icon_renderer = new Gtk.CellRendererPixbuf();
509        pack_start(icon_renderer, true);
510        add_attribute(icon_renderer, "icon_name", 1);
511    }
512
513}
514
515
516internal class Accounts.OutgoingAuthComboBox : Gtk.ComboBoxText {
517
518
519    public string label { get; private set; }
520
521    public Geary.Credentials.Requirement source {
522        get {
523            try {
524                return Geary.Credentials.Requirement.for_value(this.active_id);
525            } catch {
526                return Geary.Credentials.Requirement.USE_INCOMING;
527            }
528        }
529        set {
530            this.active_id = value.to_value();
531        }
532    }
533
534
535    public OutgoingAuthComboBox() {
536        // Translators: Label for source of SMTP authentication
537        // credentials (none, use IMAP, custom) when adding a new
538        // account
539        this.label = _("Login");
540
541        append(
542            Geary.Credentials.Requirement.NONE.to_value(),
543            // Translators: ComboBox value for source of SMTP
544            // authentication credentials (none) when adding a new
545            // account
546            _("No login needed")
547        );
548
549        append(
550            Geary.Credentials.Requirement.USE_INCOMING.to_value(),
551            // Translators: ComboBox value for source of SMTP
552            // authentication credentials (use IMAP) when adding a new
553            // account
554            _("Use same login as receiving")
555        );
556
557        append(
558            Geary.Credentials.Requirement.CUSTOM.to_value(),
559            // Translators: ComboBox value for source of SMTP
560            // authentication credentials (custom) when adding a new
561            // account
562            _("Use a different login")
563        );
564    }
565
566}
567
568
569/**
570 * Displaying and manages validation of popover-based forms.
571 */
572internal class Accounts.EditorPopover : Gtk.Popover {
573
574
575    internal Gtk.Grid layout {
576        get; private set; default = new Gtk.Grid();
577    }
578
579    protected Gtk.Widget popup_focus = null;
580
581
582    public EditorPopover() {
583        get_style_context().add_class("geary-editor");
584
585        this.layout.orientation = Gtk.Orientation.VERTICAL;
586        this.layout.set_row_spacing(6);
587        this.layout.set_column_spacing(12);
588        this.layout.show();
589        add(this.layout);
590
591        this.closed.connect_after(on_closed);
592    }
593
594    ~EditorPopover() {
595        this.closed.disconnect(on_closed);
596    }
597
598    /** {@inheritDoc} */
599    public new void popup() {
600        // Work-around GTK+ issue #1138
601        Gtk.Widget target = get_relative_to();
602
603        Gtk.Allocation content_area;
604        target.get_allocation(out content_area);
605
606        Gtk.StyleContext style = target.get_style_context();
607        Gtk.StateFlags flags = style.get_state();
608        Gtk.Border margin = style.get_margin(flags);
609
610        content_area.x = margin.left;
611        content_area.y =  margin.bottom;
612        content_area.width -= (content_area.x + margin.right);
613        content_area.height -= (content_area.y + margin.top);
614
615        set_pointing_to(content_area);
616
617        base.popup();
618
619        if (this.popup_focus != null) {
620            this.popup_focus.grab_focus();
621        }
622    }
623
624    public void add_labelled_row(string label, Gtk.Widget value) {
625        Gtk.Label label_widget = new Gtk.Label(label);
626        label_widget.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL);
627        label_widget.halign = Gtk.Align.END;
628        label_widget.show();
629
630        this.layout.add(label_widget);
631        this.layout.attach_next_to(value, label_widget, Gtk.PositionType.RIGHT);
632    }
633
634    private void on_closed() {
635        destroy();
636    }
637
638}
639