feat: add disk usage bars to side panel and computer:// view

Side panel devices show a thin progress bar below each drive name,
proportional to disk usage. Bar turns red when usage exceeds 90%.
The computer:// icon view also shows usage bars under volume names.

New files:
- thunar-shortcuts-progress-renderer.c/.h: custom cell renderer
  (extends GtkCellRendererText) that draws text + usage bar

Modified:
- thunar-shortcuts-model: added USAGE_PERCENT column
- thunar-shortcuts-view: uses progress renderer instead of plain text
- thunar-text-renderer: added usage-percent property with bar drawing
- thunar-standard-view: sets usage-percent via cell_data_func for
  computer:// entries using thunar_g_file_get_free_space()
This commit is contained in:
4aiman 2026-04-29 17:18:16 +03:00 • committed by 4aiman
commit fa6a8694c3
8 changed files with 522 additions and 12 deletions

View file

@ -172,6 +172,8 @@ thunar_SOURCES = \
thunar-shortcuts-icon-renderer.h \
thunar-shortcuts-model.c \
thunar-shortcuts-model.h \
thunar-shortcuts-progress-renderer.c \
thunar-shortcuts-progress-renderer.h \
thunar-shortcuts-pane.c \
thunar-shortcuts-pane.h \
thunar-shortcuts-view.c \

View file

@ -542,6 +542,9 @@ thunar_shortcuts_model_get_column_type (GtkTreeModel *tree_model,
case THUNAR_SHORTCUTS_MODEL_COLUMN_HIDDEN:
return G_TYPE_BOOLEAN;
case THUNAR_SHORTCUTS_MODEL_COLUMN_USAGE_PERCENT:
return G_TYPE_INT;
}
_thunar_assert_not_reached ();
@ -822,6 +825,40 @@ thunar_shortcuts_model_get_value (GtkTreeModel *tree_model,
g_value_set_boolean (value, FALSE);
break;
case THUNAR_SHORTCUTS_MODEL_COLUMN_USAGE_PERCENT:
g_value_init (value, G_TYPE_INT);
if ((shortcut->group & THUNAR_SHORTCUT_GROUP_DEVICES) != 0
&& (shortcut->group & THUNAR_SHORTCUT_GROUP_HEADER) == 0)
{
guint64 fs_free = 0, fs_total = 0;
if (shortcut->device != NULL)
file = thunar_device_get_root (shortcut->device);
else if (shortcut->file != NULL)
file = g_object_ref (thunar_file_get_file (shortcut->file));
else
file = NULL;
if (file != NULL
&& thunar_g_file_get_free_space (file, &fs_free, &fs_total)
&& fs_total > 0)
{
g_value_set_int (value, (gint) ((fs_total - fs_free) * 100 / fs_total));
}
else
{
g_value_set_int (value, -1);
}
if (file != NULL)
g_object_unref (file);
}
else
{
g_value_set_int (value, -1);
}
break;
default:
_thunar_assert_not_reached ();
}

View file

@ -53,6 +53,7 @@ typedef enum
THUNAR_SHORTCUTS_MODEL_COLUMN_BUSY,
THUNAR_SHORTCUTS_MODEL_COLUMN_BUSY_PULSE,
THUNAR_SHORTCUTS_MODEL_COLUMN_HIDDEN,
THUNAR_SHORTCUTS_MODEL_COLUMN_USAGE_PERCENT,
THUNAR_SHORTCUTS_MODEL_N_COLUMNS,
} ThunarShortcutsModelColumn;

View file

@ -0,0 +1,296 @@
/* vi:set et ai sw=2 sts=2 ts=2: */
/*-
* Copyright (c) 2026
*
* 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 of the License, 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., 59 Temple
* Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include "thunar/thunar-shortcuts-progress-renderer.h"
/* Height of the usage bar in pixels */
#define BAR_HEIGHT 4
/* Gap between the text and the bar */
#define BAR_TOP_PAD 2
/* Corner radius */
#define BAR_RADIUS 2
enum
{
PROP_0,
PROP_USAGE_PERCENT,
};
struct _ThunarShortcutsProgressRendererClass
{
GtkCellRendererTextClass __parent__;
};
struct _ThunarShortcutsProgressRenderer
{
GtkCellRendererText __parent__;
gint usage_percent; /* -1 = no bar, 0..100 = percent used */
};
G_DEFINE_TYPE (ThunarShortcutsProgressRenderer,
thunar_shortcuts_progress_renderer,
GTK_TYPE_CELL_RENDERER_TEXT)
static void
thunar_shortcuts_progress_renderer_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
ThunarShortcutsProgressRenderer *renderer = THUNAR_SHORTCUTS_PROGRESS_RENDERER (object);
switch (prop_id)
{
case PROP_USAGE_PERCENT:
g_value_set_int (value, renderer->usage_percent);
break;
default:
G_OBJECT_CLASS (thunar_shortcuts_progress_renderer_parent_class)->get_property (object, prop_id, value, pspec);
break;
}
}
static void
thunar_shortcuts_progress_renderer_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
ThunarShortcutsProgressRenderer *renderer = THUNAR_SHORTCUTS_PROGRESS_RENDERER (object);
switch (prop_id)
{
case PROP_USAGE_PERCENT:
renderer->usage_percent = g_value_get_int (value);
break;
default:
G_OBJECT_CLASS (thunar_shortcuts_progress_renderer_parent_class)->set_property (object, prop_id, value, pspec);
break;
}
}
static void
thunar_shortcuts_progress_renderer_get_preferred_height (GtkCellRenderer *cell,
GtkWidget *widget,
gint *minimum,
gint *natural)
{
ThunarShortcutsProgressRenderer *renderer = THUNAR_SHORTCUTS_PROGRESS_RENDERER (cell);
gint min_h = 0, nat_h = 0;
/* let the parent (GtkCellRendererText) calculate text height */
GTK_CELL_RENDERER_CLASS (thunar_shortcuts_progress_renderer_parent_class)->get_preferred_height (cell, widget, &min_h, &nat_h);
/* add space for the bar if usage data is available */
if (renderer->usage_percent >= 0)
{
min_h += BAR_TOP_PAD + BAR_HEIGHT;
nat_h += BAR_TOP_PAD + BAR_HEIGHT;
}
if (minimum != NULL)
*minimum = min_h;
if (natural != NULL)
*natural = nat_h;
}
static void
thunar_shortcuts_progress_renderer_render (GtkCellRenderer *cell,
cairo_t *cr,
GtkWidget *widget,
const GdkRectangle *background_area,
const GdkRectangle *cell_area,
GtkCellRendererState flags)
{
ThunarShortcutsProgressRenderer *renderer = THUNAR_SHORTCUTS_PROGRESS_RENDERER (cell);
GtkStyleContext *context;
GdkRGBA bar_bg;
GdkRGBA bar_fg;
gint text_height;
gint bar_y;
gint bar_width;
gint xpad, ypad;
GdkRectangle text_area;
gtk_cell_renderer_get_padding (cell, &xpad, &ypad);
if (renderer->usage_percent >= 0)
{
/* calculate how much space the bar needs, then shrink cell_area for text */
text_area = *cell_area;
text_area.height -= (BAR_TOP_PAD + BAR_HEIGHT);
/* let the parent draw the text in the reduced area */
GTK_CELL_RENDERER_CLASS (thunar_shortcuts_progress_renderer_parent_class)->render (
cell, cr, widget, background_area, &text_area, flags);
/* draw the usage bar below the text */
text_height = text_area.height;
bar_y = cell_area->y + ypad + text_height + BAR_TOP_PAD;
bar_width = cell_area->width - 2 * xpad;
if (bar_width <= 0)
return;
context = gtk_widget_get_style_context (widget);
/* bar background: semi-transparent grey */
bar_bg.red = 0.5;
bar_bg.green = 0.5;
bar_bg.blue = 0.5;
bar_bg.alpha = 0.3;
/* bar foreground color: red for >90%, otherwise theme selection color */
if (renderer->usage_percent > 90)
{
bar_fg.red = 0.85;
bar_fg.green = 0.2;
bar_fg.blue = 0.2;
bar_fg.alpha = 0.9;
}
else
{
if (!gtk_style_context_lookup_color (context, "theme_selected_bg_color", &bar_fg)
|| bar_fg.alpha < 0.01)
{
/* fallback blue */
bar_fg.red = 0.3;
bar_fg.green = 0.5;
bar_fg.blue = 0.8;
}
bar_fg.alpha = 0.9;
}
cairo_save (cr);
/* draw background track (rounded rectangle) */
{
gdouble x = cell_area->x + xpad;
gdouble y = bar_y;
gdouble w = bar_width;
gdouble h = BAR_HEIGHT;
gdouble r = BAR_RADIUS;
cairo_new_sub_path (cr);
cairo_arc (cr, x + w - r, y + r, r, -G_PI_2, 0);
cairo_arc (cr, x + w - r, y + h - r, r, 0, G_PI_2);
cairo_arc (cr, x + r, y + h - r, r, G_PI_2, G_PI);
cairo_arc (cr, x + r, y + r, r, G_PI, 3 * G_PI_2);
cairo_close_path (cr);
}
gdk_cairo_set_source_rgba (cr, &bar_bg);
cairo_fill (cr);
/* draw filled portion (rounded rectangle) */
if (renderer->usage_percent > 0)
{
gdouble x = cell_area->x + xpad;
gdouble y = bar_y;
gdouble w = bar_width * renderer->usage_percent / 100.0;
gdouble h = BAR_HEIGHT;
gdouble r = BAR_RADIUS;
if (w < 2 * r)
r = w / 2.0;
if (r < 0.5)
r = 0;
cairo_new_sub_path (cr);
cairo_arc (cr, x + w - r, y + r, r, -G_PI_2, 0);
cairo_arc (cr, x + w - r, y + h - r, r, 0, G_PI_2);
cairo_arc (cr, x + r, y + h - r, r, G_PI_2, G_PI);
cairo_arc (cr, x + r, y + r, r, G_PI, 3 * G_PI_2);
cairo_close_path (cr);
gdk_cairo_set_source_rgba (cr, &bar_fg);
cairo_fill (cr);
}
cairo_restore (cr);
}
else
{
/* no usage data: just draw the text normally */
GTK_CELL_RENDERER_CLASS (thunar_shortcuts_progress_renderer_parent_class)->render (
cell, cr, widget, background_area, cell_area, flags);
}
}
static void
thunar_shortcuts_progress_renderer_class_init (ThunarShortcutsProgressRendererClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
GtkCellRendererClass *cell_class = GTK_CELL_RENDERER_CLASS (klass);
gobject_class->get_property = thunar_shortcuts_progress_renderer_get_property;
gobject_class->set_property = thunar_shortcuts_progress_renderer_set_property;
cell_class->get_preferred_height = thunar_shortcuts_progress_renderer_get_preferred_height;
cell_class->render = thunar_shortcuts_progress_renderer_render;
g_object_class_install_property (gobject_class,
PROP_USAGE_PERCENT,
g_param_spec_int ("usage-percent",
"usage-percent",
"Disk usage percentage (0-100, -1 = hidden)",
-1, 100, -1,
G_PARAM_READWRITE));
}
static void
thunar_shortcuts_progress_renderer_init (ThunarShortcutsProgressRenderer *renderer)
{
renderer->usage_percent = -1;
/* match the original text renderer settings */
g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
}
GtkCellRenderer *
thunar_shortcuts_progress_renderer_new (void)
{
return g_object_new (THUNAR_TYPE_SHORTCUTS_PROGRESS_RENDERER, NULL);
}

View file

@ -0,0 +1,38 @@
/* vi:set et ai sw=2 sts=2 ts=2: */
/*-
* Copyright (c) 2026
*
* 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 of the License, 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., 59 Temple
* Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifndef __THUNAR_SHORTCUTS_PROGRESS_RENDERER_H__
#define __THUNAR_SHORTCUTS_PROGRESS_RENDERER_H__
#include <gtk/gtk.h>
G_BEGIN_DECLS;
typedef struct _ThunarShortcutsProgressRendererClass ThunarShortcutsProgressRendererClass;
typedef struct _ThunarShortcutsProgressRenderer ThunarShortcutsProgressRenderer;
#define THUNAR_TYPE_SHORTCUTS_PROGRESS_RENDERER (thunar_shortcuts_progress_renderer_get_type ())
#define THUNAR_SHORTCUTS_PROGRESS_RENDERER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), THUNAR_TYPE_SHORTCUTS_PROGRESS_RENDERER, ThunarShortcutsProgressRenderer))
GType thunar_shortcuts_progress_renderer_get_type (void) G_GNUC_CONST;
GtkCellRenderer *thunar_shortcuts_progress_renderer_new (void);
G_END_DECLS;
#endif /* !__THUNAR_SHORTCUTS_PROGRESS_RENDERER_H__ */

View file

@ -44,6 +44,7 @@
#include "thunar/thunar-private.h"
#include "thunar/thunar-shortcuts-icon-renderer.h"
#include "thunar/thunar-shortcuts-model.h"
#include "thunar/thunar-shortcuts-progress-renderer.h"
#include "thunar/thunar-shortcuts-view.h"
#include <gdk/gdkkeysyms.h>
@ -395,14 +396,13 @@ thunar_shortcuts_view_init (ThunarShortcutsView *view)
g_object_bind_property (G_OBJECT (view->preferences), "misc-symbolic-icons-in-sidepane", G_OBJECT (view->icon_renderer), "use-symbolic-icons", G_BINDING_SYNC_CREATE);
g_signal_connect_swapped (G_OBJECT (view->preferences), "notify::misc-symbolic-icons-in-sidepane", G_CALLBACK (gtk_widget_queue_draw), view);
/* allocate the text renderer (ellipsizing as required, but "File System" must fit) */
renderer = g_object_new (GTK_TYPE_CELL_RENDERER_TEXT,
"ellipsize", PANGO_ELLIPSIZE_END,
NULL);
/* allocate the text+progress renderer for device names with usage bars */
renderer = thunar_shortcuts_progress_renderer_new ();
g_signal_connect (G_OBJECT (renderer), "edited", G_CALLBACK (thunar_shortcuts_view_renamed), view);
gtk_tree_view_column_pack_start (column, renderer, TRUE);
gtk_tree_view_column_set_attributes (column, renderer,
"text", THUNAR_SHORTCUTS_MODEL_COLUMN_NAME,
"usage-percent", THUNAR_SHORTCUTS_MODEL_COLUMN_USAGE_PERCENT,
"visible", THUNAR_SHORTCUTS_MODEL_COLUMN_IS_ITEM,
NULL);

View file

@ -4818,10 +4818,27 @@ thunar_standard_view_cell_layout_data_func (GtkCellLayout *layout,
/* since this function is being used for both icon & name renderers;
* we need to make sure the right properties are applied to the right renderers */
if (THUNAR_IS_TEXT_RENDERER (cell))
{
/* set disk usage bar for computer:// items */
gint usage = -1;
GFile *gfile = thunar_file_get_file (file);
if (gfile != NULL && g_file_has_uri_scheme (gfile, "computer"))
{
GFile *root = thunar_file_get_target_location (file);
if (root == NULL)
root = gfile;
guint64 fs_free = 0, fs_total = 0;
if (thunar_g_file_get_free_space (root, &fs_free, &fs_total) && fs_total > 0)
usage = (gint) ((fs_total - fs_free) * 100 / fs_total);
if (root != gfile)
g_object_unref (root);
}
g_object_set (G_OBJECT (cell),
"foreground-rgba", foreground != NULL ? &foreground_rgba : NULL,
"highlight-color", background,
"usage-percent", usage,
NULL);
}
else if (THUNAR_IS_ICON_RENDERER (cell))
g_object_set (G_OBJECT (cell),

View file

@ -23,6 +23,11 @@
#include "thunar/thunar-text-renderer.h"
#include "thunar/thunar-util.h"
#include <math.h>
#define USAGE_BAR_HEIGHT 4
#define USAGE_BAR_TOP_PAD 2
#define USAGE_BAR_RADIUS 2
@ -32,6 +37,7 @@ enum
PROP_HIGHLIGHT_COLOR,
PROP_ROUNDED_CORNERS,
PROP_HIGHLIGHTING_ENABLED,
PROP_USAGE_PERCENT,
};
@ -55,6 +61,11 @@ thunar_text_renderer_render (GtkCellRenderer *renderer,
const GdkRectangle *background_area,
const GdkRectangle *cell_area,
GtkCellRendererState flags);
static void
thunar_text_renderer_get_preferred_height (GtkCellRenderer *cell,
GtkWidget *widget,
gint *minimum,
gint *natural);
@ -77,6 +88,7 @@ struct _ThunarTextRenderer
gchar *highlight_color;
gboolean rounded_corners;
gboolean highlighting_enabled;
gint usage_percent; /* -1 = no bar, 0..100 = percent used */
};
@ -98,6 +110,7 @@ thunar_text_renderer_class_init (ThunarTextRendererClass *klass)
klass->default_render_function = cell_class->render;
cell_class->render = thunar_text_renderer_render;
cell_class->get_preferred_height = thunar_text_renderer_get_preferred_height;
/**
* ThunarTextRenderer:highlight-color:
@ -136,6 +149,13 @@ thunar_text_renderer_class_init (ThunarTextRendererClass *klass)
g_param_spec_boolean ("highlighting-enabled", "highlighting-enabled", "highlighting-enabled",
FALSE,
EXO_PARAM_READWRITE));
g_object_class_install_property (object_class,
PROP_USAGE_PERCENT,
g_param_spec_int ("usage-percent", "usage-percent",
"Disk usage percentage (0-100, -1 = hidden)",
-1, 100, -1,
G_PARAM_READWRITE));
}
@ -144,6 +164,7 @@ static void
thunar_text_renderer_init (ThunarTextRenderer *text_renderer)
{
text_renderer->highlight_color = NULL;
text_renderer->usage_percent = -1;
}
@ -182,6 +203,10 @@ thunar_text_renderer_get_property (GObject *object,
g_value_set_boolean (value, text_renderer->highlighting_enabled);
break;
case PROP_USAGE_PERCENT:
g_value_set_int (value, text_renderer->usage_percent);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@ -213,6 +238,10 @@ thunar_text_renderer_set_property (GObject *object,
text_renderer->highlighting_enabled = g_value_get_boolean (value);
break;
case PROP_USAGE_PERCENT:
text_renderer->usage_percent = g_value_get_int (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@ -239,6 +268,91 @@ thunar_text_renderer_new (void)
static void
thunar_text_renderer_get_preferred_height (GtkCellRenderer *cell,
GtkWidget *widget,
gint *minimum,
gint *natural)
{
gint min_h = 0, nat_h = 0;
GTK_CELL_RENDERER_CLASS (thunar_text_renderer_parent_class)->get_preferred_height (cell, widget, &min_h, &nat_h);
if (minimum != NULL) *minimum = min_h;
if (natural != NULL) *natural = nat_h;
}
static void
thunar_text_renderer_draw_usage_bar (cairo_t *cr,
GtkWidget *widget,
const GdkRectangle *cell_area,
gint usage_percent)
{
GtkStyleContext *context;
GdkRGBA bar_bg = { 0.5, 0.5, 0.5, 0.3 };
GdkRGBA bar_fg;
gint xpad = 2;
gdouble x = cell_area->x + xpad;
gdouble y = cell_area->y + cell_area->height - USAGE_BAR_HEIGHT;
gdouble w = cell_area->width - 2 * xpad;
gdouble h = USAGE_BAR_HEIGHT;
gdouble r = USAGE_BAR_RADIUS;
gdouble fw;
if (w <= 0)
return;
context = gtk_widget_get_style_context (widget);
if (usage_percent > 90)
{
bar_fg.red = 0.85; bar_fg.green = 0.2; bar_fg.blue = 0.2; bar_fg.alpha = 0.9;
}
else
{
if (!gtk_style_context_lookup_color (context, "theme_selected_bg_color", &bar_fg)
|| bar_fg.alpha < 0.01)
{ bar_fg.red = 0.3; bar_fg.green = 0.5; bar_fg.blue = 0.8; }
bar_fg.alpha = 0.9;
}
cairo_save (cr);
/* background track */
cairo_new_sub_path (cr);
cairo_arc (cr, x + w - r, y + r, r, -G_PI_2, 0);
cairo_arc (cr, x + w - r, y + h - r, r, 0, G_PI_2);
cairo_arc (cr, x + r, y + h - r, r, G_PI_2, G_PI);
cairo_arc (cr, x + r, y + r, r, G_PI, 3 * G_PI_2);
cairo_close_path (cr);
gdk_cairo_set_source_rgba (cr, &bar_bg);
cairo_fill (cr);
/* filled portion */
if (usage_percent > 0)
{
fw = w * usage_percent / 100.0;
r = USAGE_BAR_RADIUS;
if (fw < 2 * r) r = fw / 2.0;
if (r < 0.5) r = 0;
cairo_new_sub_path (cr);
cairo_arc (cr, x + fw - r, y + r, r, -G_PI_2, 0);
cairo_arc (cr, x + fw - r, y + h - r, r, 0, G_PI_2);
cairo_arc (cr, x + r, y + h - r, r, G_PI_2, G_PI);
cairo_arc (cr, x + r, y + r, r, G_PI, 3 * G_PI_2);
cairo_close_path (cr);
gdk_cairo_set_source_rgba (cr, &bar_fg);
cairo_fill (cr);
}
cairo_restore (cr);
}
static void
thunar_text_renderer_render (GtkCellRenderer *cell,
cairo_t *cr,
@ -247,11 +361,16 @@ thunar_text_renderer_render (GtkCellRenderer *cell,
const GdkRectangle *cell_area,
GtkCellRendererState flags)
{
if (THUNAR_TEXT_RENDERER (cell)->highlighting_enabled)
ThunarTextRenderer *text_renderer = THUNAR_TEXT_RENDERER (cell);
if (text_renderer->highlighting_enabled)
thunar_util_clip_view_background (cell, cr, background_area, widget, flags);
/* we only needed to manipulate the background_area, otherwise everything remains the same.
Hence, we are simply running the original render function now */
THUNAR_TEXT_RENDERER_GET_CLASS (THUNAR_TEXT_RENDERER (cell))
/* draw text normally */
THUNAR_TEXT_RENDERER_GET_CLASS (text_renderer)
->default_render_function (cell, cr, widget, background_area, cell_area, flags);
/* draw usage bar at the very bottom of the cell, overlapping if needed */
if (text_renderer->usage_percent >= 0)
thunar_text_renderer_draw_usage_bar (cr, widget, cell_area, text_renderer->usage_percent);
}