1/*
2 * Copyright 2017 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
9/**
10 * A popover for editing a link in the composer.
11 *
12 * The exact appearance of the popover will depend on the {@link
13 * Type} passed to the constructor:
14 *
15 *  * For {@link Type.NEW_LINK}, the user will be presented with an
16 *    insert button and an open button.
17 *  * For {@link Type.EXISTING_LINK}, the user will be presented with
18 *    an update, delete and open buttons.
19 */
20[GtkTemplate (ui = "/org/gnome/Geary/composer-link-popover.ui")]
21public class Composer.LinkPopover : Gtk.Popover {
22
23    private const string[] HTTP_SCHEMES = { "http", "https" };
24    private const string[] OTHER_SCHEMES = {
25        "aim", "apt", "bitcoin", "cvs", "ed2k", "ftp", "file", "finger",
26        "git", "gtalk", "irc", "ircs", "irc6", "lastfm", "ldap", "ldaps",
27        "magnet", "news", "nntp", "rsync", "sftp", "skype", "smb", "sms",
28        "svn", "telnet", "tftp", "ssh", "webcal", "xmpp"
29    };
30
31    /** Determines which version of the UI is presented to the user. */
32    public enum Type {
33        /** A new link is being created. */
34        NEW_LINK,
35
36        /** An existing link is being edited. */
37        EXISTING_LINK,
38    }
39
40    /** The URL displayed in the popover */
41    public string link_uri { get { return this.url.get_text(); } }
42
43    [GtkChild] private unowned Gtk.Entry url;
44
45    [GtkChild] private unowned Gtk.Button insert;
46
47    [GtkChild] private unowned Gtk.Button update;
48
49    [GtkChild] private new unowned Gtk.Button remove;
50
51    private Geary.TimeoutManager validation_timeout;
52
53
54    /** Emitted when the link URL has changed. */
55    public signal void link_changed(Soup.URI? uri, bool is_valid);
56
57    /** Emitted when the link URL was activated. */
58    public signal void link_activate();
59
60    /** Emitted when the delete button was activated. */
61    public signal void link_delete();
62
63
64    public LinkPopover(Type type) {
65        set_default_widget(this.url);
66        set_focus_child(this.url);
67        switch (type) {
68        case Type.NEW_LINK:
69            this.update.hide();
70            this.remove.hide();
71            break;
72        case Type.EXISTING_LINK:
73            this.insert.hide();
74            break;
75        }
76        this.validation_timeout = new Geary.TimeoutManager.milliseconds(
77            150, () => { validate(); }
78        );
79    }
80
81    public override void show() {
82        base.show();
83        this.url.grab_focus();
84    }
85
86    public override void destroy() {
87        this.validation_timeout.reset();
88        base.destroy();
89    }
90
91    public void set_link_url(string url) {
92        this.url.set_text(url);
93        this.validation_timeout.reset(); // Don't update on manual set
94    }
95
96    private void validate() {
97        string? text = this.url.get_text().strip();
98        bool is_empty = Geary.String.is_empty(text);
99        bool is_valid = false;
100        bool is_nominal = false;
101        bool is_mailto = false;
102        Soup.URI? url = null;
103        if (!is_empty) {
104            url = new Soup.URI(text);
105            if (url != null) {
106                is_valid = true;
107
108                string? scheme = url.get_scheme();
109                string? path = url.get_path();
110                if (scheme in HTTP_SCHEMES) {
111                    is_nominal = Geary.Inet.is_valid_display_host(url.get_host());
112                } else if (scheme == "mailto") {
113                    is_mailto = true;
114                    is_nominal = (
115                        !Geary.String.is_empty(path) &&
116                        Geary.RFC822.MailboxAddress.is_valid_address(path)
117                    );
118                } else if (scheme in OTHER_SCHEMES) {
119                    is_nominal = !Geary.String.is_empty(path);
120                }
121            } else if (text == "http:/" || text == "https:/") {
122                // Don't let the URL entry switch to invalid and back
123                // between "http:" and "http://"
124                is_valid = true;
125            }
126        }
127
128        Gtk.StyleContext style = this.url.get_style_context();
129        Gtk.EntryIconPosition pos = Gtk.EntryIconPosition.SECONDARY;
130        if (!is_valid) {
131            style.add_class(Gtk.STYLE_CLASS_ERROR);
132            style.remove_class(Gtk.STYLE_CLASS_WARNING);
133            this.url.set_icon_from_icon_name(pos, "dialog-error-symbolic");
134            this.url.set_tooltip_text(
135                _("Link URL is not correctly formatted, e.g. http://example.com")
136            );
137        } else if (!is_nominal) {
138            style.remove_class(Gtk.STYLE_CLASS_ERROR);
139            style.add_class(Gtk.STYLE_CLASS_WARNING);
140            this.url.set_icon_from_icon_name(pos, "dialog-warning-symbolic");
141            this.url.set_tooltip_text(
142                !is_mailto ? _("Invalid link URL") : _("Invalid email address")
143            );
144        } else {
145            style.remove_class(Gtk.STYLE_CLASS_ERROR);
146            style.remove_class(Gtk.STYLE_CLASS_WARNING);
147            this.url.set_icon_from_icon_name(pos, null);
148            this.url.set_tooltip_text("");
149        }
150
151        link_changed(url, is_valid && is_nominal);
152    }
153
154    [GtkCallback]
155    private void on_url_changed() {
156        this.validation_timeout.start();
157    }
158
159    [GtkCallback]
160    private void on_activate_popover() {
161        link_activate();
162        popdown();
163    }
164
165    [GtkCallback]
166    private void on_remove_clicked() {
167        link_delete();
168        popdown();
169    }
170}
171