Files

515 lines
22 KiB
JavaScript

/*
* 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 <http://www.gnu.org/licenses/>.
* 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;