geary/src/client/composer/composer-window.vala
Eric Gregory 3467c72035 Closes #6209 Precise support for GIR WebKitGTK binding. Rolls back to deprecated context menu in composer for compatibility with WebKitGTK 1.8
Squashed commit of the following:

commit 6e066374da45dd602ee1ca3c9bc5f77b9411b6f4
Author: Eric Gregory <eric@yorba.org>
Date:   Tue Jan 15 11:36:32 2013 -0800

    Closes #6209 Precise support for GIR WebKitGTK binding. Rolls back to deprecated context menu in composer for compatibility with WebKitGTK 1.8
2013-01-15 13:04:30 -08:00

1062 lines
42 KiB
Vala

/* Copyright 2011-2012 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 {
private const string DEFAULT_TITLE = _("New Message");
private const string ACTION_UNDO = "undo";
private const string ACTION_REDO = "redo";
private const string ACTION_CUT = "cut";
private const string ACTION_COPY = "copy";
private const string ACTION_COPY_LINK = "copy link";
private const string ACTION_PASTE = "paste";
private const string ACTION_PASTE_FORMAT = "paste with formatting";
private const string ACTION_BOLD = "bold";
private const string ACTION_ITALIC = "italic";
private const string ACTION_UNDERLINE = "underline";
private const string ACTION_STRIKETHROUGH = "strikethrough";
private const string ACTION_REMOVE_FORMAT = "removeformat";
private const string ACTION_INDENT = "indent";
private const string ACTION_OUTDENT = "outdent";
private const string ACTION_JUSTIFY_LEFT = "justifyleft";
private const string ACTION_JUSTIFY_RIGHT = "justifyright";
private const string ACTION_JUSTIFY_CENTER = "justifycenter";
private const string ACTION_JUSTIFY_FULL = "justifyfull";
private const string ACTION_FONT = "font";
private const string ACTION_FONT_SIZE = "fontsize";
private const string ACTION_COLOR = "color";
private const string ACTION_INSERT_LINK = "insertlink";
private const string URI_LIST_MIME_TYPE = "text/uri-list";
private const string FILE_URI_PREFIX = "file://";
private const string REPLY_ID = "reply";
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;
}
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;
}
</style>
</head><body id="reply"></body></html>""";
// Signal sent when the "Send" button is clicked.
public signal void send(ComposerWindow composer);
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 {
reply_body = value;
editor.load_string(HTML_BODY, "text/html", "UTF8", "");
}
}
private string? reply_body = null;
private Gee.Set<File> attachment_files = new Gee.HashSet<File>(File.hash, (EqualFunc) File.equal);
private EmailEntry to_entry;
private EmailEntry cc_entry;
private EmailEntry bcc_entry;
private Gtk.Entry subject_entry;
private Gtk.Button cancel_button;
private Gtk.Button send_button;
private Gtk.ToggleToolButton font_button;
private Gtk.ToggleToolButton font_size_button;
private Gtk.Label message_overlay_label;
private Gtk.Menu? context_menu = null;
private WebKit.DOM.Element? prev_selected_link = null;
private Gtk.Box attachments_box;
private Gtk.Button add_attachment_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.RadioMenuItem font_small;
private Gtk.RadioMenuItem font_medium;
private Gtk.RadioMenuItem font_large;
private Gtk.Menu font_size_menu;
private Gtk.RadioMenuItem font_sans;
private Gtk.RadioMenuItem font_serif;
private Gtk.RadioMenuItem font_monospace;
private Gtk.Menu font_menu;
private Gtk.ActionGroup actions;
private string? hover_url = null;
private bool action_flag = false;
private bool is_attachment_overlay_visible = 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;
private ContactEntryCompletion[] contact_entry_completions;
public ComposerWindow(Geary.ContactStore? contact_store, Geary.ComposedEmail? prefill = null) {
contact_entry_completions = {
new ContactEntryCompletion(contact_store),
new ContactEntryCompletion(contact_store),
new ContactEntryCompletion(contact_store)
};
setup_drag_destination(this);
add_events(Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK);
Gtk.Builder 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;
cancel_button = builder.get_object("Cancel") as Gtk.Button;
cancel_button.clicked.connect(on_cancel);
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);
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.
to_entry = new EmailEntry();
to_entry.completion = contact_entry_completions[0];
(builder.get_object("to") as Gtk.EventBox).add(to_entry);
cc_entry = new EmailEntry();
cc_entry.completion = contact_entry_completions[1];
(builder.get_object("cc") as Gtk.EventBox).add(cc_entry);
bcc_entry = new EmailEntry();
bcc_entry.completion = contact_entry_completions[2];
(builder.get_object("bcc") as Gtk.EventBox).add(bcc_entry);
subject_entry = builder.get_object("subject") as Gtk.Entry;
Gtk.Alignment message_area = builder.get_object("message area") as Gtk.Alignment;
actions = builder.get_object("compose actions") as Gtk.ActionGroup;
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);
Gtk.Toolbar compose_toolbar = (Gtk.Toolbar) builder.get_object("compose_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_action);
actions.get_action(ACTION_ITALIC).activate.connect(on_action);
actions.get_action(ACTION_UNDERLINE).activate.connect(on_action);
actions.get_action(ACTION_STRIKETHROUGH).activate.connect(on_action);
actions.get_action(ACTION_REMOVE_FORMAT).activate.connect(on_remove_format);
actions.get_action(ACTION_INDENT).activate.connect(on_action);
actions.get_action(ACTION_OUTDENT).activate.connect(on_action);
actions.get_action(ACTION_JUSTIFY_LEFT).activate.connect(on_action);
actions.get_action(ACTION_JUSTIFY_RIGHT).activate.connect(on_action);
actions.get_action(ACTION_JUSTIFY_CENTER).activate.connect(on_action);
actions.get_action(ACTION_JUSTIFY_FULL).activate.connect(on_action);
actions.get_action(ACTION_FONT).activate.connect(on_select_font);
actions.get_action(ACTION_FONT_SIZE).activate.connect(on_select_font_size);
actions.get_action(ACTION_COLOR).activate.connect(on_select_color);
actions.get_action(ACTION_INSERT_LINK).activate.connect(on_insert_link);
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");
if (prefill != null) {
if (prefill.from != null)
from = prefill.from.to_rfc822_string();
if (prefill.to != null)
to = prefill.to.to_rfc822_string();
if (prefill.cc != null)
cc = prefill.cc.to_rfc822_string();
if (prefill.bcc != null)
bcc = prefill.bcc.to_rfc822_string();
if (prefill.in_reply_to != null)
in_reply_to = prefill.in_reply_to.value;
if (prefill.references != null)
references = prefill.references.to_rfc822_string();
if (prefill.subject != null)
subject = prefill.subject.value;
if (prefill.body_html != null)
reply_body = prefill.body_html.buffer.to_string();
if (reply_body == null && prefill.body_text != null)
reply_body = "<pre>" + prefill.body_text.buffer.to_string();
}
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.button_press_event.connect(on_button_press_event);
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);
// only do this after setting reply_body
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_button = builder.get_object("font button") as Gtk.ToggleToolButton;
font_size_button = builder.get_object("font size button") as Gtk.ToggleToolButton;
// Build font menu.
font_menu = new Gtk.Menu();
font_menu.deactivate.connect(on_deactivate_font_menu);
font_menu.attach_to_widget(font_button, null);
font_sans = new Gtk.RadioMenuItem.with_label(new SList<Gtk.RadioMenuItem>(),
_("Sans Serif"));
font_sans.activate.connect(on_font_sans);
font_menu.append(font_sans);
font_serif = new Gtk.RadioMenuItem.with_label_from_widget(font_sans, _("Serif"));
font_serif.activate.connect(on_font_serif);
font_menu.append(font_serif);
font_monospace = new Gtk.RadioMenuItem.with_label_from_widget(font_sans,
_("Fixed width"));
font_monospace.activate.connect(on_font_monospace);
font_menu.append(font_monospace);
// Build font size menu.
font_size_menu = new Gtk.Menu();
font_size_menu.deactivate.connect(on_deactivate_font_size_menu);
font_size_menu.attach_to_widget(font_size_button, null);
font_small = new Gtk.RadioMenuItem.with_label(new SList<Gtk.RadioMenuItem>(), _("Small"));
font_small.activate.connect(on_font_size_small);
font_size_menu.append(font_small);
font_medium = new Gtk.RadioMenuItem.with_label_from_widget(font_small, _("Medium"));
font_medium.activate.connect(on_font_size_medium);
font_size_menu.append(font_medium);
font_large = new Gtk.RadioMenuItem.with_label_from_widget(font_small, _("Large"));
font_large.activate.connect(on_font_size_large);
font_size_menu.append(font_large);
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;
s.enable_default_context_menu = false; // Deprecated, still needed for Precise
editor.settings = s;
scroll.add(editor);
scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
add(box);
validate_send_button();
// 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(compose_toolbar);
chain.append(attachments_box);
chain.append(button_area);
box.set_focus_chain(chain);
if(prefill != null) {
foreach(File attachment_file in prefill.attachment_files) {
add_attachment(attachment_file);
}
}
}
private void on_load_finished(WebKit.WebFrame frame) {
WebKit.DOM.HTMLElement? reply = editor.get_dom_document().get_element_by_id(
REPLY_ID) as WebKit.DOM.HTMLElement;
assert(reply != null);
if (!Geary.String.is_empty(reply_body)) {
try {
reply.set_inner_html("<br /><br />" + reply_body + "<br />");
} catch (Error e) {
debug("Failed to load email for reply: %s", e.message);
}
}
// 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();
reply.focus();
}
bind_event(editor,"a", "click", (Callback) on_link_clicked, this);
update_actions();
}
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(
Geary.RFC822.MailboxAddresses? default_from = null, DateTime? date_override = null) {
Geary.ComposedEmail email = new Geary.ComposedEmail(
date_override ?? new DateTime.now_local(),
Geary.String.is_empty(from)
? default_from
: 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 = new Geary.RFC822.MessageID(in_reply_to);
if (!Geary.String.is_empty(references))
email.references = new Geary.RFC822.MessageIDList.from_rfc822_string(references);
if (!Geary.String.is_empty(subject))
email.subject = new Geary.RFC822.Subject(subject);
email.attachment_files.add_all(attachment_files);
email.body_html = new Geary.RFC822.Text(new Geary.Memory.StringBuffer(get_html()));
email.body_text = new Geary.RFC822.Text(new Geary.Memory.StringBuffer(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();
}
public bool should_close() {
// TODO: Check if the message was (automatically) saved
if (editor.can_undo()) {
ConfirmationDialog dialog = new ConfirmationDialog(this,
_("Do you want to discard the unsaved message?"), null, Gtk.Stock.DISCARD);
if (dialog.run() != Gtk.ResponseType.OK)
return false;
}
return true;
}
public override bool delete_event(Gdk.EventAny event) {
return !should_close();
}
private void on_cancel() {
if (should_close())
destroy();
}
private bool should_send() {
if (Geary.String.is_empty(subject.strip()) ||
((Geary.String.is_empty(get_text()) && attachment_files.size == 0))) {
ConfirmationDialog dialog = new ConfirmationDialog(this,
_("Send message with an empty subject and/or body?"), null, Gtk.Stock.OK);
if (dialog.run() != Gtk.ResponseType.OK)
return false;
}
return true;
}
private void on_send() {
if (should_send()) {
linkify_document(editor.get_dom_document());
send(this);
}
}
private void on_add_attachment_button_clicked() {
bool finished = true;
do {
Gtk.FileChooserDialog dialog = new Gtk.FileChooserDialog(
_("Choose a file"), this, Gtk.FileChooserAction.OPEN,
Gtk.Stock.CANCEL, Gtk.ResponseType.CANCEL,
_("_Attach"), Gtk.ResponseType.ACCEPT);
dialog.set_local_only(false);
dialog.set_select_multiple(true);
if (dialog.run() == Gtk.ResponseType.ACCEPT) {
foreach (File file in dialog.get_files()) {
if (!add_attachment(file)) {
finished = false;
break;
}
}
} else {
finished = true;
}
dialog.destroy();
} while (!finished);
}
private void attachment_failed(string msg) {
ErrorDialog dialog = new ErrorDialog(GearyApplication.instance.get_main_window(),
_("Cannot add attachment"), msg);
dialog.run();
}
private bool add_attachment(File attachment_file) {
if (!attachment_file.query_exists()) {
attachment_failed(_("\"%s\" does not exist").printf(attachment_file.get_path()));
return false;
}
if (attachment_file.query_file_type(FileQueryInfoFlags.NONE) == FileType.DIRECTORY) {
attachment_failed(_("\"%s\" is a folder").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);
attachment_failed(_("\"%s\" could not be opened for reading").printf(attachment_file.get_path()));
return false;
}
if (!attachment_files.add(attachment_file)) {
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);
Gtk.Label label = new Gtk.Label(attachment_file.get_basename());
box.pack_start(label);
label.halign = Gtk.Align.START;
label.xpad = 4;
Gtk.Button remove_button = new Gtk.Button.from_stock(Gtk.Stock.REMOVE);
box.pack_start(remove_button, false, false);
remove_button.clicked.connect(() => remove_attachment(attachment_file, box));
attachments_box.show_all();
return true;
}
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;
}
}
}
private void on_subject_changed() {
title = Geary.String.is_empty(subject_entry.text.strip()) ? DEFAULT_TITLE :
subject_entry.text.strip();
}
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);
}
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_select_font() {
if (!font_button.active)
return;
font_menu.show_all();
font_menu.popup(null, null, GtkUtil.menu_popup_relative, 0, 0);
}
private void on_deactivate_font_menu() {
font_button.active = false;
}
private void on_select_font_size() {
if (!font_size_button.active)
return;
font_size_menu.show_all();
font_size_menu.popup(null, null, GtkUtil.menu_popup_relative, 0, 0);
}
private void on_deactivate_font_size_menu() {
font_size_button.active = false;
}
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() {
Gtk.ColorChooserDialog dialog = new Gtk.ColorChooserDialog("Select Color", this);
if (dialog.run() == Gtk.ResponseType.OK) {
string color = dialog.get_rgba().to_string();
editor.get_dom_document().exec_command("forecolor", false, color);
}
dialog.destroy();
}
private void on_insert_link() {
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(Gtk.Stock. REMOVE, Gtk.ResponseType.REJECT);
}
dialog.add_buttons(Gtk.Stock.CANCEL, Gtk.ResponseType.CANCEL, Gtk.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 editor.get_dom_document().get_body().get_inner_text();
}
private bool on_navigation_policy_decision_requested(WebKit.WebFrame frame,
WebKit.NetworkRequest request, WebKit.WebNavigationAction navigation_action,
WebKit.WebPolicyDecision policy_decision) {
policy_decision.ignore();
link_dialog(request.uri);
return true;
}
private void on_hovering_over_link(string? title, string? url) {
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;
case "Escape":
if (should_close()) {
destroy();
return true;
}
break;
}
return base.key_press_event(event);
}
private bool on_button_press_event(Gdk.EventButton event) {
if (event.button != 3)
return true;
context_menu = new Gtk.Menu();
// 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.MenuItem());
// 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
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.MenuItem());
// Select all.
Gtk.MenuItem select_all_item = new Gtk.ImageMenuItem.from_stock(Gtk.Stock.SELECT_ALL, null);
select_all_item.activate.connect(on_select_all);
context_menu.append(select_all_item);
context_menu.show_all();
context_menu.popup(null, null, null, event.button, event.time);
update_actions();
return false;
}
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();
// Style toggle buttons.
WebKit.DOM.DOMWindow window = editor.get_dom_document().get_default_view();
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;
}
}
}