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