/* bz-window.c * * 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 . * * SPDX-License-Identifier: GPL-3.0-or-later */ // This file is an utter mess #include "config.h" #include #include #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; /* Template widgets */ 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) { // BzWindow *self = BZ_WINDOW (object); 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) { // Translators: Please treat "Bazaar" as a brand name and avoid translating. if (title == NULL || *title == '\0' || g_strcmp0 (title, _ ("Bazaar")) == 0) return g_strdup (_ ("Bazaar")); /* Translators: Please treat "Bazaar" as a brand name and avoid translating. %s is the title of the current page. */ 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; /* Ignore if this is a modifier-shortcut of some sort */ 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; // Get ID early before any async operations 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)); /* Prevent Bazaar from being removed by itself */ 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 ()))); // Translators: Please treat "Bazaar" as a brand name and avoid translating. 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) /* Holding shift while invoking a transaction skips the dialog and assumes 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; // Show the dialog 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); } // Perform the transaction 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)); // Handle user data deletion 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); /* these seem unsafe but BzApplication never * 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)); }