*
* Copyright 2026 Alexander Vanhee
*
* 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 3 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, see <https://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "config.h"
#include <glib/gi18n.h>
#include <bge.h>
#include "bz-install-controls.h"
#include "bz-state-info.h"
#include "bz-update-info.h"
#include "progress-bar-designs/common.h"
#include "template-callbacks.h"
#include "util.h"
struct _BzInstallControls
{
GtkBox parent_instance;
BzEntryGroup *group;
BzStateInfo *state;
GSettings *settings;
gboolean wide;
gboolean in_progress;
BzTransactionEntryTracker *tracker;
GListModel *all_trackers;
GtkWidget *install_button;
char *install_btn_class;
char *pride_class;
GtkWidget *open_button;
GtkWidget *update_button;
GtkWidget *animated_button;
};
G_DEFINE_FINAL_TYPE (BzInstallControls, bz_install_controls, GTK_TYPE_BOX)
enum
{
PROP_0,
PROP_WIDE,
PROP_ENTRY_GROUP,
PROP_STATE,
PROP_SETTINGS,
PROP_TRACKER,
PROP_IN_PROGRESS,
LAST_PROP
};
static GParamSpec *props[LAST_PROP] = { 0 };
enum
{
SIGNAL_UPDATE,
LAST_SIGNAL,
};
static guint signals[LAST_SIGNAL];
static void
pride_flag_changed (BzInstallControls *self,
const char *key,
GSettings *settings);
static void
ensure_draw_css (BzInstallControls *self);
static void
update_tracker (BzInstallControls *self)
{
BzTransactionManager *manager = NULL;
g_autoptr (GListModel) all = NULL;
const char *group_id = NULL;
g_autoptr (BzTransactionEntryTracker) found = NULL;
if (self->state != NULL)
manager = bz_state_info_get_transaction_manager (self->state);
if (manager != NULL)
g_object_get (manager, "all-trackers", &all, NULL);
if (self->group != NULL)
group_id = bz_entry_group_get_id (self->group);
if (all != NULL && group_id != NULL)
{
for (guint i = 0; i < g_list_model_get_n_items (all); i++)
{
g_autoptr (BzTransactionEntryTracker) tracker = NULL;
BzEntry *entry = NULL;
tracker = g_list_model_get_item (all, i);
entry = bz_transaction_entry_tracker_get_entry (tracker);
if (g_strcmp0 (entry != NULL ? bz_entry_get_id (entry) : NULL, group_id) == 0)
{
found = g_steal_pointer (&tracker);
break;
}
}
}
if (g_set_object (&self->tracker, found))
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRACKER]);
}
static void
on_all_trackers_changed (GListModel *model,
guint position,
guint removed,
guint added,
BzInstallControls *self)
{
update_tracker (self);
}
static void
install_cancel_cb (BzInstallControls *self,
GtkButton *button)
{
const char *state = NULL;
if (self->group == NULL)
return;
state = bge_wdgt_renderer_get_state (BGE_WDGT_RENDERER (self->animated_button));
if (state == NULL)
return;
if (g_strcmp0 (state, "inactive") == 0)
gtk_widget_activate_action (GTK_WIDGET (self), "window.install-group", "(sb)",
bz_entry_group_get_id (self->group), TRUE);
else
gtk_widget_activate_action (GTK_WIDGET (self), "window.cancel-group", "s",
bz_entry_group_get_id (self->group));
}
static void
remove_cb (BzInstallControls *self,
GtkButton *button)
{
if (self->group == NULL)
return;
gtk_widget_activate_action (GTK_WIDGET (self), "window.remove-group", "(sb)",
bz_entry_group_get_id (self->group), TRUE);
}
static void
run_cb (BzInstallControls *self,
GtkButton *button)
{
if (self->group == NULL)
return;
gtk_widget_activate_action (GTK_WIDGET (self), "window.launch-group", "s",
bz_entry_group_get_id (self->group));
}
static GListStore *
find_matching_updates (BzInstallControls *self,
GListModel *available_updates)
{
const char *group_id = NULL;
guint n_items = 0;
GListStore *store = NULL;
if (self->group == NULL || available_updates == NULL)
return NULL;
group_id = bz_entry_group_get_id (self->group);
n_items = g_list_model_get_n_items (available_updates);
store = g_list_store_new (BZ_TYPE_ENTRY);
for (guint i = 0; i < n_items; i++)
{
g_autoptr (BzUpdatePermissionInfo) info = NULL;
BzEntry *entry = NULL;
const char *id = NULL;
info = g_list_model_get_item (available_updates, i);
entry = bz_update_permission_info_get_entry (info);
id = entry != NULL ? bz_entry_get_id (entry) : NULL;
if (g_strcmp0 (id, group_id) == 0)
g_list_store_append (store, entry);
}
if (g_list_model_get_n_items (G_LIST_MODEL (store)) == 0)
g_clear_object (&store);
return store;
}
static void
update_cb (BzInstallControls *self,
GtkButton *button)
{
GListModel *available_updates = NULL;
g_autoptr (GListStore) store = NULL;
if (self->state == NULL)
return;
available_updates = bz_state_info_get_available_updates (self->state);
store = find_matching_updates (self, available_updates);
if (store != NULL)
g_signal_emit (self, signals[SIGNAL_UPDATE], 0, G_LIST_MODEL (store));
}
static gboolean
is_not_empty_page (gpointer object,
const char *page_name)
{
if (page_name == NULL)
return FALSE;
return g_strcmp0 (page_name, "empty") != 0;
}
static char *
get_visible_page (gpointer object,
int installable,
int removable,
GListModel *available_updates,
gboolean active)
{
BzInstallControls *self = BZ_INSTALL_CONTROLS (object);
g_autoptr (GListStore) store = NULL;
if (active)
return g_strdup ("install");
if (removable > 0)
{
if (g_signal_has_handler_pending (self, signals[SIGNAL_UPDATE], 0, FALSE))
store = find_matching_updates (self, available_updates);
return g_strdup (store != NULL ? "update" : "open");
}
else if (installable > 0)
return g_strdup ("install");
else
return g_strdup ("empty");
}
static void
set_install_btn_class (BzInstallControls *self,
const char *class)
{
if (self->install_button == NULL)
return;
if (self->install_btn_class != NULL &&
g_strcmp0 (class, self->install_btn_class) == 0)
return;
if (self->install_btn_class != NULL)
gtk_widget_remove_css_class (self->install_button, self->install_btn_class);
g_clear_pointer (&self->install_btn_class, g_free);
if (class != NULL)
{
gtk_widget_add_css_class (self->install_button, class);
self->install_btn_class = g_strdup (class);
}
}
static char *
get_install_btn_state (BzInstallControls *self,
BzTransactionEntryStatus status,
gboolean active,
gboolean pending,
double progress)
{
g_autofree char *state = NULL;
const char *class = NULL;
if ((active || pending) && status == BZ_TRANSACTION_ENTRY_STATUS_CANCELLED)
{
state = g_strdup ("cancelling");
class = NULL;
}
else if (pending || status == BZ_TRANSACTION_ENTRY_STATUS_QUEUED)
{
state = g_strdup ("pending");
class = NULL;
}
else if (!active || status == BZ_TRANSACTION_ENTRY_STATUS_DONE)
{
state = g_strdup ("inactive");
class = "suggested-action";
}
else
{
state = g_strdup ("fraction");
class = NULL;
}
set_install_btn_class (self, class);
return g_steal_pointer (&state);
}
static char *
install_btn_state_fallback (BzInstallControls *self)
{
set_install_btn_class (self, "suggested-action");
return g_strdup ("inactive");
}
static void
on_tracker_state_changed (BzInstallControls *self,
GParamSpec *pspec,
BgeWdgtRenderer *animated_button)
{
const char *state = NULL;
gboolean in_progress = FALSE;
state = bge_wdgt_renderer_get_state (animated_button);
in_progress = state != NULL && g_strcmp0 (state, "inactive") != 0;
if (self->in_progress == in_progress)
return;
self->in_progress = in_progress;
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IN_PROGRESS]);
}
static gboolean
is_blocked (gpointer object,
GListModel *parental_blocked,
BzEntryGroup *group)
{
const char *id = NULL;
if (parental_blocked == NULL || group == NULL)
return FALSE;
id = bz_entry_group_get_id (group);
if (id == NULL)
return FALSE;
for (guint i = 0; i < g_list_model_get_n_items (parental_blocked); i++)
{
g_autoptr (GtkStringObject) obj = g_list_model_get_item (parental_blocked, i);
if (strstr (gtk_string_object_get_string (obj), id) != NULL)
return TRUE;
}
return FALSE;
}
static gboolean
idle_grab_focus (GWeakRef *wr)
{
g_autoptr (BzInstallControls) self = NULL;
self = g_weak_ref_get (wr);
if (self == NULL)
goto done;
if (gtk_widget_is_visible (GTK_WIDGET (self)))
{
GtkWidget *target = self->install_button;
if (self->group != NULL && bz_entry_group_get_removable (self->group) > 0)
target = find_matching_updates (self, bz_state_info_get_available_updates (self->state)) != NULL
? self->update_button
: self->open_button;
gtk_widget_grab_focus (target);
}
done:
return G_SOURCE_REMOVE;
}
static void
bz_install_controls_dispose (GObject *object)
{
BzInstallControls *self = BZ_INSTALL_CONTROLS (object);
if (self->settings != NULL)
g_signal_handlers_disconnect_by_func (
self->settings,
pride_flag_changed,
self);
if (self->all_trackers != NULL)
g_signal_handlers_disconnect_by_func (
self->all_trackers,
on_all_trackers_changed,
self);
g_clear_object (&self->group);
g_clear_object (&self->state);
g_clear_object (&self->settings);
g_clear_object (&self->tracker);
g_clear_object (&self->install_button);
g_clear_pointer (&self->install_btn_class, g_free);
g_clear_pointer (&self->pride_class, g_free);
g_clear_object (&self->all_trackers);
G_OBJECT_CLASS (bz_install_controls_parent_class)->dispose (object);
}
static void
bz_install_controls_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
BzInstallControls *self = BZ_INSTALL_CONTROLS (object);
switch (prop_id)
{
case PROP_WIDE:
g_value_set_boolean (value, self->wide);
break;
case PROP_ENTRY_GROUP:
g_value_set_object (value, self->group);
break;
case PROP_STATE:
g_value_set_object (value, self->state);
break;
case PROP_TRACKER:
g_value_set_object (value, self->tracker);
break;
case PROP_SETTINGS:
g_value_set_object (value, self->settings);
break;
case PROP_IN_PROGRESS:
g_value_set_boolean (value, self->in_progress);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
bz_install_controls_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
BzInstallControls *self = BZ_INSTALL_CONTROLS (object);
switch (prop_id)
{
case PROP_WIDE:
bz_install_controls_set_wide (self, g_value_get_boolean (value));
break;
case PROP_ENTRY_GROUP:
bz_install_controls_set_entry_group (self, g_value_get_object (value));
break;
case PROP_STATE:
bz_install_controls_set_state (self, g_value_get_object (value));
break;
case PROP_SETTINGS:
{
if (self->settings != NULL)
g_signal_handlers_disconnect_by_func (
self->settings,
pride_flag_changed,
self);
g_clear_object (&self->settings);
self->settings = g_value_dup_object (value);
if (self->settings != NULL)
{
g_signal_connect_swapped (
self->settings,
"changed::global-progress-bar-theme",
G_CALLBACK (pride_flag_changed),
self);
}
ensure_draw_css (self);
}
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
bz_install_controls_class_init (BzInstallControlsClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
object_class->dispose = bz_install_controls_dispose;
object_class->get_property = bz_install_controls_get_property;
object_class->set_property = bz_install_controls_set_property;
props[PROP_WIDE] =
g_param_spec_boolean (
"wide",
NULL, NULL,
TRUE,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_ENTRY_GROUP] =
g_param_spec_object (
"entry-group",
NULL, NULL,
BZ_TYPE_ENTRY_GROUP,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_STATE] =
g_param_spec_object (
"state",
NULL, NULL,
BZ_TYPE_STATE_INFO,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_SETTINGS] =
g_param_spec_object (
"settings",
NULL, NULL,
G_TYPE_SETTINGS,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
props[PROP_TRACKER] =
g_param_spec_object (
"tracker",
NULL, NULL,
BZ_TYPE_TRANSACTION_ENTRY_TRACKER,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
props[PROP_IN_PROGRESS] =
g_param_spec_boolean (
"in-progress",
NULL, NULL,
FALSE,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
g_object_class_install_properties (object_class, LAST_PROP, props);
signals[SIGNAL_UPDATE] =
g_signal_new (
"update",
G_OBJECT_CLASS_TYPE (klass),
G_SIGNAL_RUN_FIRST,
0,
NULL, NULL,
g_cclosure_marshal_VOID__OBJECT,
G_TYPE_NONE, 1,
G_TYPE_LIST_MODEL);
g_signal_set_va_marshaller (
signals[SIGNAL_UPDATE],
G_TYPE_FROM_CLASS (klass),
g_cclosure_marshal_VOID__OBJECTv);
g_type_ensure (BZ_TYPE_ENTRY_GROUP);
g_type_ensure (BZ_TYPE_STATE_INFO);
gtk_widget_class_set_template_from_resource (widget_class, "/io/github/kolunmi/Bazaar/bz-install-controls.ui");
bz_widget_class_bind_all_util_callbacks (widget_class);
gtk_widget_class_bind_template_child (widget_class, BzInstallControls, open_button);
gtk_widget_class_bind_template_child (widget_class, BzInstallControls, update_button);
gtk_widget_class_bind_template_child (widget_class, BzInstallControls, animated_button);
gtk_widget_class_bind_template_callback (widget_class, install_cancel_cb);
gtk_widget_class_bind_template_callback (widget_class, remove_cb);
gtk_widget_class_bind_template_callback (widget_class, run_cb);
gtk_widget_class_bind_template_callback (widget_class, update_cb);
gtk_widget_class_bind_template_callback (widget_class, is_not_empty_page);
gtk_widget_class_bind_template_callback (widget_class, get_visible_page);
gtk_widget_class_bind_template_callback (widget_class, get_install_btn_state);
gtk_widget_class_bind_template_callback (widget_class, install_btn_state_fallback);
gtk_widget_class_bind_template_callback (widget_class, on_tracker_state_changed);
gtk_widget_class_bind_template_callback (widget_class, is_blocked);
}
static void
bz_install_controls_init (BzInstallControls *self)
{
self->wide = TRUE;
gtk_widget_init_template (GTK_WIDGET (self));
self->install_button = bge_wdgt_renderer_lookup_object (
BGE_WDGT_RENDERER (self->animated_button), "btn");
gtk_widget_add_css_class (self->install_button, "pill");
g_signal_connect_swapped (
self->install_button, "clicked",
G_CALLBACK (install_cancel_cb), self);
gtk_widget_add_css_class (self->install_button, "suggested-action");
self->install_btn_class = g_strdup ("suggested-action");
}
GtkWidget *
bz_install_controls_new (void)
{
return g_object_new (BZ_TYPE_INSTALL_CONTROLS, NULL);
}
gboolean
bz_install_controls_get_wide (BzInstallControls *self)
{
g_return_val_if_fail (BZ_IS_INSTALL_CONTROLS (self), FALSE);
return self->wide;
}
void
bz_install_controls_set_wide (BzInstallControls *self,
gboolean wide)
{
g_return_if_fail (BZ_IS_INSTALL_CONTROLS (self));
wide = !!wide;
if (self->wide == wide)
return;
self->wide = wide;
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_WIDE]);
}
BzEntryGroup *
bz_install_controls_get_entry_group (BzInstallControls *self)
{
g_return_val_if_fail (BZ_IS_INSTALL_CONTROLS (self), NULL);
return self->group;
}
void
bz_install_controls_set_entry_group (BzInstallControls *self,
BzEntryGroup *group)
{
const char *title = NULL;
const char *developer = NULL;
g_return_if_fail (BZ_IS_INSTALL_CONTROLS (self));
if (self->group == group)
return;
g_clear_object (&self->group);
if (group != NULL)
self->group = g_object_ref (group);
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENTRY_GROUP]);
if (group != NULL)
{
title = bz_entry_group_get_title (group);
developer = bz_entry_group_get_developer (group);
}
if (self->install_button != NULL)
{
g_autofree char *label = NULL;
if (title != NULL && developer != NULL)
label = g_strdup_printf (_ ("Install %s from %s"), title, developer);
else if (title != NULL)
label = g_strdup_printf (_ ("Install %s"), title);
if (label != NULL)
gtk_accessible_update_property (
GTK_ACCESSIBLE (self->install_button),
GTK_ACCESSIBLE_PROPERTY_LABEL,
label,
-1);
}
update_tracker (self);
}
BzStateInfo *
bz_install_controls_get_state (BzInstallControls *self)
{
g_return_val_if_fail (BZ_IS_INSTALL_CONTROLS (self), NULL);
return self->state;
}
void
bz_install_controls_set_state (BzInstallControls *self,
BzStateInfo *state)
{
g_return_if_fail (BZ_IS_INSTALL_CONTROLS (self));
if (self->all_trackers != NULL)
{
g_signal_handlers_disconnect_by_func (self->all_trackers, on_all_trackers_changed, self);
g_clear_object (&self->all_trackers);
}
g_set_object (&self->state, state);
if (state != NULL)
{
BzTransactionManager *mgr = NULL;
mgr = bz_state_info_get_transaction_manager (state);
if (mgr != NULL)
g_object_get (mgr, "all-trackers", &self->all_trackers, NULL);
if (self->all_trackers != NULL)
g_signal_connect (self->all_trackers, "items-changed",
G_CALLBACK (on_all_trackers_changed), self);
}
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STATE]);
update_tracker (self);
}
void
bz_install_controls_grab_focus (BzInstallControls *self)
{
g_return_if_fail (BZ_IS_INSTALL_CONTROLS (self));
if (!(gtk_widget_is_visible (GTK_WIDGET (self)) &&
self->group != NULL && (bz_entry_group_get_removable (self->group) > 0 || bz_entry_group_get_installable (self->group) > 0)))
return;
g_idle_add_full (
G_PRIORITY_DEFAULT_IDLE,
(GSourceFunc) idle_grab_focus,
bz_track_weak (self), bz_weak_release);
}
static void
pride_flag_changed (BzInstallControls *self,
const char *key,
GSettings *settings)
{
ensure_draw_css (self);
}
static void
ensure_draw_css (BzInstallControls *self)
{
if (self->settings != NULL)
{
g_autofree char *id = NULL;
g_autofree char *final_id = NULL;
g_autofree char *class = NULL;
id = g_settings_get_string (self->settings, "global-progress-bar-theme");
if (g_strcmp0 (id, "accent-color") != 0)
final_id = g_strdup_printf ("%s-horizontal", id);
else
final_id = g_strdup (id);
class = bz_dup_css_class_for_pride_id (final_id);
if (self->pride_class != NULL &&
g_strcmp0 (self->pride_class, class) == 0)
return;
if (self->pride_class != NULL)
gtk_widget_remove_css_class (self->animated_button, self->pride_class);
g_clear_pointer (&self->pride_class, g_free);
gtk_widget_add_css_class (self->animated_button, class);
self->pride_class = g_steal_pointer (&class);
}
else
{
if (self->pride_class != NULL)
gtk_widget_remove_css_class (self->animated_button, self->pride_class);
g_clear_pointer (&self->pride_class, g_free);
}
}