gnome-control-center/shell/cc-window.c
Nelson Benítez León 3f3409cbc1 shell: restrict panel centering to specific cases
commit 02302c9b08 implemented vertical centering of
the panel being activated, but we should restrict
that to only happen when the activation is from the
Search view or from the set_active_panel_from_id()
CcShell iface, which covers the case of panel activated
from commandline parameter or from other panels, but
does not include activating panels manually by mouse
or keyboard which is the problematic case explained
in issue #2506

Closes #2506
2023-08-30 08:49:12 +00:00

816 lines
24 KiB
C

/*
* Copyright (c) 2009, 2010 Intel, Inc.
* Copyright (c) 2010 Red Hat, Inc.
* Copyright (c) 2016 Endless, Inc.
*
* The Control Center is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*
* The Control Center 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 the Control Center; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* Author: Thomas Wood <thos@gnome.org>
*/
#define G_LOG_DOMAIN "cc-window"
#include <config.h>
#include "cc-log.h"
#include "cc-window.h"
#include <glib/gi18n.h>
#include <gio/gio.h>
#include <gio/gdesktopappinfo.h>
#include <gtk/gtk.h>
#include <string.h>
#include <time.h>
#include "cc-application.h"
#include "cc-panel.h"
#include "cc-shell.h"
#include "cc-shell-model.h"
#include "cc-panel-list.h"
#include "cc-panel-loader.h"
#include "cc-util.h"
#define MOUSE_BACK_BUTTON 8
#define DEFAULT_WINDOW_ICON_NAME "gnome-control-center"
struct _CcWindow
{
AdwApplicationWindow parent;
GtkMessageDialog *development_warning_dialog;
AdwHeaderBar *header;
AdwNavigationSplitView *split_view;
AdwNavigationView *sidebar_view;
AdwNavigationPage *main_sidebar_page;
CcPanelList *panel_list;
GtkSearchBar *search_bar;
GtkToggleButton *search_button;
GtkSearchEntry *search_entry;
AdwWindowTitle *sidebar_title_widget;
GtkWidget *old_panel;
GtkWidget *current_panel;
char *current_panel_id;
GQueue *previous_panels;
GtkWidget *custom_titlebar;
CcShellModel *store;
CcPanel *active_panel;
GSettings *settings;
CcPanelListView previous_list_view;
};
static void cc_shell_iface_init (CcShellInterface *iface);
G_DEFINE_TYPE_WITH_CODE (CcWindow, cc_window, ADW_TYPE_APPLICATION_WINDOW,
G_IMPLEMENT_INTERFACE (CC_TYPE_SHELL, cc_shell_iface_init))
enum
{
PROP_0,
PROP_ACTIVE_PANEL,
PROP_MODEL,
PROP_COLLAPSED,
};
/* Auxiliary methods */
static void
load_window_state (CcWindow *self)
{
gint current_width = -1;
gint current_height = -1;
gboolean maximized = FALSE;
g_settings_get (self->settings,
"window-state",
"(iib)",
&current_width,
&current_height,
&maximized);
if (current_width != -1 && current_height != -1)
gtk_window_set_default_size (GTK_WINDOW (self), current_width, current_height);
if (maximized)
gtk_window_maximize (GTK_WINDOW (self));
}
static gboolean
in_flatpak_sandbox (void)
{
return g_strcmp0 (PROFILE, "development") == 0;
}
static void
on_sidebar_activated_cb (CcWindow *self)
{
adw_navigation_split_view_set_show_content (self->split_view, TRUE);
}
static gboolean
activate_panel (CcWindow *self,
const gchar *id,
GVariant *parameters,
const gchar *name,
GIcon *gicon,
CcPanelVisibility visibility)
{
g_autoptr(GTimer) timer = NULL;
gdouble ellapsed_time;
CC_ENTRY;
if (!id)
CC_RETURN (FALSE);
if (visibility == CC_PANEL_HIDDEN)
CC_RETURN (FALSE);
timer = g_timer_new ();
/* Begin the profile */
g_timer_start (timer);
if (self->current_panel)
g_signal_handlers_disconnect_by_data (self->current_panel, self);
self->current_panel = GTK_WIDGET (cc_panel_loader_load_by_name (CC_SHELL (self), id, name, parameters));
cc_shell_set_active_panel (CC_SHELL (self), CC_PANEL (self->current_panel));
adw_navigation_split_view_set_content (self->split_view, ADW_NAVIGATION_PAGE (self->current_panel));
/* Ensure we show the panel when the split view is collapsed and a sidebar
* widget's row is activated.
*/
g_signal_connect_object (self->current_panel, "sidebar-activated", G_CALLBACK (on_sidebar_activated_cb), self, G_CONNECT_SWAPPED);
/* Finish profiling */
g_timer_stop (timer);
ellapsed_time = g_timer_elapsed (timer, NULL);
g_debug ("Time to open panel '%s': %lfs", name, ellapsed_time);
g_settings_set_string (self->settings, "last-panel", id);
CC_RETURN (TRUE);
}
static void
add_current_panel_to_history (CcWindow *self,
const char *start_id)
{
g_return_if_fail (start_id != NULL);
if (!self->current_panel_id || g_strcmp0 (self->current_panel_id, start_id) == 0)
return;
g_queue_push_head (self->previous_panels, g_strdup (self->current_panel_id));
g_debug ("Added '%s' to the previous panels", self->current_panel_id);
}
static gboolean
find_iter_for_panel_id (CcWindow *self,
const gchar *panel_id,
GtkTreeIter *out_iter)
{
GtkTreeIter iter;
gboolean valid;
valid = gtk_tree_model_get_iter_first (GTK_TREE_MODEL (self->store), &iter);
while (valid)
{
g_autofree gchar *id = NULL;
gtk_tree_model_get (GTK_TREE_MODEL (self->store),
&iter,
COL_ID, &id,
-1);
if (g_strcmp0 (id, panel_id) == 0)
break;
valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (self->store), &iter);
}
g_assert (out_iter != NULL);
*out_iter = iter;
return valid;
}
static void
on_row_changed_cb (CcWindow *self,
GtkTreePath *path,
GtkTreeIter *iter,
GtkTreeModel *model)
{
g_autofree gchar *id = NULL;
CcPanelVisibility visibility;
gtk_tree_model_get (model, iter,
COL_ID, &id,
COL_VISIBILITY, &visibility,
-1);
cc_panel_list_set_panel_visibility (self->panel_list, id, visibility);
}
static void
setup_model (CcWindow *self)
{
GtkTreeModel *model;
GtkTreeIter iter;
gboolean valid;
/* CcApplication must have a valid model at this point */
g_assert (self->store != NULL);
model = GTK_TREE_MODEL (self->store);
cc_panel_loader_fill_model (self->store);
/* Create a row for each panel */
valid = gtk_tree_model_get_iter_first (model, &iter);
while (valid)
{
CcPanelCategory category;
g_autoptr(GIcon) icon = NULL;
g_autofree gchar *name = NULL;
g_autofree gchar *description = NULL;
g_autofree gchar *id = NULL;
g_auto(GStrv) keywords = NULL;
CcPanelVisibility visibility;
gboolean has_sidebar;
const gchar *icon_name = NULL;
gtk_tree_model_get (model, &iter,
COL_CATEGORY, &category,
COL_DESCRIPTION, &description,
COL_GICON, &icon,
COL_ID, &id,
COL_NAME, &name,
COL_KEYWORDS, &keywords,
COL_VISIBILITY, &visibility,
COL_HAS_SIDEBAR, &has_sidebar,
-1);
if (G_IS_THEMED_ICON (icon))
icon_name = g_themed_icon_get_names (G_THEMED_ICON (icon))[0];
cc_panel_list_add_panel (self->panel_list,
category,
id,
name,
description,
keywords,
icon_name,
visibility,
has_sidebar);
valid = gtk_tree_model_iter_next (model, &iter);
}
/* React to visibility changes */
g_signal_connect_object (model, "row-changed", G_CALLBACK (on_row_changed_cb), self, G_CONNECT_SWAPPED);
}
static gboolean
set_active_panel_from_id (CcWindow *self,
const gchar *start_id,
GVariant *parameters,
gboolean add_to_history,
gboolean force_moving_to_the_panel,
GError **error)
{
g_autoptr(GIcon) gicon = NULL;
g_autofree gchar *name = NULL;
CcPanelVisibility visibility;
GtkTreeIter iter;
CcPanelListView view;
gboolean activated;
gboolean found;
CC_ENTRY;
view = cc_panel_list_get_view (self->panel_list);
/* When loading the same panel again, just set its parameters */
if (g_strcmp0 (self->current_panel_id, start_id) == 0)
{
AdwNavigationPage *sidebar_widget;
sidebar_widget = cc_panel_get_sidebar_widget (CC_PANEL (self->current_panel));
if (sidebar_widget)
{
adw_navigation_view_push (self->sidebar_view, sidebar_widget);
CC_RETURN (TRUE);
}
g_object_set (G_OBJECT (self->current_panel), "parameters", parameters, NULL);
if (force_moving_to_the_panel || self->previous_list_view == view)
adw_navigation_split_view_set_show_content (self->split_view, TRUE);
self->previous_list_view = view;
CC_RETURN (TRUE);
}
found = find_iter_for_panel_id (self, start_id, &iter);
if (!found)
{
g_warning ("Could not find settings panel \"%s\"", start_id);
CC_RETURN (TRUE);
}
self->old_panel = self->current_panel;
if (self->old_panel)
cc_panel_deactivate (CC_PANEL (self->old_panel));
gtk_tree_model_get (GTK_TREE_MODEL (self->store),
&iter,
COL_NAME, &name,
COL_GICON, &gicon,
COL_VISIBILITY, &visibility,
-1);
/* Activate the panel */
activated = activate_panel (self, start_id, parameters, name, gicon, visibility);
/* Failed to activate the panel for some reason, let's keep the old
* panel around instead */
if (!activated)
{
g_debug ("Failed to activate panel");
CC_RETURN (TRUE);
}
if (add_to_history)
add_current_panel_to_history (self, start_id);
if (force_moving_to_the_panel)
adw_navigation_split_view_set_show_content (self->split_view, TRUE);
g_free (self->current_panel_id);
self->current_panel_id = g_strdup (start_id);
CC_TRACE_MSG ("Current panel id: %s", start_id);
cc_panel_list_set_active_panel (self->panel_list, start_id);
CC_RETURN (TRUE);
}
static void
set_active_panel (CcWindow *self,
CcPanel *panel)
{
g_return_if_fail (CC_IS_SHELL (self));
g_return_if_fail (panel == NULL || CC_IS_PANEL (panel));
if (panel != self->active_panel)
{
/* remove the old panel */
g_clear_object (&self->active_panel);
/* set the new panel */
if (panel)
self->active_panel = g_object_ref (panel);
g_object_notify (G_OBJECT (self), "active-panel");
}
}
static void
switch_to_previous_panel (CcWindow *self)
{
g_autofree gchar *previous_panel_id = NULL;
CC_ENTRY;
if (g_queue_get_length (self->previous_panels) == 0)
CC_RETURN ();
previous_panel_id = g_queue_pop_head (self->previous_panels);
g_debug ("Going to previous panel (%s)", previous_panel_id);
set_active_panel_from_id (self, previous_panel_id, NULL, FALSE, FALSE, NULL);
CC_EXIT;
}
/* Callbacks */
static void
on_split_view_collapsed_changed_cb (CcWindow *self)
{
GtkSelectionMode selection_mode;
gboolean collapsed;
g_assert (CC_IS_WINDOW (self));
collapsed = adw_navigation_split_view_get_collapsed (self->split_view);
selection_mode = collapsed ? GTK_SELECTION_NONE : GTK_SELECTION_SINGLE;
cc_panel_list_set_selection_mode (self->panel_list, selection_mode);
if (collapsed && self->current_panel && adw_navigation_view_get_visible_page (self->sidebar_view) == self->main_sidebar_page)
{
AdwNavigationPage *sidebar_widget;
sidebar_widget = cc_panel_get_sidebar_widget (CC_PANEL (self->current_panel));
if (sidebar_widget)
adw_navigation_view_push (self->sidebar_view, sidebar_widget);
}
g_object_notify (G_OBJECT (self), "collapsed");
}
static void
show_panel_cb (CcWindow *self,
const gchar *panel_id)
{
if (!panel_id)
return;
set_active_panel_from_id (self, panel_id, NULL, TRUE, FALSE, NULL);
}
static void
search_entry_activate_cb (CcWindow *self)
{
gboolean changed;
if (cc_panel_list_get_view (self->panel_list) != CC_PANEL_LIST_SEARCH)
return;
changed = cc_panel_list_activate (self->panel_list);
gtk_search_bar_set_search_mode (self->search_bar, !changed);
}
static gboolean
go_back_shortcut_cb (GtkWidget *widget,
GVariant *args,
gpointer user_data)
{
g_debug ("Going to previous panel");
switch_to_previous_panel (CC_WINDOW (widget));
return GDK_EVENT_STOP;
}
static gboolean
search_shortcut_cb (GtkWidget *widget,
GVariant *args,
gpointer user_data)
{
CcPanelListView view;
CcWindow *self;
gboolean search;
self = CC_WINDOW (widget);
view = cc_panel_list_get_view (self->panel_list);
/* The search only happens when we're in the MAIN view */
if (view != CC_PANEL_LIST_MAIN && view != CC_PANEL_LIST_SEARCH)
return GDK_EVENT_PROPAGATE;
search = !gtk_search_bar_get_search_mode (self->search_bar);
gtk_search_bar_set_search_mode (self->search_bar, search);
if (search)
gtk_widget_grab_focus (GTK_WIDGET (self->search_entry));
return GDK_EVENT_STOP;
}
static void
on_development_warning_dialog_responded_cb (CcWindow *self)
{
g_debug ("Disabling development build warning dialog");
g_settings_set_boolean (self->settings, "show-development-warning", FALSE);
gtk_window_close (GTK_WINDOW (self->development_warning_dialog));
}
/* CcShell implementation */
static gboolean
cc_window_set_active_panel_from_id (CcShell *shell,
const gchar *start_id,
GVariant *parameters,
GError **error)
{
CcWindow *self = CC_WINDOW (shell);
g_return_val_if_fail (self != NULL, FALSE);
cc_panel_list_center_activated_row (self->panel_list, TRUE);
return set_active_panel_from_id (self, start_id, parameters, TRUE, TRUE, error);
}
static GtkWidget *
cc_window_get_toplevel (CcShell *self)
{
return GTK_WIDGET (self);
}
static void
cc_shell_iface_init (CcShellInterface *iface)
{
iface->set_active_panel_from_id = cc_window_set_active_panel_from_id;
iface->get_toplevel = cc_window_get_toplevel;
}
/* GtkWidget overrides */
static void
cc_window_map (GtkWidget *widget)
{
CcWindow *self = (CcWindow *) widget;
GTK_WIDGET_CLASS (cc_window_parent_class)->map (widget);
/* Show a warning for Flatpak builds */
if (in_flatpak_sandbox () && g_settings_get_boolean (self->settings, "show-development-warning"))
gtk_window_present (GTK_WINDOW (self->development_warning_dialog));
}
static void
cc_window_unmap (GtkWidget *widget)
{
CcWindow *self = CC_WINDOW (widget);
gboolean maximized;
gint height;
gint width;
maximized = gtk_window_is_maximized (GTK_WINDOW (self));
gtk_window_get_default_size (GTK_WINDOW (self), &width, &height);
g_settings_set (self->settings,
"window-state",
"(iib)",
width,
height,
maximized);
GTK_WIDGET_CLASS (cc_window_parent_class)->unmap (widget);
}
/* GObject Implementation */
static void
cc_window_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
CcWindow *self = CC_WINDOW (object);
switch (property_id)
{
case PROP_ACTIVE_PANEL:
g_value_set_object (value, self->active_panel);
break;
case PROP_MODEL:
g_value_set_object (value, self->store);
break;
case PROP_COLLAPSED:
g_value_set_boolean (value, adw_navigation_split_view_get_collapsed (self->split_view));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
}
static void
cc_window_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
CcWindow *self = CC_WINDOW (object);
switch (property_id)
{
case PROP_ACTIVE_PANEL:
set_active_panel (self, g_value_get_object (value));
break;
case PROP_MODEL:
g_assert (self->store == NULL);
self->store = g_value_dup_object (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
}
static void
maybe_load_last_panel (CcWindow *self)
{
g_autofree char *id = NULL;
id = g_settings_get_string (self->settings, "last-panel");
if (cc_panel_list_get_current_panel (self->panel_list))
return;
/* select the last used panel, if any, or the first visible panel */
if (id != NULL && cc_shell_model_has_panel (self->store, id))
{
cc_panel_list_center_activated_row (self->panel_list, TRUE);
cc_panel_list_set_active_panel (self->panel_list, id);
}
else
cc_panel_list_activate (self->panel_list);
}
static void
cc_window_constructed (GObject *object)
{
CcWindow *self = CC_WINDOW (object);
load_window_state (self);
/* Add the panels */
setup_model (self);
/* After everything is loaded, select the last used panel, if any,
* or the first visible panel. We do that in an idle handler so we
* have a chance to skip it when another panel has been explicitly
* activated from commandline parameter or from DBus method */
g_idle_add_once ((GSourceOnceFunc) maybe_load_last_panel, self);
G_OBJECT_CLASS (cc_window_parent_class)->constructed (object);
}
static void
cc_window_dispose (GObject *object)
{
CcWindow *self = CC_WINDOW (object);
g_clear_pointer (&self->current_panel_id, g_free);
g_clear_object (&self->store);
g_clear_object (&self->active_panel);
G_OBJECT_CLASS (cc_window_parent_class)->dispose (object);
}
static void
cc_window_finalize (GObject *object)
{
CcWindow *self = CC_WINDOW (object);
if (self->previous_panels)
{
g_queue_free_full (self->previous_panels, g_free);
self->previous_panels = NULL;
}
g_clear_object (&self->settings);
G_OBJECT_CLASS (cc_window_parent_class)->finalize (object);
}
static gboolean
search_entry_key_pressed_cb (CcWindow *self,
guint keyval,
guint keycode,
GdkModifierType state,
GtkEventControllerKey *key_controller)
{
GtkWidget *toplevel;
/* When pressing Arrow Down on the entry we move focus to match results list */
if (keyval == GDK_KEY_Down || keyval == GDK_KEY_KP_Down)
{
toplevel = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (self)));
if (!toplevel)
return FALSE;
return gtk_widget_child_focus (toplevel, GTK_DIR_TAB_FORWARD);
}
return FALSE;
}
static void
cc_window_class_init (CcWindowClass *klass)
{
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->get_property = cc_window_get_property;
object_class->set_property = cc_window_set_property;
object_class->constructed = cc_window_constructed;
object_class->dispose = cc_window_dispose;
object_class->finalize = cc_window_finalize;
widget_class->map = cc_window_map;
widget_class->unmap = cc_window_unmap;
g_object_class_override_property (object_class, PROP_ACTIVE_PANEL, "active-panel");
g_object_class_install_property (object_class,
PROP_MODEL,
g_param_spec_object ("model",
"Model",
"The CcShellModel of this application",
CC_TYPE_SHELL_MODEL,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class,
PROP_COLLAPSED,
g_param_spec_boolean ("collapsed",
"Collapsed",
"Whether the window is collapsed",
FALSE,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Settings/gtk/cc-window.ui");
gtk_widget_class_bind_template_child (widget_class, CcWindow, development_warning_dialog);
gtk_widget_class_bind_template_child (widget_class, CcWindow, header);
gtk_widget_class_bind_template_child (widget_class, CcWindow, split_view);
gtk_widget_class_bind_template_child (widget_class, CcWindow, sidebar_view);
gtk_widget_class_bind_template_child (widget_class, CcWindow, main_sidebar_page);
gtk_widget_class_bind_template_child (widget_class, CcWindow, panel_list);
gtk_widget_class_bind_template_child (widget_class, CcWindow, search_bar);
gtk_widget_class_bind_template_child (widget_class, CcWindow, search_button);
gtk_widget_class_bind_template_child (widget_class, CcWindow, search_entry);
gtk_widget_class_bind_template_child (widget_class, CcWindow, sidebar_title_widget);
gtk_widget_class_bind_template_callback (widget_class, on_split_view_collapsed_changed_cb);
gtk_widget_class_bind_template_callback (widget_class, on_development_warning_dialog_responded_cb);
gtk_widget_class_bind_template_callback (widget_class, search_entry_activate_cb);
gtk_widget_class_bind_template_callback (widget_class, show_panel_cb);
gtk_widget_class_bind_template_callback (widget_class, search_entry_key_pressed_cb);
gtk_widget_class_add_binding (widget_class, GDK_KEY_Left, GDK_ALT_MASK, go_back_shortcut_cb, NULL);
gtk_widget_class_add_binding (widget_class, GDK_KEY_f, GDK_CONTROL_MASK, search_shortcut_cb, NULL);
gtk_widget_class_add_binding (widget_class, GDK_KEY_F, GDK_CONTROL_MASK, search_shortcut_cb, NULL);
gtk_widget_class_add_binding_action (widget_class, GDK_KEY_q, GDK_CONTROL_MASK, "window.close", NULL);
gtk_widget_class_add_binding_action (widget_class, GDK_KEY_Q, GDK_CONTROL_MASK, "window.close", 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_W, GDK_CONTROL_MASK, "window.close", NULL);
g_type_ensure (CC_TYPE_PANEL_LIST);
}
static void
cc_window_init (CcWindow *self)
{
gtk_widget_init_template (GTK_WIDGET (self));
self->settings = g_settings_new ("org.gnome.Settings");
self->previous_panels = g_queue_new ();
self->previous_list_view = cc_panel_list_get_view (self->panel_list);
/* Add a custom CSS class on development builds */
if (in_flatpak_sandbox ())
gtk_widget_add_css_class (GTK_WIDGET (self), "devel");
gtk_search_bar_set_key_capture_widget (self->search_bar, GTK_WIDGET (self));
}
CcWindow *
cc_window_new (GtkApplication *application,
CcShellModel *model)
{
g_return_val_if_fail (GTK_IS_APPLICATION (application), NULL);
return g_object_new (CC_TYPE_WINDOW,
"application", application,
"resizable", TRUE,
"title", _("Settings"),
"icon-name", DEFAULT_WINDOW_ICON_NAME,
"show-menubar", FALSE,
"model", model,
NULL);
}
void
cc_window_set_search_item (CcWindow *center,
const char *search)
{
gtk_search_bar_set_search_mode (center->search_bar, TRUE);
gtk_editable_set_text (GTK_EDITABLE (center->search_entry), search);
gtk_editable_set_position (GTK_EDITABLE (center->search_entry), -1);
}