/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/*
* Copyright © 2012 Igalia S.L.
*
* 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 "ephy-history-service.h"
#include "ephy-overview-store.h"
#include "ephy-removable-pixbuf-renderer.h"
#include "ephy-snapshot-service.h"
#include "ephy-widgets-type-builtins.h"
/* Update thumbnails after one week. */
#define THUMBNAIL_UPDATE_THRESHOLD (60 * 60 * 24 * 7)
#define EPHY_OVERVIEW_STORE_GET_PRIVATE(object) (G_TYPE_INSTANCE_GET_PRIVATE ((object), EPHY_TYPE_OVERVIEW_STORE, EphyOverviewStorePrivate))
struct _EphyOverviewStorePrivate
{
EphyHistoryService *history_service;
GdkPixbuf *default_icon;
GdkPixbuf *icon_frame;
};
enum
{
PROP_0,
PROP_HISTORY_SERVICE,
PROP_DEFAULT_ICON,
PROP_ICON_FRAME,
};
G_DEFINE_TYPE (EphyOverviewStore, ephy_overview_store, GTK_TYPE_LIST_STORE)
static void
ephy_overview_store_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
EphyOverviewStore *store = EPHY_OVERVIEW_STORE (object);
switch (prop_id)
{
case PROP_HISTORY_SERVICE:
store->priv->history_service = g_value_dup_object (value);
g_object_notify (object, "history-service");
break;
case PROP_DEFAULT_ICON:
ephy_overview_store_set_default_icon (store,
g_value_get_object (value));
break;
case PROP_ICON_FRAME:
ephy_overview_store_set_icon_frame (store,
g_value_get_object (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
ephy_overview_store_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
EphyOverviewStore *store = EPHY_OVERVIEW_STORE (object);
switch (prop_id)
{
case PROP_HISTORY_SERVICE:
g_value_set_object (value, store->priv->history_service);
break;
case PROP_DEFAULT_ICON:
g_value_set_object (value, store->priv->default_icon);
break;
case PROP_ICON_FRAME:
g_value_set_object (value, store->priv->icon_frame);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
ephy_overview_store_dispose (GObject *object)
{
EphyOverviewStorePrivate *priv = EPHY_OVERVIEW_STORE (object)->priv;
if (priv->history_service)
g_clear_object (&priv->history_service);
if (priv->default_icon)
g_clear_object (&priv->default_icon);
if (priv->icon_frame)
g_clear_object (&priv->icon_frame);
G_OBJECT_CLASS (ephy_overview_store_parent_class)->dispose (object);
}
static void
ephy_overview_store_class_init (EphyOverviewStoreClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->set_property = ephy_overview_store_set_property;
object_class->get_property = ephy_overview_store_get_property;
object_class->dispose = ephy_overview_store_dispose;
g_object_class_install_property (object_class,
PROP_HISTORY_SERVICE,
g_param_spec_object ("history-service",
"History service",
"History Service",
EPHY_TYPE_HISTORY_SERVICE,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB));
g_object_class_install_property (object_class,
PROP_DEFAULT_ICON,
g_param_spec_object ("default-icon",
"Default icon",
"Default Icon",
GDK_TYPE_PIXBUF,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB));
g_object_class_install_property (object_class,
PROP_ICON_FRAME,
g_param_spec_object ("icon-frame",
"Icon frame",
"Frame to display around icons",
GDK_TYPE_PIXBUF,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB));
g_type_class_add_private (object_class, sizeof(EphyOverviewStorePrivate));
}
static void
ephy_overview_store_init (EphyOverviewStore *self)
{
GType types[EPHY_OVERVIEW_STORE_NCOLS];
types[EPHY_OVERVIEW_STORE_ID] = G_TYPE_STRING;
types[EPHY_OVERVIEW_STORE_URI] = G_TYPE_STRING;
types[EPHY_OVERVIEW_STORE_TITLE] = G_TYPE_STRING;
types[EPHY_OVERVIEW_STORE_AUTHOR] = G_TYPE_STRING;
types[EPHY_OVERVIEW_STORE_SNAPSHOT] = GDK_TYPE_PIXBUF;
types[EPHY_OVERVIEW_STORE_LAST_VISIT] = G_TYPE_INT;
types[EPHY_OVERVIEW_STORE_SELECTED] = G_TYPE_BOOLEAN;
types[EPHY_OVERVIEW_STORE_SNAPSHOT_CANCELLABLE] = G_TYPE_CANCELLABLE;
types[EPHY_OVERVIEW_STORE_SNAPSHOT_MTIME] = G_TYPE_INT;
types[EPHY_OVERVIEW_STORE_CLOSE_BUTTON_RENDER_POLICY] = EPHY_TYPE_REMOVABLE_PIXBUF_RENDER_POLICY;
gtk_list_store_set_column_types (GTK_LIST_STORE (self),
EPHY_OVERVIEW_STORE_NCOLS, types);
self->priv = EPHY_OVERVIEW_STORE_GET_PRIVATE (self);
}
typedef struct {
GtkTreeRowReference *ref;
char *url;
WebKitWebView *webview;
GCancellable *cancellable;
time_t timestamp;
} PeekContext;
static void
peek_context_free (PeekContext *ctx)
{
g_free (ctx->url);
gtk_tree_row_reference_free (ctx->ref);
if (ctx->webview)
g_object_unref (ctx->webview);
if (ctx->cancellable)
g_object_unref (ctx->cancellable);
g_slice_free (PeekContext, ctx);
}
static GdkPixbuf *
ephy_overview_store_add_frame (EphyOverviewStore *store,
GdkPixbuf *snapshot)
{
GdkPixbuf *framed;
if (store->priv->icon_frame) {
framed = gdk_pixbuf_copy (store->priv->icon_frame);
gdk_pixbuf_copy_area (snapshot, 0, 0,
gdk_pixbuf_get_width (snapshot),
gdk_pixbuf_get_height (snapshot),
framed, 10, 9);
} else
framed = g_object_ref (snapshot);
return framed;
}
static void
ephy_overview_store_set_snapshot_internal (EphyOverviewStore *store,
GtkTreeIter *iter,
GdkPixbuf *snapshot,
int mtime)
{
GdkPixbuf *framed;
framed = ephy_overview_store_add_frame (store, snapshot);
gtk_list_store_set (GTK_LIST_STORE (store), iter,
EPHY_OVERVIEW_STORE_SNAPSHOT, framed,
EPHY_OVERVIEW_STORE_SNAPSHOT_MTIME, mtime,
-1);
g_object_unref (framed);
}
typedef struct {
EphyHistoryURL *url;
EphyHistoryService *history_service;
} ThumbnailTimeContext;
static void
on_snapshot_saved_cb (EphySnapshotService *service,
GAsyncResult *res,
ThumbnailTimeContext *ctx)
{
ephy_history_service_set_url_thumbnail_time (ctx->history_service,
ctx->url->url, ctx->url->thumbnail_time,
NULL, NULL, NULL);
ephy_history_url_free (ctx->url);
g_slice_free (ThumbnailTimeContext, ctx);
}
void
ephy_overview_store_set_snapshot (EphyOverviewStore *store,
GtkTreeIter *iter,
cairo_surface_t *snapshot)
{
GdkPixbuf *pixbuf;
char *url;
ThumbnailTimeContext *ctx;
EphySnapshotService *snapshot_service;
int mtime;
mtime = time (NULL);
pixbuf = ephy_snapshot_service_crop_snapshot (snapshot);
ephy_overview_store_set_snapshot_internal (store, iter, pixbuf, mtime);
gtk_tree_model_get (GTK_TREE_MODEL (store), iter,
EPHY_OVERVIEW_STORE_URI, &url,
-1);
ctx = g_slice_new (ThumbnailTimeContext);
ctx->url = ephy_history_url_new (url, NULL, 0, 0, 0);
ctx->url->thumbnail_time = mtime;
ctx->history_service = store->priv->history_service;
g_free (url);
snapshot_service = ephy_snapshot_service_get_default ();
ephy_snapshot_service_save_snapshot_async (snapshot_service,
pixbuf, ctx->url->url, ctx->url->thumbnail_time,
NULL,
(GAsyncReadyCallback) on_snapshot_saved_cb,
ctx);
g_object_unref (pixbuf);
}
static void
ephy_overview_store_set_default_icon_internal (EphyOverviewStore *store,
GtkTreeIter *iter,
GdkPixbuf *default_icon)
{
gtk_list_store_set (GTK_LIST_STORE (store), iter,
EPHY_OVERVIEW_STORE_SNAPSHOT,
default_icon,
EPHY_OVERVIEW_STORE_SNAPSHOT_MTIME, 0,
-1);
}
static void
set_snapshot (EphyOverviewStore *store,
GdkPixbuf *snapshot,
GtkTreeRowReference *ref,
time_t timestamp)
{
GtkTreePath *path;
GtkTreeIter iter;
path = gtk_tree_row_reference_get_path (ref);
gtk_tree_model_get_iter (GTK_TREE_MODEL (store), &iter, path);
gtk_tree_path_free (path);
if (snapshot)
ephy_overview_store_set_snapshot_internal (store, &iter, snapshot, timestamp);
else
ephy_overview_store_set_default_icon_internal (store, &iter, store->priv->default_icon);
gtk_list_store_set (GTK_LIST_STORE (store), &iter,
EPHY_OVERVIEW_STORE_SNAPSHOT_CANCELLABLE, NULL,
-1);
}
static void
on_snapshot_retrieved_cb (GObject *object,
GAsyncResult *res,
PeekContext *ctx)
{
GdkPixbuf *snapshot;
snapshot = ephy_snapshot_service_get_snapshot_finish (EPHY_SNAPSHOT_SERVICE (object),
res, NULL);
set_snapshot (EPHY_OVERVIEW_STORE (gtk_tree_row_reference_get_model (ctx->ref)),
snapshot, ctx->ref, ctx->timestamp);
if (snapshot)
g_object_unref (snapshot);
peek_context_free (ctx);
}
static void
on_snapshot_retrieved_for_url_cb (GObject *object,
GAsyncResult *res,
PeekContext *ctx)
{
GdkPixbuf *snapshot;
snapshot = ephy_snapshot_service_get_snapshot_for_url_finish (EPHY_SNAPSHOT_SERVICE (object),
res, NULL);
set_snapshot (EPHY_OVERVIEW_STORE (gtk_tree_row_reference_get_model (ctx->ref)),
snapshot, ctx->ref, ctx->timestamp);
if (snapshot)
g_object_unref (snapshot);
peek_context_free (ctx);
}
static void
history_service_url_cb (gpointer service,
gboolean success,
EphyHistoryURL *url,
PeekContext *ctx)
{
EphySnapshotService *snapshot_service;
snapshot_service = ephy_snapshot_service_get_default ();
ctx->timestamp = url->thumbnail_time;
if (ctx->webview)
ephy_snapshot_service_get_snapshot_async (snapshot_service,
ctx->webview, ctx->timestamp, ctx->cancellable,
(GAsyncReadyCallback) on_snapshot_retrieved_cb,
ctx);
else
ephy_snapshot_service_get_snapshot_for_url_async (snapshot_service,
ctx->url, ctx->timestamp, ctx->cancellable,
(GAsyncReadyCallback) on_snapshot_retrieved_for_url_cb,
ctx);
ephy_history_url_free (url);
}
void
ephy_overview_store_peek_snapshot (EphyOverviewStore *self,
WebKitWebView *webview,
GtkTreeIter *iter)
{
char *url;
GtkTreePath *path;
PeekContext *ctx;
GCancellable *cancellable;
gtk_tree_model_get (GTK_TREE_MODEL (self), iter,
EPHY_OVERVIEW_STORE_URI, &url,
EPHY_OVERVIEW_STORE_SNAPSHOT_CANCELLABLE, &cancellable,
-1);
if (cancellable) {
g_cancellable_cancel (cancellable);
g_object_unref (cancellable);
}
if (url == NULL || g_strcmp0 (url, "about:blank") == 0) {
gtk_list_store_set (GTK_LIST_STORE (self), iter,
EPHY_OVERVIEW_STORE_SNAPSHOT_CANCELLABLE,
NULL, -1);
return;
}
cancellable = g_cancellable_new ();
gtk_list_store_set (GTK_LIST_STORE (self), iter,
EPHY_OVERVIEW_STORE_SNAPSHOT_CANCELLABLE,
cancellable, -1);
ctx = g_slice_new (PeekContext);
path = gtk_tree_model_get_path (GTK_TREE_MODEL (self), iter);
ctx->ref = gtk_tree_row_reference_new (GTK_TREE_MODEL (self), path);
ctx->url = url;
ctx->webview = webview ? g_object_ref (webview) : NULL;
ctx->cancellable = cancellable;
ephy_history_service_get_url (self->priv->history_service,
url, NULL, (EphyHistoryJobCallback)history_service_url_cb,
ctx);
gtk_tree_path_free (path);
}
static gboolean
set_default_icon_helper (GtkTreeModel *model,
GtkTreePath *path,
GtkTreeIter *iter,
GdkPixbuf *new_default_icon)
{
EphyOverviewStorePrivate *priv;
GdkPixbuf *current_pixbuf;
priv = EPHY_OVERVIEW_STORE (model)->priv;
gtk_tree_model_get (model, iter,
EPHY_OVERVIEW_STORE_SNAPSHOT, ¤t_pixbuf,
-1);
if (current_pixbuf == priv->default_icon ||
current_pixbuf == NULL)
ephy_overview_store_set_default_icon_internal (EPHY_OVERVIEW_STORE (model), iter,
new_default_icon);
g_object_unref (current_pixbuf);
return FALSE;
}
void
ephy_overview_store_set_default_icon (EphyOverviewStore *store,
GdkPixbuf *default_icon)
{
GdkPixbuf *new_default_icon;
if (store->priv->default_icon)
g_object_unref (store->priv->default_icon);
new_default_icon = ephy_overview_store_add_frame (store, default_icon);
gtk_tree_model_foreach (GTK_TREE_MODEL (store),
(GtkTreeModelForeachFunc) set_default_icon_helper,
new_default_icon);
store->priv->default_icon = new_default_icon;
g_object_notify (G_OBJECT (store), "default-icon");
}
void
ephy_overview_store_set_icon_frame (EphyOverviewStore *store,
GdkPixbuf *icon_frame)
{
gboolean update_default = FALSE;
GdkPixbuf *old_default_icon;
if (store->priv->icon_frame == icon_frame)
return;
if (store->priv->icon_frame)
g_object_unref (store->priv->icon_frame);
else if (store->priv->default_icon)
update_default = TRUE;
store->priv->icon_frame = g_object_ref (icon_frame);
if (update_default) {
old_default_icon = g_object_ref (store->priv->default_icon);
ephy_overview_store_set_default_icon (store,
old_default_icon);
g_object_unref (old_default_icon);
}
g_object_notify (G_OBJECT (store), "icon-frame");
}
gboolean
ephy_overview_store_needs_snapshot (EphyOverviewStore *store,
GtkTreeIter *iter)
{
GdkPixbuf *icon;
GCancellable *cancellable;
gboolean needs_snapshot;
int mtime, current_mtime;
g_return_val_if_fail (EPHY_IS_OVERVIEW_STORE (store), FALSE);
g_return_val_if_fail (iter != NULL, FALSE);
current_mtime = time (NULL);
gtk_tree_model_get (GTK_TREE_MODEL (store), iter,
EPHY_OVERVIEW_STORE_SNAPSHOT, &icon,
EPHY_OVERVIEW_STORE_SNAPSHOT_MTIME, &mtime,
EPHY_OVERVIEW_STORE_SNAPSHOT_CANCELLABLE, &cancellable,
-1);
/* If the thumbnail is the default icon and there is no cancellable
in the row, then this row needs a snapshot. */
needs_snapshot = (icon == store->priv->default_icon && cancellable == NULL) ||
current_mtime - mtime > THUMBNAIL_UPDATE_THRESHOLD;
if (icon)
g_object_unref (icon);
if (cancellable)
g_object_unref (cancellable);
return needs_snapshot;
}
gboolean
ephy_overview_store_remove (EphyOverviewStore *store,
GtkTreeIter *iter)
{
GCancellable *cancellable;
g_return_val_if_fail (EPHY_IS_OVERVIEW_STORE (store), FALSE);
gtk_tree_model_get (GTK_TREE_MODEL (store), iter,
EPHY_OVERVIEW_STORE_SNAPSHOT_CANCELLABLE,
&cancellable,
-1);
if (cancellable) {
g_cancellable_cancel (cancellable);
g_object_unref (cancellable);
}
return gtk_list_store_remove (GTK_LIST_STORE (store), iter);
}
typedef struct {
GtkTreeRowReference *ref;
EphyOverviewStoreAnimRemoveFunc callback;
gpointer user_data;
} AnimRemoveContext;
static gboolean
animated_remove_func (AnimRemoveContext *ctx)
{
GtkTreeRowReference *ref;
EphyOverviewStore *store;
GtkTreePath *path;
GtkTreeIter iter;
GdkPixbuf *orig_pixbuf, *new_pixbuf;
int width, height;
gboolean valid;
ref = ctx->ref;
store = EPHY_OVERVIEW_STORE (gtk_tree_row_reference_get_model (ref));
path = gtk_tree_row_reference_get_path (ref);
gtk_tree_model_get_iter (GTK_TREE_MODEL (store), &iter, path);
gtk_tree_path_free (path);
gtk_tree_model_get (GTK_TREE_MODEL (store), &iter,
EPHY_OVERVIEW_STORE_SNAPSHOT, &orig_pixbuf, -1);
width = gdk_pixbuf_get_width (orig_pixbuf);
height = gdk_pixbuf_get_height (orig_pixbuf);
if (width > 10) {
new_pixbuf = gdk_pixbuf_scale_simple (orig_pixbuf,
width * 0.80,
height * 0.80,
GDK_INTERP_TILES);
g_object_unref (orig_pixbuf);
gtk_list_store_set (GTK_LIST_STORE (store), &iter,
EPHY_OVERVIEW_STORE_SNAPSHOT, new_pixbuf,
-1);
g_object_unref (new_pixbuf);
return TRUE;
}
g_object_unref (orig_pixbuf);
valid = ephy_overview_store_remove (store, &iter);
if (ctx->callback)
ctx->callback (store, &iter, valid, ctx->user_data);
gtk_tree_row_reference_free (ref);
g_slice_free (AnimRemoveContext, ctx);
return FALSE;
}
void
ephy_overview_store_animated_remove (EphyOverviewStore *store,
GtkTreeRowReference *ref,
EphyOverviewStoreAnimRemoveFunc callback,
gpointer user_data)
{
GtkTreePath *path;
GtkTreeIter iter;
AnimRemoveContext *ctx = g_slice_new0 (AnimRemoveContext);
ctx->ref = ref;
ctx->callback = callback;
ctx->user_data = user_data;
path = gtk_tree_row_reference_get_path (ref);
gtk_tree_model_get_iter (GTK_TREE_MODEL (store), &iter, path);
gtk_tree_path_free (path);
gtk_list_store_set (GTK_LIST_STORE (store), &iter,
EPHY_OVERVIEW_STORE_CLOSE_BUTTON_RENDER_POLICY,
EPHY_REMOVABLE_PIXBUF_RENDER_NEVER, -1);
g_timeout_add (40, (GSourceFunc) animated_remove_func, ctx);
}
gboolean
ephy_overview_store_find_url (EphyOverviewStore *store,
const char *url,
GtkTreeIter *iter)
{
gboolean valid, found = FALSE;
char *row_url;
g_return_val_if_fail (EPHY_IS_OVERVIEW_STORE (store), FALSE);
g_return_val_if_fail (url != NULL, FALSE);
valid = gtk_tree_model_get_iter_first (GTK_TREE_MODEL (store), iter);
while (valid) {
gtk_tree_model_get (GTK_TREE_MODEL (store), iter,
EPHY_OVERVIEW_STORE_URI, &row_url,
-1);
found = g_strcmp0 (row_url, url) == 0;
g_free (row_url);
if (found)
break;
valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (store), iter);
}
return found;
}