🏡 index : quine-global-labs/bazaar.git

/* bz-install-controls.c
 *
 * 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;

  /* Template widgets */
  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);
    }
}