geary/src/client/composer/composer-window.vala

1710 lines
68 KiB
Vala

/* Copyright 2011-2013 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.
*/
// Window for sending messages.
public class ComposerWindow : Gtk.Window {
public enum ComposeType {
NEW_MESSAGE,
REPLY,
REPLY_ALL,
FORWARD
}
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 DEFAULT_TITLE = _("New Message");
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 string draft_save_label_style = """
.draft-save-label {
color: shade (@bg_color, 0.6);
}""";
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 ComposeType compose_type { get; private set; default = ComposeType.NEW_MESSAGE; }
// True if composer can't close immediately (i.e. it's saving a draft)
public bool delayed_close { get; private set; default = false; }
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;
private Gtk.Entry subject_entry;
private Gtk.Button close_button;
private Gtk.Button send_button;
private Gtk.Label message_overlay_label;
private WebKit.DOM.Element? prev_selected_link = null;
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 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 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;
private 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;
private Gtk.UIManager ui;
public ComposerWindow(Geary.Account account, ComposeType compose_type,
Geary.Email? referred = null, bool is_referred_draft = false) {
this.account = account;
this.compose_type = compose_type;
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);
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_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);
// 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.
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();
(builder.get_object("to") as Gtk.EventBox).add(to_entry);
cc_entry = new EmailEntry();
(builder.get_object("cc") as Gtk.EventBox).add(cc_entry);
bcc_entry = new EmailEntry();
(builder.get_object("bcc") as Gtk.EventBox).add(bcc_entry);
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");
GtkUtil.apply_style(draft_save_label, draft_save_label_style);
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.add_overlay(message_overlay_label);
title = DEFAULT_TITLE;
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);
add_accel_group(ui.get_accel_group());
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) {
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);
}
try {
Geary.Folder? draft_folder = account.get_special_folder(Geary.SpecialFolderType.DRAFTS);
if (draft_folder != null && is_referred_draft)
draft_id = referred.id;
} catch (Error e) {
debug("Error looking up special folder: %s", e.message);
}
add_attachments(referred.attachments);
break;
case ComposeType.REPLY:
case ComposeType.REPLY_ALL:
string? sender_address = account.information.get_mailbox_address().address;
to = Geary.RFC822.Utils.create_to_addresses_for_reply(referred, sender_address);
if (compose_type == ComposeType.REPLY_ALL)
cc = Geary.RFC822.Utils.create_cc_addresses_for_reply_all(referred, sender_address);
subject = Geary.RFC822.Utils.create_subject_for_reply(referred);
in_reply_to = referred.message_id.value;
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 = Geary.RFC822.Utils.create_subject_for_forward(referred);
body_html = "\n\n" + Geary.RFC822.Utils.quote_email_for_forward(referred, true);
add_attachments(referred.attachments);
pending_attachments = referred.attachments;
break;
}
}
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_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.spell_check_changed.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.begin(cancellable_drafts);
}
public ComposerWindow.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_uri(attachment));
foreach (string attachment in headers.get("attachment"))
add_attachment(File.new_for_uri(attachment));
}
}
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.
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();
body.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() {
set_default_size(680, 600);
base.show_all();
update_from_field();
}
public bool should_close() {
if (!editor.can_undo())
return true;
present();
AlertDialog dialog;
if (drafts_folder == null) {
dialog = new ConfirmationDialog(this,
_("Do you want to discard the unsaved message?"), null, Stock._DISCARD);
} else {
dialog = new TernaryConfirmationDialog(this,
_("Do you want to discard this message?"), null, Stock._KEEP, Stock._DISCARD,
Gtk.ResponseType.CLOSE);
}
Gtk.ResponseType response = dialog.run();
if (response == Gtk.ResponseType.CANCEL || response == Gtk.ResponseType.DELETE_EVENT) {
return false; // Cancel
} else if (response == Gtk.ResponseType.OK) {
save_and_exit.begin(); // Save
return false;
} else {
delete_and_exit.begin(); // Discard
return false;
}
}
public override bool delete_event(Gdk.EventAny event) {
return !should_close();
}
private void on_close() {
if (should_close())
destroy();
}
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(this,
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();
hide();
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();
destroy(); // Only close window after draft is deleted; this closes the drafts folder.
}
// Returns the drafts folder for the current From account.
private async void open_drafts_folder(Cancellable cancellable) throws Error {
yield close_drafts_folder(cancellable);
Geary.FolderSupport.Create? folder = account.get_special_folder(Geary.SpecialFolderType.DRAFTS)
as Geary.FolderSupport.Create;
if (folder == null)
return; // No drafts folder.
yield folder.open_async(Geary.Folder.OpenFlags.FAST_OPEN, cancellable);
drafts_folder = folder;
}
private async void close_drafts_folder(Cancellable? cancellable = null) throws Error {
if (drafts_folder == null)
return;
// Close existing folder.
yield drafts_folder.close_async(cancellable);
drafts_folder = null;
}
// Save to the draft folder, if available.
// Note that drafts are NOT "linkified."
private bool save_draft() {
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)
return;
draft_save_label.label = DRAFT_SAVING_TEXT;
draft_save_timeout_id = 0;
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)), 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;
}
}
// Prevents user from editing anything. Used while waiting for draft to save before exiting window.
private void make_gui_insensitive() {
// Halt draft timer.
if (draft_save_timeout_id != 0)
Source.remove(draft_save_timeout_id);
// Disable all actions.
List<weak Gtk.Action> actions = actions.list_actions();
foreach (Gtk.Action a in actions)
a.sensitive = false;
// Disable buttons.
close_button.sensitive = send_button.sensitive =
add_attachment_button.sensitive = pending_attachments_button.sensitive = false;
// Disable editable widgets.
editor.sensitive = to_entry.sensitive = cc_entry.sensitive = bcc_entry.sensitive =
subject_entry.sensitive = from_multiple.sensitive = false;
}
private async void save_and_exit() {
delayed_close = true;
make_gui_insensitive();
// Do the save.
yield save_async(null);
destroy();
}
private async void delete_and_exit() {
delayed_close = true;
make_gui_insensitive();
// Do the delete.
yield delete_draft_async();
destroy();
}
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(this);
} 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(this, _("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));
attachments_box.show_all();
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;
}
}
check_pending_attachments();
}
private void on_subject_changed() {
title = Geary.String.is_empty(subject_entry.text.strip()) ? DEFAULT_TITLE :
subject_entry.text.strip();
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);
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 (get_focus() == editor)
editor.cut_clipboard();
else if (get_focus() is Gtk.Editable)
((Gtk.Editable) get_focus()).cut_clipboard();
}
private void on_copy() {
if (get_focus() == editor)
editor.copy_clipboard();
else if (get_focus() is Gtk.Editable)
((Gtk.Editable) 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 (get_focus() == editor)
get_clipboard(Gdk.SELECTION_CLIPBOARD).request_text(on_clipboard_text_received);
else if (get_focus() is Gtk.Editable)
((Gtk.Editable) get_focus()).paste_clipboard();
}
private void on_paste_with_formatting() {
if (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"), this);
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,
ComposerWindow 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 on_spell_check_changed() {
editor.settings.enable_spell_checking = GearyApplication.instance.config.spell_check;
}
public override bool key_press_event(Gdk.EventKey event) {
update_actions();
switch (Gdk.keyval_name(event.keyval)) {
case "Return":
case "KP_Enter":
if ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0 && 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;
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_key_press(Gdk.EventKey event) {
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() {
draft_save_label.label = "";
if (draft_save_timeout_id != 0)
Source.remove(draft_save_timeout_id);
if (drafts_folder != null)
draft_save_timeout_id = Timeout.add(DRAFT_TIMEOUT_MSEC, save_draft);
}
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;
}
// 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_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_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.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);
}
public override void destroy() {
close_drafts_folder.begin();
}
}