[gnome-documents] collections: first implementation of an "Organize" dialog



commit 2bfc65ed91c744fcde6f3c482688e05cd287a84a
Author: Cosimo Cecchi <cosimoc gnome org>
Date:   Thu Nov 10 17:08:22 2011 -0500

    collections: first implementation of an "Organize" dialog

 src/documents.js   |    2 +-
 src/lib/gd-utils.c |   28 +++
 src/lib/gd-utils.h |    7 +
 src/query.js       |   43 ++++-
 src/selections.js  |  577 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 5 files changed, 649 insertions(+), 8 deletions(-)
---
diff --git a/src/documents.js b/src/documents.js
index be7501c..7a435e1 100644
--- a/src/documents.js
+++ b/src/documents.js
@@ -857,7 +857,7 @@ DocumentManager.prototype = {
     setActiveItem: function(doc) {
         if (Manager.BaseManager.prototype.setActiveItem.call(this, doc)) {
 
-            if (doc) {
+            if (doc && !doc.collection) {
                 let recentManager = Gtk.RecentManager.get_default();
                 recentManager.add_item(doc.uri);
             }
diff --git a/src/lib/gd-utils.c b/src/lib/gd-utils.c
index 70cc6f9..ecc5bac 100644
--- a/src/lib/gd-utils.c
+++ b/src/lib/gd-utils.c
@@ -93,6 +93,34 @@ gd_item_store_set (GtkListStore *store,
                       -1);
 }
 
+/**
+ * gd_create_organize_store:
+ * 
+ * Returns: (transfer full):
+ */
+GtkListStore *
+gd_create_organize_store (void)
+{
+  return gtk_list_store_new (3,
+                             G_TYPE_STRING, // ID
+                             G_TYPE_STRING, // NAME
+                             G_TYPE_INT); // STATE
+}
+
+void
+gd_organize_store_set (GtkListStore *store,
+                       GtkTreeIter *iter,
+                       const gchar *id,
+                       const gchar *name,
+                       gint state)
+{
+  gtk_list_store_set (store, iter,
+                      0, id,
+                      1, name,
+                      2, state,
+                      -1);
+}
+
 #define ATTRIBUTES_FOR_THUMBNAIL \
   G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE"," \
   G_FILE_ATTRIBUTE_TIME_MODIFIED
diff --git a/src/lib/gd-utils.h b/src/lib/gd-utils.h
index 049be08..8fea96f 100644
--- a/src/lib/gd-utils.h
+++ b/src/lib/gd-utils.h
@@ -40,6 +40,13 @@ void gd_item_store_set (GtkListStore *store,
                         const gchar *name,
                         const gchar *heading_text);
 
+GtkListStore* gd_create_organize_store (void);
+void gd_organize_store_set (GtkListStore *store,
+                            GtkTreeIter *iter,
+                            const gchar *id,
+                            const gchar *name,
+                            gint state);
+
 void gd_queue_thumbnail_job_for_file_async (GFile *file,
                                             GAsyncReadyCallback callback,
                                             gpointer user_data);
diff --git a/src/query.js b/src/query.js
index fcf0e4c..2847392 100644
--- a/src/query.js
+++ b/src/query.js
@@ -21,6 +21,7 @@
 
 const Global = imports.global;
 
+const Gd = imports.gi.Gd;
 const Gio = imports.gi.Gio;
 const GLib = imports.gi.GLib;
 
@@ -44,6 +45,8 @@ const QueryFlags = {
     UNFILTERED: 1 << 0
 };
 
+const LOCAL_COLLECTIONS_IDENTIFIER = 'gd:collection:local:';
+
 function Query(sparql) {
     this._init(sparql);
 }
@@ -90,7 +93,8 @@ QueryBuilder.prototype = {
         let filter =
             ('((fn:starts-with (nie:url(?urn), "%s")) || ' +
              ' (fn:starts-with (nie:url(?urn), "%s")) || ' +
-             ' (fn:starts-with (nie:url(?urn), "%s")))').format(desktopURI, documentsURI, downloadsURI);
+             ' (fn:starts-with (nie:url(?urn), "%s")) || ' +
+             ' (fn:starts-with (nao:identifier(?urn), "gd:collection:local:")))').format(desktopURI, documentsURI, downloadsURI);
 
         return filter;
     },
@@ -214,5 +218,42 @@ QueryBuilder.prototype = {
              'LIMIT 4').replace('?collUrn', '<' + resource + '>');
 
         return new Query(sparql);
+    },
+
+    // queries for all the collections the given item is part of
+    buildFetchCollectionsQuery: function(resource) {
+        let sparql =
+            ('SELECT ' +
+             '?urn ' +
+             'WHERE { ?urn a nfo:DataContainer . ?docUrn nie:isPartOf ?urn }'
+            ).replace('?docUrn', '<' + resource + '>');
+
+        return new Query(sparql);
+    },
+
+    // adds or removes the given item to the given collection
+    buildSetCollectionQuery: function(itemUrn, collectionUrn, setting) {
+        let sparql = ('%s { <%s> nie:isPartOf <%s> }'
+                     ).format((setting ? 'INSERT' : 'DELETE'), itemUrn, collectionUrn);
+        return new Query(sparql);
+    },
+
+    // bumps the mtime to current time for the given resource
+    buildUpdateMtimeQuery: function(resource) {
+        let time = Gd.iso8601_from_timestamp(GLib.get_real_time() / GLib.USEC_PER_SEC);
+        let sparql = ('INSERT OR REPLACE { <%s> nie:contentLastModified \"%s\" }'
+                     ).format(resource, time);
+
+        return new Query(sparql);
+    },
+
+    buildCreateCollectionQuery: function(name) {
+        let time = Gd.iso8601_from_timestamp(GLib.get_real_time() / GLib.USEC_PER_SEC);
+        let sparql = ('INSERT { _:res a nfo:DataContainer ; a nie:DataObject ; ' +
+                      'nie:contentLastModified \"' + time + '\" ; ' +
+                      'nie:title \"' + name + '\" ; ' +
+                      'nao:identifier \"' + LOCAL_COLLECTIONS_IDENTIFIER + name + '\" }');
+
+        return new Query(sparql);
     }
 };
diff --git a/src/selections.js b/src/selections.js
index 4f59619..c43bc8e 100644
--- a/src/selections.js
+++ b/src/selections.js
@@ -19,16 +19,555 @@
  *
  */
 
+const Gd = imports.gi.Gd;
+const GLib = imports.gi.GLib;
 const Gtk = imports.gi.Gtk;
 const GtkClutter = imports.gi.GtkClutter;
+const Pango = imports.gi.Pango;
 const _ = imports.gettext.gettext;
 
+const Documents = imports.documents;
 const Global = imports.global;
+const Manager = imports.manager;
+const Query = imports.query;
 const Tweener = imports.util.tweener;
+const Utils = imports.utils;
 
 const Lang = imports.lang;
 const Signals = imports.signals;
 
+// fetch all the collections a given item is part of
+function FetchCollectionsJob(urn) {
+    this._init(urn);
+}
+
+FetchCollectionsJob.prototype = {
+    _init: function(urn) {
+        this._urn = urn;
+        this._collections = [];
+    },
+
+    run: function(callback) {
+        this._callback = callback;
+
+        let query = Global.queryBuilder.buildFetchCollectionsQuery(this._urn);
+        Global.connectionQueue.add(query.sparql, null, Lang.bind(this,
+            function(object, res) {
+                let cursor = null;
+
+                try {
+                    cursor = object.query_finish(res);
+                    cursor.next_async(null, Lang.bind(this, this._onCursorNext));
+                } catch (e) {
+                    log(e);
+                    this._emitCallback();
+                }
+            }));
+    },
+
+    _onCursorNext: function(cursor, res) {
+        let valid = false;
+
+        try {
+            valid = cursor.next_finish(res);
+        } catch (e) {
+            log(e);
+        }
+
+        if (!valid) {
+            cursor.close();
+            this._emitCallback();
+
+            return;
+        }
+
+        let urn = cursor.get_string(0)[0];
+        this._collections.push(urn);
+
+        cursor.next_async(null, Lang.bind(this, this._onCursorNext));
+    },
+
+    _emitCallback: function() {
+        this._callback(this._collections);
+    }
+};
+
+// fetch the state of every collection applicable to the selected items
+const OrganizeCollectionState = {
+    NORMAL: 0,
+    ACTIVE: 1 << 0,
+    INCONSISTENT: 1 << 1,
+    INSENSITIVE: 1 << 2
+};
+
+function FetchCollectionStateForSelectionJob() {
+    this._init();
+}
+
+FetchCollectionStateForSelectionJob.prototype = {
+    _init: function() {
+        this._collectionsForItems = {};
+        this._runningJobs = 0;
+    },
+
+    run: function(callback) {
+        this._callback = callback;
+
+        let urns = Global.selectionController.getSelection();
+        urns.forEach(Lang.bind(this,
+            function(urn) {
+                let job = new FetchCollectionsJob(urn);
+
+                this._runningJobs++;
+                job.run(Lang.bind(this, this._jobCollector, urn));
+            }));
+    },
+
+    _jobCollector: function(collectionsForItem, urn) {
+        this._collectionsForItems[urn] = collectionsForItem;
+
+        this._runningJobs--;
+        if (!this._runningJobs)
+            this._emitCallback();
+    },
+
+    _emitCallback: function() {
+        let collectionState = {};
+        let collections = Global.collectionManager.getItems();
+
+        // for all the registered collections...
+        for (collIdx in collections) {
+            let collection = collections[collIdx];
+
+            let found = false;
+            let notFound = false;
+            let sameResource = true;
+
+            for (itemIdx in this._collectionsForItems) {
+                let item = Global.documentManager.getItemById(itemIdx);
+                let collectionsForItem = this._collectionsForItems[itemIdx];
+
+                // if one of the selected items is part of this collection...
+                if (collectionsForItem.indexOf(collIdx) != -1)
+                    found = true;
+                else
+                    notFound = true;
+
+                if ((item.resourceUrn != collection.resourceUrn) &&
+                    (collection.identifier.indexOf(Query.LOCAL_COLLECTIONS_IDENTIFIER) == -1)) {
+                    sameResource = false;
+                }
+            }
+
+            let state = OrganizeCollectionState.NORMAL;
+
+            if (found && notFound)
+                // if some items are part of this collection and some are not...
+                state |= OrganizeCollectionState.INCONSISTENT;
+            else if (found)
+                // if all items are part of this collection...
+                state |= OrganizeCollectionState.ACTIVE;
+
+            if (!sameResource)
+                state |= OrganizeCollectionState.INSENSITIVE;
+
+            collectionState[collIdx] = state;
+        }
+
+        this._callback(collectionState);
+    }
+};
+
+// updates the mtime for the given resource to the current system time
+function UpdateMtimeJob(urn) {
+    this._init(urn);
+}
+
+UpdateMtimeJob.prototype = {
+    _init: function(urn) {
+        this._urn = urn;
+    },
+
+    run: function(callback) {
+        this._callback = callback;
+
+        let query = Global.queryBuilder.buildUpdateMtimeQuery(this._urn);
+        Global.connectionQueue.update(query.sparql, null, Lang.bind(this,
+            function(object, res) {
+                try {
+                    object.update_finish(res);
+                } catch (e) {
+                    log(e);
+                }
+
+                this._callback();
+            }));
+    }
+};
+
+// adds or removes the selected items to the given collection
+function SetCollectionForSelectionJob(collectionUrn, setting) {
+    this._init(collectionUrn, setting);
+}
+
+SetCollectionForSelectionJob.prototype = {
+    _init: function(collectionUrn, setting) {
+        this._collectionUrn = collectionUrn;
+        this._setting = setting;
+        this._runningJobs = 0;
+    },
+
+    run: function(callback) {
+        this._callback = callback;
+
+        let urns = Global.selectionController.getSelection();
+        urns.forEach(Lang.bind(this,
+            function(urn) {
+                let query = Global.queryBuilder.buildSetCollectionQuery(urn,
+                    this._collectionUrn, this._setting);
+                this._runningJobs++;
+
+                Global.connectionQueue.update(query.sparql, null, Lang.bind(this,
+                    function(object, res) {
+                        try {
+                            object.update_finish(res);
+                        } catch (e) {
+                            log(e);
+                        }
+
+                        this._jobCollector();
+                    }));
+            }));
+    },
+
+    _jobCollector: function() {
+        this._runningJobs--;
+
+        if (this._runningJobs == 0) {
+            let job = new UpdateMtimeJob(this._collectionUrn);
+            job.run(Lang.bind(this,
+                function() {
+                    this._callback();
+                }));
+        }
+    }
+};
+
+// creates an (empty) collection with the given name
+function CreateCollectionJob(name) {
+    this._init(name);
+}
+
+CreateCollectionJob.prototype = {
+    _init: function(name) {
+        this._name = name;
+        this._createdUrn = null;
+    },
+
+    run: function(callback) {
+        this._callback = callback;
+
+        let query = Global.queryBuilder.buildCreateCollectionQuery(this._name);
+        Global.connectionQueue.updateBlank(query.sparql, null, Lang.bind(this,
+            function(object, res) {
+                let variant = null;
+                try {
+                    variant = object.update_blank_finish(res); // variant is aaa{ss}
+                } catch (e) {
+                    log(e);
+                }
+
+                variant = variant.get_child_value(0); // variant is now aa{ss}
+                variant = variant.get_child_value(0); // variant is now a{ss}
+                variant = variant.get_child_value(0); // variant is now {ss}
+
+                let key = variant.get_child_value(0).get_string()[0];
+                let val = variant.get_child_value(1).get_string()[0];
+
+                if (key == 'res')
+                    this._createdUrn = val;
+
+                this._callback(this._createdUrn);
+            }));
+    }
+};
+
+const OrganizeModelColumns = {
+    ID: 0,
+    NAME: 1,
+    STATE: 2
+};
+
+function OrganizeCollectionModel() {
+    this._init();
+}
+
+OrganizeCollectionModel.prototype = {
+    _init: function() {
+        this.model = Gd.create_organize_store();
+        this._placeholderPath = null;
+
+        this._collAddedId =
+            Global.collectionManager.connect('item-added',
+                                             Lang.bind(this, this._onCollectionAdded));
+        this._collRemovedId =
+            Global.collectionManager.connect('item-removed',
+                                             Lang.bind(this, this._onCollectionRemoved));
+
+        // populate the model
+        let job = new FetchCollectionStateForSelectionJob();
+        job.run(Lang.bind(this, this._onFetchCollectionStateForSelection));
+    },
+
+    _clearPlaceholder: function() {
+        // remove the placeholder if it's here
+        if (this._placeholderPath) {
+            let placeholderIter = this.model.get_iter(this._placeholderPath)[1];
+
+            if (placeholderIter) {
+                this.model.remove(placeholderIter);
+                this._placeholderPath = null;
+            }
+        }
+    },
+
+    _findCollectionIter: function(item) {
+        let retval = null;
+
+        this.model.foreach(Lang.bind(this,
+            function(model, path, iter) {
+                let id = model.get_value(iter, OrganizeModelColumns.ID);
+
+                if (item.id == id) {
+                    retval = iter;
+                    return true;
+                }
+
+                return false;
+            }));
+
+        return retval;
+    },
+
+    _onFetchCollectionStateForSelection: function(collectionState) {
+        this._clearPlaceholder();
+
+        for (idx in collectionState) {
+            let item = Global.collectionManager.getItemById(idx);
+            let iter = null;
+
+            iter = this._findCollectionIter(item);
+            if (!iter)
+                iter = this.model.append();
+
+            if (iter)
+                Gd.organize_store_set(this.model, iter,
+                                      item.id, item.name, collectionState[item.id]);
+        }
+    },
+
+    _refreshState: function() {
+        let job = new FetchCollectionStateForSelectionJob();
+        job.run(Lang.bind(this, this._onFetchCollectionStateForSelection));
+    },
+
+    _onCollectionAdded: function(manager, itemAdded) {
+        this._refreshState();
+    },
+
+    _onCollectionRemoved: function(manager, itemRemoved) {
+        let iter = this._findCollectionIter(itemRemoved);
+
+        if (iter)
+            this.model.remove(iter);
+    },
+
+    refreshCollectionState: function() {
+        this._refreshState();
+    },
+
+    setPlaceholder: function(path) {
+        this._clearPlaceholder();
+        this._placeholderPath = path;
+    },
+
+    destroy: function() {
+        if (this._collAddedId != 0) {
+            Global.collectionManager.disconnect(this._collAddedId);
+            this._collAddedId = 0;
+        }
+
+        if (this._collRemovedId != 0) {
+            Global.collectionManager.disconnect(this._collRemovedId);
+            this._collRemovedId = 0;
+        }
+    }
+};
+
+function OrganizeCollectionView() {
+    this._init();
+}
+
+OrganizeCollectionView.prototype = {
+    _init: function() {
+        this._addCollectionPath = null;
+
+        this._model = new OrganizeCollectionModel();
+        this.widget = new Gtk.TreeView({ headers_visible: false,
+                                         vexpand: true,
+                                         hexpand: true });
+        this.widget.set_model(this._model.model);
+
+        this.widget.connect('destroy', Lang.bind(this,
+            function() {
+                this._model.destroy();
+            }));
+
+        this._viewCol = new Gtk.TreeViewColumn();
+        this.widget.append_column(this._viewCol);
+
+        // checkbox
+        this._rendererCheck = new Gtk.CellRendererToggle();
+        this._viewCol.pack_start(this._rendererCheck, false);
+        this._viewCol.set_cell_data_func(this._rendererCheck,
+                                         Lang.bind(this, this._checkCellFunc));
+        this._rendererCheck.connect('toggled', Lang.bind(this, this._onCheckToggled));
+
+        // item name
+        this._rendererText = new Gtk.CellRendererText();
+        this._viewCol.pack_start(this._rendererText, true);
+        this._viewCol.add_attribute(this._rendererText,
+                                    'text', Manager.BaseModelColumns.NAME);
+        this._viewCol.set_cell_data_func(this._rendererText,
+                                         Lang.bind(this, this._textCellFunc));
+
+        this._rendererText.connect('edited', Lang.bind(this, this._onTextEdited));
+        this._rendererText.connect('editing-canceled', Lang.bind(this, this._onTextEditCanceled));
+        this._rendererText.connect('editing-started', Lang.bind(this, this._onTextEditStarted));
+
+        this.widget.show();
+    },
+
+    _onCheckToggled: function(renderer, pathStr) {
+        let path = Gtk.TreePath.new_from_string(pathStr);
+        let iter = this._model.model.get_iter(path)[1];
+
+        let collUrn = this._model.model.get_value(iter, OrganizeModelColumns.ID);
+        let state = this._rendererCheck.get_active();
+
+        let job = new SetCollectionForSelectionJob(collUrn, !state);
+        job.run(Lang.bind(this,
+            function() {
+                this._model.refreshCollectionState();
+
+                // FIXME: we shouldn't be this, but tracker doesn't
+                // notify us for collection changes...
+                let coll = Global.collectionManager.getItemById(collUrn);
+                coll.refresh();
+            }));
+    },
+
+    _onTextEdited: function(cell, pathStr, newText) {
+        let path = Gtk.TreePath.new_from_string(pathStr);
+        let iter = this._model.model.get_iter(path)[1];
+
+        // don't insert collections with empty names
+        if (!newText || newText == '') {
+            this._model.model.remove(iter);
+            return;
+        }
+
+        cell.editable = false;
+        let job = new CreateCollectionJob(newText);
+        job.run(Lang.bind(this, this._onCollectionCreated));
+    },
+
+    _onCollectionCreated: function(collUrn) {
+        // FIXME: we shouldn't be doing any of this, but tracker doesn't
+        // notify us for collection changes...
+
+        let job = new Documents.SingleItemJob(collUrn);
+        job.run(Query.QueryFlags.UNFILTERED, Lang.bind(this,
+            function(cursor) {
+                if (cursor)
+                    Global.documentManager.addDocumentFromCursor(cursor);
+            }));
+    },
+
+    _onTextEditCanceled: function() {
+        if (this._addCollectionPath) {
+            let path = Gtk.TreePath.new_from_string(this._addCollectionPath);
+            let iter = this._model.model.get_iter(path)[1];
+
+            this._model.model.remove(iter);
+            this._addCollectionPath = null;
+        }
+    },
+
+    _onTextEditStarted: function(cell, editable, pathStr) {
+        this._addCollectionPath = pathStr;
+    },
+
+    _checkCellFunc: function(col, cell, model, iter) {
+        let state = model.get_value(iter, OrganizeModelColumns.STATE);
+
+        cell.active = (state & OrganizeCollectionState.ACTIVE);
+        cell.inconsistent = (state & OrganizeCollectionState.INCONSISTENT);
+        cell.sensitive = !(state & OrganizeCollectionState.INSENSITIVE);
+    },
+
+    _textCellFunc: function(col, cell, model, iter) {
+        let state = model.get_value(iter, OrganizeModelColumns.STATE);
+        cell.sensitive = !(state & OrganizeCollectionState.INSENSITIVE);
+    },
+
+    addCollection: function() {
+        let iter = this._model.model.append();
+        let path = this._model.model.get_path(iter);
+
+        Gd.organize_store_set(this._model.model, iter,
+                              'collection-placeholder', '', OrganizeCollectionState.NORMAL);
+        this._model.setPlaceholder(path);
+
+        this._rendererText.editable = true;
+        this.widget.set_cursor_on_cell(path, this._viewCol, this._rendererText, true);
+    }
+};
+
+const OrganizeCollectionDialogResponse = {
+    ADD: 1
+};
+
+function OrganizeCollectionDialog(toplevel) {
+    this._init(toplevel);
+};
+
+OrganizeCollectionDialog.prototype = {
+    _init: function(toplevel) {
+        this.widget = new Gtk.Dialog({ transient_for: toplevel,
+                                       modal: true,
+                                       destroy_with_parent: true,
+                                       default_width: 400,
+                                       default_height: 200 });
+        this.widget.add_button('gtk-add', OrganizeCollectionDialogResponse.ADD);
+
+        this.widget.add_button('gtk-ok', Gtk.ResponseType.OK);
+        this.widget.set_default_response(Gtk.ResponseType.OK);
+
+        let contentArea = this.widget.get_content_area();
+        let collView = new OrganizeCollectionView();
+        contentArea.add(collView.widget);
+
+        this.widget.connect('response', Lang.bind(this,
+            function(widget, response) {
+                if (response == OrganizeCollectionDialogResponse.ADD)
+                    collView.addCollection();
+            }));
+
+        this.widget.show();
+    }
+};
+
 function SelectionController() {
     this._init();
 };
@@ -95,14 +634,20 @@ SelectionToolbar.prototype = {
         actorWidget.get_style_context().add_class('osd');
 
         this._toolbarFavorite = new Gtk.ToggleToolButton({ icon_name: 'emblem-favorite-symbolic' });
-        this.widget.insert(this._toolbarFavorite, 0);
+        this.widget.insert(this._toolbarFavorite, -1);
         this._toolbarFavorite.connect('clicked', Lang.bind(this, this._onToolbarFavorite));
 
         this._separator = new Gtk.SeparatorToolItem();
-        this.widget.insert(this._separator, 1);
+        this.widget.insert(this._separator, -1);
+
+        this._toolbarCollection = new Gtk.ToolButton({ icon_name: 'list-add-symbolic' });
+        this._toolbarCollection.set_tooltip_text(_("Organize"));
+        this.widget.insert(this._toolbarCollection, -1);
+        this._toolbarCollection.connect('clicked', Lang.bind(this, this._onToolbarCollection));
+        this._toolbarCollection.show();
 
         this._toolbarOpen = new Gtk.ToolButton({ icon_name: 'document-open-symbolic' });
-        this.widget.insert(this._toolbarOpen, 2);
+        this.widget.insert(this._toolbarOpen, -1);
         this._toolbarOpen.connect('clicked', Lang.bind(this, this._onToolbarOpen));
 
         this.widget.show();
@@ -165,7 +710,8 @@ SelectionToolbar.prototype = {
                 if (doc.favorite)
                     favCount++;
 
-                if (apps.indexOf(doc.defaultAppName) == -1)
+                if ((doc.defaultAppName) &&
+                    (apps.indexOf(doc.defaultAppName) == -1))
                     apps.push(doc.defaultAppName);
             }));
 
@@ -183,8 +729,10 @@ SelectionToolbar.prototype = {
             openLabel = _("Open");
         }
 
-        this._toolbarOpen.set_tooltip_text(openLabel);
-        this._toolbarOpen.show();
+        if (apps.length > 0) {
+            this._toolbarOpen.set_tooltip_text(openLabel);
+            this._toolbarOpen.show();
+        }
 
         if (showFavorite) {
             let isFavorite = (favCount == selection.length);
@@ -210,6 +758,23 @@ SelectionToolbar.prototype = {
         this._insideRefresh = false;
     },
 
+    _onToolbarCollection: function() {
+        let toplevel = this.widget.get_toplevel();
+        if (!toplevel.is_toplevel())
+            return;
+
+        let dialog = new OrganizeCollectionDialog(toplevel);
+        this._fadeOut();
+
+        dialog.widget.connect('response', Lang.bind(this,
+            function(widget, response) {
+                if (response == Gtk.ResponseType.OK) {
+                    dialog.widget.destroy();
+                    this._fadeIn();
+                }
+            }));
+    },
+
     _onToolbarOpen: function(widget) {
         let selection = Global.selectionController.getSelection();
 



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