*
* Copyright 2025 Adam Masciola
*
* 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 <bzvala.h>
#include <glib/gi18n.h>
#include "bz-addons-dialog.h"
#include "bz-application.h"
#include "bz-bulk-install-dialog.h"
#include "bz-curated-view.h"
#include "bz-entry-group-util.h"
#include "bz-entry-group.h"
#include "bz-flathub-page.h"
#include "bz-flatpak-entry.h"
#include "bz-full-view.h"
#include "bz-library-page.h"
#include "bz-progress-bar.h"
#include "bz-screenshot-page.h"
#include "bz-search-bar.h"
#include "bz-search-page.h"
#include "bz-transaction-dialog.h"
#include "bz-transaction-manager.h"
#include "bz-update-history-data-point.h"
#include "bz-update-history-dialog.h"
#include "bz-update-info.h"
#include "bz-user-data-page.h"
#include "bz-window.h"
#include "env.h"
#include "error.h"
#include "hooks.h"
#include "io.h"
#include "template-callbacks.h"
#include "util.h"
struct _BzWindow
{
AdwApplicationWindow parent_instance;
BzStateInfo *state;
GtkEventController *key_controller;
BzScreenshotPage *screenshot_page;
gboolean breakpoint_applied;
AdwNavigationView *navigation_view;
BzSearchPage *search_page;
BzLibraryPage *library_page;
AdwToastOverlay *toasts;
AdwViewStack *main_view_stack;
GtkStack *main_stack;
GtkOverlay *window_overlay;
GtkDropTarget *drop_target;
};
G_DEFINE_FINAL_TYPE (BzWindow, bz_window, ADW_TYPE_APPLICATION_WINDOW)
enum
{
PROP_0,
PROP_STATE,
PROP_COMPACT,
LAST_PROP
};
static GParamSpec *props[LAST_PROP] = { 0 };
BZ_DEFINE_DATA (
transact,
Transact,
{
GWeakRef *self;
BzEntry *entry;
BzEntryGroup *group;
gboolean remove;
gboolean auto_confirm;
GtkWidget *source;
},
BZ_RELEASE_DATA (self, bz_weak_release);
BZ_RELEASE_DATA (entry, g_object_unref);
BZ_RELEASE_DATA (group, g_object_unref);
BZ_RELEASE_DATA (source, g_object_unref))
static DexFuture *
update_fiber (BzWeakRef *wr,
GListModel *entries);
static DexFuture *
transact_fiber (TransactData *data);
BZ_DEFINE_DATA (
bulk_install,
BulkInstall,
{
GWeakRef *self;
GListModel *groups;
},
BZ_RELEASE_DATA (self, bz_weak_release);
BZ_RELEASE_DATA (groups, g_object_unref))
static DexFuture *
bulk_install_fiber (BulkInstallData *data);
static DexFuture *
transact (BzEntry *entry,
gboolean remove,
GtkWidget *source);
static void
try_transact (BzWindow *self,
BzEntry *entry,
BzEntryGroup *group,
gboolean remove,
gboolean auto_confirm,
GtkWidget *source);
static void
search (BzWindow *self,
const char *text);
static void
bulk_install (BzWindow *self,
BzEntry **installs,
guint n_installs);
static void
set_page (BzWindow *self);
static BzFullView *
create_full_view (BzWindow *self,
BzEntryGroup *group);
static void
emit_hook_disown (BzWindow *self,
BzHookSignal signal,
BzEntryGroup *group);
static void
bz_window_dispose (GObject *object)
{
BzWindow *self = BZ_WINDOW (object);
g_clear_object (&self->state);
G_OBJECT_CLASS (bz_window_parent_class)->dispose (object);
}
static void
bz_window_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
BzWindow *self = BZ_WINDOW (object);
switch (prop_id)
{
case PROP_STATE:
g_value_set_object (value, self->state);
break;
case PROP_COMPACT:
g_value_set_boolean (value, self->breakpoint_applied);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
bz_window_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
switch (prop_id)
{
case PROP_STATE:
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static char *
list_length (gpointer object,
GListModel *model)
{
if (model == NULL)
return g_strdup (0);
return g_strdup_printf ("%u", g_list_model_get_n_items (model));
}
static void
update_cb (BzWindow *self,
GListModel *entries,
GtkWidget *widget)
{
g_autoptr (BzWeakRef) wr = NULL;
if (g_list_model_get_n_items (entries) == 0)
return;
wr = bz_weak_ref_new (self);
dex_future_disown (dex_scheduler_spawnv (
dex_scheduler_get_default (), bz_get_dex_stack_size (),
G_CALLBACK (update_fiber),
2, BZ_TYPE_WEAK_REF, wr, G_TYPE_OBJECT, entries));
}
static DexFuture *
update_fiber (BzWeakRef *wr,
GListModel *entries)
{
g_autoptr (BzWindow) self = NULL;
g_autoptr (BzTransaction) transaction = NULL;
g_autoptr (GPtrArray) updates = NULL;
g_autoptr (GHashTable) update_ids = NULL;
GListModel *available_updates = NULL;
g_autoptr (GPtrArray) permission_infos = NULL;
guint n_updates = 0;
bz_weak_get_or_return_reject (self, &wr->ref);
n_updates = g_list_model_get_n_items (entries);
updates = g_ptr_array_new_full (n_updates, g_object_unref);
update_ids = g_hash_table_new (g_str_hash, g_str_equal);
for (guint i = 0; i < n_updates; i++)
{
BzEntry *entry = NULL;
entry = g_list_model_get_item (entries, i);
g_ptr_array_add (updates, entry);
g_hash_table_add (update_ids, (gpointer) bz_entry_get_id (entry));
}
available_updates = bz_state_info_get_available_updates (self->state);
permission_infos = g_ptr_array_new_with_free_func (g_object_unref);
if (available_updates != NULL)
{
guint n_available = 0;
n_available = g_list_model_get_n_items (available_updates);
for (guint i = 0; i < n_available; i++)
{
g_autoptr (BzUpdatePermissionInfo) info = NULL;
BzEntry *info_entry = NULL;
const char *info_id = NULL;
BzAppPermissions *delta = NULL;
info = g_list_model_get_item (available_updates, i);
info_entry = bz_update_permission_info_get_entry (info);
info_id = bz_entry_get_id (info_entry);
delta = bz_update_permission_info_get_additional_permissions (info);
if (delta == NULL || bz_app_permissions_is_empty (delta))
continue;
if (g_hash_table_contains (update_ids, info_id))
g_ptr_array_add (permission_infos, g_object_ref (info));
}
}
if (permission_infos->len > 0)
{
gboolean confirmed = FALSE;
confirmed = dex_await_boolean (
bz_new_permissions_dialog_present_and_wait (
GTK_WIDGET (self),
(BzUpdatePermissionInfo **) permission_infos->pdata,
permission_infos->len),
NULL);
if (!confirmed)
return dex_future_new_false ();
}
transaction = bz_transaction_new_full (NULL, 0, (BzEntry **) updates->pdata,
updates->len, NULL, 0);
dex_future_disown (bz_transaction_manager_add (
bz_state_info_get_transaction_manager (self->state),
transaction));
if (G_IS_LIST_STORE (available_updates))
{
GListStore *store = NULL;
guint n_available = 0;
store = G_LIST_STORE (available_updates);
n_available = g_list_model_get_n_items (available_updates);
for (guint i = n_available; i > 0; i--)
{
g_autoptr (BzUpdatePermissionInfo) available_info = NULL;
const char *available_id = NULL;
available_info = g_list_model_get_item (available_updates, i - 1);
available_id = bz_entry_get_id (bz_update_permission_info_get_entry (available_info));
if (g_hash_table_contains (update_ids, available_id))
g_list_store_remove (store, i - 1);
}
}
g_object_notify (G_OBJECT (self->state), "available-updates");
return dex_future_new_true ();
}
static void
page_toggled_cb (BzWindow *self,
GParamSpec *pspec,
AdwToggleGroup *toggles)
{
set_page (self);
}
static void
browse_flathub_cb (BzWindow *self,
BzCuratedView *widget)
{
adw_view_stack_set_visible_child_name (self->main_view_stack, "flathub");
}
static void
open_search_cb (BzWindow *self,
BzSearchPage *widget)
{
adw_view_stack_set_visible_child_name (self->main_view_stack, "search");
}
static void
breakpoint_apply_cb (BzWindow *self,
AdwBreakpoint *breakpoint)
{
self->breakpoint_applied = TRUE;
gtk_widget_add_css_class (GTK_WIDGET (self), "narrow");
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_COMPACT]);
}
static void
breakpoint_unapply_cb (BzWindow *self,
AdwBreakpoint *breakpoint)
{
self->breakpoint_applied = FALSE;
gtk_widget_remove_css_class (GTK_WIDGET (self), "narrow");
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_COMPACT]);
}
static void
sync_cb (BzWindow *self,
GtkButton *button)
{
g_action_group_activate_action (
G_ACTION_GROUP (g_application_get_default ()),
"sync-remotes", NULL);
}
static void
transactions_clear_cb (BzWindow *self,
GtkButton *button)
{
bz_transaction_manager_clear_finished (
bz_state_info_get_transaction_manager (self->state));
}
static void
action_escape (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
BzWindow *self = BZ_WINDOW (widget);
GListModel *stack = NULL;
guint n_pages = 0;
if (adw_application_window_get_visible_dialog (ADW_APPLICATION_WINDOW (self)) != NULL)
return;
if (self->screenshot_page != NULL)
{
if (!bz_screenshot_page_is_closing (self->screenshot_page))
{
bz_screenshot_page_close (self->screenshot_page);
return;
}
}
stack = adw_navigation_view_get_navigation_stack (self->navigation_view);
n_pages = g_list_model_get_n_items (stack);
adw_navigation_view_pop (self->navigation_view);
if (n_pages <= 2)
set_page (self);
}
static char *
format_progress (gpointer object,
double value)
{
return g_strdup_printf ("%.0f%%", 100.0 * value);
}
static char *
format_title (gpointer object,
const char *title)
{
if (title == NULL || *title == '\0' || g_strcmp0 (title, _ ("Bazaar")) == 0)
return g_strdup (_ ("Bazaar"));
return g_strdup_printf (_ ("Bazaar — %s"), title);
}
static gboolean
drop_accept_cb (BzWindow *self,
GdkDrop *drop,
GtkDropTarget *target)
{
return gdk_content_formats_contain_gtype (gdk_drop_get_formats (drop), G_TYPE_FILE) &&
!bz_state_info_get_busy (self->state) &&
self->screenshot_page == NULL &&
adw_application_window_get_visible_dialog (ADW_APPLICATION_WINDOW (self)) == NULL;
}
static gboolean
drop_cb (BzWindow *self,
const GValue *value,
double x,
double y,
GtkDropTarget *target)
{
GFile *file = NULL;
g_autofree char *uri = NULL;
file = G_FILE (g_value_get_object (value));
uri = g_file_get_uri (file);
gtk_widget_activate_action (GTK_WIDGET (self), "app.open-location", "s", uri);
return TRUE;
}
static BzEntryGroup *
resolve_group_from_parameter (BzWindow *self,
GVariant *parameter,
gboolean *auto_confirm)
{
const char *id = NULL;
g_variant_get (parameter, "(&sb)", &id, auto_confirm);
return bz_application_map_factory_convert_one (
bz_state_info_get_application_factory (self->state),
gtk_string_object_new (id));
}
static void
action_install_group (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
BzWindow *self = BZ_WINDOW (widget);
g_autoptr (BzEntryGroup) group = NULL;
gboolean auto_confirm = FALSE;
group = resolve_group_from_parameter (self, parameter, &auto_confirm);
if (group != NULL)
try_transact (self, NULL, group, FALSE, auto_confirm, NULL);
}
static void
action_remove_group (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
BzWindow *self = BZ_WINDOW (widget);
g_autoptr (BzEntryGroup) group = NULL;
gboolean auto_confirm = FALSE;
group = resolve_group_from_parameter (self, parameter, &auto_confirm);
if (group != NULL)
try_transact (self, NULL, group, TRUE, auto_confirm, NULL);
}
static void
action_cancel_group (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
BzWindow *self = BZ_WINDOW (widget);
const char *id = NULL;
BzTransactionManager *manager = NULL;
BzBackend *backend = NULL;
GListModel *trackers = NULL;
guint n_items = 0;
id = g_variant_get_string (parameter, NULL);
manager = bz_state_info_get_transaction_manager (self->state);
if (manager == NULL)
return;
backend = bz_state_info_get_backend (self->state);
if (backend == NULL)
return;
trackers = bz_transaction_manager_get_all_trackers (manager);
n_items = g_list_model_get_n_items (trackers);
for (guint i = 0; i < n_items; i++)
{
g_autoptr (BzTransactionEntryTracker) tracker = NULL;
BzEntry *entry = NULL;
const char *entry_id = NULL;
tracker = g_list_model_get_item (trackers, i);
entry = bz_transaction_entry_tracker_get_entry (tracker);
if (entry == NULL)
continue;
entry_id = bz_entry_get_id (entry);
if (g_strcmp0 (entry_id, id) == 0)
{
if (bz_backend_cancel_task_for_entry (backend, entry))
g_object_set (tracker, "status", BZ_TRANSACTION_ENTRY_STATUS_CANCELLED, NULL);
break;
}
}
}
static void
parent_ui_entry_show_group_cb (BzAddonsDialog *dialog,
GParamSpec *pspec,
BzWindow *self)
{
BzEntry *entry = NULL;
g_autoptr (BzEntryGroup) group = NULL;
entry = bz_addons_dialog_get_parent_entry (dialog);
if (entry == NULL)
return;
group = bz_application_map_factory_convert_one (
bz_state_info_get_application_factory (bz_window_get_state_info (self)),
gtk_string_object_new (bz_entry_get_id (entry)));
if (group == NULL)
return;
bz_window_show_group (self, group);
}
static void
action_show_group (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
BzWindow *self = BZ_WINDOW (widget);
const char *id = NULL;
gboolean animate = FALSE;
g_autoptr (BzEntryGroup) group = NULL;
g_variant_get (parameter, "(&sb)", &id, &animate);
group = bz_application_map_factory_convert_one (
bz_state_info_get_application_factory (self->state),
gtk_string_object_new (id));
if (group == NULL)
return;
if (bz_entry_group_is_addon (group))
{
AdwDialog *dialog = NULL;
dialog = bz_addons_dialog_new_single (group);
g_signal_connect_object (dialog, "notify::parent-ui-entry",
G_CALLBACK (parent_ui_entry_show_group_cb),
self, 0);
parent_ui_entry_show_group_cb (BZ_ADDONS_DIALOG (dialog), NULL, self);
adw_dialog_present (dialog, GTK_WIDGET (self));
}
else
{
adw_navigation_view_set_animate_transitions (self->navigation_view, animate);
bz_window_show_group (self, group);
adw_navigation_view_set_animate_transitions (self->navigation_view, TRUE);
}
}
static void
action_addons_group (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
BzWindow *self = BZ_WINDOW (widget);
const char *id = NULL;
g_autoptr (BzEntryGroup) group = NULL;
AdwDialog *addons_dialog = NULL;
id = g_variant_get_string (parameter, NULL);
group = bz_application_map_factory_convert_one (
bz_state_info_get_application_factory (self->state),
gtk_string_object_new (id));
if (group == NULL)
return;
addons_dialog = bz_addons_dialog_new (group);
adw_dialog_present (addons_dialog, GTK_WIDGET (self));
}
static void
action_bulk_install (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
BzWindow *self = BZ_WINDOW (widget);
g_autoptr (GListStore) ids = NULL;
g_autoptr (GListModel) groups = NULL;
GVariantIter iter = { 0 };
const char *id = NULL;
ids = g_list_store_new (GTK_TYPE_STRING_OBJECT);
g_variant_iter_init (&iter, parameter);
while (g_variant_iter_next (&iter, "&s", &id))
{
g_autoptr (GtkStringObject) string = gtk_string_object_new (id);
g_list_store_append (ids, string);
}
groups = bz_application_map_factory_generate (
bz_state_info_get_application_factory (self->state),
G_LIST_MODEL (ids));
if (groups != NULL && g_list_model_get_n_items (groups) > 0)
bz_window_bulk_install (self, groups);
}
static void
action_user_data (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
BzWindow *self = BZ_WINDOW (widget);
AdwNavigationPage *user_data_page = NULL;
user_data_page = ADW_NAVIGATION_PAGE (bz_user_data_page_new (self->state));
adw_navigation_view_push (self->navigation_view, user_data_page);
}
static void
action_open_flathub_page (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
BzWindow *self = BZ_WINDOW (widget);
adw_navigation_view_pop_to_tag (self->navigation_view, "main");
adw_view_stack_set_visible_child_name (self->main_view_stack, "flathub");
}
static void
action_open_library (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
BzWindow *self = BZ_WINDOW (widget);
adw_navigation_view_pop_to_tag (self->navigation_view, "main");
adw_view_stack_set_visible_child_name (self->main_view_stack, "installed");
bz_library_page_reset_search (self->library_page);
}
static DexFuture *
launch_group_fiber (BzEntryGroup *group)
{
g_autoptr (GError) local_error = NULL;
g_autoptr (GListStore) store = NULL;
GtkWidget *window = NULL;
BzStateInfo *state = NULL;
state = bz_state_info_get_default ();
window = GTK_WIDGET (gtk_application_get_active_window (
GTK_APPLICATION (g_application_get_default ())));
store = dex_await_object (
bz_entry_group_dup_all_into_store (group), &local_error);
if (store == NULL)
{
if (window != NULL)
bz_show_error_for_widget (window, _ ("Failed to launch application"), local_error->message);
return dex_future_new_for_error (g_steal_pointer (&local_error));
}
for (guint i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (store)); i++)
{
g_autoptr (BzEntry) entry = NULL;
const char *ref = NULL;
gboolean result = FALSE;
entry = g_list_model_get_item (G_LIST_MODEL (store), i);
if (!BZ_IS_FLATPAK_ENTRY (entry) || !bz_entry_is_installed (entry))
continue;
{
const char *remote_repo_name = NULL;
remote_repo_name = bz_entry_get_remote_repo_name (entry);
if (g_strcmp0 (remote_repo_name, COSMIC_APPLET_REMOTE_NAME) == 0)
{
adw_dialog_present (ADW_DIALOG (bz_cosmic_place_applet_dialog_new ()), window);
return dex_future_new_false ();
}
}
ref = bz_flatpak_entry_get_addon_extension_of_ref (BZ_FLATPAK_ENTRY (entry));
if (ref != NULL)
{
g_auto (GStrv) parts = NULL;
BzApplicationMapFactory *factory = NULL;
g_autoptr (BzEntryGroup) parent = NULL;
parts = g_strsplit (ref, "/", -1);
if (parts[0] != NULL && parts[1] != NULL)
{
factory = bz_state_info_get_application_factory (state);
parent = bz_application_map_factory_convert_one (
factory, gtk_string_object_new (parts[1]));
if (parent != NULL)
return launch_group_fiber (parent);
}
}
result = bz_flatpak_entry_launch (
BZ_FLATPAK_ENTRY (entry),
BZ_FLATPAK_INSTANCE (bz_state_info_get_backend (state)),
&local_error);
if (!result && window != NULL)
bz_show_error_for_widget (window, _ ("Failed to launch application"), local_error->message);
return result ? dex_future_new_true () : dex_future_new_for_error (g_steal_pointer (&local_error));
}
return dex_future_new_false ();
}
static void
action_launch_group (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
BzWindow *self = BZ_WINDOW (widget);
const char *id = NULL;
g_autoptr (BzEntryGroup) group = NULL;
id = g_variant_get_string (parameter, NULL);
group = bz_application_map_factory_convert_one (
bz_state_info_get_application_factory (self->state),
gtk_string_object_new (id));
if (group == NULL)
return;
dex_future_disown (dex_scheduler_spawn (
dex_scheduler_get_default (),
bz_get_dex_stack_size (),
(DexFiberFunc) launch_group_fiber,
g_object_ref (group),
g_object_unref));
}
static void
action_activate_hook_app (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
BzWindow *self = BZ_WINDOW (widget);
const char *id = NULL;
g_autoptr (BzEntryGroup) group = NULL;
id = g_variant_get_string (parameter, NULL);
group = bz_entry_group_new_manual (id, NULL, NULL);
emit_hook_disown (self, BZ_HOOK_SIGNAL_ARTICLE_APP, group);
}
static void
action_open_user_data_folder (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
const char *id = NULL;
g_autofree char *path = NULL;
g_autoptr (GFile) file = NULL;
g_autoptr (GtkFileLauncher) launcher = NULL;
id = g_variant_get_string (parameter, NULL);
if (id == NULL)
return;
path = bz_dup_user_data_path (id);
file = g_file_new_for_path (path);
launcher = gtk_file_launcher_new (file);
gtk_file_launcher_launch (launcher, GTK_WINDOW (widget), NULL, NULL, NULL);
}
static void
open_uri_finish_cb (GObject *source,
GAsyncResult *result,
gpointer user_data)
{
g_autoptr (BzWindow) self = BZ_WINDOW (user_data);
GtkUriLauncher *launcher = GTK_URI_LAUNCHER (source);
g_autoptr (GError) local_error = NULL;
if (!gtk_uri_launcher_launch_finish (launcher, result, &local_error) &&
!g_error_matches (local_error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_DISMISSED))
bz_show_error_for_widget (GTK_WIDGET (self), _ ("Could not open link"), local_error->message);
}
static void
action_open_uri (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
BzWindow *self = BZ_WINDOW (widget);
const char *uri = g_variant_get_string (parameter, NULL);
g_autoptr (GtkUriLauncher) launcher = NULL;
if (uri == NULL || *uri == '\0')
return;
launcher = gtk_uri_launcher_new (uri);
gtk_uri_launcher_launch (launcher, GTK_WINDOW (self), NULL, open_uri_finish_cb, g_object_ref (self));
}
static void
bz_window_class_init (BzWindowClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
object_class->dispose = bz_window_dispose;
object_class->get_property = bz_window_get_property;
object_class->set_property = bz_window_set_property;
props[PROP_STATE] =
g_param_spec_object (
"state",
NULL, NULL,
BZ_TYPE_STATE_INFO,
G_PARAM_READABLE);
props[PROP_COMPACT] =
g_param_spec_boolean (
"compact",
NULL, NULL, FALSE,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
g_object_class_install_properties (object_class, LAST_PROP, props);
g_type_ensure (BZ_TYPE_SEARCH_BAR);
g_type_ensure (BZ_TYPE_SEARCH_PAGE);
g_type_ensure (BZ_TYPE_PROGRESS_BAR);
g_type_ensure (BZ_TYPE_CURATED_VIEW);
g_type_ensure (BZ_TYPE_FULL_VIEW);
g_type_ensure (BZ_TYPE_ENTRY);
g_type_ensure (BZ_TYPE_LIBRARY_PAGE);
g_type_ensure (BZ_TYPE_FLATHUB_PAGE);
gtk_widget_class_set_template_from_resource (widget_class, "/io/github/kolunmi/Bazaar/bz-window.ui");
bz_widget_class_bind_all_util_callbacks (widget_class);
gtk_widget_class_bind_template_child (widget_class, BzWindow, navigation_view);
gtk_widget_class_bind_template_child (widget_class, BzWindow, toasts);
gtk_widget_class_bind_template_child (widget_class, BzWindow, search_page);
gtk_widget_class_bind_template_child (widget_class, BzWindow, library_page);
gtk_widget_class_bind_template_child (widget_class, BzWindow, main_view_stack);
gtk_widget_class_bind_template_child (widget_class, BzWindow, main_stack);
gtk_widget_class_bind_template_child (widget_class, BzWindow, window_overlay);
gtk_widget_class_bind_template_child (widget_class, BzWindow, drop_target);
gtk_widget_class_bind_template_callback (widget_class, list_length);
gtk_widget_class_bind_template_callback (widget_class, update_cb);
gtk_widget_class_bind_template_callback (widget_class, page_toggled_cb);
gtk_widget_class_bind_template_callback (widget_class, breakpoint_apply_cb);
gtk_widget_class_bind_template_callback (widget_class, breakpoint_unapply_cb);
gtk_widget_class_bind_template_callback (widget_class, sync_cb);
gtk_widget_class_bind_template_callback (widget_class, transactions_clear_cb);
gtk_widget_class_bind_template_callback (widget_class, browse_flathub_cb);
gtk_widget_class_bind_template_callback (widget_class, open_search_cb);
gtk_widget_class_bind_template_callback (widget_class, format_progress);
gtk_widget_class_bind_template_callback (widget_class, format_title);
gtk_widget_class_bind_template_callback (widget_class, drop_accept_cb);
gtk_widget_class_bind_template_callback (widget_class, drop_cb);
gtk_widget_class_install_action (widget_class, "escape", NULL, action_escape);
gtk_widget_class_install_action (widget_class, "window.user-data", NULL, action_user_data);
gtk_widget_class_install_action (widget_class, "window.open-library", NULL, action_open_library);
gtk_widget_class_install_action (widget_class, "window.open-flathub-page", NULL, action_open_flathub_page);
gtk_widget_class_install_action (widget_class, "window.install-group", "(sb)", action_install_group);
gtk_widget_class_install_action (widget_class, "window.remove-group", "(sb)", action_remove_group);
gtk_widget_class_install_action (widget_class, "window.cancel-group", "s", action_cancel_group);
gtk_widget_class_install_action (widget_class, "window.show-group", "(sb)", action_show_group);
gtk_widget_class_install_action (widget_class, "window.addons-group", "s", action_addons_group);
gtk_widget_class_install_action (widget_class, "window.bulk-install", NULL, action_bulk_install);
gtk_widget_class_install_action (widget_class, "window.launch-group", "s", action_launch_group);
gtk_widget_class_install_action (widget_class, "window.activate-hook-app", "s", action_activate_hook_app);
gtk_widget_class_install_action (widget_class, "window.open-user-data-folder", "s", action_open_user_data_folder);
gtk_widget_class_install_action (widget_class, "window.open-uri", "s", action_open_uri);
gtk_widget_class_add_binding_action (widget_class, GDK_KEY_d, GDK_CONTROL_MASK, "window.open-library", NULL);
gtk_widget_class_add_binding_action (widget_class, GDK_KEY_w, GDK_CONTROL_MASK, "window.close", NULL);
gtk_widget_class_add_binding_action (widget_class, GDK_KEY_e, GDK_CONTROL_MASK, "window.open-flathub-page", NULL);
}
static gboolean
key_pressed (BzWindow *self,
guint keyval,
guint keycode,
GdkModifierType state,
GtkEventControllerKey *controller)
{
gunichar unichar = 0;
char buf[32] = { 0 };
const char *visible_child_name = NULL;
gboolean was_deeper = FALSE;
if (state & ~(GDK_NO_MODIFIER_MASK | GDK_SHIFT_MASK))
return FALSE;
if (self->screenshot_page != NULL)
return FALSE;
unichar = gdk_keyval_to_unicode (keyval);
if (unichar == 0 || !g_unichar_isgraph (unichar))
return FALSE;
g_unichar_to_utf8 (unichar, buf);
was_deeper = g_list_model_get_n_items (
adw_navigation_view_get_navigation_stack (self->navigation_view)) > 1;
adw_navigation_view_pop_to_tag (self->navigation_view, "main");
visible_child_name = adw_view_stack_get_visible_child_name (self->main_view_stack);
if (!was_deeper && g_strcmp0 (visible_child_name, "installed") == 0)
return bz_library_page_ensure_active (self->library_page, buf);
else
{
adw_view_stack_set_visible_child_name (self->main_view_stack, "search");
return bz_search_page_ensure_active (self->search_page, buf);
}
}
static void
bz_window_init (BzWindow *self)
{
gtk_widget_init_template (GTK_WIDGET (self));
#ifdef DEVELOPMENT_BUILD
gtk_widget_add_css_class (GTK_WIDGET (self), "devel");
#endif
adw_view_stack_set_visible_child_name (self->main_view_stack, "flathub");
self->key_controller = gtk_event_controller_key_new ();
g_signal_connect_swapped (self->key_controller,
"key-pressed",
G_CALLBACK (key_pressed),
self);
gtk_widget_add_controller (GTK_WIDGET (self), self->key_controller);
gtk_drop_target_set_gtypes (self->drop_target, (GType[]){ G_TYPE_FILE }, 1);
}
static void
app_busy_changed (BzWindow *self,
GParamSpec *pspec,
BzStateInfo *info)
{
bz_search_page_refresh (self->search_page);
set_page (self);
}
static void
has_inputs_changed (BzWindow *self,
GParamSpec *pspec,
BzContentProvider *provider)
{
if (!bz_content_provider_get_has_inputs (provider) &&
g_strcmp0 (adw_view_stack_get_visible_child_name (self->main_view_stack), "browse") == 0)
adw_view_stack_set_visible_child_name (self->main_view_stack, "flathub");
}
static DexFuture *
transact_fiber (TransactData *data)
{
g_autoptr (GError) local_error = NULL;
g_autoptr (BzEntry) selected_entry = NULL;
g_autoptr (DexFuture) transact_future = NULL;
g_autofree char *id_dup = NULL;
BzMainConfig *config = NULL;
GListModel *hooks = NULL;
gboolean delete_user_data = FALSE;
GdkDisplay *display = NULL;
GdkSeat *seat = NULL;
GdkDevice *keyboard = NULL;
GdkModifierType modifiers = GDK_NO_MODIFIER_MASK;
if (data->group != NULL)
id_dup = g_strdup (bz_entry_group_get_id (data->group));
else
id_dup = g_strdup (bz_entry_get_id (data->entry));
if (data->remove)
{
const char *bazaar_id = NULL;
bazaar_id = g_application_get_application_id (g_application_get_default ());
if (g_strcmp0 (id_dup, bazaar_id) == 0)
{
GtkWidget *window = NULL;
window = GTK_WIDGET (gtk_application_get_active_window (GTK_APPLICATION (g_application_get_default ())));
bz_show_error_for_widget (window, _ ("You can't remove Bazaar from Bazaar!"), _ ("You can't remove Bazaar from Bazaar!"));
return dex_future_new_false ();
}
}
config = bz_state_info_get_main_config (bz_state_info_get_default ());
if (config != NULL)
hooks = bz_main_config_get_hooks (config);
#define RUN_HOOK(_signal) \
G_STMT_START \
{ \
if (hooks != NULL && \
!dex_await ( \
bz_run_hook_emission ( \
hooks, (_signal), \
data->remove \
? BZ_HOOK_TRANSACTION_TYPE_REMOVAL \
: BZ_HOOK_TRANSACTION_TYPE_INSTALL, \
id_dup, NULL), \
&local_error)) \
return dex_future_new_for_error (g_steal_pointer (&local_error)); \
} \
G_STMT_END
RUN_HOOK (BZ_HOOK_SIGNAL_BEFORE_TRANSACTION);
display = gdk_display_get_default ();
seat = gdk_display_get_default_seat (display);
keyboard = gdk_seat_get_keyboard (seat);
if (keyboard != NULL)
modifiers = gdk_device_get_modifier_state (keyboard);
if (modifiers & GDK_SHIFT_MASK)
the first valid entry */
{
if (data->group != NULL)
{
g_autoptr (GListModel) store = NULL;
guint n_items = 0;
store = dex_await_object (
bz_entry_group_dup_all_into_store (data->group),
&local_error);
if (store == NULL)
return dex_future_new_for_error (g_steal_pointer (&local_error));
n_items = g_list_model_get_n_items (store);
for (guint i = 0; i < n_items; i++)
{
g_autoptr (BzEntry) entry = NULL;
gboolean installed = FALSE;
entry = g_list_model_get_item (store, i);
installed = bz_entry_is_installed (entry);
if ((data->remove && installed) ||
(!data->remove && !installed))
{
selected_entry = g_steal_pointer (&entry);
break;
}
}
if (selected_entry == NULL)
return dex_future_new_false ();
}
else
selected_entry = g_object_ref (data->entry);
}
else
{
g_autoptr (BzTransactionDialogResult) dialog_result = NULL;
dialog_result = dex_await_object (
bz_transaction_dialog_show (
GTK_WIDGET (gtk_application_get_active_window (
GTK_APPLICATION (g_application_get_default ()))),
data->entry,
data->group,
data->remove,
data->auto_confirm),
&local_error);
if (dialog_result == NULL)
return dex_future_new_for_error (g_steal_pointer (&local_error));
if (!bz_transaction_dialog_result_get_confirmed (dialog_result))
return dex_future_new_false ();
selected_entry = g_object_ref (
bz_transaction_dialog_result_get_selected_entry (dialog_result));
delete_user_data = bz_transaction_dialog_result_get_delete_user_data (
dialog_result);
}
transact_future = transact (
selected_entry,
data->remove,
data->source);
if (!dex_await (g_steal_pointer (&transact_future), &local_error))
return dex_future_new_for_error (g_steal_pointer (&local_error));
if (delete_user_data)
{
if (data->group != NULL)
bz_entry_group_reap_user_data (data->group);
else
dex_future_disown (bz_reap_user_data_dex (id_dup));
}
RUN_HOOK (BZ_HOOK_SIGNAL_AFTER_TRANSACTION);
#undef RUN_HOOK
return dex_future_new_true ();
}
BzWindow *
bz_window_new (BzStateInfo *state)
{
BzWindow *window = NULL;
BzMainConfig *config = NULL;
g_return_val_if_fail (BZ_IS_STATE_INFO (state), NULL);
window = g_object_new (BZ_TYPE_WINDOW, NULL);
window->state = g_object_ref (state);
config = bz_state_info_get_main_config (state);
if (config != NULL && bz_main_config_get_start_on_curated (config))
{
BzContentProvider *curated_provider = NULL;
curated_provider = bz_state_info_get_curated_provider (state);
if (curated_provider != NULL &&
g_list_model_get_n_items (G_LIST_MODEL (curated_provider)) > 0)
adw_view_stack_set_visible_child_name (window->main_view_stack, "browse");
}
g_signal_connect_object (state,
"notify::busy",
G_CALLBACK (app_busy_changed),
window, G_CONNECT_SWAPPED);
* changes the objects we are connecting to
*/
g_signal_connect_object (bz_state_info_get_curated_provider (state),
"notify::has-inputs",
G_CALLBACK (has_inputs_changed),
window, G_CONNECT_SWAPPED);
g_object_notify_by_pspec (G_OBJECT (window), props[PROP_STATE]);
set_page (window);
return window;
}
void
bz_window_search (BzWindow *self,
const char *text)
{
g_return_if_fail (BZ_IS_WINDOW (self));
search (self, text);
}
void
bz_window_show_entry (BzWindow *self,
BzEntry *entry)
{
g_autoptr (BzEntryGroup) group = NULL;
g_return_if_fail (BZ_IS_WINDOW (self));
g_return_if_fail (BZ_IS_ENTRY (entry));
group = bz_entry_group_new_for_single_entry (entry);
bz_window_show_group (self, group);
}
void
bz_window_install_entry (BzWindow *self,
BzEntry *entry)
{
g_return_if_fail (BZ_IS_WINDOW (self));
g_return_if_fail (BZ_IS_ENTRY (entry));
try_transact (self, entry, NULL, FALSE, FALSE, NULL);
}
static void
full_view_page_hidden_cb (AdwNavigationPage *page,
BzWindow *self)
{
adw_navigation_page_set_child (page, NULL);
}
static void
full_view_page_shown_cb (AdwNavigationPage *page,
BzWindow *self)
{
BzEntryGroup *group = NULL;
GtkWidget *existing = NULL;
BzFullView *full_view = NULL;
existing = adw_navigation_page_get_child (page);
if (BZ_IS_FULL_VIEW (existing))
return;
group = BZ_ENTRY_GROUP (g_object_get_data (G_OBJECT (page), "bz-entry-group"));
if (group == NULL)
return;
full_view = create_full_view (self, group);
adw_navigation_page_set_child (page, GTK_WIDGET (full_view));
}
void
bz_window_show_group (BzWindow *self,
BzEntryGroup *group)
{
BzFullView *full_view = NULL;
AdwNavigationPage *page = NULL;
const char *title = NULL;
g_return_if_fail (BZ_IS_WINDOW (self));
g_return_if_fail (BZ_IS_ENTRY_GROUP (group));
full_view = create_full_view (self, group);
title = bz_entry_group_get_title (group);
page = adw_navigation_page_new (GTK_WIDGET (full_view), title != NULL ? title : "");
g_object_set_data_full (
G_OBJECT (page), "bz-entry-group",
g_object_ref (group), g_object_unref);
g_signal_connect_object (page, "showing", G_CALLBACK (full_view_page_shown_cb), self, 0);
g_signal_connect_object (page, "hidden", G_CALLBACK (full_view_page_hidden_cb), self, 0);
adw_navigation_view_push (self->navigation_view, page);
}
void
bz_window_add_toast (BzWindow *self,
AdwToast *toast)
{
g_return_if_fail (BZ_IS_WINDOW (self));
g_return_if_fail (ADW_IS_TOAST (toast));
adw_toast_overlay_add_toast (self->toasts, toast);
}
void
bz_window_push_page (BzWindow *self, AdwNavigationPage *page)
{
g_return_if_fail (BZ_IS_WINDOW (self));
g_return_if_fail (ADW_IS_NAVIGATION_PAGE (page));
adw_navigation_view_push (self->navigation_view, page);
}
void
bz_window_open_screenshot_page (BzWindow *self,
BzScreenshotPage *page)
{
g_return_if_fail (BZ_IS_WINDOW (self));
g_return_if_fail (BZ_IS_SCREENSHOT_PAGE (page));
if (self->screenshot_page != NULL)
return;
self->screenshot_page = page;
g_signal_connect_swapped (page, "destroy",
G_CALLBACK (g_nullify_pointer),
&self->screenshot_page);
gtk_overlay_add_overlay (self->window_overlay, GTK_WIDGET (page));
}
void
bz_window_bulk_install (BzWindow *self,
GListModel *groups)
{
g_autoptr (BulkInstallData) data = NULL;
g_return_if_fail (BZ_IS_WINDOW (self));
g_return_if_fail (G_IS_LIST_MODEL (groups));
data = bulk_install_data_new ();
data->self = bz_track_weak (self);
data->groups = g_object_ref (groups);
dex_future_disown (dex_scheduler_spawn (
dex_scheduler_get_default (),
bz_get_dex_stack_size (),
(DexFiberFunc) bulk_install_fiber,
bulk_install_data_ref (data),
bulk_install_data_unref));
}
void
bz_window_show_update_history (BzWindow *self,
GVariant *history)
{
g_autoptr (GListStore) store = NULL;
GVariantIter iter = { 0 };
const char *id = NULL;
const char *old_version = NULL;
const char *new_version = NULL;
AdwDialog *dialog = NULL;
g_return_if_fail (BZ_IS_WINDOW (self));
g_return_if_fail (history != NULL);
store = g_list_store_new (BZ_TYPE_UPDATE_HISTORY_DATA_POINT);
g_variant_iter_init (&iter, history);
while (g_variant_iter_next (&iter, "(&s&s&s)", &id, &old_version, &new_version))
{
g_autoptr (BzEntryGroup) group = NULL;
g_autoptr (BzUpdateHistoryDataPoint) point = NULL;
group = bz_application_map_factory_convert_one (
bz_state_info_get_application_factory (self->state),
gtk_string_object_new (id));
point = g_object_new (BZ_TYPE_UPDATE_HISTORY_DATA_POINT,
"group", group,
"old-version", old_version,
"new-version", new_version,
NULL);
g_list_store_append (store, point);
}
dialog = bz_update_history_dialog_new (G_LIST_MODEL (store));
adw_dialog_present (dialog, GTK_WIDGET (self));
}
BzStateInfo *
bz_window_get_state_info (BzWindow *self)
{
g_return_val_if_fail (BZ_IS_WINDOW (self), NULL);
return self->state;
}
static DexFuture *
transact (BzEntry *entry,
gboolean remove,
GtkWidget *source)
{
g_autoptr (BzTransaction) transaction = NULL;
if (remove)
transaction = bz_transaction_new_full (
NULL, 0,
NULL, 0,
&entry, 1);
else
transaction = bz_transaction_new_full (
&entry, 1,
NULL, 0,
NULL, 0);
return bz_transaction_manager_add (
bz_state_info_get_transaction_manager (bz_state_info_get_default ()),
transaction);
}
static void
try_transact (BzWindow *self,
BzEntry *entry,
BzEntryGroup *group,
gboolean remove,
gboolean auto_confirm,
GtkWidget *source)
{
g_autoptr (TransactData) data = NULL;
g_return_if_fail (entry != NULL || group != NULL);
if (bz_state_info_get_busy (self->state))
{
adw_toast_overlay_add_toast (
self->toasts,
adw_toast_new_format (_ ("Can't do that right now!")));
return;
}
data = transact_data_new ();
data->self = bz_track_weak (self);
data->entry = bz_object_maybe_ref (entry);
data->group = bz_object_maybe_ref (group);
data->remove = remove;
data->auto_confirm = auto_confirm;
data->source = bz_object_maybe_ref (source);
dex_future_disown (dex_scheduler_spawn (
dex_scheduler_get_default (),
bz_get_dex_stack_size (),
(DexFiberFunc) transact_fiber,
transact_data_ref (data), transact_data_unref));
}
static void
bulk_install (BzWindow *self,
BzEntry **installs,
guint n_installs)
{
g_autoptr (BzTransaction) transaction = NULL;
g_return_if_fail (BZ_IS_WINDOW (self));
g_return_if_fail (installs != NULL);
g_return_if_fail (n_installs > 0);
if (bz_state_info_get_busy (self->state))
{
adw_toast_overlay_add_toast (
self->toasts,
adw_toast_new_format (_ ("Can't do that right now!")));
return;
}
transaction = bz_transaction_new_full (
installs, n_installs,
NULL, 0,
NULL, 0);
dex_future_disown (bz_transaction_manager_add (
bz_state_info_get_transaction_manager (self->state),
transaction));
}
static DexFuture *
bulk_install_fiber (BulkInstallData *data)
{
g_autoptr (BzWindow) self = NULL;
g_autoptr (GError) local_error = NULL;
g_autoptr (BzBulkInstallDialogResult) result = NULL;
GListModel *entries = NULL;
guint n_installs = 0;
g_autofree BzEntry **installs_buf = NULL;
bz_weak_get_or_return_reject (self, data->self);
result = dex_await_object (
bz_bulk_install_dialog_show (GTK_WIDGET (self), data->groups),
&local_error);
if (result == NULL)
return dex_future_new_for_error (g_steal_pointer (&local_error));
if (!bz_bulk_install_dialog_result_get_confirmed (result))
return dex_future_new_false ();
entries = bz_bulk_install_dialog_result_get_entries (result);
n_installs = g_list_model_get_n_items (entries);
if (n_installs == 0)
return dex_future_new_false ();
installs_buf = g_malloc_n (n_installs, sizeof (*installs_buf));
for (guint i = 0; i < n_installs; i++)
installs_buf[i] = g_list_model_get_item (entries, i);
bulk_install (self, installs_buf, n_installs);
for (guint i = 0; i < n_installs; i++)
g_object_unref (installs_buf[i]);
return dex_future_new_true ();
}
static void
search (BzWindow *self,
const char *initial)
{
if (initial != NULL && *initial != '\0')
bz_search_page_set_text (self->search_page, initial);
adw_view_stack_set_visible_child_name (self->main_view_stack, "search");
adw_navigation_view_pop_to_tag (self->navigation_view, "main");
gtk_widget_grab_focus (GTK_WIDGET (self->search_page));
}
static void
set_page (BzWindow *self)
{
if (self->state == NULL)
return;
if (bz_state_info_get_busy (self->state))
{
gtk_stack_set_visible_child_name (self->main_stack, "loading");
adw_navigation_view_pop_to_tag (self->navigation_view, "main");
}
else
gtk_stack_set_visible_child_name (self->main_stack, "main");
}
static BzFullView *
create_full_view (BzWindow *self,
BzEntryGroup *group)
{
BzFullView *full_view = NULL;
full_view = BZ_FULL_VIEW (bz_full_view_new ());
g_object_set (full_view, "state", self->state, NULL);
bz_full_view_set_entry_group (full_view, group);
g_signal_connect_object (full_view, "update", G_CALLBACK (update_cb), self, G_CONNECT_SWAPPED);
return full_view;
}
static void
emit_hook_disown (BzWindow *self,
BzHookSignal signal,
BzEntryGroup *group)
{
BzMainConfig *config = NULL;
GListModel *hooks = NULL;
if (self->state == NULL)
return;
config = bz_state_info_get_main_config (self->state);
if (config == NULL)
return;
hooks = bz_main_config_get_hooks (config);
if (hooks == NULL)
return;
dex_future_disown (bz_run_hook_emission (
hooks, signal, 0, NULL, group));
}