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 = """ +
" + 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 @@
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+