[geary/wip/713739-inline: 23/37] Allow multiple inline composers



commit b715fb78aea75a7847e9c1d5638e7f7e51241075
Author: Robert Schroll <rschroll gmail com>
Date:   Thu May 15 16:33:25 2014 -0700

    Allow multiple inline composers
    
    Replying to a message that already has a composer open will result in
    that composer refocusing.  It should also adjust the Reply/Reply
    All/Forward status, but that's still to come.
    
    When switching away from a conversation with inline composers, the only
    option is to cancel the operation or discard all compositions.  This
    will be fixed by adding automatic saving of drafts in this case.

 src/CMakeLists.txt                                 |    1 +
 src/client/application/geary-controller.vala       |   61 ++++++++++++--------
 src/client/composer/composer-embed.vala            |   14 +++--
 src/client/composer/composer-widget.vala           |   33 ++++++++---
 .../conversation-list/conversation-list-view.vala  |    4 +-
 .../conversation-viewer/conversation-web-view.vala |    9 ---
 src/client/util/util-random.vala                   |   15 +++++
 theming/message-viewer.css                         |   14 +---
 8 files changed, 90 insertions(+), 61 deletions(-)
---
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 5f339a8..40ce72a 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -375,6 +375,7 @@ client/util/util-files.vala
 client/util/util-gravatar.vala
 client/util/util-gtk.vala
 client/util/util-international.vala
+client/util/util-random.vala
 client/util/util-webkit.vala
 )
 
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 2465129..1d5d132 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -111,8 +111,6 @@ public class GearyController : Geary.BaseObject {
     // List of windows we're waiting to close before Geary closes.
     private Gee.List<ComposerWidget> waiting_to_close = new Gee.ArrayList<ComposerWidget>();
     
-    public ComposerEmbed? inline_composer = null;
-    
     /**
      * Fired when the currently selected account has changed.
      */
@@ -1817,6 +1815,9 @@ public class GearyController : Geary.BaseObject {
         if (current_account == null)
             return;
         
+        if (!should_create_new_composer(compose_type, referred))
+            return;
+        
         ComposerWidget widget;
         if (mailto != null) {
             widget = new ComposerWidget.from_mailto(current_account, mailto);
@@ -1840,42 +1841,52 @@ public class GearyController : Geary.BaseObject {
         composer_widgets.add(widget);
         widget.destroy.connect(on_composer_widget_destroy);
         
-        if (abandon_existing_composition(widget))
-            inline_composer = new ComposerEmbed(widget, main_window.conversation_viewer, referred);
+        new ComposerEmbed(widget, main_window.conversation_viewer, referred);
     }
     
-    public bool abandon_existing_composition(ComposerWidget? new_composer = null) {
-        if (inline_composer == null)
+    public bool should_create_new_composer(
+        ComposerWidget.ComposeType compose_type = ComposerWidget.ComposeType.NEW_MESSAGE,
+        Geary.Email? referred = null) {
+        if (!any_inline_composers())
+            return true;
+        
+        if (compose_type != ComposerWidget.ComposeType.NEW_MESSAGE) {
+            foreach (ComposerWidget cw in composer_widgets) {
+                if (referred != null && referred.id.equal_to(cw.referred_id)) {
+                    cw.change_compose_type(compose_type);
+                    return false;
+                }
+            }
             return true;
+        }
         
+        // TODO: Remove this in favor of automatically saving drafts
         main_window.present();
         AlertDialog dialog;
-        // TODO: Clean up when closing (like delete_and_exit) / offer save option.
-        if (new_composer != null)
-            dialog = new AlertDialog(main_window, Gtk.MessageType.QUESTION,
-                _("Do you want to discard the existing composition?"), null, Gtk.Stock.DISCARD,
-                Gtk.Stock.CANCEL, _("Open New Composition Window"), Gtk.ResponseType.YES);
-        else
-            dialog = new AlertDialog(main_window, Gtk.MessageType.QUESTION,
-                _("Do you want to discard the existing composition?"), null, Gtk.Stock.DISCARD,
-                Gtk.Stock.CANCEL, _("Move Composition to New Window"), Gtk.ResponseType.YES);
+        dialog = new AlertDialog(main_window, Gtk.MessageType.QUESTION,
+            _("Closing inline composers."), null, Gtk.Stock.DISCARD, Gtk.Stock.CANCEL,
+            null, Gtk.ResponseType.NONE);
         Gtk.ResponseType response = dialog.run();
         if (response == Gtk.ResponseType.OK) {
-            inline_composer.close();
-            return true;
-        }
-        if (new_composer != null) {
-            if (response == Gtk.ResponseType.YES)
-                new ComposerWindow(new_composer);
-            else
-                new_composer.destroy();
-        } else if (response == Gtk.ResponseType.YES) {
-            inline_composer.on_detach();
+            Gee.List<ComposerWidget> composers_to_destroy = new Gee.ArrayList<ComposerWidget>();
+            foreach (ComposerWidget cw in composer_widgets) {
+                if (cw.inline)
+                    composers_to_destroy.add(cw);
+            }
+            foreach(ComposerWidget cw in composers_to_destroy)
+                ((ComposerContainer) cw.parent).close();
             return true;
         }
         return false;
     }
     
+    public bool any_inline_composers() {
+        foreach (ComposerWidget cw in composer_widgets)
+            if (cw.inline)
+                return true;
+        return false;
+    }
+    
     private void on_composer_widget_destroy(Gtk.Widget sender) {
         composer_widgets.remove((ComposerWidget) sender);
         
diff --git a/src/client/composer/composer-embed.vala b/src/client/composer/composer-embed.vala
index f59a8d5..b025d10 100644
--- a/src/client/composer/composer-embed.vala
+++ b/src/client/composer/composer-embed.vala
@@ -6,11 +6,10 @@
 
 public class ComposerEmbed : Gtk.Box, ComposerContainer {
     
-    private static string embed_id = "composer_embed";
-    
     private ComposerWidget composer;
     private ConversationViewer conversation_viewer;
     private Gee.Set<Geary.App.Conversation>? prev_selection = null;
+    private string embed_id;
     
     public Gtk.Window top_window {
         get { return (Gtk.Window) get_toplevel(); }
@@ -41,9 +40,13 @@ public class ComposerEmbed : Gtk.Box, ComposerContainer {
         detach.clicked.connect(on_detach);
         
         WebKit.DOM.HTMLElement? email_element = null;
-        if (referred != null)
+        if (referred != null) {
             email_element = conversation_viewer.web_view.get_dom_document().get_element_by_id(
                 conversation_viewer.get_div_id(referred.id)) as WebKit.DOM.HTMLElement;
+            embed_id = referred.id.to_string() + "_reply";
+        } else {
+            embed_id = random_string(10);
+        }
         if (email_element == null) {
             ConversationListView conversation_list_view = ((MainWindow) GearyApplication.
                 instance.controller.main_window).conversation_list_view;
@@ -56,7 +59,7 @@ public class ComposerEmbed : Gtk.Box, ComposerContainer {
         try {
             conversation_viewer.show_conversation_div();
             email_element.insert_adjacent_html("afterend",
-                @"<div id='$embed_id'></div>");
+                @"<div id='$embed_id' class='composer_embed'></div>");
         } catch (Error error) {
             debug("Error creating embed element: %s", error.message);
             return;
@@ -121,7 +124,6 @@ public class ComposerEmbed : Gtk.Box, ComposerContainer {
     }
     
     public void vanish() {
-        GearyApplication.instance.controller.inline_composer = null;
         hide();
         composer.editor.focus_in_event.disconnect(on_focus_in);
         composer.editor.focus_out_event.disconnect(on_focus_out);
@@ -146,7 +148,7 @@ public class ComposerEmbed : Gtk.Box, ComposerContainer {
     }
     
     public void close() {
-        if (GearyApplication.instance.controller.inline_composer == this)
+        if (visible)
             vanish();
         conversation_viewer.compose_overlay.remove(this);
     }
diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala
index 70ec6b2..277436f 100644
--- a/src/client/composer/composer-widget.vala
+++ b/src/client/composer/composer-widget.vala
@@ -134,8 +134,14 @@ public class ComposerWidget : Gtk.EventBox {
         set { ((Gtk.ToggleAction) actions.get_action(ACTION_COMPOSE_AS_HTML)).active = value; }
     }
     
+    public bool inline {
+        get { return parent is ComposerEmbed && visible; }
+    }
+    
     public ComposeType compose_type { get; private set; default = ComposeType.NEW_MESSAGE; }
     
+    public Geary.EmailIdentifier? referred_id { get; private set; default = null; }
+    
     private ContactListStore? contact_list_store = null;
     
     private string? body_html = null;
@@ -329,6 +335,7 @@ public class ComposerWidget : Gtk.EventBox {
         from_multiple.changed.connect(on_from_changed);
         
         if (referred != null) {
+           this.referred_id = referred.id;
            switch (compose_type) {
                 case ComposeType.NEW_MESSAGE:
                     if (referred.to != null)
@@ -512,6 +519,16 @@ public class ComposerWidget : Gtk.EventBox {
         }
     }
     
+    private void 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();
+        }
+    }
+    
     private void on_load_finished(WebKit.WebFrame frame) {
         WebKit.DOM.HTMLElement? body = editor.get_dom_document().get_element_by_id(
             BODY_ID) as WebKit.DOM.HTMLElement;
@@ -527,15 +544,7 @@ public class ComposerWidget : Gtk.EventBox {
 
         protect_blockquote_styles();
         
-        // Set focus.
-        if (Geary.String.is_empty(to)) {
-            to_entry.grab_focus();
-        } else if (Geary.String.is_empty(subject)) {
-            subject_entry.grab_focus();
-        } else {
-            editor.grab_focus();
-            body.focus();
-        }
+        set_focus();
         
         // Ensure the editor is in correct mode re HTML
         on_compose_as_html();
@@ -671,6 +680,12 @@ public class ComposerWidget : Gtk.EventBox {
         update_from_field();
     }
     
+    // TODO: Make this actually do something
+    public void change_compose_type(ComposeType new_type) {
+        container.present();
+        set_focus();
+    }
+    
     private bool can_save() {
         return (drafts_folder != null && drafts_folder.get_open_state() == Geary.Folder.OpenState.BOTH
             && !drafts_folder.properties.create_never_returns_id && editor.can_undo());
diff --git a/src/client/conversation-list/conversation-list-view.vala 
b/src/client/conversation-list/conversation-list-view.vala
index 8382637..4aac993 100644
--- a/src/client/conversation-list/conversation-list-view.vala
+++ b/src/client/conversation-list/conversation-list-view.vala
@@ -204,7 +204,7 @@ public class ConversationListView : Gtk.TreeView {
         }
         
         if (!get_selection().path_is_selected(path) &&
-            !GearyApplication.instance.controller.abandon_existing_composition())
+            !GearyApplication.instance.controller.should_create_new_composer())
             return true;
         
         if (event.button == 3 && event.type == Gdk.EventType.BUTTON_PRESS) {
@@ -388,7 +388,7 @@ public class ConversationListView : Gtk.TreeView {
     // Selects the first conversation, if nothing has been selected yet and we're not composing.
     public void select_first_conversation() {
         if (get_selected_path() == null &&
-            GearyApplication.instance.controller.inline_composer == null) {
+            !GearyApplication.instance.controller.any_inline_composers()) {
             set_cursor(new Gtk.TreePath.from_indices(0, -1), null, false);
         }
     }
diff --git a/src/client/conversation-viewer/conversation-web-view.vala 
b/src/client/conversation-viewer/conversation-web-view.vala
index 56e85c0..3368d86 100644
--- a/src/client/conversation-viewer/conversation-web-view.vala
+++ b/src/client/conversation-viewer/conversation-web-view.vala
@@ -49,15 +49,6 @@ public class ConversationWebView : WebKit.WebView {
         load_string(html_text, "text/html", "UTF8", "");
     }
     
-    private string random_string(int length) {
-        // No upper case letters, since request gets lower-cased.
-        string chars = "abcdefghijklmnopqrstuvwxyz";
-        char[] random = new char[length];
-        for (int i = 0; i < length; i++)
-            random[i] = chars[Random.int_range(0, chars.length)];
-        return (string) random;
-    }
-    
     public override bool query_tooltip(int x, int y, bool keyboard_tooltip, Gtk.Tooltip tooltip) {
         // Disable tooltips from within WebKit itself.
         return false;
diff --git a/src/client/util/util-random.vala b/src/client/util/util-random.vala
new file mode 100644
index 0000000..e8939f4
--- /dev/null
+++ b/src/client/util/util-random.vala
@@ -0,0 +1,15 @@
+/* Copyright 2013-2014 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.
+ */
+
+private string random_string(int length) {
+    // No upper case letters, since request gets lower-cased.
+    string chars = "abcdefghijklmnopqrstuvwxyz";
+    char[] random = new char[length];
+    for (int i = 0; i < length; i++)
+        random[i] = chars[Random.int_range(0, chars.length)];
+    return (string) random;
+}
+
diff --git a/theming/message-viewer.css b/theming/message-viewer.css
index 9aa6b94..b7c10a9 100644
--- a/theming/message-viewer.css
+++ b/theming/message-viewer.css
@@ -88,7 +88,7 @@ hr {
     box-shadow: inset 0px 0px 1px rgba(0,0,0,0.05);
 }
 
-.email, #composer_embed {
+.email, .composer_embed {
     border: 1px rgba(0,0,0,1) solid;
     background-color: white;/* recv-normal */
     color: black;
@@ -101,7 +101,7 @@ hr {
     margin-top: 16px;
 }
 
-#composer_embed {
+.composer_embed {
     position: absolute;
     top: 0px; /* margin-top has impact here, despite absolute positioning (!?) */
     bottom: 16px;
@@ -109,7 +109,7 @@ hr {
     right: 16px;
     width: auto;
 }
-.email + #composer_embed {
+.email + .composer_embed {
     position: relative;
     top: auto;
     bottom: auto;
@@ -118,12 +118,6 @@ hr {
     width: 100%;
     height: 600px;
 }
-/* For some reason, we can't absolutely position <embed>s or <object>s, so we wrap everything
-   it in a div and then tell the <embed> to take up all the available space. */
-#composer_embed > embed {
-    width: 100%;
-    height: 100%;
-}
 
 .email.sent {
     background-color: white;/* sent-normal */
@@ -141,7 +135,7 @@ hr {
 .email.starred .unstarred {
     display: none;
 }
-.email.read, #multiple_messages .email, #composer_embed {
+.email.read, #multiple_messages .email, .composer_embed {
     border-color: rgba(0,0,0,0.4);
     box-shadow: 0 3px 11px rgba(0,0,0,0.21);
 }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]