/* * Copyright 2022 zhrexl * Copyright 2024 Dave Prowse * 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-FileCopyrightText: 2024 Dave Prowse * SPDX-License-Identifier: GPL-3.0-or-later * SPDX-FileContributor: Modified by Dave Prowse */ /* eslint version: 9.16 (2024) */ import Adw from 'gi://Adw'; import Gdk from 'gi://Gdk'; import GLib from 'gi://GLib'; import Gtk from 'gi://Gtk'; import GObject from 'gi://GObject'; import Gio from 'gi://Gio'; import {gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; import * as GimpPaletteParser from '../gimpPaletteParser.js'; import { CURATED_UUID as UUID } from '../utils.js'; const MARGIN = 10; const ROWBOX_MARGIN_PARAMS = { margin_top: MARGIN / 2, margin_bottom: MARGIN / 2, margin_start: MARGIN, margin_end: MARGIN, spacing: 4 }; const DrawingPage = GObject.registerClass({ GTypeName: `${UUID}-Drawing` }, class DrawingPage extends Adw.PreferencesPage { constructor(extensionPreferences, window) { super({}); this.window = window; this.set_title("Drawing Page"); this.set_name('drawing'); this.set_icon_name("applications-graphics-symbolic"); // Use cached drawingSettings from extensionPreferences this.settings = window._drawingSettings; this.schema = this.settings.settings_schema; this.palettes = this.settings.get_value('palettes').deep_unpack(); // New constructor code above, below is the same this.palettes_adw_group = Adw.PreferencesGroup.new() this.palettes_adw_group.set_title(_("Palettes")); this.PalettesActionRow = Adw.ActionRow.new(); this.PalettesActionRow.set_title(_("New Palette")); this.PalettesActionRow.set_subtitle(_("Create or Import palette")); let addButton = Gtk.Button.new_from_icon_name('list-add-symbolic'); addButton.set_tooltip_text(_("Add a new palette")); addButton.valign = Gtk.Align.CENTER; addButton.connect('clicked', this._addNewPalette.bind(this)); let importButton = Gtk.Button.new_from_icon_name('document-open-symbolic'); importButton.set_tooltip_text(_("Select a File")); importButton.valign = Gtk.Align.CENTER; importButton.connect('clicked', this._importPalette.bind(this)); this.PalettesActionRow.add_suffix(addButton); this.PalettesActionRow.add_suffix(importButton); this.palettes_adw_group.add(this.PalettesActionRow); this.nPalettes = []; this.settings.connect('changed::palettes', this._updatePalettes.bind(this)); this._updatePalettes(); /* Area Group */ let adw_groupArea = Adw.PreferencesGroup.new() adw_groupArea.set_title(_("Area")); let squareAreaRow = Adw.ActionRow.new(); squareAreaRow.set_title(this.schema.get_key('square-area-size').get_summary()); let squareAreaAutoButton = new Gtk.CheckButton({ label: _("Auto"), name: this.schema.get_key('square-area-auto').get_summary(), tooltip_text: this.schema.get_key('square-area-auto').get_description() }); let squareAreaSizeButton = new PixelSpinButton({ width_chars: 5, digits: 0, step: 1, range: this.schema.get_key('square-area-size').get_range(), name: this.schema.get_key('square-area-size').get_summary(), tooltip_text: this.schema.get_key('square-area-size').get_description() }); this.settings.bind('square-area-auto', squareAreaAutoButton, 'active', 0); this.settings.bind('square-area-size', squareAreaSizeButton, 'value', 0); squareAreaAutoButton.bind_property('active', squareAreaSizeButton, 'sensitive', GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.INVERT_BOOLEAN); squareAreaAutoButton.valign = Gtk.Align.CENTER; squareAreaSizeButton.valign = Gtk.Align.CENTER; squareAreaRow.add_suffix(squareAreaAutoButton); squareAreaRow.add_suffix(squareAreaSizeButton); let backgroundColorRow = Adw.ActionRow.new(); backgroundColorRow.set_title(this.schema.get_key('background-color').get_summary()); let backgroundColorButton = new ColorStringButton({ use_alpha: true, show_editor: true, name: this.schema.get_key('background-color').get_summary(), tooltip_text: this.schema.get_key('background-color').get_description() }); this.settings.bind('background-color', backgroundColorButton, 'color-string', 0); backgroundColorButton.valign = Gtk.Align.CENTER; backgroundColorRow.add_suffix(backgroundColorButton); let gridLineRow = Adw.ActionRow.new(); gridLineRow.set_title( _("Grid overlay line")); let gridLineAutoButton = new Gtk.CheckButton({ label: _("Auto"), name: this.schema.get_key('grid-line-auto').get_summary(), tooltip_text: this.schema.get_key('grid-line-auto').get_description() }); let gridLineWidthButton = new PixelSpinButton({ width_chars: 5, digits: 1, step: 0.1, range: this.schema.get_key('grid-line-width').get_range(), name: this.schema.get_key('grid-line-width').get_summary(), tooltip_text: this.schema.get_key('grid-line-width').get_description() }); let gridLineSpacingButton = new PixelSpinButton({ width_chars: 5, digits: 1, step: 1, range: this.schema.get_key('grid-line-spacing').get_range(), name: this.schema.get_key('grid-line-spacing').get_summary(), tooltip_text: this.schema.get_key('grid-line-spacing').get_description() }); this.settings.bind('grid-line-auto', gridLineAutoButton, 'active', 0); this.settings.bind('grid-line-width', gridLineWidthButton, 'value', 0); this.settings.bind('grid-line-spacing', gridLineSpacingButton, 'value', 0); gridLineAutoButton.bind_property('active', gridLineWidthButton, 'sensitive', GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.INVERT_BOOLEAN); gridLineAutoButton.bind_property('active', gridLineSpacingButton, 'sensitive', GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.INVERT_BOOLEAN); gridLineAutoButton.valign = Gtk.Align.CENTER; gridLineWidthButton.valign = Gtk.Align.CENTER; gridLineSpacingButton.valign = Gtk.Align.CENTER; gridLineRow.add_suffix(gridLineAutoButton); gridLineRow.add_suffix(gridLineWidthButton); gridLineRow.add_suffix(gridLineSpacingButton); let gridColorRow = Adw.ActionRow.new(); gridColorRow.set_title(this.schema.get_key('grid-color').get_summary()); let gridColorButton = new ColorStringButton({ use_alpha: true, show_editor: true, name: this.schema.get_key('grid-color').get_summary(), tooltip_text: this.schema.get_key('grid-color').get_description() }); this.settings.bind('grid-color', gridColorButton, 'color-string', 0); gridColorRow.add_suffix(gridColorButton); gridColorButton.valign = Gtk.Align.CENTER; adw_groupArea.add(squareAreaRow); adw_groupArea.add(backgroundColorRow); adw_groupArea.add(gridLineRow); adw_groupArea.add(gridColorRow); /* End of Area Group */ /* Tools Group */ let adw_groupTools = Adw.PreferencesGroup.new() adw_groupTools.set_title(_("Tools")); let dashArrayRow = Adw.ActionRow.new(); dashArrayRow.set_title(_("Dash array")); let dashArrayAutoButton = new Gtk.CheckButton({ label: _("Auto"), name: this.schema.get_key('dash-array-auto').get_summary(), tooltip_text: this.schema.get_key('dash-array-auto').get_description() }); let dashArrayOnButton = new PixelSpinButton({ width_chars: 5, digits: 1, step: 0.1, range: this.schema.get_key('dash-array-on').get_range(), name: this.schema.get_key('dash-array-on').get_summary(), tooltip_text: this.schema.get_key('dash-array-on').get_description() }); let dashArrayOffButton = new PixelSpinButton({ width_chars: 5, digits: 1, step: 0.1, range: this.schema.get_key('dash-array-off').get_range(), name: this.schema.get_key('dash-array-off').get_summary(), tooltip_text: this.schema.get_key('dash-array-off').get_description() }); this.settings.bind('dash-array-auto', dashArrayAutoButton, 'active', 0); this.settings.bind('dash-array-on', dashArrayOnButton, 'value', 0); this.settings.bind('dash-array-off', dashArrayOffButton, 'value', 0); dashArrayAutoButton.bind_property('active', dashArrayOnButton, 'sensitive', GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.INVERT_BOOLEAN); dashArrayAutoButton.bind_property('active', dashArrayOffButton, 'sensitive', GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.INVERT_BOOLEAN); dashArrayAutoButton.valign = Gtk.Align.CENTER; dashArrayOnButton.valign = Gtk.Align.CENTER; dashArrayOffButton.valign = Gtk.Align.CENTER; dashArrayRow.add_suffix(dashArrayAutoButton); dashArrayRow.add_suffix(dashArrayOnButton); dashArrayRow.add_suffix(dashArrayOffButton); let dashOffsetRow = Adw.ActionRow.new(); dashOffsetRow.set_title(this.schema.get_key('dash-offset').get_summary()); let dashOffsetButton = new PixelSpinButton({ width_chars: 5, digits: 1, step: 0.1, range: this.schema.get_key('dash-offset').get_range(), name: this.schema.get_key('dash-offset').get_summary(), tooltip_text: this.schema.get_key('dash-offset').get_description() }); this.settings.bind('dash-offset', dashOffsetButton, 'value', 0); dashOffsetButton.valign = Gtk.Align.CENTER; dashOffsetRow.add_suffix(dashOffsetButton); let imageLocationRow = Adw.ActionRow.new(); imageLocationRow.set_title(this.schema.get_key('image-location').get_summary()); let imageLocationButton = new FileChooserButton({ action: Gtk.FileChooserAction.SELECT_FOLDER, name: this.schema.get_key('image-location').get_summary(), tooltip_text: this.schema.get_key('image-location').get_description() }); this.settings.bind('image-location', imageLocationButton, 'location', 0); imageLocationButton.valign = Gtk.Align.CENTER; imageLocationRow.add_suffix(imageLocationButton); adw_groupTools.add(dashArrayRow); adw_groupTools.add(dashOffsetRow); adw_groupTools.add(imageLocationRow); let resetButton = new Gtk.Button({ label: _("Reset settings"), halign: Gtk.Align.CENTER }); resetButton.get_style_context().add_class('destructive-action'); resetButton.connect('clicked', () => this.schema.list_keys().forEach(key => this.settings.reset(key))); resetButton.set_margin_top(12); adw_groupTools.add(resetButton); /* End of Tools Group */ this.add(this.palettes_adw_group); this.add(adw_groupArea); this.add(adw_groupTools); }; _updatePalettes() { this.palettes = this.settings.get_value('palettes').deep_unpack(); /*TODO: I Should probably reutilize these widgets instead of just removing it */ this.nPalettes.forEach((actionrow) => { if (actionrow){ this.palettes_adw_group.remove(actionrow); } }); this.nPalettes = []; this.palettes.forEach((palette,index) => { let tmpAction = Adw.ActionRow.new(); tmpAction.set_title(palette[0]); this.nPalettes.push(tmpAction); let EditButton = Gtk.Button.new_from_icon_name('edit-symbolic'); EditButton.set_tooltip_text(_("Edit this palette colors")); EditButton.valign = Gtk.Align.CENTER; EditButton.connect('clicked', () => {this._editPalette(palette, index)}); let RemoveButton = Gtk.Button.new_from_icon_name('edit-delete-symbolic'); RemoveButton.set_tooltip_text(_("Remove this palette")); RemoveButton.valign = Gtk.Align.CENTER; RemoveButton.connect('clicked', () => { this.palettes.splice(index,1); this._savePalettes(); }); tmpAction.add_suffix(EditButton); tmpAction.add_suffix(RemoveButton); this.palettes_adw_group.add(this.nPalettes[index]); }); } _savePalettes() { this.settings.set_value('palettes', new GLib.Variant('a(sas)', this.palettes)); } _onPaletteNameChanged(index, text) { this.palettes[index][0] = text; this._savePalettes(); } _onPaletteColorChanged(paletteIndex, colorIndex, colorButton) { this.palettes[paletteIndex][1][colorIndex] = colorButton.get_rgba().to_string(); if (colorButton.tooltip_text) this.palettes[paletteIndex][1][colorIndex] += `:${colorButton.tooltip_text}`; this._savePalettes(); } _addNewPalette() { let colors = Array(9).fill('Black'); // Translators: default name of a new palette this.palettes.push([_("New palette"), colors]); this._savePalettes(); } _importPalette() { let dialog = new Gtk.FileChooserDialog({ title: _("Select a File"), action: Gtk.FileChooserAction.OPEN, modal: true, }); dialog.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL); dialog.add_button(_("_Open"), Gtk.ResponseType.ACCEPT); let filter = new Gtk.FileFilter(); filter.set_name("GIMP Palette (*.gpl)"); filter.add_pattern('*.gpl'); dialog.add_filter(filter); dialog.connect('response', (dialog, response) => { if (response == Gtk.ResponseType.ACCEPT) { let file = dialog.get_file(); let palettes = GimpPaletteParser.parseFile(file); palettes.forEach(palette => this.palettes.push(palette)); this._savePalettes(); } dialog.destroy(); }); dialog.show(); } _editPalette(palette, index) { let modal = new Gtk.Dialog({ default_height: 400, default_width: 500, modal: true, transient_for: this.window, title: 'Editing ' + palette[0] + ' palette', use_header_bar: true }); let contentArea = modal.get_content_area(); let preferencesPage = Adw.PreferencesPage.new(); let editor_palettes_group = Adw.PreferencesGroup.new() editor_palettes_group.set_title(_("Customize Palette")); let PalettesNameActionRow = Adw.ActionRow.new(); PalettesNameActionRow.set_title(_("Name")); let EntryName = Gtk.EditableLabel.new(palette[0]); EntryName.connect('changed', () => { this._onPaletteNameChanged(index, EntryName.get_text()); }); EntryName.set_alignment(1); PalettesNameActionRow.add_suffix(EntryName); editor_palettes_group.add(PalettesNameActionRow); palette[1].forEach((color, colorindex)=>{ let pActionRow = Adw.ActionRow.new(); let [cname, title] = color.split(":"); let colortitle = Gtk.EditableLabel.new(title || cname); colortitle.connect('changed', () => { this.palettes[index][1][colorindex] = cname+':'+colortitle.get_text(); this._savePalettes(); }); pActionRow.add_prefix(colortitle); let colorEntry = new ColorStringButton({ use_alpha: true, show_editor: true, name: cname, tooltip_text: title || cname}); colorEntry.color_string = cname; colorEntry.valign = Gtk.Align.CENTER; colorEntry.connect('notify::color-string', () => { this._onPaletteColorChanged(index,colorindex, colorEntry); }); pActionRow.add_suffix(colorEntry); editor_palettes_group.add(pActionRow); }); preferencesPage.add(editor_palettes_group); contentArea.append(preferencesPage); modal.show(); } _removePalette(paletteIndex) { this.palettes.splice(paletteIndex, 1); this._savePalettes(); } }); const PixelSpinButton = new GObject.Class({ Name: `${UUID}-PixelSpinButton2`, Extends: Gtk.SpinButton, Properties: { 'range': GObject.param_spec_variant('range', 'range', 'GSettings range', GLib.VariantType.new('(sv)'), null, GObject.ParamFlags.WRITABLE), 'step': GObject.ParamSpec.double('step', 'step', 'step increment', GObject.ParamFlags.WRITABLE, 0, 1000, 1) }, set range(range) { let [type, variant] = range.deep_unpack(); if (type == 'range') { let [min, max] = variant.deep_unpack(); this.adjustment.set_lower(min); this.adjustment.set_upper(max); } }, set step(step) { this.adjustment.set_step_increment(step); this.adjustment.set_page_increment(step * 10); }, on_output: function() { this.text = _("%f px").format(Number(this.value).toFixed(2)); return true; }, // Prevent accidental scrolling (GTK 3). on_scroll_event: function(event) { if (this.has_focus) { try { GObject.signal_chain_from_overridden([this, event], false); } catch(e) { } return Gdk.EVENT_STOP; } return Gdk.EVENT_PROPAGATE; } }); // A color button that can be easily bound with a color string setting. const ColorStringButton = new GObject.Class({ Name: `${UUID}-ColorStringButton2`, Extends: Gtk.ColorButton, Properties: { 'color-string': GObject.ParamSpec.string('color-string', 'colorString', 'A string that describes the color', GObject.ParamFlags.READWRITE, 'black') }, get color_string() { return this._color_string || 'black'; }, set color_string(colorString) { this._color_string = colorString; let newRgba = new Gdk.RGBA(); newRgba.parse(colorString); this.set_rgba(newRgba); }, on_color_set: function() { let oldRgba = new Gdk.RGBA(); oldRgba.parse(this.color_string); // Do nothing if the new color is equivalent to the old color (e.g. "black" and "rgb(0,0,0)"). if (!this.rgba.equal(oldRgba)) { this._color_string = this.rgba.to_string(); this.notify('color-string'); } } }); const FileChooserButton = new GObject.Class({ Name: `${UUID}-FileChooserButton2`, Extends: Gtk.Button, Properties: { 'action': GObject.ParamSpec.enum('action', 'action', 'action', GObject.ParamFlags.READWRITE, Gtk.FileChooserAction.$gtype, Gtk.FileChooserAction.SELECT_FOLDER), 'location': GObject.ParamSpec.string('location', 'location', 'location', GObject.ParamFlags.READWRITE, '') }, get location() { return this._location || ""; }, set location(location) { if (!this._location || this._location != location) { this._location = location; this.label = location ? Gio.File.new_for_commandline_arg(location).query_info('standard::display-name', Gio.FileQueryInfoFlags.NONE, null).get_display_name() : _("(None)"); this.notify('location'); } }, vfunc_clicked: function() { let dialog = new Gtk.FileChooserDialog({ title: _(this.name), action: this.action, modal: true, }); dialog.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL); dialog.add_button(_("_Select"), Gtk.ResponseType.ACCEPT); if (this.location) dialog.set_file(Gio.File.new_for_commandline_arg(this.location)); dialog.connect('response', (dialog, response) => { if (response == Gtk.ResponseType.ACCEPT) this.location = dialog.get_file().get_path(); dialog.destroy(); }); dialog.show(); } }); export default DrawingPage;