[gnome-documents] selections: New collections dialog



commit b4088a5ba09351e329c5956d163f713c08030f3e
Author: Alessandro Bono <shadow openaliasbox org>
Date:   Wed Aug 19 17:32:33 2015 +0200

    selections: New collections dialog
    
    https://bugzilla.gnome.org/show_bug.cgi?id=726450

 data/application.css                        |   13 +
 data/media/collections-placeholder.png      |  Bin 686 -> 0 bytes
 data/org.gnome.Documents.data.gresource.xml |    2 +-
 data/ui/organize-collection-dialog.ui       |  186 ++++++++
 po/POTFILES.in                              |    1 +
 src/selections.js                           |  674 ++++++++++++++++-----------
 src/view.js                                 |    4 +
 7 files changed, 595 insertions(+), 285 deletions(-)
---
diff --git a/data/application.css b/data/application.css
index edd68fe..b2a4725 100644
--- a/data/application.css
+++ b/data/application.css
@@ -100,3 +100,16 @@ GdMainIconView.content-view.cell:active {
     /* when there's no pixbuf yet */
     background-color: @osd_bg;
 }
+
+.collection-dialog .frame { border-style: solid none none none; }
+
+.collection-dialog .delete-row { background-color: @theme_bg_color }
+
+.collection-dialog .frame GtkEntry {
+  padding: 12px 8px 13px;
+  border-left: none;
+  border-right: none;
+  border-top: none;
+  border-bottom: none;
+  border-radius: 0px;
+}
diff --git a/data/org.gnome.Documents.data.gresource.xml b/data/org.gnome.Documents.data.gresource.xml
index cd0d41e..506a0f0 100644
--- a/data/org.gnome.Documents.data.gresource.xml
+++ b/data/org.gnome.Documents.data.gresource.xml
@@ -3,10 +3,10 @@
   <gresource prefix="/org/gnome/Documents">
     <file>application.css</file>
     <file preprocess="xml-stripblanks">ui/app-menu.ui</file>
+    <file preprocess="xml-stripblanks">ui/organize-collection-dialog.ui</file>
     <file preprocess="xml-stripblanks">ui/preview-context-menu.ui</file>
     <file preprocess="xml-stripblanks">ui/preview-menu.ui</file>
     <file preprocess="xml-stripblanks">ui/selection-menu.ui</file>
-    <file alias="ui/collections-placeholder.png" 
preprocess="to-pixdata">media/collections-placeholder.png</file>
     <file alias="ui/dnd-counter.svg" preprocess="to-pixdata">media/dnd-counter.svg</file>
     <file alias="ui/thumbnail-frame.png" preprocess="to-pixdata">media/thumbnail-frame.png</file>
   </gresource>
diff --git a/data/ui/organize-collection-dialog.ui b/data/ui/organize-collection-dialog.ui
new file mode 100644
index 0000000..3999e35
--- /dev/null
+++ b/data/ui/organize-collection-dialog.ui
@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <template class="Gjs_OrganizeCollectionDialog" parent="GtkWindow">
+    <property name="visible">True</property>
+    <property name="modal">True</property>
+    <property name="window_position">center-on-parent</property>
+    <property name="default_width">420</property>
+    <property name="default_height">500</property>
+    <property name="destroy_with_parent">True</property>
+    <property name="type_hint">dialog</property>
+    <child>
+      <object class="GtkStack" id="content">
+        <property name="visible">True</property>
+        <child>
+          <object class="GtkGrid" id="viewEmpty">
+            <property name="visible">True</property>
+            <property name="halign">center</property>
+            <property name="valign">center</property>
+            <property name="orientation">vertical</property>
+            <property name="row_spacing">6</property>
+            <child>
+              <object class="GtkImage" id="imageEmpty">
+                <property name="visible">True</property>
+                <property name="pixel_size">96</property>
+                <property name="icon_name">inode-directory-symbolic</property>
+                <property name="margin_bottom">18</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="labelEmpty">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">Enter a name for your first collection</property>
+                <property name="margin_bottom">6</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkEntry" id="addEntryEmpty">
+                <property name="visible">True</property>
+                <property name="has_focus">True</property>
+                <property name="activates_default">True</property>
+                <property name="placeholder_text" translatable="yes">New Collection…</property>
+                <property name="margin_bottom">6</property>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">2</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton" id="addButtonEmpty">
+                <property name="visible">True</property>
+                <property name="sensitive">False</property>
+                <property name="can_default">True</property>
+                <property name="has_default">True</property>
+                <property name="halign">center</property>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="label" translatable="yes">Add</property>
+                    <property name="visible">True</property>
+                    <property name="margin_start">36</property>
+                    <property name="margin_end">36</property>
+                  </object>
+                </child>
+                <style>
+                  <class name="suggested-action"/>
+                </style>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">3</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkGrid" id="viewCollections">
+            <property name="visible">True</property>
+            <property name="orientation">vertical</property>
+            <child>
+              <object class="GtkGrid" id="addGridCollections">
+                <property name="visible">True</property>
+                <property name="hexpand">True</property>
+                <property name="margin">5</property>
+                <child>
+                  <object class="GtkEntry" id="addEntryCollections">
+                    <property name="visible">True</property>
+                    <property name="hexpand">True</property>
+                    <property name="activates_default">True</property>
+                    <property name="placeholder_text" translatable="yes">New Collection…</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkButton" id="addButtonCollections">
+                    <property name="label" translatable="yes">Add</property>
+                    <property name="visible">True</property>
+                    <property name="sensitive">False</property>
+                    <property name="can_default">True</property>
+                    <style>
+                      <class name="suggested-action"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="top_attach">0</property>
+                  </packing>
+                </child>
+                <style>
+                  <class name="linked"/>
+                </style>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow" id="scrolledWindowCollections">
+                <property name="visible">True</property>
+                <property name="hexpand">True</property>
+                <property name="vexpand">True</property>
+                <property name="shadow_type">in</property>
+                <child>
+                  <placeholder/>
+                </child>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">1</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+    <child type="titlebar">
+      <object class="GtkHeaderBar" id="headerBar">
+        <property name="visible">True</property>
+        <property name="title">Collections</property>
+        <property name="show_close_button">True</property>
+        <child>
+          <object class="GtkButton" id="cancelButton">
+            <property name="label" translatable="yes">Cancel</property>
+            <property name="no_show_all">True</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkButton" id="doneButton">
+            <property name="label" translatable="yes">Done</property>
+            <property name="can_default">True</property>
+            <property name="no_show_all">True</property>
+            <property name="sensitive">False</property>
+            <style>
+              <class name="suggested-action"/>
+            </style>
+          </object>
+          <packing>
+            <property name="pack_type">end</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+    <style>
+      <class name="collection-dialog"/>
+    </style>
+  </template>
+</interface>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index e4e3a53..5d673b0 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -6,6 +6,7 @@ data/org.gnome.Documents.appdata.xml.in
 data/org.gnome.Documents.desktop.in
 data/org.gnome.documents.gschema.xml
 [type: gettext/glade]data/ui/app-menu.ui
+[type: gettext/glade]data/ui/organize-collection-dialog.ui
 [type: gettext/glade]data/ui/preview-context-menu.ui
 [type: gettext/glade]data/ui/preview-menu.ui
 [type: gettext/glade]data/ui/selection-menu.ui
diff --git a/src/selections.js b/src/selections.js
index 004cee2..98821aa 100644
--- a/src/selections.js
+++ b/src/selections.js
@@ -1,4 +1,5 @@
 /*
+ * Copyright © 2015 Alessandro Bono
  * Copyright (c) 2011 Red Hat, Inc.
  *
  * Gnome Documents is free software; you can redistribute it and/or modify
@@ -22,6 +23,7 @@
 const EvView = imports.gi.EvinceView;
 const Gd = imports.gi.Gd;
 const Gdk = imports.gi.Gdk;
+const Gio = imports.gi.Gio;
 const GLib = imports.gi.GLib;
 const GObject = imports.gi.GObject;
 const Gtk = imports.gi.Gtk;
@@ -31,19 +33,19 @@ const C_ = imports.gettext.pgettext;
 
 const Application = imports.application;
 const Documents = imports.documents;
+const Mainloop = imports.mainloop;
 const Manager = imports.manager;
 const Notifications = imports.notifications;
 const Properties = imports.properties;
 const Query = imports.query;
 const Sharing = imports.sharing;
+const TrackerUtils = imports.trackerUtils;
 const Utils = imports.utils;
+const WindowMode = imports.windowMode;
 
 const Lang = imports.lang;
 const Signals = imports.signals;
 
-const _COLLECTION_PLACEHOLDER_ID = 'collection-placeholder';
-const _SEPARATOR_PLACEHOLDER_ID = 'separator-placeholder';
-
 // fetch all the collections a given item is part of
 const FetchCollectionsJob = new Lang.Class({
     Name: 'FetchCollectionsJob',
@@ -310,362 +312,469 @@ const CreateCollectionJob = new Lang.Class({
     }
 });
 
-const OrganizeModelColumns = {
-    ID: 0,
-    NAME: 1,
-    STATE: 2
+const CollectionRowViews = {
+    DEFAULT: 'default-view',
+    DELETE: 'delete-view',
+    RENAME: 'rename-view'
 };
 
-const OrganizeCollectionModel = new Lang.Class({
-    Name: 'OrganizeCollectionModel',
-    Extends: Gtk.ListStore,
+const CollectionRow = new Lang.Class({
+    Name: "CollectionRow",
+    Extends: Gtk.ListBoxRow,
 
-    _init: function() {
+    _init: function(collection, collectionState) {
+        this.collection = collection;
+        this._collectionState = collectionState;
+        this._timeoutId = 0;
+        this.views = new Gtk.Stack();
         this.parent();
-        this.set_column_types(
-            [ GObject.TYPE_STRING,
-              GObject.TYPE_STRING,
-              GObject.TYPE_INT ]);
-
-        this._collAddedId = Application.documentManager.connect('item-added',
-            Lang.bind(this, this._onCollectionAdded));
-        this._collRemovedId = Application.documentManager.connect('item-removed',
-            Lang.bind(this, this._onCollectionRemoved));
+        this.add(this.views);
+        this.setDefaultView();
+    },
 
-        let iter;
+    _initDefaultView: function() {
+        let isActive = (this._collectionState & OrganizeCollectionState.ACTIVE);
+        let isInconsistent = (this._collectionState & OrganizeCollectionState.INCONSISTENT);
+
+        let grid = new Gtk.Grid({ margin_top: 6,
+                                  margin_bottom: 6,
+                                  margin_start: 12,
+                                  margin_end: 12,
+                                  orientation: Gtk.Orientation.HORIZONTAL });
+        this.checkButton = new Gtk.CheckButton({ label: this.collection.name,
+                                                 expand: true,
+                                                 active: isActive,
+                                                 inconsistent: isInconsistent });
+        this.checkButton.get_child().set_ellipsize(Pango.EllipsizeMode.END);
+        this.checkButton.connect('toggled', Lang.bind(this,
+            function(checkButton) {
+                let collId = this.collection.id;
+                let state = checkButton.get_active();
+
+                let job = new SetCollectionForSelectionJob(collId, state);
+                job.run();
+            }));
+        let menu = new Gio.Menu();
+        if (this.collection.canEdit())
+            menu.append(_("Rename…"), 'dialog.rename-collection(\'' + this.collection.id + '\')');
+        else
+            menu.append(_("Rename…"), 'dialog.action-disabled');
 
-        // add the placeholder
-        iter = this.append();
-        this.set(iter,
-            [ 0, 1, 2 ],
-            [ _COLLECTION_PLACEHOLDER_ID, '', OrganizeCollectionState.ACTIVE ]);
+        if (this.collection.canTrash())
+            menu.append(_("Delete"), 'dialog.delete-collection(\'' + this.collection.id + '\')');
+        else
+            menu.append(_("Delete"), 'dialog.action-disabled');
 
-        // add the separator
-        iter = this.append();
-        this.set(iter,
-            [ 0, 1, 2 ],
-            [ _SEPARATOR_PLACEHOLDER_ID, '', OrganizeCollectionState.ACTIVE ]);
+        let menuButton = new Gtk.MenuButton({ image: new Gtk.Image({ icon_name: 'open-menu-symbolic' }),
+                                              menu_model: menu,
+                                              relief: Gtk.ReliefStyle.NONE });
 
-        // populate the model
-        let job = new FetchCollectionStateForSelectionJob();
-        job.run(Lang.bind(this, this._onFetchCollectionStateForSelection));
+        grid.add(this.checkButton);
+        grid.add(menuButton);
+        grid.show_all();
+        this.views.add_named(grid, CollectionRowViews.DEFAULT);
     },
 
-    _findCollectionIter: function(item) {
-        let collPath = null;
+    _initDeleteView: function() {
+        let grid = new Gtk.Grid({ margin: 6, orientation: Gtk.Orientation.HORIZONTAL });
+        let message = _("“%s” removed").format(this.collection.name);
+        let deleteLabel = new Gtk.Label({ label: message,
+                                          ellipsize: Pango.EllipsizeMode.MIDDLE,
+                                          expand: true,
+                                          halign: Gtk.Align.START });
+        let undoButton = new Gtk.Button({ label: _("Undo") });
+        undoButton.connect('clicked', Lang.bind(this,
+            function() {
+                this._resetTimeout()
+                this.views.set_transition_type(Gtk.StackTransitionType.SLIDE_RIGHT);
+                this.views.set_transition_duration(200);
+                this.setDefaultView();
+            }));
+        grid.add(deleteLabel);
+        grid.add(undoButton);
 
-        this.foreach(Lang.bind(this,
-            function(model, path, iter) {
-                let id = model.get_value(iter, OrganizeModelColumns.ID);
+        grid.show_all();
+        this.views.add_named(grid, CollectionRowViews.DELETE);
+    },
 
-                if (item.id == id) {
-                    collPath = path.copy();
-                    return true;
+    _initRenameView: function() {
+        this.renameEntry = new Gtk.Entry({ activates_default: true,
+                                           expand: true,
+                                           text: this.collection.name,
+                                           secondary_icon_name: 'edit-clear-symbolic'});
+        this.renameEntry.connect('icon-press', Lang.bind(this,
+            function(renameEntry, iconPos) {
+                if (iconPos == Gtk.EntryIconPosition.SECONDARY) {
+                    renameEntry.set_text("");
                 }
-
-                return false;
             }));
-
-        if (collPath)
-            return this.get_iter(collPath)[1];
-
-        return null;
+        this.renameEntry.connect('changed', Lang.bind(this,
+            function(renameEntry) {
+                if (renameEntry.get_text() != "")
+                    renameEntry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, 
'edit-clear-symbolic');
+                else
+                    renameEntry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, null);
+            }));
+        this.renameEntry.show();
+        this.views.add_named(this.renameEntry, CollectionRowViews.RENAME);
     },
 
-    _onFetchCollectionStateForSelection: function(collectionState) {
-        for (let idx in collectionState) {
-            let item = Application.documentManager.getItemById(idx);
-
-            if ((collectionState[item.id] & OrganizeCollectionState.HIDDEN) != 0)
-                continue;
+    _resetTimeout: function() {
+        if (this._timeoutId != 0) {
+            Mainloop.source_remove(this._timeoutId);
+            this._timeoutId = 0;
+        }
+    },
 
-            let iter = this._findCollectionIter(item);
+    applyRename: function() {
+        let newName = this.renameEntry.get_text();
+        this.collection.name = newName;
+        TrackerUtils.setEditedName(newName, this.collection.id, null);
+        this.checkButton.set_label(newName);
+    },
 
-            if (!iter)
-                iter = this.append();
+    conceal: function() {
+        let revealer = new Gtk.Revealer({ reveal_child: true, transition_duration: 500 });
+        revealer.show();
+        // inserting revealer between (this) and (this.views)
+        this.remove(this.views);
+        revealer.add(this.views);
+        this.add(revealer);
 
-            this.set(iter,
-                [ 0, 1, 2 ],
-                [ item.id, item.name, collectionState[item.id] ]);
-        }
+        revealer.connect("notify::child-revealed", Lang.bind(this, this.deleteCollection));
+        revealer.reveal_child = false;
     },
 
-    _refreshState: function() {
-        let job = new FetchCollectionStateForSelectionJob();
-        job.run(Lang.bind(this, this._onFetchCollectionStateForSelection));
+    deleteCollection: function() {
+        this._resetTimeout();
+        Application.documentManager.removeItem(this.collection);
+        this.collection.trash();
     },
 
-    _onCollectionAdded: function(manager, itemAdded) {
-        this._refreshState();
+    setDefaultView: function() {
+        if (!this.views.get_child_by_name(CollectionRowViews.DEFAULT))
+            this._initDefaultView();
+
+        this.get_style_context().remove_class('delete-row');
+        this.views.set_visible_child_name(CollectionRowViews.DEFAULT);
     },
 
-    _onCollectionRemoved: function(manager, itemRemoved) {
-        let iter = this._findCollectionIter(itemRemoved);
+    setDeleteView: function() {
+        if (!this.views.get_child_by_name(CollectionRowViews.DELETE))
+            this._initDeleteView();
 
-        if (iter)
-            this.remove(iter);
-    },
+        this._timeoutId = Mainloop.timeout_add_seconds(Notifications.DELETE_TIMEOUT, Lang.bind(this,
+            function() {
+                this._timeoutId = 0;
+                this.conceal();
+                return false;
+            }));
+        this.views.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT);
+        this.views.set_transition_duration(500);
+        this.get_style_context().add_class('delete-row');
+        this.views.set_visible_child_name(CollectionRowViews.DELETE);
 
-    refreshCollectionState: function() {
-        this._refreshState();
     },
 
-    destroy: function() {
-        if (this._collAddedId != 0) {
-            Application.documentManager.disconnect(this._collAddedId);
-            this._collAddedId = 0;
+    setRenameView: function(onTextChanged) {
+        if (!this.views.get_child_by_name(CollectionRowViews.RENAME)) {
+            this._initRenameView();
+            this.renameEntry.connect('changed', onTextChanged);
         }
 
-        if (this._collRemovedId != 0) {
-            Application.documentManager.disconnect(this._collRemovedId);
-            this._collRemovedId = 0;
-        }
-    }
+        this.views.set_transition_type(Gtk.StackTransitionType.CROSSFADE);
+        this.views.set_transition_duration(200);
+        this.renameEntry.set_text(this.collection.name);
+        this.views.set_visible_child_name(CollectionRowViews.RENAME);
+    },
+
 });
 
-const OrganizeCollectionView = new Lang.Class({
-    Name: 'OrganizeCollectionView',
-    Extends: Gtk.Overlay,
+const CollectionList = new Lang.Class({
+    Name: 'CollectionList',
+    Extends: Gtk.ListBox,
 
     _init: function() {
-        this._choiceConfirmed = false;
+        this.parent({ vexpand: false,
+                      margin: 0,
+                      selection_mode: Gtk.SelectionMode.NONE });
 
-        this.parent();
+        let collAddedId = Application.documentManager.connect('item-added',
+            Lang.bind(this, this._onCollectionAdded));
+        let collRemovedId = Application.documentManager.connect('item-removed',
+            Lang.bind(this, this._onCollectionRemoved));
 
-        this._sw = new Gtk.ScrolledWindow({ shadow_type: Gtk.ShadowType.IN,
-                                            margin_start: 5,
-                                            margin_end: 5,
-                                            margin_bottom: 3 });
-        this.add(this._sw);
-
-        this._model = new OrganizeCollectionModel();
-        this._view = new Gtk.TreeView({ headers_visible: false,
-                                        vexpand: true,
-                                        hexpand: true });
-        this._view.set_model(this._model);
-        this._view.set_row_separator_func(Lang.bind(this,
-            function(model, iter) {
-                let id = model.get_value(iter, OrganizeModelColumns.ID);
-                return (id == _SEPARATOR_PLACEHOLDER_ID);
-            }));
-        this._sw.add(this._view);
-
-        this._msgGrid = new Gtk.Grid({ orientation: Gtk.Orientation.VERTICAL,
-                                       row_spacing: 12,
-                                       halign: Gtk.Align.CENTER,
-                                       margin_top: 64 });
-        this.add_overlay(this._msgGrid);
-
-        this._icon = new Gtk.Image({ resource: '/org/gnome/Documents/ui/collections-placeholder.png' });
-        this._msgGrid.add(this._icon);
-
-        this._label = new Gtk.Label({
-            justify: Gtk.Justification.CENTER,
-            label: _("You don't have any collections yet. Enter a new collection name above."),
-            max_width_chars: 32,
-            wrap: true });
-        this._label.get_style_context().add_class('dim-label');
-        this._msgGrid.add(this._label);
-
-        // show the overlay only if there aren't any collections in the model
-        this._msgGrid.visible = (this._model.iter_n_children(null) < 2);
-        this._model.connect('row-inserted', Lang.bind(this,
-            function() {
-                this._msgGrid.hide();
+        this.set_header_func(Lang.bind(this,
+            function(row, before) {
+                if (!before) {
+                    row.set_header(null);
+                    return;
+                }
+                let current = row.get_header();
+                if (!current) {
+                    current = new Gtk.Separator({ orientation: Gtk.Orientation.HORIZONTAL });
+                    row.set_header(current);
+                }
             }));
 
-        // force the editable row to be unselected
-        this.selection = this._view.get_selection();
-        let selectionChangedId = this.selection.connect('changed', Lang.bind(this,
-            function() {
-                this.selection.unselect_all();
-                if (selectionChangedId != 0) {
-                    this.selection.disconnect(selectionChangedId);
-                    selectionChangedId = 0;
-                }
+        this.set_sort_func(Lang.bind(this,
+            function(row1, row2) {
+                return row2.collection.mtime - row1.collection.mtime;
             }));
 
-        this._view.connect('destroy', Lang.bind(this,
+        this.connect('destroy', Lang.bind(this,
             function() {
-                this._model.destroy();
+                let rows = this.get_children();
+               rows.forEach(function(row) {
+                   let currentView = row.views.get_visible_child_name();
+                   if (currentView == CollectionRowViews.DELETE) {
+                       row.deleteCollection();
+                   }
+                });
+               Application.documentManager.disconnect(collAddedId);
+               Application.documentManager.disconnect(collRemovedId);
             }));
 
-        this._viewCol = new Gtk.TreeViewColumn();
-        this._view.append_column(this._viewCol);
+        // populate the list
+        let job = new FetchCollectionStateForSelectionJob();
+        job.run(Lang.bind(this, this._onFetchCollectionStateForSelection));
+    },
 
-        // 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));
+    _onCollectionAdded: function(manager, itemAdded) {
+        let collection =  new CollectionRow(itemAdded, OrganizeCollectionState.ACTIVE);
+        collection.show_all();
+        this.add(collection);
+    },
 
-        // icon
-        this._rendererIcon = new Gtk.CellRendererPixbuf();
-        this._viewCol.pack_start(this._rendererIcon, false);
-        this._viewCol.set_cell_data_func(this._rendererIcon,
-                                         Lang.bind(this, this._iconCellFunc));
+    _onCollectionRemoved: function(manager, itemRemoved) {
+        let rows = this.get_children();
+        for (let i = 0; i < rows.length; i++) {
+            if (rows[i].collection.id == itemRemoved.id) {
+                this.remove(rows[i]);
+                return;
+            }
+        }
+    },
 
-        // item name
-        this._rendererText = new Gtk.CellRendererText();
-        this._viewCol.pack_start(this._rendererText, true);
-        this._viewCol.set_cell_data_func(this._rendererText,
-                                         Lang.bind(this, this._textCellFunc));
+    _onFetchCollectionStateForSelection: function(collectionState) {
+        for (let idx in collectionState) {
+            let item = Application.documentManager.getItemById(idx);
 
-        this._rendererDetail = new Gd.StyledTextRenderer({ xpad: 16 });
-        this._rendererDetail.add_class('dim-label');
-        this._viewCol.pack_start(this._rendererDetail, false);
-        this._viewCol.set_cell_data_func(this._rendererDetail,
-                                         Lang.bind(this, this._detailCellFunc));
+            if ((collectionState[item.id] & OrganizeCollectionState.HIDDEN) != 0)
+                continue;
 
-        this._rendererText.connect('edited', Lang.bind(this, this._onTextEdited));
-        this._rendererText.connect('editing-canceled', Lang.bind(this, this._onTextEditCanceled));
+            let collection = new CollectionRow(item, collectionState[item.id]);
+            collection.show_all();
 
-        this._view.show();
+            this.add(collection);
+        }
     },
 
-    _onCheckToggled: function(renderer, pathStr) {
-        let path = Gtk.TreePath.new_from_string(pathStr);
-        let iter = this._model.get_iter(path)[1];
+    isEmpty: function() {
+        let rows = this.get_children();
+        return (rows.length == 0);
+    },
 
-        let collUrn = this._model.get_value(iter, OrganizeModelColumns.ID);
-        let state = this._rendererCheck.get_active();
+    isValidName: function(name) {
+        if (!name || name == '')
+            return false;
 
-        let job = new SetCollectionForSelectionJob(collUrn, !state);
-        job.run(Lang.bind(this,
-            function() {
-                this._model.refreshCollectionState();
+        let rows = this.get_children();
+        for (let i = 0; i < rows.length; i++) {
+            if (rows[i].collection.name == name)
+                return false;
+        }
+
+        return true;
+    }
+});
+
+const OrganizeCollectionDialog = new Lang.Class({
+    Name: 'OrganizeCollectionDialog',
+    Extends: Gtk.Window,
+    Template: 'resource:///org/gnome/Documents/ui/organize-collection-dialog.ui',
+    InternalChildren: [ 'content',
+                        'viewEmpty',
+                        'addEntryEmpty',
+                        'addButtonEmpty',
+                        'viewCollections',
+                        'addGridCollections',
+                        'addEntryCollections',
+                        'addButtonCollections',
+                        'scrolledWindowCollections',
+                        'headerBar',
+                        'cancelButton',
+                        'doneButton' ],
+
+    _init: function(toplevel) {
+        this.parent({ transient_for: toplevel });
+
+        this._renameMode = false;
+
+        this._keyPressEventId = this.connect('key-press-event', Lang.bind(this, this._onKeyPressed));
+        this._addButtonEmpty.connect('clicked', Lang.bind(this, this._onAddClicked));
+        this._addButtonCollections.connect('clicked', Lang.bind(this, this._onAddClicked));
+        this._addEntryEmpty.connect('changed', Lang.bind(this, this._onTextChanged));
+        this._addEntryCollections.connect('changed', Lang.bind(this, this._onTextChanged));
+
+        let actionGroup = new Gio.SimpleActionGroup();
+        let deleteAction = new Gio.SimpleAction({ name: 'delete-collection',
+                                                  parameter_type: GLib.VariantType.new('s') });
+        let renameAction = new Gio.SimpleAction({ name: 'rename-collection',
+                                                  parameter_type: GLib.VariantType.new('s') });
+        actionGroup.add_action(deleteAction);
+        actionGroup.add_action(renameAction);
+        this.insert_action_group('dialog', actionGroup);
+
+        renameAction.connect('activate', Lang.bind(this, this._renameModeStart));
+        deleteAction.connect('activate', Lang.bind(this,
+            function(action, parameter) {
+                let collId = parameter.get_string()[0];
+                let rows = this._collectionList.get_children();
+                rows.forEach(function(row) {
+                    if (row.collection.id != collId)
+                        return;
+
+                    row.setDeleteView();
+                });
             }));
-    },
 
-    _onTextEditedReal: function(cell, newText) {
-        //cell.editable = false;
+        this._cancelButton.connect('clicked', Lang.bind(this, function() { this._renameModeStop(false); }));
+        this._doneButton.connect('clicked', Lang.bind(this, function() { this._renameModeStop(true); }));
 
-        if (!newText || newText == '') {
-            // don't insert collections with empty names
-            return;
-        }
+        this._collectionList = new CollectionList();
+        let addId = this._collectionList.connect('add', Lang.bind(this, this._onCollectionListChanged));
+        let removeId = this._collectionList.connect('remove', Lang.bind(this, 
this._onCollectionListChanged));
+        this._scrolledWindowCollections.add(this._collectionList);
 
-        // update the new name immediately
-        let iter = this._model.append();
-        this._model.set_value(iter, OrganizeModelColumns.NAME, newText);
+        this.show_all();
+
+        this.connect('destroy', Lang.bind(this,
+            function() {
+                this._collectionList.disconnect(addId);
+                this._collectionList.disconnect(removeId);
+            }));
 
-        // force the editable row to be unselected
-        this.selection.unselect_all();
+        /* We want a CROSSFADE effect when switching from ViewEmpty to ViewCollections (and the other way 
around)
+         * but when we create the dialog we don't want to see the effect, so for the first second we don't 
use
+         * any effect and after that we use the CROSSFADE effect.
+         */
+        Mainloop.timeout_add_seconds(1, Lang.bind(this,
+            function() {
+                this._content.set_transition_type(Gtk.StackTransitionType.CROSSFADE)
+                return false;
+            }));
+    },
 
-        // actually create the new collection
+    _onAddClicked: function() {
+        let addEntry = this._collectionList.isEmpty() ? this._addEntryEmpty : this._addEntryCollections;
+        let newText = addEntry.get_text();
         let job = new CreateCollectionJob(newText);
         job.run(Lang.bind(this,
             function(createdUrn) {
                 if (!createdUrn)
                     return;
 
-                this._model.set_value(iter, OrganizeModelColumns.ID, createdUrn);
-
+                addEntry.set_text('');
                 let job = new SetCollectionForSelectionJob(createdUrn, true);
                 job.run(null);
             }));
+        if (!this._collectionList.isEmpty())
+            this._scrolledWindowCollections.get_vadjustment().set_value(0);
     },
 
-    _onTextEdited: function(cell, pathStr, newText) {
-        this._onTextEditedReal(cell, newText);
+    _onTextChanged: function(entry) {
+        let sensitive = this._collectionList.isValidName(entry.get_text());
+        if (this._renameMode)
+            this._doneButton.set_sensitive(sensitive);
+        else {
+            let addButton = this._collectionList.isEmpty() ? this._addButtonEmpty : 
this._addButtonCollections;
+            addButton.set_sensitive(sensitive);
+        }
     },
 
-    _onTextEditCanceled: function(cell) {
-        if (this._choiceConfirmed) {
-            this._choiceConfirmed = false;
+    _onKeyPressed: function (window, event) {
+        let keyval = event.get_keyval()[1];
+        if (keyval == Gdk.KEY_Escape) {
+            if (this._renameMode)
+                this._renameModeStop(false);
+            else
+                this.destroy();
 
-            let entry = this._viewCol.cell_area.get_edit_widget();
-            if (entry)
-                this._onTextEditedReal(cell, entry.get_text());
+            return Gdk.EVENT_STOP;
         }
+        return Gdk.EVENT_PROPAGATE;
     },
 
-    _checkCellFunc: function(col, cell, model, iter) {
-        let state = model.get_value(iter, OrganizeModelColumns.STATE);
-        let id = model.get_value(iter, OrganizeModelColumns.ID);
+    _renameModeStart: function(action, parameter) {
+        let collId = parameter.get_string()[0];
+        this._setRenameMode(true);
 
-        cell.active = (state & OrganizeCollectionState.ACTIVE);
-        cell.inconsistent = (state & OrganizeCollectionState.INCONSISTENT);
-        cell.visible = (id != _COLLECTION_PLACEHOLDER_ID);
-    },
+        let rows = this._collectionList.get_children();
+        rows.forEach(Lang.bind(this,
+            function(row) {
+                let currentView = row.views.get_visible_child_name();
+                if (currentView == CollectionRowViews.DELETE) {
+                    row.conceal();
+                    return;
+                }
 
-    _iconCellFunc: function(col, cell, model, iter) {
-        let id = model.get_value(iter, OrganizeModelColumns.ID);
+                if (row.collection.id != collId) {
+                    row.set_sensitive(false);
+                    return;
+                }
 
-        cell.icon_name = "list-add";
-        cell.visible = (id == _COLLECTION_PLACEHOLDER_ID);
+                row.setRenameView(Lang.bind(this, this._onTextChanged));
+            }));
     },
 
-    _textCellFunc: function(col, cell, model, iter) {
-        let id = model.get_value(iter, OrganizeModelColumns.ID);
-        let name = model.get_value(iter, OrganizeModelColumns.NAME);
+    _renameModeStop: function(rename) {
+        this._setRenameMode(false);
 
-        if (id == _COLLECTION_PLACEHOLDER_ID) {
-            cell.editable = true;
-            cell.text = '';
-            cell.placeholder_text = _("Create new collection");
-        } else {
-            cell.editable = false;
-            cell.text = name;
-        }
-    },
+        let rows = this._collectionList.get_children();
+        rows.forEach(function(row) {
+            let currentView = row.views.get_visible_child_name();
+            if (currentView != CollectionRowViews.RENAME) {
+                row.set_sensitive(true);
+                return;
+            }
 
-    _detailCellFunc: function(col, cell, model, iter) {
-        let id = model.get_value(iter, OrganizeModelColumns.ID);
-        let item = Application.documentManager.getItemById(id);
+            if (rename)
+                row.applyRename();
 
-        if (item && item.identifier.indexOf(Query.LOCAL_COLLECTIONS_IDENTIFIER) == -1) {
-            cell.text = Application.sourceManager.getItemById(item.resourceUrn).name;
-            cell.visible = true;
-        } else {
-            cell.text = '';
-            cell.visible = false;
-        }
+            row.setDefaultView();
+        });
     },
 
-    confirmedChoice: function() {
-        this._choiceConfirmed = true;
-    }
-});
-
-const OrganizeCollectionDialog = new Lang.Class({
-    Name: 'OrganizeCollectionDialog',
-    Extends: Gtk.Dialog,
-
-    _init: function(toplevel) {
-        this.parent({ transient_for: toplevel,
-                      modal: true,
-                      destroy_with_parent: true,
-                      use_header_bar: true,
-                      default_width: 400,
-                      default_height: 300,
-        // Translators: "Collections" refers to documents in this context
-                      title: C_("Dialog Title", "Collections") });
-
-        let closeButton = this.add_button('gtk-close', Gtk.ResponseType.CLOSE);
-        this.set_default_response(Gtk.ResponseType.CLOSE);
-
-        let contentArea = this.get_content_area();
-        let collView = new OrganizeCollectionView();
-        contentArea.add(collView);
-
-        // HACK:
-        // - We want clicking on "Close" to add the typed-in collection if we're
-        //   editing.
-        // - Unfortunately, since we focus out of the editable entry in order to
-        //   click the button, we'll get an editing-canceled signal on the renderer
-        //   from GTK. As this handler will run before focus-out, we here signal the
-        //   view to ignore the next editing-canceled signal and add the collection in
-        //   that case instead.
-        //
-        closeButton.connect('button-press-event', Lang.bind(this,
-            function() {
-                collView.confirmedChoice();
-                return false;
-            }));
+    _onCollectionListChanged: function() {
+        if (this._collectionList.isEmpty()) {
+            this._content.set_visible_child(this._viewEmpty);
+            this._addEntryEmpty.grab_focus();
+            this._addButtonEmpty.grab_default();
+        } else {
+            this._content.set_visible_child(this._viewCollections);
+           this._addEntryCollections.grab_focus();
+           this._addButtonCollections.grab_default();
+       }
+    },
 
-        this.show_all();
+    _setRenameMode: function(renameMode) {
+        this._renameMode = renameMode;
+        if (this._renameMode) {
+            this._headerBar.set_title(_("Rename"));
+            this._cancelButton.show();
+            this._doneButton.show();
+            this._doneButton.grab_default();
+        } else {
+            // Translators: "Collections" refers to documents in this context
+            this._headerBar.set_title(C_("Dialog Title", "Collections"));
+            this._cancelButton.hide();
+            this._doneButton.hide();
+            let addButton = this._collectionList.isEmpty() ? this._addButtonEmpty : 
this._addButtonCollections;
+            addButton.grab_default();
+        }
+        this._headerBar.set_show_close_button(!this._renameMode);
+        this._addGridCollections.set_sensitive(!this._renameMode);
     }
 });
 
@@ -689,7 +798,6 @@ const SelectionController = new Lang.Class({
 
                 return (item.id != value);
             }));
-
         if (changed) {
             this._selection = filtered;
             this.emit('selection-changed', this._selection);
@@ -775,8 +883,8 @@ const SelectionToolbar = new Lang.Class({
         toolbar.pack_end(this._toolbarProperties);
         this._toolbarProperties.connect('clicked', Lang.bind(this, this._onToolbarProperties));
 
-        // organize button
-        this._toolbarCollection = new Gtk.Button({ label: _("Add to Collection") });
+        // collections button
+        this._toolbarCollection = new Gtk.Button({ label: _("Collections") });
         toolbar.pack_end(this._toolbarCollection);
         this._toolbarCollection.connect('clicked', Lang.bind(this, this._onToolbarCollection));
 
@@ -885,10 +993,8 @@ const SelectionToolbar = new Lang.Class({
             return;
 
         let dialog = new OrganizeCollectionDialog(toplevel);
-
-        dialog.connect('response', Lang.bind(this,
-            function(widget, response) {
-                dialog.destroy();
+        dialog.connect('destroy', Lang.bind(this,
+            function() {
                 Application.selectionController.setSelectionMode(false);
             }));
     },
diff --git a/src/view.js b/src/view.js
index 044b1d8..89a5d3c 100644
--- a/src/view.js
+++ b/src/view.js
@@ -582,6 +582,10 @@ const ViewContainer = new Lang.Class({
     },
 
     _onViewSelectionChanged: function() {
+        let mode = Application.modeController.getWindowMode();
+        if (this._mode != mode)
+            return;
+
         // update the selection on the controller when the view signals a change
         let selectedURNs = Utils.getURNsFromPaths(this.view.get_selection(),
                                                   this._model);



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