[geary: 5/5] Paned composer for handling multi-replies



commit 526f6e7889734ec8516f76c7f130a48734fd48ac
Author: Robert Schroll <rschroll gmail com>
Date:   Tue Jan 13 17:06:58 2015 -0800

    Paned composer for handling multi-replies
    
    When the user replies with a quote to a second message, the composer
    moves into a paned below the conversation viewer.  This makes it easy to
    scroll through the conversation and select text for replies.
    
    The Gtk.Paned acutally holds a Box, which in turn can hold many
    ComposerBoxes.  Only one is shown at a time, but the model used
    elsewhere is that each ComposerWidget has a ComposerContainer until it
    is destroyed.  When a composer is closed, it hides while finishing up
    asynchronous work.  This allows us to hold hidden paned composers as
    they finish up their work.
    
    The logic for focus handling at detachment is moved into the
    ComposerWidget from ComposerEmbed, since it may also be detached from
    the paned state.  ComposerContainers gain a remove_composer() method
    that does the container's clean up, as well as returning the focused
    widget.  The ComposerWindow's remove_composer() method should never be
    called.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=738188

 desktop/org.yorba.geary.gschema.xml                |    6 ++
 src/CMakeLists.txt                                 |    1 +
 src/client/application/geary-config.vala           |    5 ++
 src/client/composer/composer-box.vala              |   74 ++++++++++++++++++++
 src/client/composer/composer-container.vala        |    1 +
 src/client/composer/composer-embed.vala            |   14 +---
 src/client/composer/composer-widget.vala           |   36 ++++++++--
 src/client/composer/composer-window.vala           |    5 ++
 .../conversation-viewer/conversation-viewer.vala   |   19 +++++-
 9 files changed, 142 insertions(+), 19 deletions(-)
---
diff --git a/desktop/org.yorba.geary.gschema.xml b/desktop/org.yorba.geary.gschema.xml
index 3e5bb33..cef596d 100644
--- a/desktop/org.yorba.geary.gschema.xml
+++ b/desktop/org.yorba.geary.gschema.xml
@@ -32,6 +32,12 @@
         <description>Position of the message list Paned grabber.</description>
     </key>
     
+    <key name="composer-pane-position" type="i">
+        <default>350</default>
+        <summary>position of the composer pane</summary>
+        <description>Position of the composer Paned grabber, when open.</description>
+    </key>
+    
     <key name="autoselect" type="b">
         <default>true</default>
         <summary>autoselect next message</summary>
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 0d04959..5f0abb5 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -333,6 +333,7 @@ client/components/status-bar.vala
 client/components/stock.vala
 client/components/stylish-webview.vala
 
+client/composer/composer-box.vala
 client/composer/composer-container.vala
 client/composer/composer-embed.vala
 client/composer/composer-headerbar.vala
diff --git a/src/client/application/geary-config.vala b/src/client/application/geary-config.vala
index 7976167..a015aec 100644
--- a/src/client/application/geary-config.vala
+++ b/src/client/application/geary-config.vala
@@ -11,6 +11,7 @@ public class Configuration {
     public const string WINDOW_MAXIMIZE_KEY = "window-maximize";
     public const string FOLDER_LIST_PANE_POSITION_KEY = "folder-list-pane-position";
     public const string MESSAGES_PANE_POSITION_KEY = "messages-pane-position";
+    public const string COMPOSER_PANE_POSITION_KEY = "composer-pane-position";
     public const string AUTOSELECT_KEY = "autoselect";
     public const string DISPLAY_PREVIEW_KEY = "display-preview";
     public const string SPELL_CHECK_KEY = "spell-check";
@@ -45,6 +46,10 @@ public class Configuration {
         get { return settings.get_int(MESSAGES_PANE_POSITION_KEY); }
     }
     
+    public int composer_pane_position {
+        get { return settings.get_int(COMPOSER_PANE_POSITION_KEY); }
+    }
+    
     public bool autoselect {
         get { return settings.get_boolean(AUTOSELECT_KEY); }
     }
diff --git a/src/client/composer/composer-box.vala b/src/client/composer/composer-box.vala
new file mode 100644
index 0000000..36c3ffe
--- /dev/null
+++ b/src/client/composer/composer-box.vala
@@ -0,0 +1,74 @@
+/* Copyright 2015 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.
+ */
+
+public class ComposerBox : Gtk.EventBox, ComposerContainer {
+    
+    private ComposerWidget composer;
+    private bool has_accel_group = false;
+    
+    public Gtk.Window top_window {
+        get { return (Gtk.Window) get_toplevel(); }
+    }
+    
+    public ComposerBox(ComposerWidget composer) {
+        this.composer = composer;
+        
+        add(composer);
+        composer.editor.focus_in_event.connect(on_focus_in);
+        composer.editor.focus_out_event.connect(on_focus_out);
+        show();
+    }
+    
+    public Gtk.Widget? remove_composer() {
+        if (composer.editor.has_focus)
+            on_focus_out();
+        composer.editor.focus_in_event.disconnect(on_focus_in);
+        composer.editor.focus_out_event.disconnect(on_focus_out);
+        Gtk.Widget? focus = top_window.get_focus();
+        
+        remove(composer);
+        close_container();
+        return focus;
+    }
+    
+    
+    private bool on_focus_in() {
+        // For some reason, on_focus_in gets called a bunch upon construction.
+        if (!has_accel_group)
+            top_window.add_accel_group(composer.ui.get_accel_group());
+        has_accel_group = true;
+        return false;
+    }
+    
+    private bool on_focus_out() {
+        top_window.remove_accel_group(composer.ui.get_accel_group());
+        has_accel_group = false;
+        return false;
+    }
+    
+    public void present() {
+        top_window.present();
+    }
+    
+    public unowned Gtk.Widget get_focus() {
+        return top_window.get_focus();
+    }
+    
+    public void vanish() {
+        hide();
+        parent.hide();
+        composer.state = ComposerWidget.ComposerState.DETACHED;
+        composer.editor.focus_in_event.disconnect(on_focus_in);
+        composer.editor.focus_out_event.disconnect(on_focus_out);
+    }
+    
+    public void close_container() {
+        if (visible)
+            vanish();
+        parent.remove(this);
+    }
+}
+
diff --git a/src/client/composer/composer-container.vala b/src/client/composer/composer-container.vala
index be42724..34e9294 100644
--- a/src/client/composer/composer-container.vala
+++ b/src/client/composer/composer-container.vala
@@ -11,4 +11,5 @@ public interface ComposerContainer {
     public abstract unowned Gtk.Widget get_focus();
     public abstract void vanish();
     public abstract void close_container();
+    public abstract Gtk.Widget? remove_composer();
 }
diff --git a/src/client/composer/composer-embed.vala b/src/client/composer/composer-embed.vala
index 3bcc270..6629ab9 100644
--- a/src/client/composer/composer-embed.vala
+++ b/src/client/composer/composer-embed.vala
@@ -127,8 +127,7 @@ public class ComposerEmbed : Gtk.EventBox, ComposerContainer {
         get_style_context().changed.connect(update_style);
     }
     
-    public void on_detach() {
-        composer.state = ComposerWidget.ComposerState.DETACHED;
+    public Gtk.Widget? remove_composer() {
         if (composer.editor.has_focus)
             on_focus_out();
         composer.editor.focus_in_event.disconnect(on_focus_in);
@@ -138,7 +137,7 @@ public class ComposerEmbed : Gtk.EventBox, ComposerContainer {
         disable_scroll_reroute(this);
         Gtk.ScrolledWindow win = (Gtk.ScrolledWindow) composer.editor.parent;
         win.get_vscrollbar().show();
-        Gtk.Widget focus = top_window.get_focus();
+        Gtk.Widget? focus = top_window.get_focus();
         
         try {
             composer.editor.get_dom_document().body.get_class_list().remove("embedded");
@@ -147,15 +146,8 @@ public class ComposerEmbed : Gtk.EventBox, ComposerContainer {
         }
         
         remove(composer);
-        ComposerWindow window = new ComposerWindow(composer);
-        if (focus != null) {
-            ComposerWindow focus_win = focus.get_toplevel() as ComposerWindow;
-            if (focus_win != null && focus_win == window)
-                focus.grab_focus();
-        } else {
-            composer.set_focus();
-        }
         close_container();
+        return focus;
     }
     
     public bool set_position(ref Gdk.Rectangle allocation, double hscroll, double vscroll,
diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala
index 5de16d1..8129f2a 100644
--- a/src/client/composer/composer-widget.vala
+++ b/src/client/composer/composer-widget.vala
@@ -21,6 +21,7 @@ public class ComposerWidget : Gtk.EventBox {
     
     public enum ComposerState {
         DETACHED,
+        PANED,
         INLINE_NEW,
         INLINE,
         INLINE_COMPACT
@@ -286,7 +287,7 @@ public class ComposerWidget : Gtk.EventBox {
             Gtk.Widget widget = builder.get_object(name) as Gtk.Widget;
             bind_property("state", widget, "visible", BindingFlags.SYNC_CREATE,
                 (binding, source_value, ref target_value) => {
-                    target_value = (state != ComposerState.INLINE);
+                    target_value = (state != ComposerState.INLINE && state != ComposerState.PANED);
                     return true;
                 });
         }
@@ -797,8 +798,10 @@ public class ComposerWidget : Gtk.EventBox {
             document.exec_command("insertHTML", false,
                 Geary.RFC822.Utils.quote_email_for_reply(referred, quote, true));
             
-            if (!referred_ids.contains(referred.id))
+            if (!referred_ids.contains(referred.id)) {
                 add_recipients_and_ids(new_type, referred);
+                ensure_paned();
+            }
         } else if (new_type != compose_type) {
             bool recipients_modified = to_entry.modified || cc_entry.modified || bcc_entry.modified;
             switch (new_type) {
@@ -816,7 +819,8 @@ public class ComposerWidget : Gtk.EventBox {
                 break;
                 
                 case ComposeType.FORWARD:
-                    state = ComposerState.INLINE;
+                    if (state == ComposerState.INLINE_COMPACT)
+                        state = ComposerState.INLINE;
                     subject = forward_subject;
                     if (!recipients_modified) {
                         to = "";
@@ -959,8 +963,25 @@ public class ComposerWidget : Gtk.EventBox {
     }
     
     private void on_detach() {
-        if (parent is ComposerEmbed)
-            ((ComposerEmbed) parent).on_detach();
+        Gtk.Widget? focus = container.remove_composer();
+        ComposerWindow window = new ComposerWindow(this);
+        if (focus != null) {
+            ComposerWindow focus_win = focus.get_toplevel() as ComposerWindow;
+            if (focus_win != null && focus_win == window)
+                focus.grab_focus();
+        } else {
+            set_focus();
+        }
+        state = ComposerWidget.ComposerState.DETACHED;
+    }
+    
+    private void ensure_paned() {
+        if (state == ComposerState.PANED)
+            return;
+        container.remove_composer();
+        GearyApplication.instance.controller.main_window.conversation_viewer
+            .set_paned_composer(this);
+        state = ComposerWidget.ComposerState.PANED;
     }
     
     // compares all keys to all tokens according to user-supplied comparison function
@@ -2071,8 +2092,9 @@ public class ComposerWidget : Gtk.EventBox {
             return;
         }
         
-        // Don't show in inline or compact modes.
-        if (state == ComposerState.INLINE || state == ComposerState.INLINE_COMPACT)
+        // Don't show in inline, compact, or paned modes.
+        if (state == ComposerState.INLINE || state == ComposerState.INLINE_COMPACT ||
+            state == ComposerState.PANED)
             return;
         
         // If there's only one account, show nothing. (From fields are hidden above.)
diff --git a/src/client/composer/composer-window.vala b/src/client/composer/composer-window.vala
index 8d1af40..5c44092 100644
--- a/src/client/composer/composer-window.vala
+++ b/src/client/composer/composer-window.vala
@@ -58,5 +58,10 @@ public class ComposerWindow : Gtk.Window, ComposerContainer {
     public void vanish() {
         hide();
     }
+    
+    public Gtk.Widget? remove_composer() {
+        warning(@"Detached composer received remove");
+        return null;
+    }
 }
 
diff --git a/src/client/conversation-viewer/conversation-viewer.vala 
b/src/client/conversation-viewer/conversation-viewer.vala
index be3dce6..c899e90 100644
--- a/src/client/conversation-viewer/conversation-viewer.vala
+++ b/src/client/conversation-viewer/conversation-viewer.vala
@@ -143,6 +143,9 @@ public class ConversationViewer : Gtk.Box {
     // Overlay containing any inline composers.
     public ScrollableOverlay compose_overlay;
     
+    // Paned for holding any paned composers.
+    private Gtk.Box composer_boxes;
+    
     // Maps emails to their corresponding elements.
     private Gee.HashMap<Geary.EmailIdentifier, WebKit.DOM.HTMLElement> email_to_element = new
         Gee.HashMap<Geary.EmailIdentifier, WebKit.DOM.HTMLElement>();
@@ -215,7 +218,15 @@ public class ConversationViewer : Gtk.Box {
         
         message_overlay = new Gtk.Overlay();
         message_overlay.add(conversation_viewer_scrolled);
-        pack_start(message_overlay);
+        
+        Gtk.Paned composer_paned = new Gtk.Paned(Gtk.Orientation.VERTICAL);
+        composer_paned.pack1(message_overlay, true, false);
+        composer_boxes = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
+        composer_boxes.no_show_all = true;
+        composer_paned.pack2(composer_boxes, true, false);
+        Configuration config = GearyApplication.instance.config;
+        config.bind(Configuration.COMPOSER_PANE_POSITION_KEY, composer_paned, "position");
+        pack_start(composer_paned);
         
         conversation_find_bar = new ConversationFindBar(web_view);
         conversation_find_bar.no_show_all = true;
@@ -224,6 +235,12 @@ public class ConversationViewer : Gtk.Box {
         pack_start(conversation_find_bar, false);
     }
     
+    public void set_paned_composer(ComposerWidget composer) {
+        ComposerBox container = new ComposerBox(composer);
+        composer_boxes.pack_start(container);
+        composer_boxes.show();
+    }
+    
     public Geary.Email? get_last_message() {
         return messages.is_empty ? null : messages.last();
     }


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