Closes #4550. HTML composition.

This commit is contained in:
Eric Gregory 2012-04-04 19:01:55 -07:00
parent 19b8e871ab
commit 459af216b2
11 changed files with 714 additions and 104 deletions

View file

@ -301,16 +301,21 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
return builder;
}
// Loads a UI file (in the ui directory) into the UI manager.
public void load_ui_file(string ui_filename) {
// Loads a UI file (in the ui directory) into the specified UI manager.
public void load_ui_file_for_manager(Gtk.UIManager ui, string ui_filename) {
try {
ui_manager.add_ui_from_file(get_resource_directory().get_child("ui").get_child(
ui.add_ui_from_file(get_resource_directory().get_child("ui").get_child(
ui_filename).get_path());
} catch(GLib.Error error) {
warning("Unable to create Gtk.UIManager: %s".printf(error.message));
}
}
// Loads a UI file (in the ui directory) into the UI manager.
public void load_ui_file(string ui_filename) {
load_ui_file_for_manager(ui_manager, ui_filename);
}
public Gtk.Window get_main_window() {
return controller.main_window;
}

View file

@ -8,12 +8,32 @@
public class ComposerWindow : Gtk.Window {
private static string DEFAULT_TITLE = _("New Message");
private Gtk.Entry to_entry;
private Gtk.Entry cc_entry;
private Gtk.Entry bcc_entry;
private Gtk.Entry subject_entry;
private Gtk.SourceView message_text = new Gtk.SourceView();
private Gtk.Button send_button;
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: 11pt !important;
}
blockquote {
margin: 10px;
padding: 5px;
background-color: white;
border: 0;
border-left: 3px #aaa solid;
}
</style>
</head><body>
<p id="top"></p><br /><br />
<span id="reply"></span>
<br />
</body></html>""";
// Signal sent when the "Send" button is clicked.
public signal void send(ComposerWindow composer);
public string from { get; set; }
@ -41,12 +61,24 @@ public class ComposerWindow : Gtk.Window {
}
public string message {
owned get { return message_text.buffer.text; }
set { message_text.buffer.text = value; }
owned get { return get_html(); }
set {
reply_body = value;
editor.load_string(HTML_BODY, "text/html", "UTF8", "");
}
}
// Signal sent when the "Send" button is clicked.
public signal void send(ComposerWindow composer);
private string? reply_body = null;
private Gtk.Entry to_entry;
private Gtk.Entry cc_entry;
private Gtk.Entry bcc_entry;
private Gtk.Entry subject_entry;
private Gtk.Button send_button;
private Gtk.Label message_overlay_label;
private WebKit.WebView editor;
private Gtk.UIManager ui;
public ComposerWindow(Geary.ComposedEmail? prefill = null) {
add_events(Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK);
@ -60,10 +92,21 @@ public class ComposerWindow : Gtk.Window {
cc_entry = builder.get_object("cc") as Gtk.Entry;
bcc_entry = builder.get_object("bcc") as Gtk.Entry;
subject_entry = builder.get_object("subject") as Gtk.Entry;
Gtk.ScrolledWindow scroll = builder.get_object("scrolledwindow") as Gtk.ScrolledWindow;
scroll.add(message_text);
message_text.set_wrap_mode(Gtk.WrapMode.WORD_CHAR);
((Gtk.SourceBuffer) message_text.buffer).highlight_matching_brackets = false;
Gtk.Alignment msg_area = builder.get_object("message area") as Gtk.Alignment;
Gtk.ActionGroup 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);
msg_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);
@ -71,6 +114,37 @@ public class ComposerWindow : Gtk.Window {
cc_entry.changed.connect(validate_send_button);
bcc_entry.changed.connect(validate_send_button);
actions.get_action("undo").activate.connect(on_action);
actions.get_action("redo").activate.connect(on_action);
actions.get_action("cut").activate.connect(on_cut);
actions.get_action("copy").activate.connect(on_copy);
actions.get_action("paste").activate.connect(on_paste);
actions.get_action("bold").activate.connect(on_action);
actions.get_action("italic").activate.connect(on_action);
actions.get_action("underline").activate.connect(on_action);
actions.get_action("strikethrough").activate.connect(on_action);
actions.get_action("removeformat").activate.connect(on_remove_format);
actions.get_action("indent").activate.connect(on_action);
actions.get_action("outdent").activate.connect(on_action);
actions.get_action("justifyleft").activate.connect(on_action);
actions.get_action("justifyright").activate.connect(on_action);
actions.get_action("justifycenter").activate.connect(on_action);
actions.get_action("justifyfull").activate.connect(on_action);
actions.get_action("font").activate.connect(on_select_font);
actions.get_action("color").activate.connect(on_select_color);
actions.get_action("insertlink").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();
@ -86,19 +160,40 @@ public class ComposerWindow : Gtk.Window {
references = prefill.references.to_rfc822_string();
if (prefill.subject != null)
subject = prefill.subject.value;
if (prefill.body != null)
message = prefill.body.buffer.to_utf8();
if (!Geary.String.is_empty(to) && !Geary.String.is_empty(subject))
message_text.grab_focus();
else if (!Geary.String.is_empty(to))
subject_entry.grab_focus();
if (prefill.body_html != null)
reply_body = prefill.body_html.buffer.to_utf8();
if (reply_body == null && prefill.body_text != null)
reply_body = "<pre>" + prefill.body_text.buffer.to_utf8();
}
editor = new WebKit.WebView();
editor.set_editable(true);
editor.load_finished.connect(on_load_finished);
editor.hovering_over_link.connect(on_hovering_over_link);
editor.load_string(HTML_BODY, "text/html", "UTF8", ""); // only do this after setting reply_body
if (!Geary.String.is_empty(to) && !Geary.String.is_empty(subject))
editor.grab_focus();
else if (!Geary.String.is_empty(to))
subject_entry.grab_focus();
editor.navigation_policy_decision_requested.connect(on_navigation_policy_decision_requested);
editor.new_window_policy_decision_requested.connect(on_navigation_policy_decision_requested);
WebKit.WebSettings s = new WebKit.WebSettings();
s.enable_spell_checking = true;
s.auto_load_images = false;
s.enable_default_context_menu = true;
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();
message_text.move_cursor(Gtk.MovementStep.BUFFER_ENDS, -1, false);
}
public Geary.ComposedEmail get_composed_email(
@ -128,20 +223,21 @@ public class ComposerWindow : Gtk.Window {
if (!Geary.String.is_empty(subject))
email.subject = new Geary.RFC822.Subject(subject);
email.body = new Geary.RFC822.Text(new Geary.Memory.StringBuffer(message));
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()));
return email;
}
public override void show_all() {
set_default_size(650, 550);
set_default_size(680, 600);
base.show_all();
}
private bool should_close() {
// TODO: Check if the message was (automatically) saved
if (((Gtk.SourceBuffer) message_text.buffer).can_undo) {
if (editor.can_undo()) {
var dialog = new Gtk.MessageDialog(this, 0,
Gtk.MessageType.WARNING, Gtk.ButtonsType.NONE,
_("Do you want to discard the unsaved message?"));
@ -176,6 +272,114 @@ public class ComposerWindow : Gtk.Window {
!Geary.String.is_empty(bcc_entry.get_text().strip());
}
private void on_action(Gtk.Action action) {
editor.get_dom_document().exec_command(action.get_name(), false, "");
}
private void on_cut() {
editor.cut_clipboard();
}
private void on_copy() {
editor.copy_clipboard();
}
private void on_paste() {
editor.paste_clipboard();
}
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() {
Gtk.FontChooserDialog dialog = new Gtk.FontChooserDialog("Select font", this);
if (dialog.run() == Gtk.ResponseType.OK) {
editor.get_dom_document().exec_command("fontname", false, dialog.get_font_family().
get_name());
editor.get_dom_document().exec_command("fontsize", false,
(((double) dialog.get_font_size()) / 4000.0).to_string());
}
dialog.destroy();
}
private void on_select_color() {
Gtk.ColorSelectionDialog dialog = new Gtk.ColorSelectionDialog("Select Color");
if (dialog.run() == Gtk.ResponseType.OK) {
string color = ((Gtk.ColorSelection) dialog.get_color_selection()).
current_rgba.to_string();
editor.get_dom_document().exec_command("forecolor", false, color);
}
dialog.destroy();
}
private void on_insert_link() {
link_dialog("http://");
}
private void link_dialog(string link) {
Gtk.Dialog dialog = new Gtk.Dialog.with_buttons("", this, 0,
Gtk.Stock.CANCEL, Gtk.ResponseType.CANCEL, Gtk.Stock.OK, Gtk.ResponseType.OK);
Gtk.Entry entry = new Gtk.Entry();
dialog.get_content_area().pack_start(new Gtk.Label("Link URL:"));
dialog.get_content_area().pack_start(entry);
dialog.set_default_response(Gtk.ResponseType.OK);
dialog.show_all();
entry.set_text(link);
if (dialog.run() == Gtk.ResponseType.OK) {
if (!Geary.String.is_empty(entry.text.strip()))
editor.get_dom_document().exec_command("createLink", false, entry.text);
else
editor.get_dom_document().exec_command("unlink", false, "");
}
dialog.destroy();
}
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 void on_load_finished(WebKit.WebFrame frame) {
if (reply_body == null)
return;
WebKit.DOM.HTMLElement? reply = editor.get_dom_document().get_element_by_id(
REPLY_ID) as WebKit.DOM.HTMLElement;
assert(reply != null);
try {
reply.set_inner_html(reply_body);
} catch (Error e) {
debug("Failed to load email for reply: %s", e.message);
}
}
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;
}
public override bool key_press_event(Gdk.EventKey event) {
bool handled = true;

View file

@ -17,7 +17,7 @@ public class MainToolbar : Gtk.Box {
GearyApplication.instance.load_ui_file("toolbar_mark_menu.ui");
mark_menu = GearyApplication.instance.ui_manager.get_widget("/ui/ToolbarMarkMenu") as Gtk.Menu;
GearyApplication.instance.load_ui_file("toolbar_menu.ui");
menu = GearyApplication.instance.ui_manager.get_widget("/ui/ToolbarMenu") as Gtk.Menu;
@ -48,48 +48,27 @@ public class MainToolbar : Gtk.Box {
as Gtk.ToolButton;
archive_message.set_related_action(GearyApplication.instance.actions.get_action(
GearyController.ACTION_DELETE_MESSAGE));
mark_menu_button = builder.get_object(GearyController.ACTION_MARK_AS_MENU) as Gtk.ToolButton;
mark_menu_button.set_related_action(GearyApplication.instance.actions.get_action(
GearyController.ACTION_MARK_AS_MENU));
mark_menu.attach_to_widget(mark_menu_button, null);
mark_menu_button.clicked.connect(on_show_mark_menu);
menu_button = builder.get_object("menu_button") as Gtk.ToolButton;
menu.attach_to_widget(menu_button, null);
menu_button.clicked.connect(on_show_menu);
toolbar.get_style_context().add_class("primary-toolbar");
add(toolbar);
}
private void on_show_menu() {
menu.popup(null, null, popup_pos, 0, 0);
menu.popup(null, null, menu_popup_relative, 0, 0);
}
public void on_show_mark_menu() {
mark_menu.popup(null, null, popup_pos, 0, 0);
}
// Calculates the position of menu popups. It handles both menus in the toolbar.
private void popup_pos(Gtk.Menu menu, out int x, out int y, out bool push_in) {
menu.realize();
int rx, ry;
get_window().get_origin(out rx, out ry);
Gtk.Allocation menu_button_allocation;
if (menu == mark_menu) {
mark_menu_button.get_allocation(out menu_button_allocation);
} else {
menu_button.get_allocation(out menu_button_allocation);
}
Gtk.Allocation toolbar_allocation;
get_allocation(out toolbar_allocation);
x = rx + menu_button_allocation.x;
y = ry + toolbar_allocation.height;
push_in = false;
private void on_show_mark_menu() {
mark_menu.popup(null, null, menu_popup_relative, 0, 0);
}
}

View file

@ -0,0 +1,26 @@
/* 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.
*/
// Use this MenuPositionFunc to position a popup menu relative to a widget
// with Gtk.Menu.popup().
//
// You *must* attach the button widget with Gtk.Menu.attach_to_widget() before
// this function can be used.
public void menu_popup_relative(Gtk.Menu menu, out int x, out int y, out bool push_in) {
menu.realize();
int rx, ry;
menu.get_attach_widget().get_window().get_origin(out rx, out ry);
Gtk.Allocation menu_button_allocation;
menu.get_attach_widget().get_allocation(out menu_button_allocation);
x = rx + menu_button_allocation.x;
y = ry + menu_button_allocation.y + menu_button_allocation.height;
push_in = false;
}

View file

@ -28,6 +28,7 @@ client_src = [
'util/util-email.vala',
'util/util-keyring.vala',
'util/util-menu.vala',
]
gsettings_schemas = [

View file

@ -24,7 +24,8 @@ public class Geary.ComposedEmail : Object {
public Geary.Email? reply_to_email { get; set; default = null; }
public RFC822.MessageIDList? references { get; set; default = null; }
public RFC822.Subject? subject { get; set; default = null; }
public RFC822.Text? body { get; set; default = null; }
public RFC822.Text? body_text { get; set; default = null; }
public RFC822.Text? body_html { get; set; default = null; }
public ComposedEmail(DateTime date, RFC822.MailboxAddresses from,
RFC822.MailboxAddresses? to = null) {
@ -42,8 +43,10 @@ public class Geary.ComposedEmail : Object {
subject = create_subject_for_reply(source);
set_reply_references(source);
body = new RFC822.Text(new Geary.Memory.StringBuffer("\n\n" +
Geary.RFC822.Utils.quote_email_for_reply(source)));
body_text = new RFC822.Text(new Geary.Memory.StringBuffer("\n\n" +
Geary.RFC822.Utils.quote_email_for_reply(source, false)));
body_html = new RFC822.Text(new Geary.Memory.StringBuffer("\n\n" +
Geary.RFC822.Utils.quote_email_for_reply(source, true)));
}
public ComposedEmail.as_reply_all(DateTime date, RFC822.MailboxAddresses from, Geary.Email source) {
@ -56,8 +59,10 @@ public class Geary.ComposedEmail : Object {
subject = create_subject_for_reply(source);
set_reply_references(source);
body = new RFC822.Text(new Geary.Memory.StringBuffer("\n\n" +
Geary.RFC822.Utils.quote_email_for_reply(source)));
body_text = new RFC822.Text(new Geary.Memory.StringBuffer("\n\n" +
Geary.RFC822.Utils.quote_email_for_reply(source, false)));
body_html = new RFC822.Text(new Geary.Memory.StringBuffer("\n\n" +
Geary.RFC822.Utils.quote_email_for_reply(source, true)));
}
public ComposedEmail.as_forward(DateTime date, RFC822.MailboxAddresses from, Geary.Email source) {
@ -65,8 +70,10 @@ public class Geary.ComposedEmail : Object {
subject = create_subject_for_forward(source);
body = new RFC822.Text(new Geary.Memory.StringBuffer("\n\n" +
Geary.RFC822.Utils.quote_email_for_forward(source)));
body_text = new RFC822.Text(new Geary.Memory.StringBuffer("\n\n" +
Geary.RFC822.Utils.quote_email_for_forward(source, false)));
body_html = new RFC822.Text(new Geary.Memory.StringBuffer("\n\n" +
Geary.RFC822.Utils.quote_email_for_forward(source, true)));
}
private void set_reply_references(Geary.Email source) {

View file

@ -43,6 +43,8 @@ public class Geary.RFC822.Message : Object {
}
public Message.from_composed_email(Geary.ComposedEmail email) {
GMime.Part? body_html = null;
GMime.Part? body_text = null;
message = new GMime.Message(true);
// Required headers
@ -86,16 +88,38 @@ public class Geary.RFC822.Message : Object {
message.set_subject(email.subject.value);
}
// Body (also optional)
if (email.body != null) {
// Body: text format (optional)
if (email.body_text != null) {
GMime.DataWrapper content = new GMime.DataWrapper.with_stream(
new GMime.StreamMem.with_buffer(email.body.buffer.get_array()),
new GMime.StreamMem.with_buffer(email.body_text.buffer.get_array()),
GMime.ContentEncoding.DEFAULT);
GMime.Part part = new GMime.Part();
part.set_content_object(content);
body_text = new GMime.Part();
body_text.set_content_type(new GMime.ContentType("text", "plain"));
body_text.set_content_object(content);
}
// Body: HTML format (also optional)
if (email.body_html != null) {
GMime.DataWrapper content = new GMime.DataWrapper.with_stream(
new GMime.StreamMem.with_buffer(email.body_html.buffer.get_array()),
GMime.ContentEncoding.DEFAULT);
message.set_mime_part(part);
body_html = new GMime.Part();
body_html.set_content_type(new GMime.ContentType("text", "html"));
body_html.set_content_object(content);
}
// Setup body depending on what MIME components were filled out.
if (body_text != null && body_html != null) {
GMime.Multipart multipart = new GMime.Multipart.with_subtype("alternative");
multipart.add(body_text);
multipart.add(body_html);
message.set_mime_part(multipart);
} else if (body_text != null) {
message.set_mime_part(body_text);
} else if (body_html != null) {
message.set_mime_part(body_html);
}
}

View file

@ -11,10 +11,11 @@ namespace Geary.RFC822.Utils {
*
* If there's no message body in the supplied email, this function will
* return the empty string.
*
* TODO: Support HTML as an option.
*
* If html_format is true, the message will be quoted in HTML format.
* Otherwise it will be in plain text.
*/
public string quote_email_for_reply(Geary.Email email) {
public string quote_email_for_reply(Geary.Email email, bool html_format) {
if (email.body == null)
return "";
@ -26,8 +27,11 @@ public string quote_email_for_reply(Geary.Email email) {
if (email.from != null)
quoted += _("%s wrote:").printf(email.from.to_string());
if (html_format)
quoted += "<br />";
if (email.body != null)
quoted += "\n" + quote_body(email);
quoted += "\n" + quote_body(email, true, html_format);
return quoted;
}
@ -38,9 +42,10 @@ public string quote_email_for_reply(Geary.Email email) {
* If there's no message body in the supplied email, this function will
* return the empty string.
*
* TODO: Support HTML as an option.
* If html_format is true, the message will be quoted in HTML format.
* Otherwise it will be in plain text.
*/
public string quote_email_for_forward(Geary.Email email) {
public string quote_email_for_forward(Geary.Email email, bool html_format) {
if (email.body == null)
return "";
@ -53,35 +58,67 @@ public string quote_email_for_forward(Geary.Email email) {
quoted += _("Date: %s\n").printf(email.date != null ? email.date.to_string() : "");
quoted += _("To: %s\n").printf(email.to != null ? email.to.to_string() : "");
if (html_format)
quoted = quoted.replace("\n", "<br />");
if (email.body != null)
quoted += "\n" + quote_body(email, false);
quoted += "\n" + quote_body(email, false, html_format);
return quoted;
}
private string quote_body(Geary.Email email, bool line_start_char = true) {
private string text_from_message(Geary.Email email, bool html_format) throws Error {
if (html_format) {
return email.get_message().get_first_mime_part_of_content_type("text/html").to_utf8();
} else {
return email.get_message().get_first_mime_part_of_content_type("text/plain").to_utf8();
}
}
private string quote_body(Geary.Email email, bool use_quotes, bool html_format) {
string body_text = "";
try {
body_text = email.get_message().get_first_mime_part_of_content_type("text/plain").to_utf8();
} catch (Error err) {
if (html_format) {
try {
body_text = Geary.HTML.remove_html_tags(email.get_message().
get_first_mime_part_of_content_type("text/html").to_utf8());
} catch (Error err2) {
debug("Could not get message text. %s", err2.message);
body_text = text_from_message(email, true);
} catch (Error err) {
try {
body_text = text_from_message(email, false).replace("\n", "<br />");
} catch (Error err2) {
debug("Could not get message text. %s", err2.message);
}
}
}
string ret = "";
string[] lines = body_text.split("\n");
for (int i = 0; i < lines.length; i++) {
if (line_start_char)
ret += "> ";
ret += lines[i];
// Wrap the whole thing in a blockquote.
if (use_quotes)
body_text = "<blockquote>%s</blockquote>".printf(body_text);
return body_text;
} else {
// Get message text. First we'll try text, but if that fails we'll
// resort to stripping tags out of the HTML section.
try {
body_text = text_from_message(email, false);
} catch (Error err) {
try {
body_text = Geary.HTML.remove_html_tags(text_from_message(email, false));
} catch (Error err2) {
debug("Could not get message text. %s", err2.message);
}
}
// Add the quoted message > symbols.
string ret = "";
string[] lines = body_text.split("\n");
for (int i = 0; i < lines.length; i++) {
if (use_quotes)
ret += "> ";
ret += lines[i];
}
return ret;
}
return ret;
}
}

View file

@ -84,7 +84,7 @@ int main(string[] args) {
if (!Geary.String.is_empty(subject))
composed_email.subject = new Geary.RFC822.Subject(subject);
if (!Geary.String.is_empty(builder.str))
composed_email.body = new Geary.RFC822.Text(new Geary.Memory.StringBuffer(builder.str));
composed_email.body_text = new Geary.RFC822.Text(new Geary.Memory.StringBuffer(builder.str));
main_loop = new MainLoop();

View file

@ -1,6 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<!-- interface-requires gtk+ 3.0 -->
<object class="GtkActionGroup" id="compose actions">
<child>
<object class="GtkAction" id="undo">
<property name="stock_id">gtk-undo</property>
</object>
<accelerator key="z" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkAction" id="redo">
<property name="stock_id">gtk-redo</property>
</object>
<accelerator key="z" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkAction" id="cut">
<property name="stock_id">gtk-cut</property>
</object>
<accelerator key="x" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkAction" id="copy">
<property name="stock_id">gtk-copy</property>
</object>
<accelerator key="c" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkAction" id="paste">
<property name="stock_id">gtk-paste</property>
</object>
<accelerator key="v" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkAction" id="bold">
<property name="stock_id">gtk-bold</property>
</object>
<accelerator key="b" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkAction" id="italic">
<property name="stock_id">gtk-italic</property>
</object>
<accelerator key="i" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkAction" id="underline">
<property name="stock_id">gtk-underline</property>
</object>
</child>
<child>
<object class="GtkAction" id="strikethrough">
<property name="stock_id">gtk-strikethrough</property>
</object>
</child>
<child>
<object class="GtkAction" id="justifyleft">
<property name="label" translatable="yes">_Left</property>
<property name="stock_id">gtk-justify-left</property>
</object>
</child>
<child>
<object class="GtkAction" id="justifyright">
<property name="label" translatable="yes">_Right</property>
<property name="stock_id">gtk-justify-right</property>
</object>
</child>
<child>
<object class="GtkAction" id="justifycenter">
<property name="label" translatable="yes">_Center</property>
<property name="stock_id">gtk-justify-center</property>
</object>
</child>
<child>
<object class="GtkAction" id="justifyfull">
<property name="label" translatable="yes">_Justify</property>
<property name="stock_id">gtk-justify-fill</property>
</object>
</child>
<child>
<object class="GtkAction" id="insertlink">
<property name="label" translatable="yes">Lin_k</property>
<property name="icon_name">insert-link</property>
</object>
<accelerator key="l" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkAction" id="color">
<property name="label" translatable="yes">C_olor</property>
<property name="stock_id">gtk-select-color</property>
</object>
</child>
<child>
<object class="GtkAction" id="font">
<property name="label" translatable="yes">Font</property>
<property name="stock_id">gtk-select-font</property>
</object>
</child>
<child>
<object class="GtkAction" id="composemenu">
<property name="stock_id">gtk-properties</property>
</object>
</child>
<child>
<object class="GtkAction" id="indent">
<property name="stock_id">gtk-indent</property>
</object>
</child>
<child>
<object class="GtkAction" id="outdent">
<property name="stock_id">gtk-unindent</property>
</object>
</child>
<child>
<object class="GtkAction" id="removeformat">
<property name="label" translatable="yes">Remove formatting</property>
<property name="stock_id">gtk-cancel</property>
</object>
</child>
</object>
<object class="GtkBox" id="composer">
<property name="visible">True</property>
<property name="can_focus">False</property>
@ -192,12 +310,194 @@
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolledwindow">
<object class="GtkToolbar" id="compose_toolbar">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_top">6</property>
<property name="hscrollbar_policy">never</property>
<property name="shadow_type">in</property>
<property name="can_focus">False</property>
<child>
<object class="GtkToolButton" id="bold button">
<property name="related_action">bold</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Bold</property>
<property name="related_action">bold</property>
<property name="label" translatable="yes">toolbutton2</property>
<property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="italic button">
<property name="related_action">italic</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Italic</property>
<property name="related_action">italic</property>
<property name="label" translatable="yes">toolbutton2</property>
<property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="strikethrough button">
<property name="related_action">strikethrough</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Strikethrough</property>
<property name="related_action">strikethrough</property>
<property name="label" translatable="yes">toolbutton1</property>
<property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="underline button">
<property name="related_action">underline</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Underline</property>
<property name="related_action">underline</property>
<property name="label" translatable="yes">toolbutton1</property>
<property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkSeparatorToolItem" id="toolbutton5">
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_action_appearance">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="indent button">
<property name="related_action">indent</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Indent</property>
<property name="related_action">indent</property>
<property name="label" translatable="yes">toolbutton6</property>
<property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="outdent1">
<property name="related_action">outdent</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Un-indent</property>
<property name="related_action">outdent</property>
<property name="label" translatable="yes">toolbutton6</property>
<property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkSeparatorToolItem" id="toolbutton3">
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_action_appearance">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="font button">
<property name="related_action">font</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Fonts</property>
<property name="related_action">font</property>
<property name="label" translatable="yes">toolbutton4</property>
<property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="color button">
<property name="related_action">color</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Color</property>
<property name="related_action">color</property>
<property name="label" translatable="yes">toolbutton4</property>
<property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="link button">
<property name="related_action">insertlink</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Link</property>
<property name="related_action">insertlink</property>
<property name="label" translatable="yes">toolbutton4</property>
<property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="remove format button">
<property name="related_action">removeformat</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Remove formatting</property>
<property name="related_action">removeformat</property>
<property name="label" translatable="yes">toolbutton6</property>
<property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkAlignment" id="message area">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<placeholder/>
</child>
@ -205,7 +505,7 @@
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
<property name="position">3</property>
</packing>
</child>
</object>

View file

@ -0,0 +1,27 @@
<ui>
<accelerator action="undo" />
<accelerator action="redo" />
<accelerator action="cut" />
<accelerator action="copy" />
<accelerator action="paste" />
<accelerator action="bold" />
<accelerator action="italic" />
<accelerator action="underline" />
<accelerator action="strikethrough" />
<accelerator action="removeformat" />
<accelerator action="indent" />
<accelerator action="outdent" />
<accelerator action="justifyleft" />
<accelerator action="justifyright" />
<accelerator action="justifycenter" />
<accelerator action="justifyfull" />
<accelerator action="font" />
<accelerator action="color" />
<accelerator action="insertlink" />
</ui>