diff --git a/area.js b/area.js index 5e1fecc..9192106 100644 --- a/area.js +++ b/area.js @@ -64,14 +64,19 @@ var Tools = Object.assign({ }, Shapes, Manipulations); Object.defineProperty(Tools, 'getNameOf', { enumerable: false }); +// toJSON provides a string suitable for SVG color attribute whereas +// toString provides a string suitable for displaying the color name to the user. const getColorFromString = function(string, fallback) { - let [success, color] = Clutter.Color.from_string(string); - color.toString = () => string; + let [colorString, displayName] = string.split(':'); + let [success, color] = Clutter.Color.from_string(colorString); + color.toJSON = () => colorString; + color.toString = () => displayName || colorString; if (success) return color; log(`${Me.metadata.uuid}: "${string}" color cannot be parsed.`); color = Clutter.Color.get_static(Clutter.StaticColor[fallback.toUpperCase()]); + color.toJSON = () => fallback; color.toString = () => fallback; return color; }; diff --git a/elements.js b/elements.js index 82338dd..52ea4c4 100644 --- a/elements.js +++ b/elements.js @@ -113,7 +113,7 @@ const _DrawingElement = new Lang.Class({ toJSON: function() { return { shape: this.shape, - color: this.color.toString(), + color: this.color, line: this.line, dash: this.dash, fill: this.fill, @@ -314,7 +314,7 @@ const _DrawingElement = new Lang.Class({ _drawSvg: function(transAttribute, bgcolorString) { let row = "\n "; let points = this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100]); - let color = this.eraser ? bgcolorString : this.color.toString(); + let color = this.eraser ? bgcolorString : this.color.toJSON(); let fill = this.fill && !this.isStraightLine; let attributes = this.eraser ? `class="eraser" ` : ''; @@ -633,7 +633,7 @@ const TextElement = new Lang.Class({ return { shape: this.shape, - color: this.color.toString(), + color: this.color, eraser: this.eraser, transformations: this.transformations, text: this.text, @@ -703,7 +703,7 @@ const TextElement = new Lang.Class({ _drawSvg: function(transAttribute, bgcolorString) { let row = "\n "; let [x, y, height] = [Math.round(this.x*100)/100, Math.round(this.y*100)/100, Math.round(this.height*100)/100]; - let color = this.eraser ? bgcolorString : this.color.toString(); + let color = this.eraser ? bgcolorString : this.color.toJSON(); let attributes = this.eraser ? `class="eraser" ` : ''; if (this.points.length == 2) { @@ -775,7 +775,7 @@ const ImageElement = new Lang.Class({ toJSON: function() { return { shape: this.shape, - color: this.color.toString(), + color: this.color, fill: this.fill, eraser: this.eraser, transformations: this.transformations, diff --git a/gimpPaletteParser.js b/gimpPaletteParser.js new file mode 100644 index 0000000..c1eb139 --- /dev/null +++ b/gimpPaletteParser.js @@ -0,0 +1,95 @@ +/* jslint esversion: 6 */ +/* exported parseFile */ + +/* + * Copyright 2019 Abakkk + * + * This file is part of DrawOnYourScreen, a drawing extension for GNOME Shell. + * https://framagit.org/abakkk/DrawOnYourScreen + * + * 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 2 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 . + */ + +const ByteArray = imports.byteArray; + +/* + * [ + * [ + * 'palette name 1', // a palette for each column + * [ + * 'rgb(...)', + * 'rgb(...):color display name', // the optional name separated with ':' + * ... + * ] + * ], + * [ + * 'palette name 2', + * [...] + * ], + * ... + * ] +*/ + +function parse(contents) { + let lines = contents.split('\n'); + let line, name, columnNumber; + + line = lines.shift(); + if (!line || !line.startsWith('GIMP Palette')) + log("Missing magic header"); + + line = lines.shift(); + if (line.startsWith('Name:')) { + name = line.slice(5).trim() || file.get_basename(); + line = lines.shift(); + } + if (line.startsWith('Columns:')) { + columnNumber = Number(line.slice(8).trim()) || 1; + line = lines.shift(); + } + + let columns = (new Array(columnNumber)).fill(null).map(() => []); + + lines.forEach((line, index) => { + if (!line || line.startsWith('#')) + return; + + line = line.split('#')[0].trim(); + + let [, color, displayName] = line.split(/(^[\d\s]+)/); + + let values = color.trim().split(/\D+/gi).filter(value => value >= 0 && value <= 255); + if (values.length < 3) + return; + + let string = `rgb(${values[0]},${values[1]},${values[2]})`; + if (displayName.trim()) + string += `:${displayName.trim()}`; + + columns[index % columns.length].push(string); + }); + + return columns.map((column, index) => [columnNumber > 1 ? `${name} ${index + 1}` : name, column]); +} + +function parseFile(file) { + if (!file.query_exists(null)) + return []; + + let [, contents] = file.load_contents(null); + if (contents instanceof Uint8Array) + contents = ByteArray.toString(contents); + + return parse(contents); +} diff --git a/prefs.js b/prefs.js index 30369ea..ab266e2 100644 --- a/prefs.js +++ b/prefs.js @@ -31,6 +31,7 @@ const Gtk = imports.gi.Gtk; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Convenience = ExtensionUtils.getSettings && ExtensionUtils.initTranslations ? ExtensionUtils : Me.imports.convenience; +const GimpPaletteParser = Me.imports.gimpPaletteParser; const Shortcuts = Me.imports.shortcuts; const gettext = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; const _ = function(string) { @@ -150,26 +151,32 @@ const DrawingPage = new GObject.Class({ let palettesFrame = new Frame({ label: _("Palettes") }); box.add(palettesFrame); + let palettesFrameBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL }); + palettesFrame.add(palettesFrameBox); - let palettesScrolledWindow = new Gtk.ScrolledWindow({ vscrollbar_policy: Gtk.PolicyType.NEVER, margin_top: MARGIN / 2, margin_bottom: MARGIN / 2 }); - palettesFrame.add(palettesScrolledWindow); + let palettesScrolledWindow = new Gtk.ScrolledWindow({ vscrollbar_policy: Gtk.PolicyType.NEVER }); + palettesFrameBox.add(palettesScrolledWindow); + let palettesViewport = new Gtk.Viewport({ margin_top: MARGIN / 2, margin_bottom: MARGIN / 2 }); + palettesScrolledWindow.add(palettesViewport); this.palettesListBox = new Gtk.ListBox({ selection_mode: 0, hexpand: true }); this.palettesListBox.get_style_context().add_class('background'); this.palettesListBox.get_accessible().set_name(this.schema.get_key('palettes').get_summary()); this.palettesListBox.get_accessible().set_description(this.schema.get_key('palettes').get_description()); - palettesScrolledWindow.add(this.palettesListBox); + palettesViewport.add(this.palettesListBox); this.settings.connect('changed::palettes', this._updatePalettes.bind(this)); this._updatePalettes(); - this.addBox = new Gtk.Box(ROWBOX_MARGIN_PARAMS); - this.addBox.margin_bottom = MARGIN; // add space for the scrollbar + let addBox = new Gtk.Box(ROWBOX_MARGIN_PARAMS); let addButton = Gtk.Button.new_from_icon_name('list-add-symbolic', Gtk.IconSize.BUTTON); addButton.set_tooltip_text(_("Add a new palette")); - this.addBox.pack_start(addButton, true, true, 4); + addBox.pack_start(addButton, true, true, 4); addButton.connect('clicked', this._addNewPalette.bind(this)); - this.palettesListBox.add(this.addBox); - this.addBox.get_parent().set_activatable(false); + let importButton = Gtk.Button.new_from_icon_name('document-open-symbolic', Gtk.IconSize.BUTTON); + importButton.set_tooltip_text(_GTK("Select a File")); + addBox.pack_start(importButton, true, true, 4); + importButton.connect('clicked', this._importPalette.bind(this)); + palettesFrameBox.add(addBox); let areaFrame = new Frame({ label: _("Area") }); box.add(areaFrame); @@ -285,10 +292,9 @@ const DrawingPage = new GObject.Class({ _updatePalettes: function() { this.palettes = this.settings.get_value('palettes').deep_unpack(); - this.palettesListBox.get_children().filter(row=> row.get_child() != this.addBox) - .slice(this.palettes.length) + this.palettesListBox.get_children().slice(this.palettes.length) .forEach(row => this.palettesListBox.remove(row)); - let paletteBoxes = this.palettesListBox.get_children().map(row => row.get_child()).filter(child => child != this.addBox); + let paletteBoxes = this.palettesListBox.get_children().map(row => row.get_child()); this.palettes.forEach((palette, paletteIndex) => { let [name, colors] = palette; @@ -316,17 +322,20 @@ const DrawingPage = new GObject.Class({ paletteBox.get_parent().set_activatable(false); } - colors.splice(9); while (colors.length < 9) colors.push('transparent'); let colorsBox = paletteBox.get_children()[1]; let colorButtons = colorsBox.get_children(); colors.forEach((color, colorIndex) => { + let [colorString, displayName] = color.split(':'); if (colorButtons[colorIndex]) { - colorButtons[colorIndex].color_string = color; + colorButtons[colorIndex].color_string = colorString; + colorButtons[colorIndex].tooltip_text = displayName || null; } else { - let colorButton = new ColorStringButton({ color_string: color, use_alpha: true, show_editor: true, halign: Gtk.Align.START, hexpand: false }); + let colorButton = new ColorStringButton({ color_string: colorString, tooltip_text: displayName || null, + use_alpha: true, show_editor: true, + halign: Gtk.Align.START, hexpand: false }); colorButton.connect('notify::color-string', this._onPaletteColorChanged.bind(this, paletteIndex, colorIndex)); colorsBox.add(colorButton); } @@ -347,6 +356,8 @@ const DrawingPage = new GObject.Class({ _onPaletteColorChanged: function(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(); }, @@ -357,6 +368,20 @@ const DrawingPage = new GObject.Class({ this._savePalettes(); }, + _importPalette: function() { + let dialog = new Gtk.FileChooserNative({ action: Gtk.FileChooserAction.OPEN, title: _GTK("Select a File") }); + let filter = new Gtk.FileFilter(); + filter.set_name("GIMP Palette (*.gpl)"); + filter.add_pattern('*.gpl'); + dialog.add_filter(filter); + if (dialog.run() == Gtk.ResponseType.ACCEPT) { + let file = dialog.get_file(); + let palettes = GimpPaletteParser.parseFile(file); + palettes.forEach(palette => this.palettes.push(palette)); + this._savePalettes(); + } + }, + _removePalette: function(paletteIndex) { this.palettes.splice(paletteIndex, 1); this._savePalettes(); diff --git a/schemas/gschemas.compiled b/schemas/gschemas.compiled index ec56cc3..208135f 100644 Binary files a/schemas/gschemas.compiled and b/schemas/gschemas.compiled differ diff --git a/schemas/org.gnome.shell.extensions.draw-on-your-screen.gschema.xml b/schemas/org.gnome.shell.extensions.draw-on-your-screen.gschema.xml index b7abae0..e13b393 100644 --- a/schemas/org.gnome.shell.extensions.draw-on-your-screen.gschema.xml +++ b/schemas/org.gnome.shell.extensions.draw-on-your-screen.gschema.xml @@ -100,12 +100,12 @@ [ - ("Palette", ["HotPink","Cyan","yellow","Orangered","Chartreuse","DarkViolet","White","Gray","Black"]), - ("GNOME HIG lighter", ["rgb(153,193,241)","rgb(143,240,164)","rgb(249,240,107)","rgb(255,190,111)","rgb(246,97,81)","rgb(220,138,221)","rgb(205,171,143)","rgb(255,255,255)","rgb(119,118,123)"]), - ("GNOME HIG light", ["rgb(98,160,241)","rgb(87,227,137)","rgb(248,228,92)","rgb(255,163,72)","rgb(237,51,59)","rgb(192,97,203)","rgb(181,131,90)","rgb(246,245,244)","rgb(94,92,100)"]), - ("GNOME HIG normal", ["rgb(53,132,228)","rgb(51,209,122)","rgb(246,211,45)","rgb(255,120,0)","rgb(224,27,36)","rgb(145,65,172)","rgb(152,106,68)","rgb(222,221,218)","rgb(61,56,70)"]), - ("GNOME HIG dark", ["rgb(28,113,216)","rgb(46,194,126)","rgb(245,194,17)","rgb(230,97,0)","rgb(192,28,40)","rgb(129,61,156)","rgb(134,94,60)","rgb(192,191,188)","rgb(36,31,49)"]), - ("GNOME HIG darker", ["rgb(26,095,180)","rgb(38,162,105)","rgb(229,165,10)","rgb(198,70,0)","rgb(165,29,45)","rgb(97,53,131)","rgb(99,69,44)","rgb(154,153,150)","rgb(0,0,0)"]) + ('Palette', ['HotPink', 'Cyan', 'yellow', 'Orangered', 'Chartreuse', 'DarkViolet', 'White', 'Gray', 'Black']), + ('GNOME HIG lighter', ['rgb(153,193,241):Blue 1', 'rgb(143,240,164):Green 1', 'rgb(249,240,107):Yellow 1', 'rgb(255,190,111):Orange 1', 'rgb(246,97,81):Red 1', 'rgb(220,138,221):Purple 1', 'rgb(205,171,143):Brown 1', 'rgb(255,255,255):Light 1', 'rgb(119,118,123):Dark 1']), + ('GNOME HIG light', ['rgb(98,160,234):Blue 2', 'rgb(87,227,137):Green 2', 'rgb(248,228,92):Yellow 2', 'rgb(255,163,72):Orange 2', 'rgb(237,51,59):Red 2', 'rgb(192,97,203):Purple 2', 'rgb(181,131,90):Brown 2', 'rgb(246,245,244):Light 2', 'rgb(94,92,100):Dark 2']), + ('GNOME HIG normal', ['rgb(53,132,228):Blue 3', 'rgb(51,209,122):Green 3', 'rgb(246,211,45):Yellow 3', 'rgb(255,120,0):Orange 3', 'rgb(224,27,36):Red 3', 'rgb(145,65,172):Purple 3', 'rgb(152,106,68):Brown 3', 'rgb(222,221,218):Light 3', 'rgb(61,56,70):Dark 3']), + ('GNOME HIG dark', ['rgb(28,113,216):Blue 4', 'rgb(46,194,126):Green 4', 'rgb(245,194,17):Yellow 4', 'rgb(230,97,0):Orange 4', 'rgb(192,28,40):Red 4', 'rgb(129,61,156):Purple 4', 'rgb(134,94,60):Brown 4', 'rgb(192,191,188):Light 4', 'rgb(36,31,49):Dark 4']), + ('GNOME HIG darker', ['rgb(26,95,180):Blue 5', 'rgb(38,162,105):Green 5', 'rgb(229,165,10):Yellow 5', 'rgb(198,70,0):Orange 5', 'rgb(165,29,45):Red 5', 'rgb(97,53,131):Purple 5', 'rgb(99,69,44):Brown 5', 'rgb(154,153,150):Light 5', 'rgb(0,0,0):Dark 5']) ] Color palettes