[gnome-documents] Add bookmarks place



commit b0eb19903f9db0802bfcc1b10747071360087e23
Author: William Jon McCann <jmccann redhat com>
Date:   Sun Jan 27 14:40:31 2013 -0500

    Add bookmarks place
    
    https://bugzilla.gnome.org/show_bug.cgi?id=691254

 src/Makefile-lib.am           |   11 +-
 src/application.js            |    7 +-
 src/documents.js              |   11 +-
 src/lib/gd-bookmark.c         |  185 +++++++++++
 src/lib/gd-bookmark.h         |   50 +++
 src/lib/gd-bookmarks.c        |  342 ++++++++++++++++++++
 src/lib/gd-bookmarks.h        |   55 ++++
 src/lib/gd-nav-bar.c          |    2 +-
 src/lib/gd-places-bookmarks.c |  708 +++++++++++++++++++++++++++++++++++++++++
 src/lib/gd-places-bookmarks.h |   58 ++++
 src/lib/gd-places-links.c     |   23 +-
 src/places.js                 |   52 +++-
 src/preview.js                |   60 +++-
 13 files changed, 1529 insertions(+), 35 deletions(-)
---
diff --git a/src/Makefile-lib.am b/src/Makefile-lib.am
index da1b13e..51347b5 100644
--- a/src/Makefile-lib.am
+++ b/src/Makefile-lib.am
@@ -1,5 +1,6 @@
 gdprivate_cflags = \
     -I$(top_srcdir)/src \
+    -I$(top_srcdir)/libgd \
     -DPREFIX=\"$(prefix)\" \
     -DLIBDIR=\"$(libdir)\" \
     -DG_LOG_DOMAIN=\"Gdprivate\" \
@@ -11,7 +12,10 @@ gdprivate_source_h = \
     lib/gd-metadata.h \
     lib/gd-pdf-loader.h \
     lib/gd-nav-bar.h \
+    lib/gd-bookmark.h \
+    lib/gd-bookmarks.h \
     lib/gd-places-page.h \
+    lib/gd-places-bookmarks.h \
     lib/gd-places-links.h \
     $(NULL)
 
@@ -20,7 +24,10 @@ gdprivate_source_c = \
     lib/gd-metadata.c \
     lib/gd-pdf-loader.c \
     lib/gd-nav-bar.c \
+    lib/gd-bookmark.c \
+    lib/gd-bookmarks.c \
     lib/gd-places-page.c \
+    lib/gd-places-bookmarks.c \
     lib/gd-places-links.c \
     $(NULL)
 
@@ -28,7 +35,9 @@ pkglib_LTLIBRARIES += libgdprivate-1.0.la
 
 libgdprivate_1_0_la_LIBADD = \
     $(DOCUMENTS_LIBS) \
-    $(LIBM)
+    $(LIBM) \
+    $(top_builddir)/libgd/libgd.la
+
 
 libgdprivate_1_0_la_LDFLAGS = \
     -avoid-version
diff --git a/src/application.js b/src/application.js
index 4129ce2..cba72d9 100644
--- a/src/application.js
+++ b/src/application.js
@@ -404,8 +404,13 @@ const Application = new Lang.Class({
             { name: 'properties',
               callback: this._onActionProperties,
               window_mode: WindowMode.WindowMode.PREVIEW },
+            { name: 'bookmark-page',
+              callback: this._onActionToggle,
+              state: GLib.Variant.new('b', false),
+              accel: '<Primary>d',
+              window_mode: WindowMode.WindowMode.PREVIEW },
             { name: 'places',
-              accel: 'F3',
+              accel: '<Primary>b',
               window_mode: WindowMode.WindowMode.PREVIEW }
         ];
 
diff --git a/src/documents.js b/src/documents.js
index bb6eee3..bedf24f 100644
--- a/src/documents.js
+++ b/src/documents.js
@@ -938,10 +938,11 @@ const DocumentManager = new Lang.Class({
         // save loaded model and signal
         this._activeDocModel = docModel;
         this._activeDocModel.set_continuous(false);
-        this.emit('load-finished', doc, docModel);
 
         // load metadata
         this._connectMetadata(docModel);
+
+        this.emit('load-finished', doc, docModel);
     },
 
     reloadActiveItem: function() {
@@ -1004,6 +1005,7 @@ const DocumentManager = new Lang.Class({
                     this._activeDocModel.disconnect(id);
                 }));
 
+            this.metadata = null;
             this._activeDocModel = null;
             this._activeDocModelIds = [];
         }
@@ -1012,20 +1014,19 @@ const DocumentManager = new Lang.Class({
     _connectMetadata: function(docModel) {
         let evDoc = docModel.get_document();
         let file = Gio.File.new_for_uri(evDoc.get_uri());
-
         if (!GdPrivate.is_metadata_supported_for_file(file))
             return;
 
-        let metadata = new GdPrivate.Metadata({ file: file });
+        this.metadata = new GdPrivate.Metadata({ file: file });
 
         // save current page in metadata
-        let [res, val] = metadata.get_int('page');
+        let [res, val] = this.metadata.get_int('page');
         if (res)
             docModel.set_page(val);
         this._activeDocModelIds.push(
             docModel.connect('page-changed', Lang.bind(this,
                 function(source, oldPage, newPage) {
-                    metadata.set_int('page', newPage);
+                    this.metadata.set_int('page', newPage);
                 }))
         );
     }
diff --git a/src/lib/gd-bookmark.c b/src/lib/gd-bookmark.c
new file mode 100644
index 0000000..25345aa
--- /dev/null
+++ b/src/lib/gd-bookmark.c
@@ -0,0 +1,185 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8;  -*-
+ *
+ *  Copyright (C) 2013 Red Hat, Inc.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include "gd-bookmark.h"
+
+enum {
+        PROP_0,
+        PROP_PAGE_NUMBER,
+        PROP_TITLE,
+};
+
+struct _GdBookmark {
+        GObject base;
+
+        char   *title;
+        guint   page_num;
+};
+
+struct _GdBookmarkClass {
+        GObjectClass base_class;
+};
+
+G_DEFINE_TYPE (GdBookmark, gd_bookmark, G_TYPE_OBJECT)
+
+int
+gd_bookmark_compare (GdBookmark *a,
+                     GdBookmark *b)
+{
+        if (a->page_num < b->page_num) {
+                return -1;
+        }
+        if (a->page_num > b->page_num) {
+                return 1;
+        }
+
+        return 0;
+}
+
+const char *
+gd_bookmark_get_title (GdBookmark *bookmark)
+{
+        return bookmark->title;
+}
+
+void
+gd_bookmark_set_title (GdBookmark *bookmark,
+                       const char *title)
+{
+        if (g_strcmp0 (title, bookmark->title) == 0) {
+                return;
+        }
+
+        g_free (bookmark->title);
+        bookmark->title = g_strdup (title);
+        g_object_notify (G_OBJECT (bookmark), "title");
+}
+
+guint
+gd_bookmark_get_page_number (GdBookmark *bookmark)
+{
+        return bookmark->page_num;
+}
+
+void
+gd_bookmark_set_page_number (GdBookmark *bookmark,
+                             guint       page_num)
+{
+        if (page_num == bookmark->page_num) {
+                return;
+        }
+
+        bookmark->page_num = page_num;
+        g_object_notify (G_OBJECT (bookmark), "page-number");
+}
+
+static void
+gd_bookmark_finalize (GObject *object)
+{
+        GdBookmark *bookmark = GD_BOOKMARK (object);
+
+        g_free (bookmark->title);
+
+        G_OBJECT_CLASS (gd_bookmark_parent_class)->finalize (object);
+}
+
+static void
+gd_bookmark_init (GdBookmark *bookmark)
+{
+}
+
+static void
+gd_bookmark_get_property (GObject    *object,
+                          guint       prop_id,
+                          GValue     *value,
+                          GParamSpec *pspec)
+{
+        GdBookmark *self = GD_BOOKMARK (object);
+
+        switch (prop_id) {
+        case PROP_TITLE:
+                g_value_set_string (value, self->title);
+                break;
+        case PROP_PAGE_NUMBER:
+                g_value_set_uint (value, self->page_num);
+                break;
+        default:
+                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+                break;
+        }
+}
+
+static void
+gd_bookmark_set_property (GObject      *object,
+                          guint         prop_id,
+                          const GValue *value,
+                          GParamSpec   *pspec)
+{
+        GdBookmark *self = GD_BOOKMARK (object);
+
+        switch (prop_id) {
+        case PROP_TITLE:
+                gd_bookmark_set_title (self, g_value_get_string (value));
+                break;
+        case PROP_PAGE_NUMBER:
+                gd_bookmark_set_page_number (self, g_value_get_uint (value));
+                break;
+        default:
+                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+        }
+}
+
+static void
+gd_bookmark_class_init (GdBookmarkClass *klass)
+{
+        GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+        gobject_class->get_property = gd_bookmark_get_property;
+        gobject_class->set_property = gd_bookmark_set_property;
+        gobject_class->finalize = gd_bookmark_finalize;
+
+        g_object_class_install_property (gobject_class,
+                                         PROP_PAGE_NUMBER,
+                                         g_param_spec_uint ("page-number",
+                                                            "Page Number",
+                                                            "Page Number",
+                                                            0,
+                                                            G_MAXUINT,
+                                                            0,
+                                                            G_PARAM_READWRITE |
+                                                            G_PARAM_STATIC_STRINGS));
+        g_object_class_install_property (gobject_class,
+                                         PROP_TITLE,
+                                         g_param_spec_string ("title",
+                                                              "Title",
+                                                              "Title",
+                                                              NULL,
+                                                              G_PARAM_READWRITE |
+                                                              G_PARAM_STATIC_STRINGS));
+}
+
+GdBookmark *
+gd_bookmark_new (void)
+{
+        return GD_BOOKMARK (g_object_new (GD_TYPE_BOOKMARK, NULL));
+}
diff --git a/src/lib/gd-bookmark.h b/src/lib/gd-bookmark.h
new file mode 100644
index 0000000..fc5937d
--- /dev/null
+++ b/src/lib/gd-bookmark.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8;  -*-
+ *
+ *  Copyright (C) 2013 Red Hat, Inc.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef GD_BOOKMARK_H
+#define GD_BOOKMARK_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GD_TYPE_BOOKMARK         (gd_bookmark_get_type())
+#define GD_BOOKMARK(object)      (G_TYPE_CHECK_INSTANCE_CAST((object), GD_TYPE_BOOKMARK, GdBookmark))
+#define GD_BOOKMARK_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GD_TYPE_BOOKMARK, GdBookmarkClass))
+#define GD_IS_BOOKMARK(object)   (G_TYPE_CHECK_INSTANCE_TYPE((object), GD_TYPE_BOOKMARK))
+
+typedef struct _GdBookmark      GdBookmark;
+typedef struct _GdBookmarkClass GdBookmarkClass;
+
+GType        gd_bookmark_get_type         (void) G_GNUC_CONST;
+
+GdBookmark  *gd_bookmark_new              (void);
+
+void         gd_bookmark_set_page_number  (GdBookmark *bookmark,
+                                           guint       num);
+guint        gd_bookmark_get_page_number  (GdBookmark *bookmark);
+void         gd_bookmark_set_title        (GdBookmark *bookmark,
+                                           const char *title);
+const char * gd_bookmark_get_title        (GdBookmark *bookmark);
+
+int          gd_bookmark_compare          (GdBookmark *a,
+                                           GdBookmark *b);
+G_END_DECLS
+
+#endif /* GD_BOOKMARK_H */
diff --git a/src/lib/gd-bookmarks.c b/src/lib/gd-bookmarks.c
new file mode 100644
index 0000000..982792c
--- /dev/null
+++ b/src/lib/gd-bookmarks.c
@@ -0,0 +1,342 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8;  -*-
+ *
+ *  Copyright (C) 2010 Carlos Garcia Campos  <carlosgc gnome org>
+ *  Copyright (C) 2013 Red Hat, Inc.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include "gd-bookmarks.h"
+
+enum {
+        PROP_0,
+        PROP_METADATA,
+        PROP_N_ITEMS
+};
+
+enum {
+        CHANGED,
+        N_SIGNALS
+};
+
+struct _GdBookmarks {
+        GObject base;
+
+        GdMetadata *metadata;
+        GList *items;
+};
+
+struct _GdBookmarksClass {
+        GObjectClass base_class;
+
+        void (*changed) (GdBookmarks *bookmarks);
+};
+
+G_DEFINE_TYPE (GdBookmarks, gd_bookmarks, G_TYPE_OBJECT)
+
+static guint signals[N_SIGNALS];
+
+static void
+gd_bookmarks_finalize (GObject *object)
+{
+        GdBookmarks *self = GD_BOOKMARKS (object);
+
+        g_list_free_full (self->items, g_object_unref);
+
+        g_clear_object (&self->metadata);
+
+        G_OBJECT_CLASS (gd_bookmarks_parent_class)->finalize (object);
+}
+
+static void
+gd_bookmarks_init (GdBookmarks *bookmarks)
+{
+}
+
+guint
+gd_bookmarks_get_n_items (GdBookmarks *bookmarks)
+{
+        g_return_val_if_fail (GD_IS_BOOKMARKS (bookmarks), 0);
+
+        return g_list_length (bookmarks->items);
+}
+
+static void
+gd_bookmarks_get_property (GObject      *object,
+                           guint         prop_id,
+                           GValue       *value,
+                           GParamSpec   *pspec)
+{
+        GdBookmarks *self = GD_BOOKMARKS (object);
+
+        switch (prop_id) {
+        case PROP_N_ITEMS:
+                g_value_set_uint (value, gd_bookmarks_get_n_items (self));
+                break;
+        default:
+                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+        }
+}
+
+static void
+gd_bookmarks_set_property (GObject      *object,
+                           guint         prop_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
+{
+        GdBookmarks *self = GD_BOOKMARKS (object);
+
+        switch (prop_id) {
+        case PROP_METADATA:
+                self->metadata = (GdMetadata *)g_value_dup_object (value);
+                break;
+        default:
+                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+        }
+}
+
+static void
+gd_bookmarks_constructed (GObject *object)
+{
+        GdBookmarks *self = GD_BOOKMARKS (object);
+        const char  *bm_list_str;
+        GVariant    *bm_list;
+        GVariantIter iter;
+        GVariant    *child;
+        GError      *error = NULL;
+
+        if (!gd_metadata_get_string (self->metadata, "bookmarks", &bm_list_str)) {
+                return;
+        }
+
+        if (bm_list_str == NULL || bm_list_str[0] == '\0') {
+                return;
+        }
+
+        bm_list = g_variant_parse ((const GVariantType *)"a(us)",
+                                   bm_list_str, NULL, NULL,
+                                   &error);
+        if (bm_list == NULL) {
+                g_warning ("Error getting bookmarks: %s\n", error->message);
+                g_error_free (error);
+
+                return;
+        }
+
+        g_variant_iter_init (&iter, bm_list);
+        while ((child = g_variant_iter_next_value (&iter))) {
+                guint page_num;
+                const char *title = NULL;
+
+                g_variant_get (child, "(u&s)", &page_num, &title);
+                if (title != NULL) {
+                        GdBookmark *bm = gd_bookmark_new ();
+                        gd_bookmark_set_title (bm, title);
+                        gd_bookmark_set_page_number (bm, page_num);
+                        self->items = g_list_prepend (self->items, bm);
+                        g_object_notify (G_OBJECT (self), "n-items");
+                }
+                g_variant_unref (child);
+        }
+        g_variant_unref (bm_list);
+
+        self->items = g_list_reverse (self->items);
+}
+
+static void
+gd_bookmarks_class_init (GdBookmarksClass *klass)
+{
+        GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+        gobject_class->get_property = gd_bookmarks_get_property;
+        gobject_class->set_property = gd_bookmarks_set_property;
+        gobject_class->finalize = gd_bookmarks_finalize;
+        gobject_class->constructed = gd_bookmarks_constructed;
+
+        g_object_class_install_property (gobject_class,
+                                         PROP_METADATA,
+                                         g_param_spec_object ("metadata",
+                                                              "Metadata",
+                                                              "The document metadata",
+                                                              GD_TYPE_METADATA,
+                                                              G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE |
+                                                              G_PARAM_STATIC_STRINGS));
+        g_object_class_install_property (gobject_class,
+                                         PROP_N_ITEMS,
+                                         g_param_spec_uint ("n-items",
+                                                            "N Items",
+                                                            "Number of bookmark items",
+                                                            0,
+                                                            G_MAXUINT,
+                                                            0,
+                                                            G_PARAM_READABLE |
+                                                            G_PARAM_STATIC_STRINGS));
+
+        /* Signals */
+        signals[CHANGED] = g_signal_new ("changed",
+                                         GD_TYPE_BOOKMARKS,
+                                         G_SIGNAL_RUN_LAST,
+                                         G_STRUCT_OFFSET (GdBookmarksClass, changed),
+                                         NULL, NULL,
+                                         g_cclosure_marshal_VOID__VOID,
+                                         G_TYPE_NONE, 0);
+}
+
+GdBookmarks *
+gd_bookmarks_new (GdMetadata *metadata)
+{
+        g_return_val_if_fail (GD_IS_METADATA (metadata), NULL);
+
+        return GD_BOOKMARKS (g_object_new (GD_TYPE_BOOKMARKS,
+                                           "metadata", metadata,
+                                           NULL));
+}
+
+static void
+gd_bookmarks_save (GdBookmarks *self)
+{
+        GList          *l;
+        GVariantBuilder builder;
+        GVariant       *bm_list;
+        char           *bm_list_str;
+
+        if (self->items == NULL) {
+                gd_metadata_set_string (self->metadata, "bookmarks", "");
+                return;
+        }
+
+        g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY);
+        for (l = self->items; l; l = g_list_next (l)) {
+                GdBookmark *bm = (GdBookmark *)l->data;
+                const char *title = gd_bookmark_get_title (bm);
+                guint page_num = gd_bookmark_get_page_number (bm);
+
+                g_variant_builder_add (&builder, "(u&s)",
+                                       page_num,
+                                       title != NULL ? title : "");
+        }
+        bm_list = g_variant_builder_end (&builder);
+
+        bm_list_str = g_variant_print (bm_list, FALSE);
+        g_variant_unref (bm_list);
+        gd_metadata_set_string (self->metadata, "bookmarks", bm_list_str);
+        g_free (bm_list_str);
+}
+
+/**
+ * gd_bookmarks_find_bookmark:
+ * @bookmarks:
+ * @bookmark:
+ *
+ * Returns: (transfer none)
+ */
+GdBookmark *
+gd_bookmarks_find_bookmark (GdBookmarks *bookmarks,
+                            GdBookmark  *bookmark)
+{
+        GList *l;
+
+        l = g_list_find_custom (bookmarks->items, bookmark, (GCompareFunc)gd_bookmark_compare);
+        if (l != NULL)
+                return l->data;
+
+        return NULL;
+}
+
+/**
+ * gd_bookmarks_get_bookmarks:
+ * @bookmarks:
+ *
+ * Returns: (transfer container) (element-type GdBookmark): A list of #GdBookmark objects
+ */
+GList *
+gd_bookmarks_get_bookmarks (GdBookmarks *bookmarks)
+{
+        g_return_val_if_fail (GD_IS_BOOKMARKS (bookmarks), NULL);
+
+        return g_list_copy (bookmarks->items);
+}
+
+void
+gd_bookmarks_add (GdBookmarks *bookmarks,
+                  GdBookmark  *bookmark)
+{
+        GdBookmark *bm;
+
+        g_return_if_fail (GD_IS_BOOKMARKS (bookmarks));
+
+        bm = gd_bookmarks_find_bookmark (bookmarks, bookmark);
+        if (bm != NULL) {
+                return;
+        }
+
+        bookmarks->items = g_list_append (bookmarks->items, g_object_ref (bookmark));
+        g_object_notify (G_OBJECT (bookmarks), "n-items");
+        g_signal_emit (bookmarks, signals[CHANGED], 0);
+        gd_bookmarks_save (bookmarks);
+}
+
+void
+gd_bookmarks_remove (GdBookmarks *bookmarks,
+                     GdBookmark  *bookmark)
+{
+        GdBookmark *bm;
+
+        g_return_if_fail (GD_IS_BOOKMARKS (bookmarks));
+
+        bm = gd_bookmarks_find_bookmark (bookmarks, bookmark);
+        if (bm == NULL) {
+                return;
+        }
+
+        bookmarks->items = g_list_remove (bookmarks->items, bm);
+        g_object_unref (bm);
+        g_object_notify (G_OBJECT (bookmarks), "n-items");
+        g_signal_emit (bookmarks, signals[CHANGED], 0);
+        gd_bookmarks_save (bookmarks);
+}
+
+void
+gd_bookmarks_update (GdBookmarks *bookmarks,
+                     GdBookmark  *bookmark)
+{
+        GList      *bm_link;
+        GdBookmark *bm;
+        const char *title_a;
+        const char *title_b;
+
+        g_return_if_fail (GD_IS_BOOKMARKS (bookmarks));
+
+        bm_link = g_list_find_custom (bookmarks->items, bookmark, (GCompareFunc)gd_bookmark_compare);
+        if (bm_link == NULL) {
+                return;
+        }
+
+        bm = (GdBookmark *)bm_link->data;
+
+        title_a = gd_bookmark_get_title (bm);
+        title_b = gd_bookmark_get_title (bookmark);
+
+        if (g_strcmp0 (title_a, title_b) == 0) {
+                return;
+        }
+
+        g_signal_emit (bookmarks, signals[CHANGED], 0);
+        gd_bookmarks_save (bookmarks);
+}
diff --git a/src/lib/gd-bookmarks.h b/src/lib/gd-bookmarks.h
new file mode 100644
index 0000000..a6e5855
--- /dev/null
+++ b/src/lib/gd-bookmarks.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8;  -*-
+ *
+ *  Copyright (C) 2010 Carlos Garcia Campos  <carlosgc gnome org>
+ *  Copyright (C) 2013 Red Hat, Inc.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef GD_BOOKMARKS_H
+#define GD_BOOKMARKS_H
+
+#include <glib-object.h>
+
+#include "gd-bookmark.h"
+#include "gd-metadata.h"
+
+G_BEGIN_DECLS
+
+#define GD_TYPE_BOOKMARKS         (gd_bookmarks_get_type())
+#define GD_BOOKMARKS(object)      (G_TYPE_CHECK_INSTANCE_CAST((object), GD_TYPE_BOOKMARKS, GdBookmarks))
+#define GD_BOOKMARKS_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GD_TYPE_BOOKMARKS, GdBookmarksClass))
+#define GD_IS_BOOKMARKS(object)   (G_TYPE_CHECK_INSTANCE_TYPE((object), GD_TYPE_BOOKMARKS))
+
+typedef struct _GdBookmarks      GdBookmarks;
+typedef struct _GdBookmarksClass GdBookmarksClass;
+
+GType        gd_bookmarks_get_type      (void) G_GNUC_CONST;
+
+GdBookmarks *gd_bookmarks_new           (GdMetadata  *metadata);
+guint        gd_bookmarks_get_number    (GdBookmarks *bookmarks);
+GList       *gd_bookmarks_get_bookmarks (GdBookmarks *bookmarks);
+GdBookmark  *gd_bookmarks_find_bookmark (GdBookmarks *bookmarks,
+                                         GdBookmark  *bookmark);
+void         gd_bookmarks_add           (GdBookmarks *bookmarks,
+                                         GdBookmark  *bookmark);
+void         gd_bookmarks_remove        (GdBookmarks *bookmarks,
+                                         GdBookmark  *bookmark);
+void         gd_bookmarks_update        (GdBookmarks *bookmarks,
+                                         GdBookmark  *bookmark);
+
+G_END_DECLS
+
+#endif /* GD_BOOKMARKS_H */
diff --git a/src/lib/gd-nav-bar.c b/src/lib/gd-nav-bar.c
index a071164..6df2480 100644
--- a/src/lib/gd-nav-bar.c
+++ b/src/lib/gd-nav-bar.c
@@ -917,7 +917,7 @@ gd_nav_bar_init (GdNavBar *self)
         gtk_widget_set_hexpand (GTK_WIDGET (inner_box), TRUE);
         gtk_container_add (GTK_CONTAINER (self), inner_box);
 
-        priv->button_area = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5);
+        priv->button_area = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
         gtk_widget_set_margin_left (priv->button_area, 5);
         gtk_widget_set_margin_right (priv->button_area, 5);
         gtk_widget_show (priv->button_area);
diff --git a/src/lib/gd-places-bookmarks.c b/src/lib/gd-places-bookmarks.c
new file mode 100644
index 0000000..73c021b
--- /dev/null
+++ b/src/lib/gd-places-bookmarks.c
@@ -0,0 +1,708 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8;  -*-
+ *
+ *  Copyright (C) 2010 Carlos Garcia Campos  <carlosgc gnome org>
+ *  Copyright (C) 2013 Red Hat, Inc.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include <evince-document.h>
+#include <evince-view.h>
+
+#include <libgd/gd.h>
+
+#include "gd-places-bookmarks.h"
+#include "gd-places-page.h"
+
+struct _GdPlacesBookmarksPrivate {
+        EvDocumentModel *document_model;
+        GdBookmarks     *bookmarks;
+        const char      *name;
+        GtkWidget       *tree_view;
+
+        EvJob           *job;
+
+        guint            activated_id;
+};
+
+enum {
+        PROP_0,
+        PROP_NAME,
+        PROP_DOCUMENT_MODEL,
+        PROP_BOOKMARKS,
+};
+
+enum {
+        COLUMN_MARKUP,
+        COLUMN_PAGE_LABEL,
+        COLUMN_BOOKMARK,
+        N_COLUMNS
+};
+
+enum {
+        BOOKMARK_ACTIVATED,
+        N_SIGNALS
+};
+
+static guint signals[N_SIGNALS];
+
+static void gd_places_bookmarks_page_iface_init (GdPlacesPageInterface *iface);
+
+G_DEFINE_TYPE_EXTENDED (GdPlacesBookmarks,
+                        gd_places_bookmarks,
+                        GTK_TYPE_BOX,
+                        0,
+                        G_IMPLEMENT_INTERFACE (GD_TYPE_PLACES_PAGE,
+                                               gd_places_bookmarks_page_iface_init))
+
+static GdBookmark *
+gd_places_bookmarks_get_selected_bookmark (GdPlacesBookmarks *self,
+                                           GtkTreeSelection  *selection)
+{
+        GtkTreeModel *model;
+        GtkTreeIter   iter;
+
+        if (gtk_tree_selection_get_selected (selection, &model, &iter)) {
+                GdBookmark *bookmark;
+
+                gtk_tree_model_get (model, &iter,
+                                    COLUMN_BOOKMARK, &bookmark,
+                                    -1);
+                return bookmark;
+        }
+
+        return NULL;
+}
+
+typedef struct {
+        EvDocument *document;
+        guint page_number;
+        char *markup;
+} LinkModelData;
+
+static gboolean
+link_model_foreach (GtkTreeModel *model,
+                    GtkTreePath  *path,
+                    GtkTreeIter  *iter,
+                    gpointer      user_data)
+{
+        LinkModelData *data = user_data;
+        EvLink *link = NULL;
+        char *markup = NULL;
+        int link_page;
+        gboolean ret = FALSE;
+
+        gtk_tree_model_get (model, iter,
+                            EV_DOCUMENT_LINKS_COLUMN_LINK, &link,
+                            EV_DOCUMENT_LINKS_COLUMN_MARKUP, &markup,
+                            -1);
+        if (link != NULL) {
+                link_page = ev_document_links_get_link_page (EV_DOCUMENT_LINKS (data->document), link);
+                if (link_page == data->page_number) {
+                        GtkTreeIter parent;
+
+                        if (gtk_tree_model_iter_parent (model, &parent, iter)) {
+                                char *parent_markup = NULL;
+                                gtk_tree_model_get (model, &parent,
+                                                    EV_DOCUMENT_LINKS_COLUMN_MARKUP, &parent_markup,
+                                                    -1);
+                                if (parent_markup != NULL) {
+                                        data->markup = g_strdup_printf ("%s ï %s", parent_markup, markup);
+                                        g_free (parent_markup);
+                                }
+                        }
+
+                        if (data->markup == NULL) {
+                                data->markup = g_strdup (markup);
+                        }
+
+                        ret = TRUE;
+                }
+        }
+
+        g_free (markup);
+        g_clear_object (&link);
+
+        return ret;
+}
+
+static char *
+get_link_title_for_page (EvDocument   *document,
+                         GtkTreeModel *links_model,
+                         guint         page)
+{
+        LinkModelData *data;
+        char *ret;
+
+        data = g_new0 (LinkModelData, 1);
+        data->page_number = page;
+        data->document = document;
+        gtk_tree_model_foreach (links_model, link_model_foreach, data);
+        ret = data->markup;
+        g_free (data);
+
+        return ret;
+}
+
+static void
+enable_selection (GdPlacesBookmarks *self,
+                  gboolean           enabled)
+{
+        GtkTreeSelection *selection;
+
+        selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self->priv->tree_view));
+        gtk_tree_selection_set_mode (selection, enabled ? GTK_SELECTION_SINGLE : GTK_SELECTION_NONE);
+}
+
+#define MAX_LEN_LABEL 200
+#define MIN_LEN_LABEL 20
+
+static char *
+get_pretty_name (const char *text)
+{
+        char *name = NULL;
+        char trimmed[MAX_LEN_LABEL];
+        char basename[MAX_LEN_LABEL];
+        int i;
+        int last_word = -1;
+        int last_sentence = -1;
+        int last_nonspace = -1;
+        int num_attrs;
+        PangoLogAttr *attrs;
+        gboolean ellipse = TRUE;
+
+        num_attrs = MIN (g_utf8_strlen (text, -1) + 1, MAX_LEN_LABEL);
+        attrs = g_new (PangoLogAttr, num_attrs);
+        g_utf8_strncpy (trimmed, text, num_attrs - 1);
+        pango_get_log_attrs (trimmed, -1, -1, pango_language_get_default (), attrs, num_attrs);
+
+        /* since the end of the text will always match a word boundary don't include it */
+        for (i = 0; (i < num_attrs - 1); i++) {
+                if (!attrs[i].is_white) {
+                        last_nonspace = i;
+                }
+                if (attrs[i].is_sentence_end) {
+                        last_sentence = i;
+                }
+                if (attrs[i].is_word_boundary) {
+                        last_word = last_nonspace;
+                }
+        }
+        g_free (attrs);
+
+        if (last_sentence > 0) {
+                i = last_sentence;
+                ellipse = FALSE;
+        } else {
+                i = last_word;
+        }
+
+        g_utf8_strncpy (basename, trimmed, i);
+        if (ellipse) {
+                name = g_strdup_printf ("â%sââ", basename);
+        } else {
+                name = g_strdup_printf ("â%sâ", basename);
+        }
+
+        return name;
+}
+
+static char *
+remove_duplicate_whitespace (const char *old)
+{
+        char   *new;
+        GRegex *re;
+        GError *error;
+
+        error = NULL;
+        re = g_regex_new ("[ \t\n\r]+", G_REGEX_MULTILINE, 0, &error);
+        if (re == NULL) {
+                g_warning ("Error building regex: %s", error->message);
+                g_error_free (error);
+                return g_strdup (old);
+        }
+
+        new = g_regex_replace (re, old, -1, 0, " ", 0, &error);
+        g_regex_unref (re);
+        if (new == NULL) {
+                g_warning ("Error replacing string: %s", error->message);
+                g_error_free (error);
+                return g_strdup (old);
+        }
+
+        return new;
+}
+
+static void
+load_bookmark_model (GdPlacesBookmarks *self,
+                     GtkTreeModel      *links_model)
+{
+        GtkListStore *model;
+        GList *items;
+        GList *l;
+        EvDocument *document;
+
+        if (self->priv->bookmarks == NULL) {
+                return;
+        }
+
+        model = GTK_LIST_STORE (gtk_tree_view_get_model (GTK_TREE_VIEW (self->priv->tree_view)));
+
+        document = ev_document_model_get_document (self->priv->document_model);
+
+        items = gd_bookmarks_get_bookmarks (self->priv->bookmarks);
+        items = g_list_sort (items, (GCompareFunc)gd_bookmark_compare);
+        for (l = items; l; l = g_list_next (l)) {
+                GdBookmark *bookmark = (GdBookmark *)l->data;
+                GtkTreeIter iter;
+                const char *title;
+                char *label = NULL;
+                char *markup = NULL;
+                guint page;
+
+                title = gd_bookmark_get_title (bookmark);
+                page = gd_bookmark_get_page_number (bookmark);
+
+                if (ev_document_has_text_page_labels (document)) {
+                        label = ev_document_get_page_label (document, page);
+                } else {
+                        label = g_strdup_printf ("%d", page + 1);
+                }
+
+                if (links_model != NULL) {
+                        markup = get_link_title_for_page (document, links_model, page);
+                }
+
+                if (markup == NULL && EV_IS_DOCUMENT_TEXT (document)) {
+                        char *text;
+                        char *trimmed;
+                        char *stripped;
+                        EvPage *ev_page;
+
+                        ev_page = ev_document_get_page (document, page);
+                        text = ev_document_text_get_text (EV_DOCUMENT_TEXT (document), ev_page);
+                        trimmed = g_utf8_substring (text, 0, MAX_LEN_LABEL * 2);
+                        g_free (text);
+                        stripped = remove_duplicate_whitespace (trimmed);
+                        g_free (trimmed);
+                        markup = get_pretty_name (stripped);
+                        g_free (stripped);
+                }
+
+                if (markup == NULL) {
+                        markup = g_strdup_printf (_("Page %s"), label);
+                }
+
+                gtk_list_store_append (model, &iter);
+                gtk_list_store_set (model, &iter,
+                                    COLUMN_MARKUP, markup != NULL ? markup : title,
+                                    COLUMN_PAGE_LABEL, label,
+                                    COLUMN_BOOKMARK, bookmark,
+                                    -1);
+                g_free (label);
+                g_free (markup);
+        }
+
+        enable_selection (self, TRUE);
+
+        g_list_free (items);
+}
+
+static void
+job_finished_cb (EvJobLinks        *job,
+                 GdPlacesBookmarks *self)
+{
+        GdPlacesBookmarksPrivate *priv = self->priv;
+        GtkListStore             *model;
+
+        model = GTK_LIST_STORE (gtk_tree_view_get_model (GTK_TREE_VIEW (priv->tree_view)));
+        gtk_list_store_clear (model);
+        load_bookmark_model (self, job->model);
+
+        g_clear_object (&priv->job);
+}
+
+static void
+gd_places_bookmarks_update (GdPlacesBookmarks *self)
+{
+        GdPlacesBookmarksPrivate *priv = self->priv;
+        GtkListStore             *model;
+        GList                    *l;
+        GtkTreeIter               iter;
+        guint                     n_items = 0;
+        EvDocument               *document;
+
+        if (priv->document_model == NULL) {
+                /* not loaded yet */
+                return;
+        }
+
+        if (priv->job != NULL) {
+                ev_job_cancel (priv->job);
+                g_clear_object (&priv->job);
+        }
+
+        model = GTK_LIST_STORE (gtk_tree_view_get_model (GTK_TREE_VIEW (priv->tree_view)));
+        gtk_list_store_clear (model);
+        enable_selection (self, FALSE);
+
+        if (priv->bookmarks != NULL) {
+                n_items = gd_bookmarks_get_n_items (priv->bookmarks);
+        }
+
+        document = ev_document_model_get_document (priv->document_model);
+        if (n_items == 0) {
+                gtk_list_store_append (model, &iter);
+                gtk_list_store_set (model, &iter,
+                                    COLUMN_MARKUP, _("No bookmarks"),
+                                    COLUMN_PAGE_LABEL, NULL,
+                                    COLUMN_BOOKMARK, NULL,
+                                    -1);
+        } else if (ev_document_links_has_document_links (EV_DOCUMENT_LINKS (document))) {
+                gtk_list_store_append (model, &iter);
+                gtk_list_store_set (model, &iter,
+                                    COLUMN_MARKUP, _("Loadingâ"),
+                                    COLUMN_PAGE_LABEL, NULL,
+                                    COLUMN_BOOKMARK, NULL,
+                                    -1);
+                priv->job = ev_job_links_new (document);
+                g_signal_connect (priv->job,
+                                  "finished",
+                                  G_CALLBACK (job_finished_cb),
+                                  self);
+
+                /* The priority doesn't matter for this job */
+                ev_job_scheduler_push_job (priv->job, EV_JOB_PRIORITY_NONE);
+        } else {
+                load_bookmark_model (self, NULL);
+        }
+}
+
+static void
+gd_places_bookmarks_changed (GdBookmarks       *bookmarks,
+                             GdPlacesBookmarks *self)
+{
+        gd_places_bookmarks_update (self);
+}
+
+static gboolean
+emit_activated (GdPlacesBookmarks *self)
+{
+        GtkTreeSelection *selection;
+        GdBookmark       *bookmark;
+
+        selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self->priv->tree_view));
+        bookmark = gd_places_bookmarks_get_selected_bookmark (self, selection);
+
+        if (bookmark != NULL) {
+                g_signal_emit (self, signals[BOOKMARK_ACTIVATED], 0, bookmark);
+
+                g_object_unref (bookmark);
+        }
+
+        self->priv->activated_id = 0;
+
+        return FALSE;
+}
+
+static void
+schedule_emit_activated (GdPlacesBookmarks *self)
+{
+        /* jump through some hoops to avoid destroying in the middle
+           of a button release handler */
+        if (self->priv->activated_id == 0) {
+                self->priv->activated_id = g_idle_add ((GSourceFunc) emit_activated, self);
+        }
+}
+
+static void
+gd_places_bookmarks_set_document_model (GdPlacesPage    *page,
+                                        EvDocumentModel *model)
+{
+        GdPlacesBookmarks *self = GD_PLACES_BOOKMARKS (page);
+        GdPlacesBookmarksPrivate *priv = self->priv;
+
+        if (priv->document_model == model)
+                return;
+
+        if (priv->document_model != NULL) {
+                g_signal_handlers_disconnect_by_func (priv->document_model,
+                                                      gd_places_bookmarks_update,
+                                                      page);
+        }
+
+        g_clear_object (&priv->document_model);
+        priv->document_model = model;
+
+        if (priv->document_model != NULL) {
+                g_object_ref (priv->document_model);
+                g_signal_connect_swapped (priv->document_model,
+                                          "notify::document",
+                                          G_CALLBACK (gd_places_bookmarks_update),
+                                          page);
+        }
+
+        gd_places_bookmarks_update (self);
+}
+
+void
+gd_places_bookmarks_set_bookmarks (GdPlacesBookmarks *self,
+                                   GdBookmarks       *bookmarks)
+{
+        GdPlacesBookmarksPrivate *priv = self->priv;
+
+        g_return_if_fail (GD_IS_BOOKMARKS (bookmarks));
+
+        if (priv->bookmarks == bookmarks)
+                return;
+
+        if (priv->bookmarks != NULL) {
+                g_signal_handlers_disconnect_by_func (priv->bookmarks,
+                                                      G_CALLBACK (gd_places_bookmarks_update),
+                                                      self);
+        }
+
+        g_clear_object (&priv->bookmarks);
+        priv->bookmarks = g_object_ref (bookmarks);
+        g_signal_connect_swapped (priv->bookmarks, "changed",
+                                  G_CALLBACK (gd_places_bookmarks_update),
+                                  self);
+
+        gd_places_bookmarks_update (self);
+}
+
+static void
+gd_places_bookmarks_set_property (GObject      *object,
+                                  guint         prop_id,
+                                  const GValue *value,
+                                  GParamSpec   *pspec)
+{
+
+        GdPlacesBookmarks *self = GD_PLACES_BOOKMARKS (object);
+
+        switch (prop_id) {
+        case PROP_DOCUMENT_MODEL:
+                gd_places_bookmarks_set_document_model (GD_PLACES_PAGE (self), g_value_get_object (value));
+                break;
+        case PROP_BOOKMARKS:
+                gd_places_bookmarks_set_bookmarks (self, g_value_get_object (value));
+                break;
+        default:
+                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+                break;
+        }
+}
+
+static void
+gd_places_bookmarks_get_property (GObject    *object,
+                                  guint       prop_id,
+                                  GValue     *value,
+                                  GParamSpec *pspec)
+{
+        GdPlacesBookmarks *self = GD_PLACES_BOOKMARKS (object);
+
+        switch (prop_id) {
+        case PROP_NAME:
+                g_value_set_string (value, self->priv->name);
+                break;
+        case PROP_DOCUMENT_MODEL:
+                g_value_set_object (value, self->priv->document_model);
+                break;
+        case PROP_BOOKMARKS:
+                g_value_set_object (value, self->priv->bookmarks);
+                break;
+        default:
+                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+                break;
+        }
+}
+
+static void
+gd_places_bookmarks_dispose (GObject *object)
+{
+        GdPlacesBookmarks *self = GD_PLACES_BOOKMARKS (object);
+        GdPlacesBookmarksPrivate *priv = self->priv;
+
+        if (priv->bookmarks != NULL) {
+                g_signal_handlers_disconnect_by_func (priv->bookmarks,
+                                                      G_CALLBACK (gd_places_bookmarks_changed),
+                                                      self);
+        }
+
+        if (priv->document_model != NULL) {
+                g_signal_handlers_disconnect_by_func (priv->document_model,
+                                                      gd_places_bookmarks_update,
+                                                      self);
+        }
+
+        if (self->priv->job != NULL) {
+                ev_job_cancel (self->priv->job);
+                g_clear_object (&self->priv->job);
+        }
+
+        if (self->priv->activated_id > 0) {
+                g_source_remove (self->priv->activated_id);
+                self->priv->activated_id = 0;
+        }
+
+        g_clear_object (&priv->document_model);
+        g_clear_object (&priv->bookmarks);
+
+        G_OBJECT_CLASS (gd_places_bookmarks_parent_class)->dispose (object);
+}
+
+static void
+gd_places_bookmarks_construct (GdPlacesBookmarks *self)
+{
+        GdPlacesBookmarksPrivate *priv = self->priv;
+        GtkWidget                *swindow;
+        GtkWidget                *hbox;
+        GtkListStore             *model;
+        GtkTreeViewColumn        *column;
+        GtkCellRenderer          *renderer;
+
+        swindow = gtk_scrolled_window_new (NULL, NULL);
+        gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (swindow),
+                                             GTK_SHADOW_IN);
+        gtk_box_pack_start (GTK_BOX (self), swindow, TRUE, TRUE, 0);
+        gtk_widget_show (swindow);
+
+        model = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, GD_TYPE_BOOKMARK);
+        priv->tree_view = gtk_tree_view_new_with_model (GTK_TREE_MODEL (model));
+        enable_selection (self, FALSE);
+        gtk_tree_view_set_rules_hint (GTK_TREE_VIEW (priv->tree_view), TRUE);
+        gtk_tree_view_set_activate_on_single_click (GTK_TREE_VIEW (priv->tree_view), TRUE);
+        gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (priv->tree_view), FALSE);
+        g_object_unref (model);
+
+        g_signal_connect_swapped (priv->tree_view, "row-activated",
+                                  G_CALLBACK (schedule_emit_activated),
+                                  self);
+
+        column = gtk_tree_view_column_new ();
+        gtk_tree_view_column_set_expand (GTK_TREE_VIEW_COLUMN (column), TRUE);
+        gtk_tree_view_append_column (GTK_TREE_VIEW (priv->tree_view), column);
+
+        renderer = gtk_cell_renderer_text_new ();
+        g_object_set (renderer,
+                      "wrap-mode", PANGO_WRAP_WORD,
+                      "wrap-width", 350,
+                      "weight", PANGO_WEIGHT_BOLD,
+                      "xpad", 10,
+                      NULL);
+        gtk_tree_view_column_pack_start (GTK_TREE_VIEW_COLUMN (column), renderer, TRUE);
+        gtk_tree_view_column_set_attributes (GTK_TREE_VIEW_COLUMN (column), renderer,
+                                             "markup", COLUMN_MARKUP,
+                                             NULL);
+
+        renderer = gd_styled_text_renderer_new ();
+        gd_styled_text_renderer_add_class (GD_STYLED_TEXT_RENDERER (renderer), "dim-label");
+        g_object_set (renderer,
+                      "max-width-chars", 12,
+                      "scale", PANGO_SCALE_SMALL,
+                      "xalign", 1.0,
+                      "xpad", 10,
+                      NULL);
+        gtk_tree_view_column_pack_end (GTK_TREE_VIEW_COLUMN (column), renderer, FALSE);
+        gtk_tree_view_column_set_attributes (GTK_TREE_VIEW_COLUMN (column), renderer,
+                                             "text", COLUMN_PAGE_LABEL,
+                                             NULL);
+
+        gtk_container_add (GTK_CONTAINER (swindow), priv->tree_view);
+        gtk_widget_show (priv->tree_view);
+
+        hbox = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL);
+
+        gtk_box_pack_end (GTK_BOX (self), hbox, FALSE, TRUE, 0);
+        gtk_widget_show (hbox);
+        gtk_widget_show (GTK_WIDGET (self));
+}
+
+static void
+gd_places_bookmarks_init (GdPlacesBookmarks *self)
+{
+
+        self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
+                                                  GD_TYPE_PLACES_BOOKMARKS,
+                                                  GdPlacesBookmarksPrivate);
+
+        self->priv->name = _("Bookmarks");
+
+        gd_places_bookmarks_construct (self);
+}
+
+static void
+gd_places_bookmarks_class_init (GdPlacesBookmarksClass *klass)
+{
+        GObjectClass   *oclass = G_OBJECT_CLASS (klass);
+        GtkWidgetClass *wclass = GTK_WIDGET_CLASS (klass);
+
+        oclass->get_property = gd_places_bookmarks_get_property;
+        oclass->set_property = gd_places_bookmarks_set_property;
+        oclass->dispose = gd_places_bookmarks_dispose;
+
+        signals[BOOKMARK_ACTIVATED] = g_signal_new ("bookmark-activated",
+                                                    G_TYPE_FROM_CLASS (oclass),
+                                                    G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                                    0,
+                                                    NULL, NULL,
+                                                    g_cclosure_marshal_VOID__OBJECT,
+                                                    G_TYPE_NONE, 1, G_TYPE_OBJECT);
+
+        g_object_class_install_property (oclass,
+                                         PROP_BOOKMARKS,
+                                         g_param_spec_object ("bookmarks",
+                                                              "Bookmarks",
+                                                              "Bookmarks",
+                                                              GD_TYPE_BOOKMARKS,
+                                                              G_PARAM_READWRITE |
+                                                              G_PARAM_STATIC_STRINGS));
+
+        g_object_class_override_property (oclass, PROP_NAME, "name");
+        g_object_class_override_property (oclass, PROP_DOCUMENT_MODEL, "document-model");
+
+        g_type_class_add_private (oclass, sizeof (GdPlacesBookmarksPrivate));
+}
+
+GtkWidget *
+gd_places_bookmarks_new (void)
+{
+        return GTK_WIDGET (g_object_new (GD_TYPE_PLACES_BOOKMARKS, NULL));
+}
+
+static gboolean
+gd_places_bookmarks_supports_document (GdPlacesPage *page,
+                                       EvDocument   *document)
+{
+        return TRUE;
+}
+
+static const char *
+gd_places_bookmarks_get_name (GdPlacesPage *page)
+{
+        return GD_PLACES_BOOKMARKS (page)->priv->name;
+}
+
+static void
+gd_places_bookmarks_page_iface_init (GdPlacesPageInterface *iface)
+{
+        iface->supports_document = gd_places_bookmarks_supports_document;
+        iface->set_document_model = gd_places_bookmarks_set_document_model;
+        iface->get_name = gd_places_bookmarks_get_name;
+}
diff --git a/src/lib/gd-places-bookmarks.h b/src/lib/gd-places-bookmarks.h
new file mode 100644
index 0000000..ddc7330
--- /dev/null
+++ b/src/lib/gd-places-bookmarks.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8;  -*-
+ *
+ *  Copyright (C) 2010 Carlos Garcia Campos  <carlosgc gnome org>
+ *  Copyright (C) 2013 Red Hat, Inc.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2, or (at your option)
+ *  any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef __GD_PLACES_BOOKMARKS_H__
+#define __GD_PLACES_BOOKMARKS_H__
+
+#include <gtk/gtk.h>
+#include <glib-object.h>
+
+#include "gd-bookmarks.h"
+
+G_BEGIN_DECLS
+
+typedef struct _GdPlacesBookmarks        GdPlacesBookmarks;
+typedef struct _GdPlacesBookmarksClass   GdPlacesBookmarksClass;
+typedef struct _GdPlacesBookmarksPrivate GdPlacesBookmarksPrivate;
+
+#define GD_TYPE_PLACES_BOOKMARKS              (gd_places_bookmarks_get_type())
+#define GD_PLACES_BOOKMARKS(object)           (G_TYPE_CHECK_INSTANCE_CAST((object), GD_TYPE_PLACES_BOOKMARKS, GdPlacesBookmarks))
+#define GD_PLACES_BOOKMARKS_CLASS(klass)      (G_TYPE_CHECK_CLASS_CAST((klass), GD_TYPE_PLACES_BOOKMARKS, GdPlacesBookmarksClass))
+#define GD_IS_PLACES_BOOKMARKS(object)        (G_TYPE_CHECK_INSTANCE_TYPE((object), GD_TYPE_PLACES_BOOKMARKS))
+#define GD_IS_PLACES_BOOKMARKS_CLASS(klass)   (G_TYPE_CHECK_CLASS_TYPE((klass), GD_TYPE_PLACES_BOOKMARKS))
+#define GD_PLACES_BOOKMARKS_GET_CLASS(object) (G_TYPE_INSTANCE_GET_CLASS((object), GD_TYPE_PLACES_BOOKMARKS, GdPlacesBookmarksClass))
+
+struct _GdPlacesBookmarks {
+        GtkBox base_instance;
+
+        GdPlacesBookmarksPrivate *priv;
+};
+
+struct _GdPlacesBookmarksClass {
+        GtkBoxClass base_class;
+};
+
+GType      gd_places_bookmarks_get_type      (void) G_GNUC_CONST;
+GtkWidget *gd_places_bookmarks_new           (void);
+void       gd_places_bookmarks_set_bookmarks (GdPlacesBookmarks *places_bookmarks,
+                                              GdBookmarks       *bookmarks);
+G_END_DECLS
+
+#endif /* __GD_PLACES_BOOKMARKS_H__ */
diff --git a/src/lib/gd-places-links.c b/src/lib/gd-places-links.c
index 5b8e5c2..04dd289 100644
--- a/src/lib/gd-places-links.c
+++ b/src/lib/gd-places-links.c
@@ -46,11 +46,10 @@ struct _GdPlacesLinksPrivate {
         const char *name;
 };
 
-enum
-{
-  PROP_0,
-  PROP_NAME,
-  PROP_DOCUMENT_MODEL,
+enum {
+        PROP_0,
+        PROP_NAME,
+        PROP_DOCUMENT_MODEL,
 };
 
 enum {
@@ -392,7 +391,7 @@ gd_places_links_document_changed_cb (EvDocumentModel *model,
 }
 
 static gboolean
-gd_places_links_supports_document (GdPlacesPage *places_page,
+gd_places_links_supports_document (GdPlacesPage *page,
                                    EvDocument   *document)
 {
         return (EV_IS_DOCUMENT_LINKS (document) &&
@@ -400,10 +399,10 @@ gd_places_links_supports_document (GdPlacesPage *places_page,
 }
 
 static void
-gd_places_links_set_document_model (GdPlacesPage    *places_page,
+gd_places_links_set_document_model (GdPlacesPage    *page,
                                     EvDocumentModel *model)
 {
-        GdPlacesLinks *self = GD_PLACES_LINKS (places_page);
+        GdPlacesLinks *self = GD_PLACES_LINKS (page);
         GdPlacesLinksPrivate *priv = self->priv;
 
         if (priv->document_model == model) {
@@ -418,7 +417,7 @@ gd_places_links_set_document_model (GdPlacesPage    *places_page,
         if (priv->document_model != NULL) {
                 g_signal_handlers_disconnect_by_func (priv->document_model,
                                                       gd_places_links_document_changed_cb,
-                                                      places_page);
+                                                      page);
         }
 
         g_clear_object (&priv->document_model);
@@ -430,7 +429,7 @@ gd_places_links_set_document_model (GdPlacesPage    *places_page,
                 g_signal_connect (priv->document_model,
                                   "notify::document",
                                   G_CALLBACK (gd_places_links_document_changed_cb),
-                                  places_page);
+                                  page);
                 gd_places_links_document_changed_cb (priv->document_model,
                                                      NULL,
                                                      self);
@@ -438,9 +437,9 @@ gd_places_links_set_document_model (GdPlacesPage    *places_page,
 }
 
 static const char *
-gd_places_links_get_name (GdPlacesPage *places_page)
+gd_places_links_get_name (GdPlacesPage *page)
 {
-        return GD_PLACES_LINKS (places_page)->priv->name;
+        return GD_PLACES_LINKS (page)->priv->name;
 }
 
 static void
diff --git a/src/places.js b/src/places.js
index 76c7bf7..24051c4 100644
--- a/src/places.js
+++ b/src/places.js
@@ -21,20 +21,23 @@ const Gio = imports.gi.Gio;
 const GLib = imports.gi.GLib;
 const Gtk = imports.gi.Gtk;
 const _ = imports.gettext.gettext;
+const Gd = imports.gi.Gd;
 
 const EvDocument = imports.gi.EvinceDocument;
 const GdPrivate = imports.gi.GdPrivate;
 const Application = imports.application;
 const Documents = imports.documents;
 const Mainloop = imports.mainloop;
+const MainToolbar = imports.mainToolbar;
 
 const Lang = imports.lang;
 
 const PlacesDialog = new Lang.Class({
     Name: 'PlacesDialog',
 
-    _init: function(model) {
+    _init: function(model, bookmarks) {
         this._model = model;
+        this._bookmarks = bookmarks;
         this._createWindow();
         this.widget.show_all();
     },
@@ -49,12 +52,29 @@ const PlacesDialog = new Lang.Class({
                                         default_height: 600,
                                         title: "",
                                         hexpand: true });
-        this.widget.add_button(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE);
+
+        let box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL });
+        let contentArea = this.widget.get_content_area();
+        contentArea.pack_start(box, true, true, 0);
+
+        this._toolbar = new Gd.MainToolbar({ icon_size: Gtk.IconSize.MENU,
+                                             show_modes: true,
+                                             vexpand: false });
+        this._toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_MENUBAR);
+        let button = this._toolbar.add_button(null, _('Close'), false);
+        button.connect('clicked', Lang.bind(this,
+            function() {
+                this.widget.response(Gtk.ResponseType.CLOSE);
+            }));
+
+        box.pack_start(this._toolbar, false, false, 0);
 
         this._notebook = new Gtk.Notebook ({ show_tabs: false,
-                                             border_width: 5 });
+                                             border_width: 5,
+                                             vexpand: true });
+        box.pack_start(this._notebook, true, true, 0);
 
-        this._linksPage = new GdPrivate.PlacesLinks ();
+        this._linksPage = new GdPrivate.PlacesLinks();
         this._linksPage.connect('link-activated', Lang.bind(this,
             function(widget, link) {
                 this._handleLink(link);
@@ -62,8 +82,12 @@ const PlacesDialog = new Lang.Class({
 
         this._addPage(this._linksPage);
 
-        let contentArea = this.widget.get_content_area();
-        contentArea.pack_start(this._notebook, true, true, 0);
+        this._bookmarksPage = new GdPrivate.PlacesBookmarks({ bookmarks: this._bookmarks });
+        this._bookmarksPage.connect('bookmark-activated', Lang.bind(this,
+            function(widget, link) {
+                this._handleBookmark(link);
+            }));
+        this._addPage(this._bookmarksPage);
     },
 
     _handleLink: function(link) {
@@ -73,6 +97,11 @@ const PlacesDialog = new Lang.Class({
         this.widget.response(Gtk.ResponseType.CLOSE);
     },
 
+    _handleBookmark: function(bookmark) {
+        this._model.set_page(bookmark.page_number);
+        this.widget.response(Gtk.ResponseType.CLOSE);
+    },
+
     _gotoDest: function(dest) {
         switch (dest.type) {
         case EvDocument.LinkDestType.PAGE:
@@ -92,9 +121,14 @@ const PlacesDialog = new Lang.Class({
     },
 
     _addPage: function(widget) {
-        let label = new Gtk.Label({ label: widget.get_label() });
-        widget.set_document_model(this._model);
-        this._notebook.append_page(widget, label);
+        let label = new Gtk.Label({ label: widget.name });
+        widget.document_model = this._model;
+        let index = this._notebook.append_page(widget, label);
+        let button = this._toolbar.add_mode(widget.name);
+        button.connect('toggled', Lang.bind(this,
+            function() {
+                this._notebook.page = index;
+            }));
     }
 
 });
diff --git a/src/preview.js b/src/preview.js
index 52c8ee2..b4d1a6d 100644
--- a/src/preview.js
+++ b/src/preview.js
@@ -78,6 +78,10 @@ const PreviewView = new Lang.Class({
 
         this.widget.show_all();
 
+        Application.application.connect('action-state-changed::bookmark-page',
+            Lang.bind(this, this._onActionStateChanged));
+        this._onActionStateChanged(Application.application, 'bookmark-page', Application.application.get_action_state('bookmark-page'));
+
         this._zoomIn = Application.application.lookup_action('zoom-in');
         this._zoomIn.connect('activate', Lang.bind(this,
             function() {
@@ -115,10 +119,51 @@ const PreviewView = new Lang.Class({
             }));
         let showPlaces = Application.application.lookup_action('places');
         showPlaces.connect('activate', Lang.bind(this, this._showPlaces));
+
+        Application.documentManager.connect('load-started',
+                                            Lang.bind(this, this._onLoadStarted));
+        Application.documentManager.connect('load-finished',
+                                            Lang.bind(this, this._onLoadFinished));
+    },
+
+   _onLoadStarted: function() {
+        this._showPlaces.enabled = false;
+    },
+
+    _onLoadFinished: function(manager, doc, docModel) {
+        this._showPlaces.enabled = true;
+
+        if (!Application.documentManager.metadata)
+            return;
+
+        this._bookmarks = new GdPrivate.Bookmarks({ metadata: Application.documentManager.metadata });
+    },
+
+    _onActionStateChanged: function(source, actionName, state) {
+        if (!this._model)
+            return;
+
+        let page_number = this._model.page;
+        let bookmark = new GdPrivate.Bookmark({ page_number: page_number });
+
+        if (state.get_boolean())
+            this._bookmarks.add(bookmark);
+        else
+            this._bookmarks.remove(bookmark);
+    },
+
+    _onPageChanged: function() {
+        this._pageChanged = true;
+
+        let page_number = this._model.page;
+        let bookmark = new GdPrivate.Bookmark({ page_number: page_number });
+
+        let bookmark = this._bookmarks.find_bookmark(bookmark);
+        Application.application.change_action_state('bookmark-page', GLib.Variant.new('b', (bookmark != null)));
     },
 
     _showPlaces: function() {
-        let dialog = new Places.PlacesDialog(this._model);
+        let dialog = new Places.PlacesDialog(this._model, this._bookmarks);
         dialog.widget.connect('response', Lang.bind(this,
             function(widget, response) {
                 widget.destroy();
@@ -349,11 +394,7 @@ const PreviewView = new Lang.Class({
             this._createView();
             this.view.set_model(this._model);
             this._navBar.widget.document_model = model;
-            this._model.connect('page-changed', Lang.bind(this,
-                function() {
-                    this._pageChanged = true;
-                }));
-
+            this._model.connect('page-changed', Lang.bind(this, this._onPageChanged));
         }
     },
 
@@ -388,6 +429,13 @@ const PreviewNav = new Lang.Class({
         let button_area = this.widget.get_button_area();
         button_area.pack_start(button, false, false, 0);
 
+        let button = new Gtk.ToggleButton({ action_name: 'app.bookmark-page',
+                                            child: new Gtk.Image({ icon_name: 'bookmark-add-symbolic',
+                                                                   pixel_size: 16 }),
+                                            valign: Gtk.Align.CENTER
+                                          });
+        button_area.pack_start(button, false, false, 0);
+
         this.actor = new GtkClutter.Actor({ contents: this.widget,
                                             visible: false,
                                             margin_top: _PREVIEW_NAVBAR_MARGIN,



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