[geary/wip/712895-sigs: 2/5] First commit from Gustavo Rubio for email sigs
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/wip/712895-sigs: 2/5] First commit from Gustavo Rubio for email sigs
- Date: Thu, 29 May 2014 22:41:53 +0000 (UTC)
commit bf99080217bb9491c92a197a6b486bedef770051
Merge: 80a7c0d 7246c5f
Author: Jim Nelson <jim yorba org>
Date: Thu May 29 15:09:43 2014 -0700
First commit from Gustavo Rubio for email sigs
Conflicts:
src/client/accounts/add-edit-page.vala
src/client/composer/composer-window.vala
src/engine/api/geary-account-information.vala
ui/login.glade
Due to significant changes from inline composer feature, needed
to re-merge this commit from separate branch. Small changes made
to accomodate other changes as well, including the Accounts dialog
being updated visually and the "save drafts?" feature.
src/client/accounts/add-edit-page.vala | 41 ++++++++++++++
src/client/composer/composer-widget.vala | 14 +++++
src/engine/api/geary-account-information.vala | 20 +++++++
ui/login.glade | 71 +++++++++++++++++++------
4 files changed, 130 insertions(+), 16 deletions(-)
---
diff --cc src/client/accounts/add-edit-page.vala
index c59f274,cf7e09b..5b08a14
--- a/src/client/accounts/add-edit-page.vala
+++ b/src/client/accounts/add-edit-page.vala
@@@ -49,7 -49,21 +49,21 @@@ public class AddEditPage : Gtk.Box
get { return check_remember_password.active; }
set { check_remember_password.active = value; }
}
+
+ public bool use_email_signature {
+ get { return check_use_email_signature.active; }
+ set { check_use_email_signature.active = value;}
+ }
+ public string email_signature {
+ owned get {
+ return textview_email_signature.buffer.text;
+ }
+ set {
- textview_email_signature.buffer.text = value;
++ textview_email_signature.buffer.text = value ?? "";
+ }
+ }
+
public bool save_sent_mail {
get { return check_save_sent_mail.active; }
set { check_save_sent_mail.active = value; }
@@@ -155,6 -164,10 +169,10 @@@
private Gtk.ComboBoxText combo_service;
private Gtk.CheckButton check_remember_password;
private Gtk.CheckButton check_save_sent_mail;
+
- //Signature
++ // Signature
+ private Gtk.CheckButton check_use_email_signature;
+ private Gtk.TextView textview_email_signature;
private Gtk.Alignment other_info;
@@@ -322,7 -335,8 +344,9 @@@
info.default_smtp_use_imap_credentials,
info.default_smtp_server_noauth,
info.prefetch_period_days,
+ info.save_drafts,
+ info.use_email_signature,
+ info.email_signature,
result);
}
@@@ -350,7 -364,8 +374,9 @@@
bool initial_default_smtp_use_imap_credentials = false,
bool initial_default_smtp_noauth = false,
int prefetch_period_days = Geary.AccountInformation.DEFAULT_PREFETCH_PERIOD_DAYS,
+ bool initial_save_drafts = true,
+ bool initial_use_email_signature = false,
+ string? initial_email_signature = null,
Geary.Engine.ValidationResult result = Geary.Engine.ValidationResult.OK) {
// Set defaults
@@@ -607,7 -633,8 +646,9 @@@
account_information.default_smtp_use_imap_credentials = smtp_use_imap_credentials;
account_information.default_smtp_server_noauth = smtp_noauth;
account_information.prefetch_period_days = get_storage_length();
+ account_information.save_drafts = save_drafts;
+ account_information.use_email_signature = use_email_signature;
+ account_information.email_signature = email_signature;
if (smtp_noauth)
account_information.smtp_credentials = null;
diff --cc src/client/composer/composer-widget.vala
index 593335e,0000000..15b92d0
mode 100644,000000..100644
--- a/src/client/composer/composer-widget.vala
+++ b/src/client/composer/composer-widget.vala
@@@ -1,1926 -1,0 +1,1940 @@@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+// Widget for sending messages.
+public class ComposerWidget : Gtk.EventBox {
+ public enum ComposeType {
+ NEW_MESSAGE,
+ REPLY,
+ REPLY_ALL,
+ FORWARD
+ }
+
+ public enum CloseStatus {
+ DO_CLOSE,
+ PENDING_CLOSE,
+ CANCEL_CLOSE
+ }
+
+ public enum ComposerState {
+ DETACHED,
+ INLINE_NEW,
+ INLINE,
+ INLINE_COMPACT
+ }
+
+ public const string ACTION_UNDO = "undo";
+ public const string ACTION_REDO = "redo";
+ public const string ACTION_CUT = "cut";
+ public const string ACTION_COPY = "copy";
+ public const string ACTION_COPY_LINK = "copy link";
+ public const string ACTION_PASTE = "paste";
+ public const string ACTION_PASTE_FORMAT = "paste with formatting";
+ public const string ACTION_BOLD = "bold";
+ public const string ACTION_ITALIC = "italic";
+ public const string ACTION_UNDERLINE = "underline";
+ public const string ACTION_STRIKETHROUGH = "strikethrough";
+ public const string ACTION_REMOVE_FORMAT = "removeformat";
+ public const string ACTION_INDENT = "indent";
+ public const string ACTION_OUTDENT = "outdent";
+ public const string ACTION_JUSTIFY_LEFT = "justifyleft";
+ public const string ACTION_JUSTIFY_RIGHT = "justifyright";
+ public const string ACTION_JUSTIFY_CENTER = "justifycenter";
+ public const string ACTION_JUSTIFY_FULL = "justifyfull";
+ public const string ACTION_MENU = "menu";
+ public const string ACTION_COLOR = "color";
+ public const string ACTION_INSERT_LINK = "insertlink";
+ public const string ACTION_COMPOSE_AS_HTML = "compose as html";
+ public const string ACTION_CLOSE = "close";
+
+ private const string DRAFT_SAVED_TEXT = _("Saved");
+ private const string DRAFT_SAVING_TEXT = _("Saving");
+ private const string DRAFT_ERROR_TEXT = _("Error saving");
+
+ private const string URI_LIST_MIME_TYPE = "text/uri-list";
+ private const string FILE_URI_PREFIX = "file://";
+ private const string BODY_ID = "message-body";
+ private const string HTML_BODY = """
+ <html><head><title></title>
+ <style>
+ body {
+ margin: 10px !important;
+ padding: 0 !important;
+ background-color: white !important;
+ font-size: medium !important;
+ }
+ body.plain, body.plain * {
+ font-family: monospace !important;
+ font-weight: normal;
+ font-style: normal;
+ font-size: 10pt;
+ color: black;
+ text-decoration: none;
+ }
+ body.plain a {
+ cursor: text;
+ }
+ blockquote {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ margin-left: 10px;
+ margin-right: 10px;
+ padding-left: 5px;
+ padding-right: 5px;
+ background-color: white;
+ border: 0;
+ border-left: 3px #aaa solid;
+ }
+ pre {
+ white-space: pre-wrap;
+ margin: 0;
+ }
+ </style>
+ </head><body id="message-body"></body></html>""";
+
+ private const int DRAFT_TIMEOUT_MSEC = 2000; // 2 seconds
+
+ public const string ATTACHMENT_KEYWORDS_GENERIC = ".doc|.pdf|.xls|.ppt|.rtf|.pps";
+ /// A list of keywords, separated by pipe ("|") characters, that suggest an attachment
+ public const string ATTACHMENT_KEYWORDS_LOCALIZED = _("attach|enclosed|enclosing|cover letter");
+
+ public Geary.Account account { get; private set; }
+
+ public string from { get; set; }
+
+ public string to {
+ get { return to_entry.get_text(); }
+ set { to_entry.set_text(value); }
+ }
+
+ public string cc {
+ get { return cc_entry.get_text(); }
+ set { cc_entry.set_text(value); }
+ }
+
+ public string bcc {
+ get { return bcc_entry.get_text(); }
+ set { bcc_entry.set_text(value); }
+ }
+
+ public string in_reply_to { get; set; }
+ public string references { get; set; }
+
+ public string subject {
+ get { return subject_entry.get_text(); }
+ set { subject_entry.set_text(value); }
+ }
+
+ public string message {
+ owned get { return get_html(); }
+ set {
+ body_html = value;
+ editor.load_string(HTML_BODY, "text/html", "UTF8", "");
+ }
+ }
+
+ public bool compose_as_html {
+ get { return ((Gtk.ToggleAction) actions.get_action(ACTION_COMPOSE_AS_HTML)).active; }
+ set { ((Gtk.ToggleAction) actions.get_action(ACTION_COMPOSE_AS_HTML)).active = value; }
+ }
+
+ public ComposerState state { get; set; }
+
+ public ComposeType compose_type { get; private set; default = ComposeType.NEW_MESSAGE; }
+
+ public Geary.EmailIdentifier? referred_id { get; private set; default = null; }
+
+ public bool blank {
+ get {
+ return to_entry.empty && cc_entry.empty && bcc_entry.empty &&
+ subject_entry.buffer.length == 0 && !editor.can_undo() && attachment_files.size == 0;
+ }
+ }
+
+ private ContactListStore? contact_list_store = null;
+
+ private string? body_html = null;
+ private Gee.Set<File> attachment_files = new Gee.HashSet<File>(Geary.Files.nullable_hash,
+ Geary.Files.nullable_equal);
+
+ private Gtk.Builder builder;
+ private Gtk.Label from_label;
+ private Gtk.Label from_single;
+ private Gtk.ComboBoxText from_multiple = new Gtk.ComboBoxText();
+ private EmailEntry to_entry;
+ private EmailEntry cc_entry;
+ private EmailEntry bcc_entry;
+ public Gtk.Entry subject_entry;
+ private Gtk.Button close_button;
+ private Gtk.Button send_button;
+ private Gtk.Button detach_button;
+ private Gtk.Label message_overlay_label;
+ private WebKit.DOM.Element? prev_selected_link = null;
+ private Gtk.Separator attachments_separator;
+ private Gtk.Box attachments_box;
+ private Gtk.Button add_attachment_button;
+ private Gtk.Button pending_attachments_button;
+ private Gtk.Alignment hidden_on_attachment_drag_over;
+ private Gtk.Alignment visible_on_attachment_drag_over;
+ private Gtk.Widget hidden_on_attachment_drag_over_child;
+ private Gtk.Widget visible_on_attachment_drag_over_child;
+ private Gtk.Label compact_header_label;
+ private Gtk.Label draft_save_label;
+
+ private Gtk.Menu menu = new Gtk.Menu();
+ private Gtk.RadioMenuItem font_small;
+ private Gtk.RadioMenuItem font_medium;
+ private Gtk.RadioMenuItem font_large;
+ private Gtk.RadioMenuItem font_sans;
+ private Gtk.RadioMenuItem font_serif;
+ private Gtk.RadioMenuItem font_monospace;
+ private Gtk.MenuItem color_item;
+ private Gtk.MenuItem html_item;
+ private Gtk.MenuItem html_item2;
+
+ private Gtk.ActionGroup actions;
+ private string? hover_url = null;
+ private bool action_flag = false;
+ private bool is_attachment_overlay_visible = false;
+ private Gee.List<Geary.Attachment>? pending_attachments = null;
+ private string reply_to_addresses = "";
+ private string reply_cc_addresses = "";
+ private string reply_subject = "";
+ private string forward_subject = "";
+ private string reply_message_id = "";
+
+ private Geary.FolderSupport.Create? drafts_folder = null;
+ private Geary.EmailIdentifier? draft_id = null;
+ private uint draft_save_timeout_id = 0;
+ private Cancellable cancellable_drafts = new Cancellable();
+ private Cancellable cancellable_save_draft = new Cancellable();
+ private bool in_draft_save = false;
+
+ public WebKit.WebView editor;
+ // We need to keep a reference to the edit-fixer in composer-window, so it doesn't get
+ // garbage-collected.
+ private WebViewEditFixer edit_fixer;
+ public Gtk.UIManager ui;
+ private ComposerContainer container {
+ get { return (ComposerContainer) parent; }
+ }
+
+ public ComposerWidget(Geary.Account account, ComposeType compose_type,
+ Geary.Email? referred = null, bool is_referred_draft = false) {
+ this.account = account;
+ this.compose_type = compose_type;
+ if (compose_type == ComposeType.NEW_MESSAGE)
+ state = ComposerState.INLINE_NEW;
+ else if (compose_type == ComposeType.FORWARD)
+ state = ComposerState.INLINE;
+ else
+ state = ComposerState.INLINE_COMPACT;
+
+ setup_drag_destination(this);
+
+ add_events(Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK);
+ builder = GearyApplication.instance.create_builder("composer.glade");
+
+ // Add the content-view style class for the elementary GTK theme.
+ Gtk.Box button_area = (Gtk.Box) builder.get_object("button_area");
+ button_area.get_style_context().add_class("content-view");
+
+ Gtk.Box box = builder.get_object("composer") as Gtk.Box;
+ close_button = builder.get_object("Close") as Gtk.Button;
+ close_button.clicked.connect(on_close);
+ send_button = builder.get_object("Send") as Gtk.Button;
+ send_button.clicked.connect(on_send);
+ detach_button = builder.get_object("Detach") as Gtk.Button;
+ detach_button.clicked.connect(on_detach);
+ bind_property("state", detach_button, "visible", BindingFlags.SYNC_CREATE,
+ (binding, source_value, ref target_value) => {
+ target_value = (state != ComposerState.DETACHED);
+ return true;
+ });
+ add_attachment_button = builder.get_object("add_attachment_button") as Gtk.Button;
+ add_attachment_button.clicked.connect(on_add_attachment_button_clicked);
+ pending_attachments_button = builder.get_object("add_pending_attachments") as Gtk.Button;
+ pending_attachments_button.clicked.connect(on_pending_attachments_button_clicked);
+ attachments_separator = builder.get_object("separator") as Gtk.Separator;
+ attachments_box = builder.get_object("attachments_box") as Gtk.Box;
+ hidden_on_attachment_drag_over = (Gtk.Alignment)
builder.get_object("hidden_on_attachment_drag_over");
+ hidden_on_attachment_drag_over_child = (Gtk.Widget)
builder.get_object("hidden_on_attachment_drag_over_child");
+ visible_on_attachment_drag_over = (Gtk.Alignment)
builder.get_object("visible_on_attachment_drag_over");
+ visible_on_attachment_drag_over_child = (Gtk.Widget)
builder.get_object("visible_on_attachment_drag_over_child");
+ visible_on_attachment_drag_over.remove(visible_on_attachment_drag_over_child);
+
+ Gtk.Widget recipients = builder.get_object("recipients") as Gtk.Widget;
+ bind_property("state", recipients, "visible", BindingFlags.SYNC_CREATE,
+ (binding, source_value, ref target_value) => {
+ target_value = (state != ComposerState.INLINE_COMPACT);
+ return true;
+ });
+ Gtk.Widget compact_header = builder.get_object("compact_recipients") as Gtk.Widget;
+ bind_property("state", compact_header, "visible", BindingFlags.SYNC_CREATE,
+ (binding, source_value, ref target_value) => {
+ target_value = (state == ComposerState.INLINE_COMPACT);
+ return true;
+ });
+ string[] subject_elements = {"subject label", "subject"};
+ foreach (string name in subject_elements) {
+ Gtk.Widget widget = builder.get_object(name) as Gtk.Widget;
+ bind_property("state", widget, "visible", BindingFlags.SYNC_CREATE,
+ (binding, source_value, ref target_value) => {
+ target_value = (state != ComposerState.INLINE);
+ return true;
+ });
+ }
+ notify["state"].connect((s, p) => { update_from_field(); });
+ compact_header_label = builder.get_object("compact_recipients_label") as Gtk.Label;
+ Gtk.Button expand_button = builder.get_object("expand_button") as Gtk.Button;
+ expand_button.clicked.connect(() => { state = ComposerState.INLINE; });
+ // Set the visibilities later, after show_all is called on the widget.
+ Idle.add(() => {
+ state = state; // Triggers visibilities
+ show_attachments();
+ return false;
+ });
+
+ from_label = (Gtk.Label) builder.get_object("from label");
+ from_single = (Gtk.Label) builder.get_object("from_single");
+ from_multiple = (Gtk.ComboBoxText) builder.get_object("from_multiple");
+ to_entry = new EmailEntry(this);
+ (builder.get_object("to") as Gtk.EventBox).add(to_entry);
+ cc_entry = new EmailEntry(this);
+ (builder.get_object("cc") as Gtk.EventBox).add(cc_entry);
+ bcc_entry = new EmailEntry(this);
+ (builder.get_object("bcc") as Gtk.EventBox).add(bcc_entry);
+
+ Gtk.Label to_label = (Gtk.Label) builder.get_object("to label");
+ Gtk.Label cc_label = (Gtk.Label) builder.get_object("cc label");
+ Gtk.Label bcc_label = (Gtk.Label) builder.get_object("bcc label");
+ to_label.set_mnemonic_widget(to_entry);
+ cc_label.set_mnemonic_widget(cc_entry);
+ bcc_label.set_mnemonic_widget(bcc_entry);
+
+ // TODO: It would be nicer to set the completions inside the EmailEntry constructor. But in
+ // testing, this can cause non-deterministic segfaults. Investigate why, and fix if possible.
+ set_entry_completions();
+ subject_entry = builder.get_object("subject") as Gtk.Entry;
+ Gtk.Alignment message_area = builder.get_object("message area") as Gtk.Alignment;
+ draft_save_label = (Gtk.Label) builder.get_object("draft_save_label");
+ draft_save_label.get_style_context().add_class("dim-label");
+ actions = builder.get_object("compose actions") as Gtk.ActionGroup;
+ // Can only happen after actions exits
+ compose_as_html = GearyApplication.instance.config.compose_as_html;
+
+ // Listen to account signals to update from menu.
+ Geary.Engine.instance.account_available.connect(update_from_field);
+ Geary.Engine.instance.account_unavailable.connect(update_from_field);
+
+ Gtk.ScrolledWindow scroll = new Gtk.ScrolledWindow(null, null);
+ scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
+
+ Gtk.Overlay message_overlay = new Gtk.Overlay();
+ message_overlay.add(scroll);
+ message_area.add(message_overlay);
+
+ message_overlay_label = new Gtk.Label(null);
+ message_overlay_label.ellipsize = Pango.EllipsizeMode.MIDDLE;
+ message_overlay_label.halign = Gtk.Align.START;
+ message_overlay_label.valign = Gtk.Align.END;
+ message_overlay_label.realize.connect(on_message_overlay_label_realize);
+ message_overlay.add_overlay(message_overlay_label);
+
+ subject_entry.changed.connect(on_subject_changed);
+ to_entry.changed.connect(validate_send_button);
+ cc_entry.changed.connect(validate_send_button);
+ bcc_entry.changed.connect(validate_send_button);
+
+ if (get_direction () == Gtk.TextDirection.RTL) {
+ actions.get_action(ACTION_INDENT).icon_name = "format-indent-more-rtl-symbolic";
+ actions.get_action(ACTION_OUTDENT).icon_name = "format-indent-less-rtl-symbolic";
+ } else {
+ actions.get_action(ACTION_INDENT).icon_name = "format-indent-more-symbolic";
+ actions.get_action(ACTION_OUTDENT).icon_name = "format-indent-less-symbolic";
+ }
+
+ ComposerToolbar composer_toolbar = new ComposerToolbar(actions, menu);
+ Gtk.Alignment toolbar_area = (Gtk.Alignment) builder.get_object("toolbar area");
+ toolbar_area.add(composer_toolbar);
+
+ actions.get_action(ACTION_UNDO).activate.connect(on_action);
+ actions.get_action(ACTION_REDO).activate.connect(on_action);
+
+ actions.get_action(ACTION_CUT).activate.connect(on_cut);
+ actions.get_action(ACTION_COPY).activate.connect(on_copy);
+ actions.get_action(ACTION_COPY_LINK).activate.connect(on_copy_link);
+ actions.get_action(ACTION_PASTE).activate.connect(on_paste);
+ actions.get_action(ACTION_PASTE_FORMAT).activate.connect(on_paste_with_formatting);
+
+ actions.get_action(ACTION_BOLD).activate.connect(on_formatting_action);
+ actions.get_action(ACTION_ITALIC).activate.connect(on_formatting_action);
+ actions.get_action(ACTION_UNDERLINE).activate.connect(on_formatting_action);
+ actions.get_action(ACTION_STRIKETHROUGH).activate.connect(on_formatting_action);
+
+ actions.get_action(ACTION_REMOVE_FORMAT).activate.connect(on_remove_format);
+ actions.get_action(ACTION_COMPOSE_AS_HTML).activate.connect(on_compose_as_html);
+
+ actions.get_action(ACTION_INDENT).activate.connect(on_indent);
+ actions.get_action(ACTION_OUTDENT).activate.connect(on_action);
+
+ actions.get_action(ACTION_JUSTIFY_LEFT).activate.connect(on_formatting_action);
+ actions.get_action(ACTION_JUSTIFY_RIGHT).activate.connect(on_formatting_action);
+ actions.get_action(ACTION_JUSTIFY_CENTER).activate.connect(on_formatting_action);
+ actions.get_action(ACTION_JUSTIFY_FULL).activate.connect(on_formatting_action);
+
+ actions.get_action(ACTION_COLOR).activate.connect(on_select_color);
+ actions.get_action(ACTION_INSERT_LINK).activate.connect(on_insert_link);
+
+ actions.get_action(ACTION_CLOSE).activate.connect(on_close);
+
+ ui = new Gtk.UIManager();
+ ui.insert_action_group(actions, 0);
+ GearyApplication.instance.load_ui_file_for_manager(ui, "composer_accelerators.ui");
+
+ add_extra_accelerators();
+
+ from = account.information.get_from().to_rfc822_string();
+ update_from_field();
+ from_multiple.changed.connect(on_from_changed);
+
+ if (referred != null) {
+ this.referred_id = referred.id;
+ string? sender_address = account.information.get_mailbox_address().address;
+ reply_to_addresses = Geary.RFC822.Utils.create_to_addresses_for_reply(referred, sender_address);
+ reply_cc_addresses = Geary.RFC822.Utils.create_cc_addresses_for_reply_all(referred,
sender_address);
+ reply_subject = Geary.RFC822.Utils.create_subject_for_reply(referred);
+ forward_subject = Geary.RFC822.Utils.create_subject_for_forward(referred);
+ reply_message_id = referred.message_id.value;
+ switch (compose_type) {
+ case ComposeType.NEW_MESSAGE:
+ if (referred.to != null)
+ to = referred.to.to_rfc822_string();
+ if (referred.cc != null)
+ cc = referred.cc.to_rfc822_string();
+ if (referred.bcc != null)
+ bcc = referred.bcc.to_rfc822_string();
+ if (referred.in_reply_to != null)
+ in_reply_to = referred.in_reply_to.value;
+ if (referred.references != null)
+ references = referred.references.to_rfc822_string();
+ if (referred.subject != null)
+ subject = referred.subject.value;
+ try {
+ body_html = referred.get_message().get_body(true);
+ } catch (Error error) {
+ debug("Error getting message body: %s", error.message);
+ }
+
+ if (is_referred_draft)
+ draft_id = referred.id;
+
+ add_attachments(referred.attachments);
+ break;
+
+ case ComposeType.REPLY:
+ case ComposeType.REPLY_ALL:
+ to = reply_to_addresses;
+ if (compose_type == ComposeType.REPLY_ALL)
+ cc = reply_cc_addresses;
+ to_entry.modified = cc_entry.modified = false;
+ subject = reply_subject;
+ in_reply_to = reply_message_id;
+ references = Geary.RFC822.Utils.reply_references(referred);
+ body_html = "\n\n" + Geary.RFC822.Utils.quote_email_for_reply(referred, true);
+ pending_attachments = referred.attachments;
+ break;
+
+ case ComposeType.FORWARD:
+ subject = forward_subject;
+ body_html = "\n\n" + Geary.RFC822.Utils.quote_email_for_forward(referred, true);
+ add_attachments(referred.attachments);
+ pending_attachments = referred.attachments;
+ break;
+ }
+ }
+
++ // only add signature if the option is actually set
++ if (account.information.use_email_signature)
++ add_signature();
++
+ editor = new WebKit.WebView();
+ edit_fixer = new WebViewEditFixer(editor);
+
+ editor.editable = true;
+ editor.load_finished.connect(on_load_finished);
+ editor.hovering_over_link.connect(on_hovering_over_link);
+ editor.context_menu.connect(on_context_menu);
+ editor.move_focus.connect(update_actions);
+ editor.copy_clipboard.connect(update_actions);
+ editor.cut_clipboard.connect(update_actions);
+ editor.paste_clipboard.connect(update_actions);
+ editor.undo.connect(update_actions);
+ editor.redo.connect(update_actions);
+ editor.selection_changed.connect(update_actions);
+ editor.key_press_event.connect(on_editor_key_press);
+ editor.user_changed_contents.connect(reset_draft_timer);
+
+ // only do this after setting body_html
+ editor.load_string(HTML_BODY, "text/html", "UTF8", "");
+
+ editor.navigation_policy_decision_requested.connect(on_navigation_policy_decision_requested);
+ editor.new_window_policy_decision_requested.connect(on_navigation_policy_decision_requested);
+
+ GearyApplication.instance.config.settings.changed[Configuration.SPELL_CHECK_KEY].connect(
+ on_spell_check_changed);
+
+ // Font family menu items.
+ font_sans = new Gtk.RadioMenuItem(new SList<Gtk.RadioMenuItem>());
+ font_sans.activate.connect(on_font_sans);
+ font_sans.related_action = ui.get_action("ui/font_sans");
+ font_serif = new Gtk.RadioMenuItem.from_widget(font_sans);
+ font_serif.activate.connect(on_font_serif);
+ font_serif.related_action = ui.get_action("ui/font_serif");
+ font_monospace = new Gtk.RadioMenuItem.from_widget(font_sans);
+ font_monospace.related_action = ui.get_action("ui/font_monospace");
+ font_monospace.activate.connect(on_font_monospace);
+
+ // Font size menu items.
+ font_small = new Gtk.RadioMenuItem(new SList<Gtk.RadioMenuItem>());
+ font_small.related_action = ui.get_action("ui/font_small");
+ font_small.activate.connect(on_font_size_small);
+ font_medium = new Gtk.RadioMenuItem.from_widget(font_small);
+ font_medium.related_action = ui.get_action("ui/font_medium");
+ font_medium.activate.connect(on_font_size_medium);
+ font_large = new Gtk.RadioMenuItem.from_widget(font_small);
+ font_large.related_action = ui.get_action("ui/font_large");
+ font_large.activate.connect(on_font_size_large);
+
+ color_item = new Gtk.MenuItem();
+ color_item.related_action = ui.get_action("ui/color");
+ html_item = new Gtk.CheckMenuItem();
+ html_item.related_action = ui.get_action("ui/htmlcompose");
+
+ html_item2 = new Gtk.CheckMenuItem();
+ html_item2.related_action = ui.get_action("ui/htmlcompose");
+
+ WebKit.WebSettings s = new WebKit.WebSettings();
+ s.enable_spell_checking = GearyApplication.instance.config.spell_check;
+ s.auto_load_images = false;
+ s.enable_scripts = false;
+ s.enable_java_applet = false;
+ s.enable_plugins = false;
+ editor.settings = s;
+
+ scroll.add(editor);
+ scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
+
+ add(box);
+ validate_send_button();
+
+ check_pending_attachments();
+
+ // Place the message area before the compose toolbar in the focus chain, so that
+ // the user can tab directly from the Subject: field to the message area.
+ List<Gtk.Widget> chain = new List<Gtk.Widget>();
+ chain.append(hidden_on_attachment_drag_over);
+ chain.append(message_area);
+ chain.append(composer_toolbar);
+ chain.append(attachments_box);
+ chain.append(button_area);
+ box.set_focus_chain(chain);
+
+ // If there's only one account, open the drafts folder. If there's more than one account,
+ // the drafts folder will be opened by on_from_changed().
+ if (!from_multiple.visible)
+ open_drafts_folder_async.begin(cancellable_drafts);
+
+ destroy.connect(() => { close_drafts_folder_async.begin(); });
+ }
+
+ public ComposerWidget.from_mailto(Geary.Account account, string mailto) {
+ this(account, ComposeType.NEW_MESSAGE);
+
+ Gee.HashMultiMap<string, string> headers = new Gee.HashMultiMap<string, string>();
+ if (mailto.length > Geary.ComposedEmail.MAILTO_SCHEME.length) {
+ // Parse the mailto link.
+ string[] parts = mailto.substring(Geary.ComposedEmail.MAILTO_SCHEME.length).split("?", 2);
+ string email = Uri.unescape_string(parts[0]);
+ string[] params = parts.length == 2 ? parts[1].split("&") : new string[0];
+ foreach (string param in params) {
+ string[] param_parts = param.split("=", 2);
+ if (param_parts.length == 2) {
+ headers.set(Uri.unescape_string(param_parts[0]).down(),
+ Uri.unescape_string(param_parts[1]));
+ }
+ }
+
+ // Assemble the headers.
+ if (email.length > 0 && headers.contains("to"))
+ to = "%s,%s".printf(email, Geary.Collection.get_first(headers.get("to")));
+ else if (email.length > 0)
+ to = email;
+ else if (headers.contains("to"))
+ to = Geary.Collection.get_first(headers.get("to"));
+
+ if (headers.contains("cc"))
+ cc = Geary.Collection.get_first(headers.get("cc"));
+
+ if (headers.contains("bcc"))
+ bcc = Geary.Collection.get_first(headers.get("bcc"));
+
+ if (headers.contains("subject"))
+ subject = Geary.Collection.get_first(headers.get("subject"));
+
+ if (headers.contains("body"))
+ body_html = Geary.HTML.preserve_whitespace(Geary.HTML.escape_markup(
+ Geary.Collection.get_first(headers.get("body"))));
+
+ foreach (string attachment in headers.get("attach"))
+ add_attachment(File.new_for_commandline_arg(attachment));
+ foreach (string attachment in headers.get("attachment"))
+ add_attachment(File.new_for_commandline_arg(attachment));
+ }
+ }
+
+ public void set_focus() {
+ if (Geary.String.is_empty(to)) {
+ to_entry.grab_focus();
+ } else if (Geary.String.is_empty(subject)) {
+ subject_entry.grab_focus();
+ } else {
+ editor.grab_focus();
+ }
+ }
+
+ private void on_load_finished(WebKit.WebFrame frame) {
+ WebKit.DOM.HTMLElement? body = editor.get_dom_document().get_element_by_id(
+ BODY_ID) as WebKit.DOM.HTMLElement;
+ assert(body != null);
+
+ if (!Geary.String.is_empty(body_html)) {
+ try {
+ body.set_inner_html(body_html);
+ } catch (Error e) {
+ debug("Failed to load prefilled body: %s", e.message);
+ }
+ }
+
+ protect_blockquote_styles();
+
+ set_focus();
+
+ // Ensure the editor is in correct mode re HTML
+ on_compose_as_html();
+
+ bind_event(editor,"a", "click", (Callback) on_link_clicked, this);
+ update_actions();
+ }
+
+ // Glade only allows one accelerator per-action. This method adds extra accelerators not defined
+ // in the Glade file.
+ private void add_extra_accelerators() {
+ GtkUtil.add_accelerator(ui, actions, "Escape", ACTION_CLOSE);
+ }
+
+ private void setup_drag_destination(Gtk.Widget destination) {
+ const Gtk.TargetEntry[] target_entries = { { URI_LIST_MIME_TYPE, 0, 0 } };
+ Gtk.drag_dest_set(destination, Gtk.DestDefaults.MOTION | Gtk.DestDefaults.HIGHLIGHT,
+ target_entries, Gdk.DragAction.COPY);
+ destination.drag_data_received.connect(on_drag_data_received);
+ destination.drag_drop.connect(on_drag_drop);
+ destination.drag_motion.connect(on_drag_motion);
+ destination.drag_leave.connect(on_drag_leave);
+ }
+
+ private void show_attachment_overlay(bool visible) {
+ if (is_attachment_overlay_visible == visible)
+ return;
+
+ is_attachment_overlay_visible = visible;
+
+ // If we just make the widget invisible, it can still intercept drop signals. So we
+ // completely remove it instead.
+ if (visible) {
+ int height = hidden_on_attachment_drag_over.get_allocated_height();
+ hidden_on_attachment_drag_over.remove(hidden_on_attachment_drag_over_child);
+ visible_on_attachment_drag_over.add(visible_on_attachment_drag_over_child);
+ visible_on_attachment_drag_over.set_size_request(-1, height);
+ } else {
+ hidden_on_attachment_drag_over.add(hidden_on_attachment_drag_over_child);
+ visible_on_attachment_drag_over.remove(visible_on_attachment_drag_over_child);
+ visible_on_attachment_drag_over.set_size_request(-1, -1);
+ }
+ }
+
+ private bool on_drag_motion() {
+ show_attachment_overlay(true);
+ return false;
+ }
+
+ private void on_drag_leave() {
+ show_attachment_overlay(false);
+ }
+
+ private void on_drag_data_received(Gtk.Widget sender, Gdk.DragContext context, int x, int y,
+ Gtk.SelectionData selection_data, uint info, uint time_) {
+
+ bool dnd_success = false;
+ if (selection_data.get_length() >= 0) {
+ dnd_success = true;
+
+ string uri_list = (string) selection_data.get_data();
+ string[] uris = uri_list.strip().split("\n");
+ foreach (string uri in uris) {
+ if (!uri.has_prefix(FILE_URI_PREFIX))
+ continue;
+
+ add_attachment(File.new_for_uri(uri.strip()));
+ }
+ }
+
+ Gtk.drag_finish(context, dnd_success, false, time_);
+ }
+
+ private bool on_drag_drop(Gtk.Widget sender, Gdk.DragContext context, int x, int y, uint time_) {
+ if (context.list_targets() == null)
+ return false;
+
+ uint length = context.list_targets().length();
+ Gdk.Atom? target_type = null;
+ for (uint i = 0; i < length; i++) {
+ Gdk.Atom target = context.list_targets().nth_data(i);
+ if (target.name() == URI_LIST_MIME_TYPE)
+ target_type = target;
+ }
+
+ if (target_type == null)
+ return false;
+
+ Gtk.drag_get_data(sender, context, target_type, time_);
+ return true;
+ }
+
+ public Geary.ComposedEmail get_composed_email(DateTime? date_override = null,
+ bool only_html = false) {
+ Geary.ComposedEmail email = new Geary.ComposedEmail(
+ date_override ?? new DateTime.now_local(),
+ new Geary.RFC822.MailboxAddresses.from_rfc822_string(from)
+ );
+
+ if (to_entry.addresses != null)
+ email.to = to_entry.addresses;
+
+ if (cc_entry.addresses != null)
+ email.cc = cc_entry.addresses;
+
+ if (bcc_entry.addresses != null)
+ email.bcc = bcc_entry.addresses;
+
+ if (!Geary.String.is_empty(in_reply_to))
+ email.in_reply_to = in_reply_to;
+
+ if (!Geary.String.is_empty(references))
+ email.references = references;
+
+ if (!Geary.String.is_empty(subject))
+ email.subject = subject;
+
+ email.attachment_files.add_all(attachment_files);
+
+ if (compose_as_html || only_html)
+ email.body_html = get_html();
+ if (!only_html)
+ email.body_text = get_text();
+
+ // User-Agent
+ email.mailer = GearyApplication.PRGNAME + "/" + GearyApplication.VERSION;
+
+ return email;
+ }
+
+ public override void show_all() {
+ base.show_all();
+ update_from_field();
+ }
+
+ public void change_compose_type(ComposeType new_type) {
+ if (new_type != compose_type) {
+ bool recipients_modified = to_entry.modified || cc_entry.modified || bcc_entry.modified;
+ switch (new_type) {
+ case ComposeType.REPLY:
+ case ComposeType.REPLY_ALL:
+ subject = reply_subject;
+ if (!recipients_modified) {
+ to = reply_to_addresses;
+ cc = (new_type == ComposeType.REPLY_ALL ? reply_cc_addresses : "");
+ to_entry.modified = cc_entry.modified = false;
+ } else {
+ to_entry.select_region(0, -1);
+ }
+ in_reply_to = reply_message_id;
+ break;
+
+ case ComposeType.FORWARD:
+ state = ComposerState.INLINE;
+ subject = forward_subject;
+ if (!recipients_modified) {
+ to = "";
+ cc = "";
+ to_entry.modified = cc_entry.modified = false;
+ } else {
+ to_entry.select_region(0, -1);
+ }
+ in_reply_to = "";
+ break;
+
+ default:
+ assert_not_reached();
+ }
+ compose_type = new_type;
+ }
+
+ container.present();
+ set_focus();
+ }
+
++ private void add_signature() {
++ string signature = account.information.email_signature;
++ signature = Geary.HTML.escape_markup(signature);
++
++ if (body_html == null)
++ body_html = Geary.HTML.preserve_whitespace("\n\n" + signature);
++ else
++ body_html = body_html + Geary.HTML.preserve_whitespace("\n\n" + signature);
++ }
++
+ private bool can_save() {
+ return (drafts_folder != null && drafts_folder.get_open_state() == Geary.Folder.OpenState.BOTH
+ && !drafts_folder.properties.create_never_returns_id && editor.can_undo()
+ && account.information.save_drafts);
+ }
+
+ public CloseStatus should_close() {
+ bool try_to_save = can_save();
+
+ container.present();
+ AlertDialog dialog;
+
+ if (drafts_folder == null && try_to_save) {
+ dialog = new ConfirmationDialog(container.top_window,
+ _("Do you want to discard the unsaved message?"), null, Stock._DISCARD);
+ } else if (try_to_save) {
+ dialog = new TernaryConfirmationDialog(container.top_window,
+ _("Do you want to discard this message?"), null, Stock._KEEP, Stock._DISCARD,
+ Gtk.ResponseType.CLOSE);
+ } else {
+ dialog = new ConfirmationDialog(container.top_window,
+ _("Do you want to discard this message?"), null, Stock._DISCARD);
+ }
+
+ Gtk.ResponseType response = dialog.run();
+ if (response == Gtk.ResponseType.CANCEL || response == Gtk.ResponseType.DELETE_EVENT) {
+ return CloseStatus.CANCEL_CLOSE; // Cancel
+ } else if (response == Gtk.ResponseType.OK) {
+ if (try_to_save) {
+ save_and_exit.begin(); // Save
+ return CloseStatus.PENDING_CLOSE;
+ } else {
+ return CloseStatus.DO_CLOSE;
+ }
+ } else {
+ delete_and_exit.begin(); // Discard
+ return CloseStatus.PENDING_CLOSE;
+ }
+ }
+
+ private void on_close() {
+ if (should_close() == CloseStatus.DO_CLOSE)
+ container.close_container();
+ }
+
+ private void on_detach() {
+ if (parent is ComposerEmbed)
+ ((ComposerEmbed) parent).on_detach();
+ }
+
+ private bool email_contains_attachment_keywords() {
+ // Filter out all content contained in block quotes
+ string filtered = @"$subject\n";
+ filtered += Util.DOM.get_text_representation(editor.get_dom_document(), "blockquote");
+
+ Regex url_regex = null;
+ try {
+ // Prepare to ignore urls later
+ url_regex = new Regex(URL_REGEX, RegexCompileFlags.CASELESS);
+ } catch (Error error) {
+ debug("Error building regex in keyword checker: %s", error.message);
+ }
+
+ string[] keys = ATTACHMENT_KEYWORDS_GENERIC.casefold().split("|");
+ foreach (string key in ATTACHMENT_KEYWORDS_LOCALIZED.casefold().split("|")) {
+ keys += key;
+ }
+
+ string folded;
+ foreach (string line in filtered.split("\n")) {
+ // Stop looking once we hit forwarded content
+ if (line.has_prefix("--")) {
+ break;
+ }
+
+ folded = line.casefold();
+ foreach (string key in keys) {
+ if (key in folded) {
+ try {
+ // Make sure the match isn't coming from a url
+ if (key in url_regex.replace(folded, -1, 0, "")) {
+ return true;
+ }
+ } catch (Error error) {
+ debug("Regex replacement error in keyword checker: %s", error.message);
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private bool should_send() {
+ bool has_subject = !Geary.String.is_empty(subject.strip());
+ bool has_body = !Geary.String.is_empty(get_html());
+ bool has_attachment = attachment_files.size > 0;
+ bool has_body_or_attachment = has_body || has_attachment;
+
+ string? confirmation = null;
+ if (!has_subject && !has_body_or_attachment) {
+ confirmation = _("Send message with an empty subject and body?");
+ } else if (!has_subject) {
+ confirmation = _("Send message with an empty subject?");
+ } else if (!has_body_or_attachment) {
+ confirmation = _("Send message with an empty body?");
+ } else if (!has_attachment && email_contains_attachment_keywords()) {
+ confirmation = _("Send message without an attachment?");
+ }
+ if (confirmation != null) {
+ ConfirmationDialog dialog = new ConfirmationDialog(container.top_window,
+ confirmation, null, Stock._OK);
+ if (dialog.run() != Gtk.ResponseType.OK)
+ return false;
+ }
+ return true;
+ }
+
+ // Sends the current message.
+ private void on_send() {
+ if (should_send())
+ on_send_async.begin();
+ }
+
+ // Used internally by on_send()
+ private async void on_send_async() {
+ cancellable_save_draft.cancel();
+
+ container.vanish();
+
+ linkify_document(editor.get_dom_document());
+
+ // Perform send.
+ try {
+ yield account.send_email_async(get_composed_email());
+ } catch (Error e) {
+ GLib.message("Error sending email: %s", e.message);
+ }
+
+ yield delete_draft_async();
+ container.close_container(); // Only close window after draft is deleted; this closes the drafts
folder.
+ }
+
+ private void on_drafts_opened(Geary.Folder.OpenState open_state, int count) {
+ if (open_state == Geary.Folder.OpenState.BOTH)
+ reset_draft_timer();
+ }
+
+ // Returns the drafts folder for the current From account.
+ private async void open_drafts_folder_async(Cancellable cancellable) throws Error {
+ yield close_drafts_folder_async(cancellable);
+
+ if (!account.information.save_drafts)
+ return;
+
+ Geary.FolderSupport.Create? folder = (yield account.get_required_special_folder_async(
+ Geary.SpecialFolderType.DRAFTS, cancellable)) as Geary.FolderSupport.Create;
+
+ if (folder == null)
+ return; // No drafts folder.
+
+ yield folder.open_async(Geary.Folder.OpenFlags.FAST_OPEN | Geary.Folder.OpenFlags.NO_DELAY,
+ cancellable);
+
+ drafts_folder = folder;
+ drafts_folder.opened.connect(on_drafts_opened);
+ }
+
+ private async void close_drafts_folder_async(Cancellable? cancellable = null) throws Error {
+ if (drafts_folder == null)
+ return;
+
+ // Close existing folder.
+ drafts_folder.opened.disconnect(on_drafts_opened);
+ yield drafts_folder.close_async(cancellable);
+ drafts_folder = null;
+ }
+
+ // Save to the draft folder, if available.
+ // Note that drafts are NOT "linkified."
+ private bool on_save_draft_timeout() {
+ // since all control paths return false, this is not rescheduled by the event loop, so
+ // kill the timeout id
+ draft_save_timeout_id = 0;
+
+ if (in_draft_save)
+ return false;
+
+ in_draft_save = true;
+ save_async.begin(cancellable_save_draft, () => { in_draft_save = false; });
+
+ return false;
+ }
+
+ private async void save_async(Cancellable? cancellable) {
+ if (drafts_folder == null || !can_save())
+ return;
+
+ cancel_draft_timer();
+
+ draft_save_label.label = DRAFT_SAVING_TEXT;
+
+ Geary.EmailFlags flags = new Geary.EmailFlags();
+ flags.add(Geary.EmailFlags.DRAFT);
+
+ try {
+ // only save HTML drafts to avoid resetting the DOM (which happens when converting the
+ // HTML to flowed text)
+ draft_id = yield drafts_folder.create_email_async(new Geary.RFC822.Message.from_composed_email(
+ get_composed_email(null, true), null), flags, null, draft_id, cancellable);
+
+ draft_save_label.label = DRAFT_SAVED_TEXT;
+ } catch (Error e) {
+ GLib.message("Error saving draft: %s", e.message);
+ draft_save_label.label = DRAFT_ERROR_TEXT;
+ }
+ }
+
+ // Used while waiting for draft to save before closing widget.
+ private void make_gui_insensitive() {
+ container.vanish();
+ cancel_draft_timer();
+ }
+
+ private async void save_and_exit() {
+ make_gui_insensitive();
+
+ // Do the save.
+ yield save_async(null);
+
+ container.close_container();
+ }
+
+ private async void delete_and_exit() {
+ make_gui_insensitive();
+
+ // Do the delete.
+ yield delete_draft_async();
+
+ container.close_container();
+ }
+
+ private async void delete_draft_async() {
+ if (drafts_folder == null || draft_id == null)
+ return;
+
+ Geary.FolderSupport.Remove? removable_drafts = drafts_folder as Geary.FolderSupport.Remove;
+ if (removable_drafts == null) {
+ debug("Draft folder does not support remove.\n");
+
+ return;
+ }
+
+ try {
+ yield removable_drafts.remove_single_email_async(draft_id);
+ } catch (Error e) {
+ debug("Unable to delete draft: %s", e.message);
+ }
+ }
+
+ private void on_add_attachment_button_clicked() {
+ AttachmentDialog dialog = null;
+ do {
+ // Transient parent of AttachmentDialog is this ComposerWindow
+ // But this generates the following warning:
+ // Attempting to add a widget with type AttachmentDialog to a
+ // ComposerWindow, but as a GtkBin subclass a ComposerWindow can
+ // only contain one widget at a time;
+ // it already contains a widget of type GtkBox
+ dialog = new AttachmentDialog(container.top_window);
+ } while (!dialog.is_finished(add_attachment));
+ }
+
+ private void on_pending_attachments_button_clicked() {
+ add_attachments(pending_attachments, false);
+ }
+
+ private void check_pending_attachments() {
+ if (pending_attachments != null) {
+ foreach (Geary.Attachment attachment in pending_attachments) {
+ if (!attachment_files.contains(attachment.file)) {
+ pending_attachments_button.show();
+ return;
+ }
+ }
+ }
+ pending_attachments_button.hide();
+ }
+
+ private void attachment_failed(string msg) {
+ ErrorDialog dialog = new ErrorDialog(container.top_window, _("Cannot add attachment"), msg);
+ dialog.run();
+ }
+
+ private bool add_attachment(File attachment_file, bool alert_errors = true) {
+ FileInfo attachment_file_info;
+ try {
+ attachment_file_info = attachment_file.query_info("standard::size,standard::type",
+ FileQueryInfoFlags.NONE);
+ } catch(Error e) {
+ if (alert_errors)
+ attachment_failed(_("\"%s\" could not be found.").printf(attachment_file.get_path()));
+
+ return false;
+ }
+
+ if (attachment_file_info.get_file_type() == FileType.DIRECTORY) {
+ if (alert_errors)
+ attachment_failed(_("\"%s\" is a folder.").printf(attachment_file.get_path()));
+
+ return false;
+ }
+
+ if (attachment_file_info.get_size() == 0){
+ if (alert_errors)
+ attachment_failed(_("\"%s\" is an empty file.").printf(attachment_file.get_path()));
+
+ return false;
+ }
+
+ try {
+ FileInputStream? stream = attachment_file.read();
+ if (stream != null)
+ stream.close();
+ } catch(Error e) {
+ debug("File '%s' could not be opened for reading. Error: %s", attachment_file.get_path(),
+ e.message);
+
+ if (alert_errors)
+ attachment_failed(_("\"%s\" could not be opened for
reading.").printf(attachment_file.get_path()));
+
+ return false;
+ }
+
+ if (!attachment_files.add(attachment_file)) {
+ if (alert_errors)
+ attachment_failed(_("\"%s\" already attached for
delivery.").printf(attachment_file.get_path()));
+
+ return false;
+ }
+
+ Gtk.Box box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6);
+ attachments_box.pack_start(box);
+
+ /// In the composer, the filename followed by its filesize, i.e. "notes.txt (1.12KB)"
+ string label_text = _("%s (%s)").printf(attachment_file.get_basename(),
+ Files.get_filesize_as_string(attachment_file_info.get_size()));
+ Gtk.Label label = new Gtk.Label(label_text);
+ box.pack_start(label);
+ label.halign = Gtk.Align.START;
+ label.xpad = 4;
+
+ Gtk.Button remove_button = new Gtk.Button.with_mnemonic(Stock._REMOVE);
+ box.pack_start(remove_button, false, false);
+ remove_button.clicked.connect(() => remove_attachment(attachment_file, box));
+
+ show_attachments();
+
+ check_pending_attachments();
+
+ return true;
+ }
+
+ private void add_attachments(Gee.List<Geary.Attachment> attachments, bool alert_errors = true) {
+ foreach(Geary.Attachment attachment in attachments)
+ add_attachment(attachment.file, alert_errors);
+ }
+
+ private void remove_attachment(File file, Gtk.Box box) {
+ if (!attachment_files.remove(file))
+ return;
+
+ foreach (weak Gtk.Widget child in attachments_box.get_children()) {
+ if (child == box) {
+ attachments_box.remove(box);
+ break;
+ }
+ }
+
+ show_attachments();
+
+ check_pending_attachments();
+ }
+
+ private void show_attachments() {
+ if (attachment_files.size > 0 ) {
+ attachments_box.show_all();
+ attachments_separator.show();
+ } else {
+ attachments_box.hide();
+ attachments_separator.hide();
+ }
+ }
+
+ private void on_subject_changed() {
+ reset_draft_timer();
+ }
+
+ private void validate_send_button() {
+ send_button.sensitive =
+ to_entry.valid_or_empty && cc_entry.valid_or_empty && bcc_entry.valid_or_empty
+ && (!to_entry.empty || !cc_entry.empty || !bcc_entry.empty);
+ bool tocc = !to_entry.empty && !cc_entry.empty,
+ ccbcc = !(to_entry.empty && cc_entry.empty) && !bcc_entry.empty;
+ if (state == ComposerState.INLINE_COMPACT)
+ compact_header_label.label = to_entry.buffer.text + (tocc ? ", " : "")
+ + cc_entry.buffer.text + (ccbcc ? ", " : "") + bcc_entry.buffer.text;
+
+ reset_draft_timer();
+ }
+
+ private void on_formatting_action(Gtk.Action action) {
+ if (compose_as_html)
+ on_action(action);
+ }
+
+ private void on_action(Gtk.Action action) {
+ if (action_flag)
+ return;
+
+ action_flag = true; // prevents recursion
+ editor.get_dom_document().exec_command(action.get_name(), false, "");
+ action_flag = false;
+ }
+
+ private void on_cut() {
+ if (container.get_focus() == editor)
+ editor.cut_clipboard();
+ else if (container.get_focus() is Gtk.Editable)
+ ((Gtk.Editable) container.get_focus()).cut_clipboard();
+ }
+
+ private void on_copy() {
+ if (container.get_focus() == editor)
+ editor.copy_clipboard();
+ else if (container.get_focus() is Gtk.Editable)
+ ((Gtk.Editable) container.get_focus()).copy_clipboard();
+ }
+
+ private void on_copy_link() {
+ Gtk.Clipboard c = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD);
+ c.set_text(hover_url, -1);
+ c.store();
+ }
+
+ private WebKit.DOM.Node? get_left_text(WebKit.DOM.Node node, long offset) {
+ WebKit.DOM.Document document = editor.get_dom_document();
+ string node_value = node.node_value;
+
+ // Offset is in unicode characters, but index is in bytes. We need to get the corresponding
+ // byte index for the given offset.
+ int char_count = node_value.char_count();
+ int index = offset > char_count ? node_value.length : node_value.index_of_nth_char(offset);
+
+ return offset > 0 ? document.create_text_node(node_value[0:index]) : null;
+ }
+
+ private void on_clipboard_text_received(Gtk.Clipboard clipboard, string? text) {
+ if (text == null)
+ return;
+
+ // Insert plain text from clipboard.
+ WebKit.DOM.Document document = editor.get_dom_document();
+ document.exec_command("inserttext", false, text);
+
+ // The inserttext command will not scroll if needed, but we can't use the clipboard
+ // for plain text. WebKit allows us to scroll a node into view, but not an arbitrary
+ // position within a text node. So we add a placeholder node at the cursor position,
+ // scroll to that, then remove the placeholder node.
+ try {
+ WebKit.DOM.DOMSelection selection = document.default_view.get_selection();
+ WebKit.DOM.Node selection_base_node = selection.get_base_node();
+ long selection_base_offset = selection.get_base_offset();
+
+ WebKit.DOM.NodeList selection_child_nodes = selection_base_node.get_child_nodes();
+ WebKit.DOM.Node ref_child = selection_child_nodes.item(selection_base_offset);
+
+ WebKit.DOM.Element placeholder = document.create_element("SPAN");
+ WebKit.DOM.Text placeholder_text = document.create_text_node("placeholder");
+ placeholder.append_child(placeholder_text);
+
+ if (selection_base_node.node_name == "#text") {
+ WebKit.DOM.Node? left = get_left_text(selection_base_node, selection_base_offset);
+
+ WebKit.DOM.Node parent = selection_base_node.parent_node;
+ if (left != null)
+ parent.insert_before(left, selection_base_node);
+ parent.insert_before(placeholder, selection_base_node);
+ parent.remove_child(selection_base_node);
+
+ placeholder.scroll_into_view_if_needed(false);
+ parent.insert_before(selection_base_node, placeholder);
+ if (left != null)
+ parent.remove_child(left);
+ parent.remove_child(placeholder);
+ selection.set_base_and_extent(selection_base_node, selection_base_offset,
selection_base_node, selection_base_offset);
+ } else {
+ selection_base_node.insert_before(placeholder, ref_child);
+ placeholder.scroll_into_view_if_needed(false);
+ selection_base_node.remove_child(placeholder);
+ }
+
+ } catch (Error err) {
+ debug("Error scrolling pasted text into view: %s", err.message);
+ }
+ }
+
+ private void on_paste() {
+ if (container.get_focus() == editor)
+ get_clipboard(Gdk.SELECTION_CLIPBOARD).request_text(on_clipboard_text_received);
+ else if (container.get_focus() is Gtk.Editable)
+ ((Gtk.Editable) container.get_focus()).paste_clipboard();
+ }
+
+ private void on_paste_with_formatting() {
+ if (container.get_focus() == editor)
+ editor.paste_clipboard();
+ }
+
+ private void on_select_all() {
+ editor.select_all();
+ }
+
+ private void on_remove_format() {
+ editor.get_dom_document().exec_command("removeformat", false, "");
+ editor.get_dom_document().exec_command("removeparaformat", false, "");
+ editor.get_dom_document().exec_command("unlink", false, "");
+ editor.get_dom_document().exec_command("backcolor", false, "#ffffff");
+ editor.get_dom_document().exec_command("forecolor", false, "#000000");
+ }
+
+ private void on_compose_as_html() {
+ WebKit.DOM.DOMTokenList body_classes = editor.get_dom_document().body.get_class_list();
+ if (!compose_as_html) {
+ toggle_toolbar_buttons(false);
+ build_plaintext_menu();
+ try {
+ body_classes.add("plain");
+ } catch (Error error) {
+ debug("Error setting composer style: %s", error.message);
+ }
+ } else {
+ toggle_toolbar_buttons(true);
+ build_html_menu();
+ try {
+ body_classes.remove("plain");
+ } catch (Error error) {
+ debug("Error setting composer style: %s", error.message);
+ }
+ }
+ GearyApplication.instance.config.compose_as_html = compose_as_html;
+ }
+
+ private void toggle_toolbar_buttons(bool show) {
+ actions.get_action(ACTION_BOLD).visible =
+ actions.get_action(ACTION_ITALIC).visible =
+ actions.get_action(ACTION_UNDERLINE).visible =
+ actions.get_action(ACTION_STRIKETHROUGH).visible =
+ actions.get_action(ACTION_INSERT_LINK).visible =
+ actions.get_action(ACTION_REMOVE_FORMAT).visible = show;
+ }
+
+ private void build_plaintext_menu() {
+ GtkUtil.clear_menu(menu);
+
+ menu.append(html_item2);
+ menu.show_all();
+ }
+
+ private void build_html_menu() {
+ GtkUtil.clear_menu(menu);
+
+ menu.append(font_sans);
+ menu.append(font_serif);
+ menu.append(font_monospace);
+ menu.append(new Gtk.SeparatorMenuItem());
+
+ menu.append(font_small);
+ menu.append(font_medium);
+ menu.append(font_large);
+ menu.append(new Gtk.SeparatorMenuItem());
+
+ menu.append(color_item);
+ menu.append(new Gtk.SeparatorMenuItem());
+
+ menu.append(html_item);
+ menu.show_all(); // Call this or only menu items associated with actions will be displayed.
+ }
+
+ private void on_font_sans() {
+ if (!action_flag)
+ editor.get_dom_document().exec_command("fontname", false, "sans");
+ }
+
+ private void on_font_serif() {
+ if (!action_flag)
+ editor.get_dom_document().exec_command("fontname", false, "serif");
+ }
+
+ private void on_font_monospace() {
+ if (!action_flag)
+ editor.get_dom_document().exec_command("fontname", false, "monospace");
+ }
+
+ private void on_font_size_small() {
+ if (!action_flag)
+ editor.get_dom_document().exec_command("fontsize", false, "1");
+ }
+
+ private void on_font_size_medium() {
+ if (!action_flag)
+ editor.get_dom_document().exec_command("fontsize", false, "3");
+ }
+
+ private void on_font_size_large() {
+ if (!action_flag)
+ editor.get_dom_document().exec_command("fontsize", false, "7");
+ }
+
+ private void on_select_color() {
+ if (compose_as_html) {
+ Gtk.ColorChooserDialog dialog = new Gtk.ColorChooserDialog(_("Select Color"),
+ container.top_window);
+ if (dialog.run() == Gtk.ResponseType.OK)
+ editor.get_dom_document().exec_command("forecolor", false, dialog.get_rgba().to_string());
+
+ dialog.destroy();
+ }
+ }
+
+ private void on_indent(Gtk.Action action) {
+ on_action(action);
+
+ // Undo styling of blockquotes
+ try {
+ WebKit.DOM.NodeList node_list = editor.get_dom_document().query_selector_all(
+ "blockquote[style=\"margin: 0 0 0 40px; border: none; padding: 0px;\"]");
+ for (int i = 0; i < node_list.length; ++i) {
+ WebKit.DOM.Element element = (WebKit.DOM.Element) node_list.item(i);
+ element.remove_attribute("style");
+ element.set_attribute("type", "cite");
+ }
+ } catch (Error error) {
+ debug("Error removing blockquote style: %s", error.message);
+ }
+ }
+
+ private void protect_blockquote_styles() {
+ // We will search for an remove a particular styling when we quote text. If that style
+ // exists in the quoted text, we alter it slightly so we don't mess with it later.
+ try {
+ WebKit.DOM.NodeList node_list = editor.get_dom_document().query_selector_all(
+ "blockquote[style=\"margin: 0 0 0 40px; border: none; padding: 0px;\"]");
+ for (int i = 0; i < node_list.length; ++i) {
+ ((WebKit.DOM.Element) node_list.item(i)).set_attribute("style",
+ "margin: 0 0 0 40px; padding: 0px; border:none;");
+ }
+ } catch (Error error) {
+ debug("Error protecting blockquotes: %s", error.message);
+ }
+ }
+
+ private void on_insert_link() {
+ if (compose_as_html)
+ link_dialog("http://");
+ }
+
+ private static void on_link_clicked(WebKit.DOM.Element element, WebKit.DOM.Event event,
+ ComposerWidget composer) {
+ try {
+ composer.editor.get_dom_document().get_default_view().get_selection().
+ select_all_children(element);
+ } catch (Error e) {
+ debug("Error selecting link: %s", e.message);
+ }
+
+ composer.prev_selected_link = element;
+ }
+
+ private void link_dialog(string link) {
+ Gtk.Dialog dialog = new Gtk.Dialog();
+ bool existing_link = false;
+
+ // Allow user to remove link if they're editing an existing one.
+ WebKit.DOM.Node selected = editor.get_dom_document().get_default_view().
+ get_selection().focus_node;
+ if (selected != null && (selected is WebKit.DOM.HTMLAnchorElement ||
+ selected.get_parent_element() is WebKit.DOM.HTMLAnchorElement)) {
+ existing_link = true;
+ dialog.add_buttons(Stock._REMOVE, Gtk.ResponseType.REJECT);
+ }
+
+ dialog.add_buttons(Stock._CANCEL, Gtk.ResponseType.CANCEL, Stock._OK,
+ Gtk.ResponseType.OK);
+
+ Gtk.Entry entry = new Gtk.Entry();
+ entry.changed.connect(() => {
+ // Only allow OK when there's text in the box.
+ dialog.set_response_sensitive(Gtk.ResponseType.OK,
+ !Geary.String.is_empty(entry.text.strip()));
+ });
+
+ dialog.width_request = 350;
+ dialog.get_content_area().spacing = 7;
+ dialog.get_content_area().border_width = 10;
+ dialog.get_content_area().pack_start(new Gtk.Label("Link URL:"));
+ dialog.get_content_area().pack_start(entry);
+ dialog.get_widget_for_response(Gtk.ResponseType.OK).can_default = true;
+ dialog.set_default_response(Gtk.ResponseType.OK);
+ dialog.show_all();
+
+ entry.set_text(link);
+ entry.activates_default = true;
+ entry.move_cursor(Gtk.MovementStep.BUFFER_ENDS, 0, false);
+
+ int response = dialog.run();
+
+ // If it's an existing link, re-select it. This is necessary because selecting
+ // text in the Gtk.Entry will de-select all in the WebView.
+ if (existing_link) {
+ try {
+ editor.get_dom_document().get_default_view().get_selection().
+ select_all_children(prev_selected_link);
+ } catch (Error e) {
+ debug("Error selecting link: %s", e.message);
+ }
+ }
+
+ if (response == Gtk.ResponseType.OK)
+ editor.get_dom_document().exec_command("createLink", false, entry.text);
+ else if (response == Gtk.ResponseType.REJECT)
+ editor.get_dom_document().exec_command("unlink", false, "");
+
+ dialog.destroy();
+
+ // Re-bind to anchor links. This must be done every time link have changed.
+ bind_event(editor,"a", "click", (Callback) on_link_clicked, this);
+ }
+
+ private string get_html() {
+ return editor.get_dom_document().get_body().get_inner_html();
+ }
+
+ private string get_text() {
+ return html_to_flowed_text(editor.get_dom_document());
+ }
+
+ private bool on_navigation_policy_decision_requested(WebKit.WebFrame frame,
+ WebKit.NetworkRequest request, WebKit.WebNavigationAction navigation_action,
+ WebKit.WebPolicyDecision policy_decision) {
+ policy_decision.ignore();
+ if (compose_as_html)
+ link_dialog(request.uri);
+ return true;
+ }
+
+ private void on_hovering_over_link(string? title, string? url) {
+ if (compose_as_html) {
+ message_overlay_label.label = url;
+ hover_url = url;
+ update_actions();
+ }
+ }
+
+ private void update_message_overlay_label_style() {
+ Gdk.RGBA window_background = container.top_window.get_style_context()
+ .get_background_color(Gtk.StateFlags.NORMAL);
+ Gdk.RGBA label_background = message_overlay_label.get_style_context()
+ .get_background_color(Gtk.StateFlags.NORMAL);
+
+ if (label_background == window_background)
+ return;
+
+ message_overlay_label.get_style_context().changed.disconnect(
+ on_message_overlay_label_style_changed);
+ message_overlay_label.override_background_color(Gtk.StateFlags.NORMAL, window_background);
+ message_overlay_label.get_style_context().changed.connect(
+ on_message_overlay_label_style_changed);
+ }
+
+ private void on_message_overlay_label_realize() {
+ update_message_overlay_label_style();
+ }
+
+ private void on_message_overlay_label_style_changed() {
+ update_message_overlay_label_style();
+ }
+
+ private void on_spell_check_changed() {
+ editor.settings.enable_spell_checking = GearyApplication.instance.config.spell_check;
+ }
+
+ // This overrides the keypress handling for the *widget*; the WebView editor's keypress overrides
+ // are handled by on_editor_key_press
+ public override bool key_press_event(Gdk.EventKey event) {
+ update_actions();
+
+ switch (Gdk.keyval_name(event.keyval)) {
+ case "Return":
+ case "KP_Enter":
+ // always trap Ctrl+Enter/Ctrl+KeypadEnter to prevent the Enter leaking through
+ // to the controls, but only send if send is available
+ if ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0) {
+ if (send_button.sensitive)
+ on_send();
+
+ return true;
+ }
+ break;
+ }
+
+ return base.key_press_event(event);
+ }
+
+ private bool on_context_menu(Gtk.Widget default_menu, WebKit.HitTestResult hit_test_result,
+ bool keyboard_triggered) {
+ Gtk.Menu context_menu = (Gtk.Menu) default_menu;
+ Gtk.MenuItem? ignore_spelling = null, learn_spelling = null;
+ bool suggestions = false;
+
+ GLib.List<weak Gtk.Widget> children = context_menu.get_children();
+ foreach (weak Gtk.Widget child in children) {
+ Gtk.MenuItem item = (Gtk.MenuItem) child;
+ if (item.is_sensitive()) {
+ WebKit.ContextMenuAction action = WebKit.context_menu_item_get_action(item);
+ if (action == WebKit.ContextMenuAction.SPELLING_GUESS) {
+ suggestions = true;
+ continue;
+ }
+
+ if (action == WebKit.ContextMenuAction.IGNORE_SPELLING)
+ ignore_spelling = item;
+ else if (action == WebKit.ContextMenuAction.LEARN_SPELLING)
+ learn_spelling = item;
+ }
+ context_menu.remove(child);
+ }
+
+ if (suggestions)
+ context_menu.append(new Gtk.SeparatorMenuItem());
+ if (ignore_spelling != null)
+ context_menu.append(ignore_spelling);
+ if (learn_spelling != null)
+ context_menu.append(learn_spelling);
+ if (ignore_spelling != null || learn_spelling != null)
+ context_menu.append(new Gtk.SeparatorMenuItem());
+
+ // Undo
+ Gtk.MenuItem undo = new Gtk.ImageMenuItem();
+ undo.related_action = actions.get_action(ACTION_UNDO);
+ context_menu.append(undo);
+
+ // Redo
+ Gtk.MenuItem redo = new Gtk.ImageMenuItem();
+ redo.related_action = actions.get_action(ACTION_REDO);
+ context_menu.append(redo);
+
+ context_menu.append(new Gtk.SeparatorMenuItem());
+
+ // Cut
+ Gtk.MenuItem cut = new Gtk.ImageMenuItem();
+ cut.related_action = actions.get_action(ACTION_CUT);
+ context_menu.append(cut);
+
+ // Copy
+ Gtk.MenuItem copy = new Gtk.ImageMenuItem();
+ copy.related_action = actions.get_action(ACTION_COPY);
+ context_menu.append(copy);
+
+ // Copy link.
+ Gtk.MenuItem copy_link = new Gtk.ImageMenuItem();
+ copy_link.related_action = actions.get_action(ACTION_COPY_LINK);
+ context_menu.append(copy_link);
+
+ // Paste
+ Gtk.MenuItem paste = new Gtk.ImageMenuItem();
+ paste.related_action = actions.get_action(ACTION_PASTE);
+ context_menu.append(paste);
+
+ // Paste with formatting
+ if (compose_as_html) {
+ Gtk.MenuItem paste_format = new Gtk.ImageMenuItem();
+ paste_format.related_action = actions.get_action(ACTION_PASTE_FORMAT);
+ context_menu.append(paste_format);
+ }
+
+ context_menu.append(new Gtk.SeparatorMenuItem());
+
+ // Select all.
+ Gtk.MenuItem select_all_item = new Gtk.MenuItem.with_mnemonic(Stock.SELECT__ALL);
+ select_all_item.activate.connect(on_select_all);
+ context_menu.append(select_all_item);
+
+ context_menu.show_all();
+
+ update_actions();
+
+ return false;
+ }
+
+ private bool on_editor_key_press(Gdk.EventKey event) {
+ // widget's keypress override doesn't receive non-modifier keys when the editor processes
+ // them, regardless if true or false is called; this deals with that issue (specifically
+ // so Ctrl+Enter will send the message)
+ if (event.is_modifier == 0) {
+ if (key_press_event(event))
+ return true;
+ }
+
+ if ((event.state & Gdk.ModifierType.MOD1_MASK) != 0)
+ return false;
+
+ if ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0) {
+ if (event.keyval == Gdk.Key.Tab) {
+ child_focus(Gtk.DirectionType.TAB_FORWARD);
+ return true;
+ }
+ if (event.keyval == Gdk.Key.ISO_Left_Tab) {
+ child_focus(Gtk.DirectionType.TAB_BACKWARD);
+ return true;
+ }
+ return false;
+ }
+
+ WebKit.DOM.Document document = editor.get_dom_document();
+ if (event.keyval == Gdk.Key.Tab) {
+ document.exec_command("inserthtml", false,
+ "<span style='white-space: pre-wrap'>\t</span>");
+ return true;
+ }
+
+ if (event.keyval == Gdk.Key.ISO_Left_Tab) {
+ // If there is no selection and the character before the cursor is tab, delete it.
+ WebKit.DOM.DOMSelection selection = document.get_default_view().get_selection();
+ if (selection.is_collapsed) {
+ selection.modify("extend", "backward", "character");
+ try {
+ if (selection.get_range_at(0).get_text() == "\t")
+ selection.delete_from_document();
+ else
+ selection.collapse_to_end();
+ } catch (Error error) {
+ debug("Error handling Left Tab: %s", error.message);
+ }
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ // Resets the draft save timeout.
+ private void reset_draft_timer() {
+ if (!can_save())
+ return;
+
+ draft_save_label.label = "";
+ cancel_draft_timer();
+
+ if (drafts_folder != null)
+ draft_save_timeout_id = Timeout.add(DRAFT_TIMEOUT_MSEC, on_save_draft_timeout);
+ }
+
+ // Cancels the draft save timeout
+ private void cancel_draft_timer() {
+ if (draft_save_timeout_id == 0)
+ return;
+
+ Source.remove(draft_save_timeout_id);
+ draft_save_timeout_id = 0;
+ }
+
+ private void update_actions() {
+ // Undo/redo.
+ actions.get_action(ACTION_UNDO).sensitive = editor.can_undo();
+ actions.get_action(ACTION_REDO).sensitive = editor.can_redo();
+
+ // Clipboard.
+ actions.get_action(ACTION_CUT).sensitive = editor.can_cut_clipboard();
+ actions.get_action(ACTION_COPY).sensitive = editor.can_copy_clipboard();
+ actions.get_action(ACTION_COPY_LINK).sensitive = hover_url != null;
+ actions.get_action(ACTION_PASTE).sensitive = editor.can_paste_clipboard();
+ actions.get_action(ACTION_PASTE_FORMAT).sensitive = editor.can_paste_clipboard() && compose_as_html;
+
+ // Style toggle buttons.
+ WebKit.DOM.DOMWindow window = editor.get_dom_document().get_default_view();
+ actions.get_action(ACTION_REMOVE_FORMAT).sensitive = !window.get_selection().is_collapsed;
+
+ WebKit.DOM.Element? active = window.get_selection().focus_node as WebKit.DOM.Element;
+ if (active == null && window.get_selection().focus_node != null)
+ active = window.get_selection().focus_node.get_parent_element();
+
+ if (active != null && !action_flag) {
+ action_flag = true;
+
+ WebKit.DOM.CSSStyleDeclaration styles = window.get_computed_style(active, "");
+
+ ((Gtk.ToggleAction) actions.get_action(ACTION_BOLD)).active =
+ styles.get_property_value("font-weight") == "bold";
+
+ ((Gtk.ToggleAction) actions.get_action(ACTION_ITALIC)).active =
+ styles.get_property_value("font-style") == "italic";
+
+ ((Gtk.ToggleAction) actions.get_action(ACTION_UNDERLINE)).active =
+ styles.get_property_value("text-decoration") == "underline";
+
+ ((Gtk.ToggleAction) actions.get_action(ACTION_STRIKETHROUGH)).active =
+ styles.get_property_value("text-decoration") == "line-through";
+
+ // Font family.
+ string font_name = styles.get_property_value("font-family").down();
+ if (font_name.contains("sans-serif") ||
+ font_name.contains("arial") ||
+ font_name.contains("trebuchet") ||
+ font_name.contains("helvetica"))
+ font_sans.activate();
+ else if (font_name.contains("serif") ||
+ font_name.contains("georgia") ||
+ font_name.contains("times"))
+ font_serif.activate();
+ else if (font_name.contains("monospace") ||
+ font_name.contains("courier") ||
+ font_name.contains("console"))
+ font_monospace.activate();
+
+ // Font size.
+ int font_size;
+ styles.get_property_value("font-size").scanf("%dpx", out font_size);
+ if (font_size < 11)
+ font_small.activate();
+ else if (font_size > 20)
+ font_large.activate();
+ else
+ font_medium.activate();
+
+ action_flag = false;
+ }
+ }
+
+ private void update_from_field() {
+ from_single.visible = from_multiple.visible = from_label.visible = false;
+
+ Gee.Map<string, Geary.AccountInformation> accounts;
+ try {
+ accounts = Geary.Engine.instance.get_accounts();
+ } catch (Error e) {
+ debug("Could not fetch account info: %s", e.message);
+
+ return;
+ }
+
+ // Don't show in inline or compact modes.
+ if (state == ComposerState.INLINE || state == ComposerState.INLINE_COMPACT)
+ return;
+
+ // If there's only one account, show nothing. (From fields are hidden above.)
+ if (accounts.size <= 1)
+ return;
+
+ from_label.visible = true;
+
+ if (compose_type == ComposeType.NEW_MESSAGE) {
+ // For new messages, show the account combo-box.
+ from_label.set_use_underline(true);
+ from_label.set_mnemonic_widget(from_multiple);
+ // Composer label (with mnemonic underscore) for the account selector
+ // when choosing what address to send a message from.
+ from_label.set_text_with_mnemonic(_("_From:"));
+
+ from_multiple.visible = true;
+ from_multiple.remove_all();
+ foreach (Geary.AccountInformation a in accounts.values)
+ from_multiple.append(a.email, a.get_mailbox_address().get_full_address());
+
+ // Set the active account to the currently selected account, or failing that, set it
+ // to the first account in the list.
+ if (!from_multiple.set_active_id(account.information.email))
+ from_multiple.set_active(0);
+ } else {
+ // For other types of messages, just show the from account.
+ from_label.set_use_underline(false);
+ // Composer label (without mnemonic underscore) for the account selector
+ // when choosing what address to send a message from.
+ from_label.set_text(_("From:"));
+
+ from_single.label = account.information.get_mailbox_address().get_full_address();
+ from_single.visible = true;
+ }
+ }
+
+ private void on_from_changed() {
+ if (compose_type != ComposeType.NEW_MESSAGE)
+ return;
+
+ // Since we've set the combo box ID to the email addresses, we can
+ // fetch that and use it to grab the account from the engine.
+ string? id = from_multiple.get_active_id();
+ Geary.AccountInformation? new_account_info = null;
+
+ if (id != null) {
+ try {
+ new_account_info = Geary.Engine.instance.get_accounts().get(id);
+ if (new_account_info != null) {
+ account = Geary.Engine.instance.get_account_instance(new_account_info);
+ from = new_account_info.get_from().to_rfc822_string();
+ set_entry_completions();
+
+ open_drafts_folder_async.begin(cancellable_drafts);
+ }
+ } catch (Error e) {
+ debug("Error updating account in Composer: %s", e.message);
+ }
+ }
+
+ reset_draft_timer();
+ }
+
+ private void set_entry_completions() {
+ if (contact_list_store != null && contact_list_store.contact_store == account.get_contact_store())
+ return;
+
+ contact_list_store = new ContactListStore(account.get_contact_store());
+
+ to_entry.completion = new ContactEntryCompletion(contact_list_store);
+ cc_entry.completion = new ContactEntryCompletion(contact_list_store);
+ bcc_entry.completion = new ContactEntryCompletion(contact_list_store);
+ }
+
+}
+
diff --cc src/engine/api/geary-account-information.vala
index 9c3155d,e3e1874..793df1b
--- a/src/engine/api/geary-account-information.vala
+++ b/src/engine/api/geary-account-information.vala
@@@ -32,7 -32,8 +32,9 @@@ public class Geary.AccountInformation
private const string SENT_MAIL_FOLDER_KEY = "sent_mail_folder";
private const string SPAM_FOLDER_KEY = "spam_folder";
private const string TRASH_FOLDER_KEY = "trash_folder";
+ private const string SAVE_DRAFTS_KEY = "save_drafts";
+ private const string USE_EMAIL_SIGNATURE_KEY = "use_email_signature";
+ private const string EMAIL_SIGNATURE_KEY = "email_signature";
//
// "Retired" keys
@@@ -133,7 -134,10 +137,9 @@@
DEFAULT_PREFETCH_PERIOD_DAYS);
save_sent_mail = get_bool_value(key_file, GROUP, SAVE_SENT_MAIL_KEY, true);
ordinal = get_int_value(key_file, GROUP, ORDINAL_KEY, default_ordinal++);
+ use_email_signature = get_bool_value(key_file, GROUP, USE_EMAIL_SIGNATURE_KEY);
+ email_signature = get_escaped_string(key_file, GROUP, EMAIL_SIGNATURE_KEY);
-
-
+
if (ordinal >= default_ordinal)
default_ordinal = ordinal + 1;
@@@ -200,7 -202,8 +206,9 @@@
sent_mail_folder_path = from.sent_mail_folder_path;
spam_folder_path = from.spam_folder_path;
trash_folder_path = from.trash_folder_path;
+ save_drafts = from.save_drafts;
+ use_email_signature = from.use_email_signature;
+ email_signature = from.email_signature;
}
/**
@@@ -514,6 -517,16 +522,16 @@@
return def;
}
+
+ private string get_escaped_string(KeyFile key_file, string group, string key, string def = "") {
+ try {
+ return key_file.get_string(group, key);
+ } catch (KeyFileError err) {
- //ignore
++ // ignore
+ }
+
+ return def;
+ }
private Gee.List<string> get_string_list_value(KeyFile key_file, string group, string key) {
try {
diff --cc ui/login.glade
index a971933,1aa9f6d..c37ddfb
--- a/ui/login.glade
+++ b/ui/login.glade
@@@ -1,7 -1,13 +1,13 @@@
<?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.16.0 on Thu May 8 15:52:51 2014 -->
+<!-- Generated with glade 3.16.1 -->
<interface>
- <requires lib="gtk+" version="3.0"/>
- <!-- interface-requires gtk+ 3.10 -->
++ <requires lib="gtk+" version="3.10"/>
+ <object class="GtkAdjustment" id="adjustment1">
+ <property name="upper">100</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ </object>
+ <object class="GtkTextBuffer" id="buffer: email_signature"/>
<object class="GtkBox" id="container">
<property name="visible">True</property>
<property name="can_focus">False</property>
@@@ -293,14 -280,51 +295,53 @@@
</packing>
</child>
<child>
- <object class="GtkLabel" id="label: email_signature">
++ <object class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="xalign">0</property>
- <property name="label" translatable="yes">Signature:</property>
- <property name="selectable">True</property>
++ <property name="can_focus">True</property>
++ <property name="shadow_type">in</property>
++ <child>
++ <object class="GtkTextView" id="textview: email_signature">
++ <property name="visible">True</property>
++ <property name="sensitive">False</property>
++ <property name="can_focus">True</property>
++ <property name="wrap_mode">word</property>
++ <property name="buffer">buffer: email_signature</property>
++ </object>
++ </child>
+ </object>
+ <packing>
- <property name="left_attach">0</property>
- <property name="top_attach">7</property>
++ <property name="left_attach">1</property>
++ <property name="top_attach">9</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="check: use_email_signature">
- <property name="label" translatable="yes">_Use email signature</property>
++ <property name="label" translatable="yes">_Use email signature:</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="xalign">0</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
- <property name="top_attach">7</property>
++ <property name="top_attach">8</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
- <object class="GtkScrolledWindow" id="scrolledwindow1">
+ <object class="GtkCheckButton" id="check: save_drafts">
+ <property name="label" translatable="yes">Save dra_fts on server</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
- <property name="shadow_type">in</property>
- <child>
- <object class="GtkTextView" id="textview: email_signature">
- <property name="visible">True</property>
- <property name="sensitive">False</property>
- <property name="can_focus">True</property>
- <property name="wrap_mode">word</property>
- <property name="buffer">buffer: email_signature</property>
- </object>
- </child>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="xalign">0</property>
+ <property name="draw_indicator">True</property>
</object>
<packing>
<property name="left_attach">1</property>
@@@ -318,6 -342,6 +359,12 @@@
<child>
<placeholder/>
</child>
++ <child>
++ <placeholder/>
++ </child>
++ <child>
++ <placeholder/>
++ </child>
</object>
<packing>
<property name="expand">False</property>
@@@ -891,11 -876,8 +932,11 @@@
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="xpad">12</property>
- <property name="label" translatable="yes">_Download mail:</property>
+ <property name="label" translatable="yes">_Download mail</property>
<property name="use_underline">True</property>
- <style>
- <class name="dim-label"/>
- </style>
++ <style>
++ <class name="dim-label"/>
++ </style>
</object>
<packing>
<property name="left_attach">0</property>
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]