315 lines
8.3 KiB
C
315 lines
8.3 KiB
C
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
|
||
* vi:set noexpandtab tabstop=8 shiftwidth=8:
|
||
*
|
||
* Copyright (C) 2021 Endless OS Foundation, Inc
|
||
*
|
||
* Author: Philip Withnall <pwithnall@endlessos.org>
|
||
*
|
||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||
*/
|
||
|
||
/**
|
||
* SECTION:gs-icon
|
||
* @short_description: Utilities for handling #GIcons
|
||
*
|
||
* This file provides several utilities for creating and handling #GIcon
|
||
* instances. #GIcon is used for representing icon sources throughout
|
||
* gnome-software, as it has low memory overheads, and allows the most
|
||
* appropriate icon data to be loaded when it’s needed to be used in a UI.
|
||
*
|
||
* gnome-software uses various classes which implement #GIcon, mostly the
|
||
* built-in ones provided by GIO, but also #GsRemoteIcon. All of them are tagged
|
||
* with `width` and `height` metadata (when that data was available at
|
||
* construction time). See gs_icon_get_width().
|
||
*
|
||
* Since: 40
|
||
*/
|
||
|
||
#include "config.h"
|
||
|
||
#include <appstream.h>
|
||
#include <gdk-pixbuf/gdk-pixbuf.h>
|
||
#include <gio/gio.h>
|
||
#include <glib.h>
|
||
#include <glib-object.h>
|
||
#include <gtk/gtk.h>
|
||
|
||
#include "gs-icon.h"
|
||
#include "gs-remote-icon.h"
|
||
|
||
/**
|
||
* gs_icon_get_width:
|
||
* @icon: a #GIcon
|
||
*
|
||
* Get the width of an icon, if it was attached as metadata when the #GIcon was
|
||
* created from an #AsIcon.
|
||
*
|
||
* Returns: width of the icon (in device pixels), or `0` if unknown
|
||
* Since: 40
|
||
*/
|
||
guint
|
||
gs_icon_get_width (GIcon *icon)
|
||
{
|
||
g_return_val_if_fail (G_IS_ICON (icon), 0);
|
||
|
||
return GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (icon), "width"));
|
||
}
|
||
|
||
/**
|
||
* gs_icon_set_width:
|
||
* @icon: a #GIcon
|
||
* @width: width of the icon, in device pixels
|
||
*
|
||
* Set the width of an icon. See gs_icon_get_width().
|
||
*
|
||
* Since: 40
|
||
*/
|
||
void
|
||
gs_icon_set_width (GIcon *icon,
|
||
guint width)
|
||
{
|
||
g_return_if_fail (G_IS_ICON (icon));
|
||
|
||
g_object_set_data (G_OBJECT (icon), "width", GUINT_TO_POINTER (width));
|
||
}
|
||
|
||
/**
|
||
* gs_icon_get_height:
|
||
* @icon: a #GIcon
|
||
*
|
||
* Get the height of an icon, if it was attached as metadata when the #GIcon was
|
||
* created from an #AsIcon.
|
||
*
|
||
* Returns: height of the icon (in device pixels), or `0` if unknown
|
||
* Since: 40
|
||
*/
|
||
guint
|
||
gs_icon_get_height (GIcon *icon)
|
||
{
|
||
g_return_val_if_fail (G_IS_ICON (icon), 0);
|
||
|
||
return GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (icon), "height"));
|
||
}
|
||
|
||
/**
|
||
* gs_icon_set_height:
|
||
* @icon: a #GIcon
|
||
* @height: height of the icon, in device pixels
|
||
*
|
||
* Set the height of an icon. See gs_icon_get_height().
|
||
*
|
||
* Since: 40
|
||
*/
|
||
void
|
||
gs_icon_set_height (GIcon *icon,
|
||
guint height)
|
||
{
|
||
g_return_if_fail (G_IS_ICON (icon));
|
||
|
||
g_object_set_data (G_OBJECT (icon), "height", GUINT_TO_POINTER (height));
|
||
}
|
||
|
||
/**
|
||
* gs_icon_get_scale:
|
||
* @icon: a #GIcon
|
||
*
|
||
* Get the scale of an icon, if it was attached as metadata when the #GIcon was
|
||
* created from an #AsIcon.
|
||
*
|
||
* See gtk_widget_get_scale_factor() for more information about scales.
|
||
*
|
||
* Returns: scale of the icon, or `1` if unknown; guaranteed to always be
|
||
* greater than or equal to 1
|
||
* Since: 40
|
||
*/
|
||
guint
|
||
gs_icon_get_scale (GIcon *icon)
|
||
{
|
||
g_return_val_if_fail (G_IS_ICON (icon), 0);
|
||
|
||
return MAX (1, GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (icon), "scale")));
|
||
}
|
||
|
||
/**
|
||
* gs_icon_set_scale:
|
||
* @icon: a #GIcon
|
||
* @scale: scale of the icon, which must be greater than or equal to 1
|
||
*
|
||
* Set the scale of an icon. See gs_icon_get_scale().
|
||
*
|
||
* Since: 40
|
||
*/
|
||
void
|
||
gs_icon_set_scale (GIcon *icon,
|
||
guint scale)
|
||
{
|
||
g_return_if_fail (G_IS_ICON (icon));
|
||
g_return_if_fail (scale >= 1);
|
||
|
||
g_object_set_data (G_OBJECT (icon), "scale", GUINT_TO_POINTER (scale));
|
||
}
|
||
|
||
static GIcon *
|
||
gs_icon_load_local (AsIcon *icon)
|
||
{
|
||
const gchar *filename = as_icon_get_filename (icon);
|
||
g_autoptr(GFile) file = NULL;
|
||
|
||
if (filename == NULL)
|
||
return NULL;
|
||
|
||
file = g_file_new_for_path (filename);
|
||
return g_file_icon_new (file);
|
||
}
|
||
|
||
static GIcon *
|
||
gs_icon_load_stock (AsIcon *icon)
|
||
{
|
||
const gchar *name = as_icon_get_name (icon);
|
||
|
||
if (name == NULL)
|
||
return NULL;
|
||
|
||
return g_themed_icon_new (name);
|
||
}
|
||
|
||
static GIcon *
|
||
gs_icon_load_remote (AsIcon *icon)
|
||
{
|
||
const gchar *url = as_icon_get_url (icon);
|
||
|
||
if (url == NULL)
|
||
return NULL;
|
||
|
||
/* Load local files directly. */
|
||
if (g_str_has_prefix (url, "file:")) {
|
||
g_autoptr(GFile) file = g_file_new_for_path (url + strlen ("file:"));
|
||
return g_file_icon_new (file);
|
||
}
|
||
|
||
/* Only HTTP and HTTPS are supported. */
|
||
if (!g_str_has_prefix (url, "http:") &&
|
||
!g_str_has_prefix (url, "https:"))
|
||
return NULL;
|
||
|
||
return gs_remote_icon_new (url);
|
||
}
|
||
|
||
static GIcon *
|
||
gs_icon_load_cached (AsIcon *icon)
|
||
{
|
||
const gchar *filename = as_icon_get_filename (icon);
|
||
const gchar *name = as_icon_get_name (icon);
|
||
g_autofree gchar *name_allocated = NULL;
|
||
g_autofree gchar *full_filename = NULL;
|
||
g_autoptr(GFile) file = NULL;
|
||
|
||
if (filename == NULL || name == NULL)
|
||
return NULL;
|
||
|
||
/* FIXME: Work around https://github.com/hughsie/appstream-glib/pull/390
|
||
* where appstream files generated with appstream-builder from
|
||
* appstream-glib, with its hidpi option enabled, will contain an
|
||
* unnecessary size subdirectory in the icon name. */
|
||
if (g_str_has_prefix (name, "64x64/"))
|
||
name = name_allocated = g_strdup (name + strlen ("64x64/"));
|
||
else if (g_str_has_prefix (name, "128x128/"))
|
||
name = name_allocated = g_strdup (name + strlen ("128x128/"));
|
||
|
||
if (!g_str_has_suffix (filename, name)) {
|
||
/* Spec: https://www.freedesktop.org/software/appstream/docs/sect-AppStream-IconCache.html#spec-iconcache-location */
|
||
if (as_icon_get_scale (icon) <= 1) {
|
||
full_filename = g_strdup_printf ("%s/%ux%u/%s",
|
||
filename,
|
||
as_icon_get_width (icon),
|
||
as_icon_get_height (icon),
|
||
name);
|
||
} else {
|
||
full_filename = g_strdup_printf ("%s/%ux%u@%u/%s",
|
||
filename,
|
||
as_icon_get_width (icon),
|
||
as_icon_get_height (icon),
|
||
as_icon_get_scale (icon),
|
||
name);
|
||
}
|
||
|
||
filename = full_filename;
|
||
}
|
||
|
||
file = g_file_new_for_path (filename);
|
||
return g_file_icon_new (file);
|
||
}
|
||
|
||
/**
|
||
* gs_icon_new_for_appstream_icon:
|
||
* @appstream_icon: an #AsIcon
|
||
*
|
||
* Create a new #GIcon representing the given #AsIcon. The actual type of the
|
||
* returned icon will vary depending on the #AsIconKind of @appstream_icon.
|
||
*
|
||
* If the width or height of the icon are set on the #AsIcon, they are stored
|
||
* as the `width` and `height` data associated with the returned object, using
|
||
* g_object_set_data().
|
||
*
|
||
* This can fail (and return %NULL) if the @appstream_icon has invalid or
|
||
* missing properties.
|
||
*
|
||
* Returns: (transfer full) (nullable): the #GIcon, or %NULL
|
||
* Since: 40
|
||
*/
|
||
GIcon *
|
||
gs_icon_new_for_appstream_icon (AsIcon *appstream_icon)
|
||
{
|
||
g_autoptr(GIcon) icon = NULL;
|
||
|
||
g_return_val_if_fail (AS_IS_ICON (appstream_icon), NULL);
|
||
|
||
switch (as_icon_get_kind (appstream_icon)) {
|
||
case AS_ICON_KIND_LOCAL:
|
||
icon = gs_icon_load_local (appstream_icon);
|
||
break;
|
||
case AS_ICON_KIND_STOCK:
|
||
icon = gs_icon_load_stock (appstream_icon);
|
||
break;
|
||
case AS_ICON_KIND_REMOTE:
|
||
icon = gs_icon_load_remote (appstream_icon);
|
||
break;
|
||
case AS_ICON_KIND_CACHED:
|
||
icon = gs_icon_load_cached (appstream_icon);
|
||
break;
|
||
default:
|
||
g_assert_not_reached ();
|
||
}
|
||
|
||
if (icon == NULL) {
|
||
g_debug ("Error creating GIcon for AsIcon of kind %s",
|
||
as_icon_kind_to_string (as_icon_get_kind (appstream_icon)));
|
||
return NULL;
|
||
}
|
||
|
||
/* Store the width, height and scale as associated metadata (if
|
||
* available) so that #GsApp can sort icons by size and return the most
|
||
* appropriately sized one in gs_app_get_icon_by_size().
|
||
*
|
||
* FIXME: Ideally we’d store these as properties on the objects, but
|
||
* GIO currently doesn’t allow subclassing of its #GIcon classes. If we
|
||
* were to implement a #GLoadableIcon with these as properties, all the
|
||
* fast paths in GTK for loading icon data (particularly named icons)
|
||
* would be ignored.
|
||
*
|
||
* Storing the width and height as associated metadata means GObject
|
||
* creates a hash table for each GIcon object. This is a waste of memory
|
||
* (compared to using properties), but seems like the least-worst
|
||
* option.
|
||
*
|
||
* See https://gitlab.gnome.org/GNOME/glib/-/issues/2345
|
||
*/
|
||
if (as_icon_get_width (appstream_icon) != 0 || as_icon_get_height (appstream_icon) != 0) {
|
||
gs_icon_set_width (icon, as_icon_get_width (appstream_icon));
|
||
gs_icon_set_height (icon, as_icon_get_height (appstream_icon));
|
||
}
|
||
if (as_icon_get_scale (appstream_icon) != 0)
|
||
gs_icon_set_scale (icon, as_icon_get_scale (appstream_icon));
|
||
|
||
return g_steal_pointer (&icon);
|
||
}
|