diff --git a/src/client/geary-application.vala b/src/client/geary-application.vala index b2b77145..9f4c8bfb 100644 --- a/src/client/geary-application.vala +++ b/src/client/geary-application.vala @@ -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; } diff --git a/src/client/ui/composer-window.vala b/src/client/ui/composer-window.vala index 9edf3cbe..5cd538cc 100644 --- a/src/client/ui/composer-window.vala +++ b/src/client/ui/composer-window.vala @@ -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 = """ + + + +



+ +
+ """; + + // 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 = "
" + 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;
         
diff --git a/src/client/ui/main-toolbar.vala b/src/client/ui/main-toolbar.vala
index 092da8f4..77618712 100644
--- a/src/client/ui/main-toolbar.vala
+++ b/src/client/ui/main-toolbar.vala
@@ -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);
     }
 }
diff --git a/src/client/util/util-menu.vala b/src/client/util/util-menu.vala
new file mode 100644
index 00000000..c7b844b2
--- /dev/null
+++ b/src/client/util/util-menu.vala
@@ -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;
+}
+
diff --git a/src/client/wscript_build b/src/client/wscript_build
index 195d794e..86394138 100644
--- a/src/client/wscript_build
+++ b/src/client/wscript_build
@@ -28,6 +28,7 @@ client_src = [
 
 'util/util-email.vala',
 'util/util-keyring.vala',
+'util/util-menu.vala',
 ]
 
 gsettings_schemas = [
diff --git a/src/engine/api/geary-composed-email.vala b/src/engine/api/geary-composed-email.vala
index a806d13d..7b28e855 100644
--- a/src/engine/api/geary-composed-email.vala
+++ b/src/engine/api/geary-composed-email.vala
@@ -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) {
diff --git a/src/engine/rfc822/rfc822-message.vala b/src/engine/rfc822/rfc822-message.vala
index 55b63052..cb8493bf 100644
--- a/src/engine/rfc822/rfc822-message.vala
+++ b/src/engine/rfc822/rfc822-message.vala
@@ -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);
         }
     }
     
diff --git a/src/engine/rfc822/rfc822-utils.vala b/src/engine/rfc822/rfc822-utils.vala
index 5d11abc2..35c894b8 100644
--- a/src/engine/rfc822/rfc822-utils.vala
+++ b/src/engine/rfc822/rfc822-utils.vala
@@ -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 += "
"; + 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", "
"); + 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", "
"); + } 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 = "
%s
".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; } } diff --git a/src/norman/main.vala b/src/norman/main.vala index d249a445..b3e290c3 100644 --- a/src/norman/main.vala +++ b/src/norman/main.vala @@ -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(); diff --git a/ui/composer.glade b/ui/composer.glade index 6f042d59..de711518 100644 --- a/ui/composer.glade +++ b/ui/composer.glade @@ -1,6 +1,124 @@ + + + + gtk-undo + + + + + + gtk-redo + + + + + + gtk-cut + + + + + + gtk-copy + + + + + + gtk-paste + + + + + + gtk-bold + + + + + + gtk-italic + + + + + + gtk-underline + + + + + gtk-strikethrough + + + + + _Left + gtk-justify-left + + + + + _Right + gtk-justify-right + + + + + _Center + gtk-justify-center + + + + + _Justify + gtk-justify-fill + + + + + Lin_k + insert-link + + + + + + C_olor + gtk-select-color + + + + + Font + gtk-select-font + + + + + gtk-properties + + + + + gtk-indent + + + + + gtk-unindent + + + + + Remove formatting + gtk-cancel + + + True False @@ -192,12 +310,194 @@ - + True - True - 6 - never - in + False + + + bold + True + False + Bold + bold + toolbutton2 + True + + + False + True + + + + + italic + True + False + Italic + italic + toolbutton2 + True + + + False + True + + + + + strikethrough + True + False + Strikethrough + strikethrough + toolbutton1 + True + + + False + True + + + + + underline + True + False + Underline + underline + toolbutton1 + True + + + False + True + + + + + False + True + False + False + + + False + True + + + + + indent + True + False + Indent + indent + toolbutton6 + True + + + False + True + + + + + outdent + True + False + Un-indent + outdent + toolbutton6 + True + + + False + True + + + + + False + True + False + False + + + False + True + + + + + font + True + False + Fonts + font + toolbutton4 + True + + + False + True + + + + + color + True + False + Color + color + toolbutton4 + True + + + False + True + + + + + insertlink + True + False + Link + insertlink + toolbutton4 + True + + + False + True + + + + + removeformat + True + False + Remove formatting + removeformat + toolbutton6 + True + + + False + True + + + + + False + True + 2 + + + + + True + False @@ -205,7 +505,7 @@ True True - 2 + 3 diff --git a/ui/composer_accelerators.ui b/ui/composer_accelerators.ui new file mode 100644 index 00000000..2de83b39 --- /dev/null +++ b/ui/composer_accelerators.ui @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +