From 8e9f222526746447a9189dce08036d390b8c23c7 Mon Sep 17 00:00:00 2001 From: Cameron White Date: Thu, 20 Aug 2026 23:09:58 -0400 Subject: [PATCH] Initial work on adding a preferences dialog - Use the libadwaita preferences dialog, and define the layout using an xml .ui file to test out this process for the first time. - Move the color scheme preference into the dialog as a test. By adding an event when settings are modified, changes to the settings can be decoupled from the dialog itself - Add a Preferences menu item. Depending on the platform, this is in the application menu on macOS, or in the Edit menu for a menubar layout, or in the main menu when using a headerbar layout. - Also simplified logic around how the macOS app menu commands are registered, and fixed the keyboard shortcut dialog to use different shortcuts from the prefs dialog --- Pinta.Core/Actions/AppActions.cs | 25 +++++++---- Pinta.Core/Actions/FileActions.cs | 15 +++---- Pinta.Core/Actions/HelpActions.cs | 20 +++------ Pinta.Core/Actions/ViewActions.cs | 19 -------- Pinta.Core/Managers/SettingsManager.cs | 14 ++++++ Pinta.Resources/Icons.cs | 2 +- Pinta/ActionHandlers.cs | 3 +- Pinta/Actions/Edit/PreferencesDialogAction.cs | 37 ++++++++++++++++ .../Actions/View/ColorSchemeChangedAction.cs | 21 +++++---- Pinta/Dialogs/PreferencesDialog.cs | 44 +++++++++++++++++++ Pinta/Dialogs/PreferencesDialog.ui | 32 ++++++++++++++ Pinta/MainWindow.cs | 29 +++++++----- Pinta/Pinta.csproj | 6 +++ 13 files changed, 197 insertions(+), 70 deletions(-) create mode 100644 Pinta/Actions/Edit/PreferencesDialogAction.cs create mode 100644 Pinta/Dialogs/PreferencesDialog.cs create mode 100644 Pinta/Dialogs/PreferencesDialog.ui diff --git a/Pinta.Core/Actions/AppActions.cs b/Pinta.Core/Actions/AppActions.cs index df246f8cb5..db3168c284 100644 --- a/Pinta.Core/Actions/AppActions.cs +++ b/Pinta.Core/Actions/AppActions.cs @@ -1,21 +1,21 @@ -// +// // AppActions.cs -// +// // Author: // Jonathan Pobst -// +// // Copyright (c) 2010 Jonathan Pobst -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -31,6 +31,7 @@ namespace Pinta.Core; public sealed class AppActions { public Command About { get; } + public Command Preferences { get; } public Command KeyboardShortcuts { get; } public Command Exit { get; } @@ -43,12 +44,17 @@ public AppActions () Translations.GetString ("About"), null, Resources.StandardIcons.HelpAbout); + Preferences = new Command ( + "preferences", + Translations.GetString ("Preferences..."), + null, + Resources.StandardIcons.Preferences, + shortcuts: ["comma"]); KeyboardShortcuts = new Command ( "keyboardshortcuts", Translations.GetString ("Keyboard Shortcuts"), - null, - Resources.StandardIcons.KeyboardShortcuts, - shortcuts: ["comma"]); + null, null, + shortcuts: ["question"]); Exit = new Command ( "quit", Translations.GetString ("Quit"), @@ -61,6 +67,7 @@ public void RegisterActions (Gtk.Application app) { app.AddCommands ([ About, + Preferences, KeyboardShortcuts, Exit]); } diff --git a/Pinta.Core/Actions/FileActions.cs b/Pinta.Core/Actions/FileActions.cs index 27b3e229f9..4034435868 100644 --- a/Pinta.Core/Actions/FileActions.cs +++ b/Pinta.Core/Actions/FileActions.cs @@ -1,21 +1,21 @@ -// +// // FileActions.cs -// +// // Author: // Jonathan Pobst -// +// // Copyright (c) 2010 Jonathan Pobst -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -138,9 +138,6 @@ public void RegisterActions (Gtk.Application application, Gio.Menu menu) SaveAs, Close]); - - if (!isMac) - application.AddCommand (app.Exit); // This is part of the application menu on macOS } public void RegisterHandlers () { } diff --git a/Pinta.Core/Actions/HelpActions.cs b/Pinta.Core/Actions/HelpActions.cs index 6435603a2e..80a3a23de7 100644 --- a/Pinta.Core/Actions/HelpActions.cs +++ b/Pinta.Core/Actions/HelpActions.cs @@ -1,21 +1,21 @@ -// +// // HelpActions.cs -// +// // Author: // Jonathan Pobst -// +// // Copyright (c) 2010 Jonathan Pobst -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -73,9 +73,6 @@ public void RegisterActions (Gtk.Application application, Gio.Menu menu) { menu.AppendItem (Contents.CreateMenuItem ()); - if (system.OperatingSystem != OS.Mac) { - application.AddCommand (app.KeyboardShortcuts); - } menu.AppendItem (app.KeyboardShortcuts.CreateMenuItem ()); menu.AppendItem (Website.CreateMenuItem ()); @@ -93,10 +90,7 @@ public void RegisterActions (Gtk.Application application, Gio.Menu menu) var about_section = Gio.Menu.New (); menu.AppendSection (null, about_section); - - Command about = app.About; - application.AddCommand (about); - about_section.AppendItem (about.CreateMenuItem ()); + about_section.AppendItem (app.About.CreateMenuItem ()); } } diff --git a/Pinta.Core/Actions/ViewActions.cs b/Pinta.Core/Actions/ViewActions.cs index f6e9463da5..b5546f39c4 100644 --- a/Pinta.Core/Actions/ViewActions.cs +++ b/Pinta.Core/Actions/ViewActions.cs @@ -46,7 +46,6 @@ public sealed class ViewActions public ToggleCommand ToolBox { get; } public ToggleCommand Rulers { get; } public Gio.SimpleAction RulerMetric { get; } - public Gio.SimpleAction ColorScheme { get; } public Command Fullscreen { get; } public ToolBarComboBox ZoomComboBox { get; } @@ -155,11 +154,6 @@ public ViewActions (ChromeManager chrome, WorkspaceManager workspace) GtkExtensions.IntVariantType, GLib.Variant.NewInt32 (0)); - ColorScheme = Gio.SimpleAction.NewStateful ( // TODO: Make `Command` - "colorscheme", - GtkExtensions.IntVariantType, - GLib.Variant.NewInt32 (0)); - Fullscreen = new Command ( "Fullscreen", Translations.GetString ("Fullscreen"), @@ -242,22 +236,10 @@ public void RegisterActions (Gtk.Application app, Gio.Menu menu) Gio.Menu show_hide_section = Gio.Menu.New (); show_hide_section.AppendSubmenu (Translations.GetString ("Show/Hide"), show_hide_menu); - Gio.Menu color_scheme_menu = Gio.Menu.New (); - // Translators: This refers to using the system's default color scheme. - color_scheme_menu.Append (Translations.GetString ("Default"), $"app.{ColorScheme.Name}(0)"); - // Translators: This refers to using a light variant of the color scheme. - color_scheme_menu.Append (Translations.GetString ("Light"), $"app.{ColorScheme.Name}(1)"); - // Translators: This refers to using a dark variant of the color scheme. - color_scheme_menu.Append (Translations.GetString ("Dark"), $"app.{ColorScheme.Name}(2)"); - - Gio.Menu color_scheme_section = Gio.Menu.New (); - color_scheme_section.AppendSubmenu (Translations.GetString ("Color Scheme"), color_scheme_menu); - menu.AppendSection (null, zoom_section); menu.AppendSection (null, grid_section); menu.AppendSection (null, metric_section); menu.AppendSection (null, show_hide_section); - menu.AppendSection (null, color_scheme_section); app.AddCommands ([ ZoomIn, @@ -276,7 +258,6 @@ public void RegisterActions (Gtk.Application app, Gio.Menu menu) // TODO: Make `Command`s app.AddAction (RulerMetric); - app.AddAction (ColorScheme); if (mainToolbarPresent) app.AddCommand (ToolBar); diff --git a/Pinta.Core/Managers/SettingsManager.cs b/Pinta.Core/Managers/SettingsManager.cs index ebf847b305..8527434ed9 100644 --- a/Pinta.Core/Managers/SettingsManager.cs +++ b/Pinta.Core/Managers/SettingsManager.cs @@ -34,6 +34,11 @@ namespace Pinta.Core; +public sealed class SettingChangedEventArgs (string key) : EventArgs +{ + public string Key { get; } = key; +} + public interface ISettingsService { /// @@ -57,6 +62,11 @@ public interface ISettingsService /// a chance to call PutSetting to store setting. /// event EventHandler? SaveSettingsBeforeQuit; + + /// + /// An event that is fired when a setting has been changed. + /// + event EventHandler? SettingChanged; } public sealed class SettingsManager : ISettingsService @@ -71,6 +81,8 @@ public sealed class SettingsManager : ISettingsService /// public event EventHandler? SaveSettingsBeforeQuit; + public event EventHandler? SettingChanged; + public SettingsManager () { var settings_file = Path.Combine (GetUserSettingsDirectory (), SETTINGS_FILE); @@ -142,6 +154,8 @@ public T GetSetting (string key, T defaultValue) public void PutSetting (string key, object value) { settings[key] = value; + + SettingChanged?.Invoke (this, new (key)); } public void DoSaveSettingsBeforeQuit () diff --git a/Pinta.Resources/Icons.cs b/Pinta.Resources/Icons.cs index 79d502a83e..e2dbac23a9 100644 --- a/Pinta.Resources/Icons.cs +++ b/Pinta.Resources/Icons.cs @@ -61,7 +61,7 @@ public static class StandardIcons public const string ImageGeneric = "image-x-generic-symbolic"; public const string ImageMissing = "image-missing-symbolic"; - public const string KeyboardShortcuts = "preferences-system-symbolic"; + public const string Preferences = "preferences-system-symbolic"; public const string LayerMoveUp = "pan-up-symbolic"; public const string LayerMoveDown = "pan-down-symbolic"; diff --git a/Pinta/ActionHandlers.cs b/Pinta/ActionHandlers.cs index d135435f5c..34fa90c6a9 100644 --- a/Pinta/ActionHandlers.cs +++ b/Pinta/ActionHandlers.cs @@ -68,6 +68,7 @@ public ActionHandlers () new PasteIntoNewImageAction (actions, chrome, workspace), new ResizePaletteAction (actions.Edit, chrome, palette), new AddinManagerAction (actions.Addins, chrome, system), + new PreferencesDialogAction(actions.App, chrome, settings), // Image new ResizeImageAction (actions.Image, chrome, workspace, settings), @@ -84,7 +85,7 @@ public ActionHandlers () new ToolWindowsToggledAction (actions.View, chrome), new StatusBarToggledAction (actions.View, chrome), new ToolBoxToggledAction (actions.View, chrome), - new ColorSchemeChangedAction (actions.View), + new ColorSchemeChangedAction (settings), new EditCanvasGridAction (actions.View, chrome, canvasGrid), // Window diff --git a/Pinta/Actions/Edit/PreferencesDialogAction.cs b/Pinta/Actions/Edit/PreferencesDialogAction.cs new file mode 100644 index 0000000000..ef1131a1c1 --- /dev/null +++ b/Pinta/Actions/Edit/PreferencesDialogAction.cs @@ -0,0 +1,37 @@ +namespace Pinta.Actions; + +using System; +using Pinta.Core; + +internal sealed class PreferencesDialogAction : IActionHandler +{ + private readonly AppActions app; + private readonly IChromeService chrome; + private readonly ISettingsService settings; + + internal PreferencesDialogAction ( + AppActions app, + IChromeService chrome, + ISettingsService settings) + { + this.app = app; + this.chrome = chrome; + this.settings = settings; + } + + void IActionHandler.Initialize () + { + app.Preferences.Activated += Activated; + } + + void IActionHandler.Uninitialize () + { + app.Preferences.Activated -= Activated; + } + + private void Activated (object sender, EventArgs e) + { + using PreferencesDialog dialog = PreferencesDialog.New (settings); + dialog.Present (chrome.MainWindow); + } +} diff --git a/Pinta/Actions/View/ColorSchemeChangedAction.cs b/Pinta/Actions/View/ColorSchemeChangedAction.cs index 0721905a30..d4fe01a817 100644 --- a/Pinta/Actions/View/ColorSchemeChangedAction.cs +++ b/Pinta/Actions/View/ColorSchemeChangedAction.cs @@ -5,27 +5,32 @@ namespace Pinta.Actions; internal sealed class ColorSchemeChangedAction : IActionHandler { - private readonly ViewActions view; - internal ColorSchemeChangedAction (ViewActions view) + private readonly ISettingsService settings; + internal ColorSchemeChangedAction (ISettingsService settings) { - this.view = view; + this.settings = settings; } void IActionHandler.Initialize () { - view.ColorScheme.OnActivate += Activated; + settings.SettingChanged += OnSettingChanged; + + // Load the initial color scheme setting. + OnSettingChanged (null, new (SettingNames.COLOR_SCHEME)); } void IActionHandler.Uninitialize () { - view.ColorScheme.OnActivate -= Activated; + settings.SettingChanged -= OnSettingChanged; } - private void Activated (SimpleAction action, SimpleAction.ActivateSignalArgs args) + private void OnSettingChanged (object? sender, SettingChangedEventArgs e) { - action.ChangeState (args.Parameter!); + if (e.Key != SettingNames.COLOR_SCHEME) + return; - Adw.ColorScheme scheme = args.Parameter!.GetInt32 () switch { + int schemeIndex = PintaCore.Settings.GetSetting (SettingNames.COLOR_SCHEME, 0); + Adw.ColorScheme scheme = schemeIndex switch { 1 => Adw.ColorScheme.ForceLight, 2 => Adw.ColorScheme.ForceDark, _ => Adw.ColorScheme.Default, diff --git a/Pinta/Dialogs/PreferencesDialog.cs b/Pinta/Dialogs/PreferencesDialog.cs new file mode 100644 index 0000000000..e221b82cc0 --- /dev/null +++ b/Pinta/Dialogs/PreferencesDialog.cs @@ -0,0 +1,44 @@ +using GObject; + +namespace Pinta; + +using Pinta.Core; + +[GObject.Subclass (qualifiedName: nameof (PreferencesDialog))] +[Gtk.Template ("PreferencesDialog.ui")] +internal sealed partial class PreferencesDialog +{ + private ISettingsService settings = null!; // NRT - set by factory method + + [Gtk.Connect ("color_scheme_comborow")] + private Adw.ComboRow color_scheme_row; + + public static PreferencesDialog New (ISettingsService settings) + { + PreferencesDialog dialog = NewWithProperties ([]); + dialog.LoadSettings (settings); + return dialog; + } + + partial void Initialize () + { + Adw.ComboRow.SelectedPropertyDefinition.Notify (color_scheme_row, OnColorSchemeChanged); + } + + /// + /// Initialize the UI widgets from the existing settings. + /// + private void LoadSettings (ISettingsService settingsService) + { + settings = settingsService; + + int schemeIndex = settings.GetSetting (SettingNames.COLOR_SCHEME, 0); + color_scheme_row.SetSelected ((uint) schemeIndex); + } + + private void OnColorSchemeChanged (Object sender, NotifySignalArgs args) + { + int schemeIndex = (int) color_scheme_row.Selected; + settings.PutSetting (SettingNames.COLOR_SCHEME, schemeIndex); + } +} diff --git a/Pinta/Dialogs/PreferencesDialog.ui b/Pinta/Dialogs/PreferencesDialog.ui new file mode 100644 index 0000000000..f8ad2030f8 --- /dev/null +++ b/Pinta/Dialogs/PreferencesDialog.ui @@ -0,0 +1,32 @@ + + + + diff --git a/Pinta/MainWindow.cs b/Pinta/MainWindow.cs index 2e59a14ce7..86300d4ec4 100644 --- a/Pinta/MainWindow.cs +++ b/Pinta/MainWindow.cs @@ -406,12 +406,12 @@ private void CreateMainMenu () if (usingMenuBar) app.Menubar = menuBar; - if (isMac) { - // Since GTK 4.14 there is an autogenerated Application menu, so we just need - // to register actions with matching names for About, Quit, etc - // https://gitlab.gnome.org/GNOME/gtk/-/issues/6762 - PintaCore.Actions.App.RegisterActions (app); - } + // Since GTK 4.14 there is an autogenerated Application menu, so we just need + // to register actions with matching names for About, Quit, etc + // https://gitlab.gnome.org/GNOME/gtk/-/issues/6762 + // For non-macOS, we still register the commands here and then other menus add the items. + PintaCore.Actions.App.RegisterActions (app); + PintaCore.Actions.File.RegisterActions (app, fileMenu); PintaCore.Actions.Edit.RegisterActions (app, editMenu); PintaCore.Actions.View.RegisterActions (app, viewMenu); @@ -421,6 +421,19 @@ private void CreateMainMenu () PintaCore.Actions.Window.RegisterActions (app, windowMenu); PintaCore.Actions.Help.RegisterActions (app, helpMenu); + // When using a header bar, show preferences in the main menu. + // Otherwise, add it to the Edit menu (except for macOS, which shows this in the App menu). + if (!usingMenuBar || !isMac) { + Gio.Menu prefsSection = Gio.Menu.New (); + prefsSection.AppendItem (PintaCore.Actions.App.Preferences.CreateMenuItem ()); + + if (!usingMenuBar) { + menuBar.AppendSection (null, prefsSection); + } else { + editMenu.AppendSection (null, prefsSection); + } + } + PintaCore.Chrome.InitializeMainMenu (adjustmentsMenu, effectsMenu); // --- References to keep @@ -552,9 +565,6 @@ private void LoadUserSettings () MetricType ruler_metric = (MetricType) PintaCore.Settings.GetSetting (SettingNames.RULER_METRIC, (int) MetricType.Pixels); PintaCore.Actions.View.RulerMetric.Activate (GLib.Variant.NewInt32 ((int) ruler_metric)); - - int color_scheme = PintaCore.Settings.GetSetting (SettingNames.COLOR_SCHEME, 0); - PintaCore.Actions.View.ColorScheme.Activate (GLib.Variant.NewInt32 (color_scheme)); } private void SaveUserSettings () @@ -568,7 +578,6 @@ private void SaveUserSettings () } PintaCore.Settings.PutSetting (SettingNames.RULER_METRIC, (int) GetCurrentRulerMetric ()); - PintaCore.Settings.PutSetting (SettingNames.COLOR_SCHEME, PintaCore.Actions.View.ColorScheme.GetState ()!.GetInt32 ()); PintaCore.Settings.PutSetting (SettingNames.WINDOW_MAXIMIZED, window_shell.Window.IsMaximized ()); PintaCore.Settings.PutSetting (SettingNames.RULER_SHOWN, PintaCore.Actions.View.Rulers.Value); PintaCore.Settings.PutSetting (SettingNames.IMAGE_TABS_SHOWN, PintaCore.Actions.View.ImageTabs.Value); diff --git a/Pinta/Pinta.csproj b/Pinta/Pinta.csproj index 41f7860d0d..2898726bdb 100644 --- a/Pinta/Pinta.csproj +++ b/Pinta/Pinta.csproj @@ -26,6 +26,12 @@ + + + %(Filename)%(Extension) + + +