diff --git a/NEWS b/NEWS index b4a505a..70bf954 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,13 @@ v6.4 - September 2020 ===================== +* Can import GIMP palettes. +* Can color SVG images. +* Integrate color picker (GS buit-in or Color Picker extension). + +v6.4 - September 2020 +===================== + * Prepare GS 3.38 compatibility. * Add tooltips to sub-menu buttons also. * Fix drawing directory monitoring when it does not exist yet. diff --git a/README.md b/README.md index 88f8f48..82e1cf7 100644 --- a/README.md +++ b/README.md @@ -52,3 +52,7 @@ Then save your beautiful work by taking a screenshot. [Screenshot Tool](https://extensions.gnome.org/extension/1112/screenshot-tool/) is a convenient extension to “create, copy, store and upload screenshots”. In order to select a screenshoot area with your pointer while keeping the drawing in place, you need first to tell DrawOnYourScreen to ungrab the pointer (`Ctrl + Super + Alt + D`). +* Color Picker extension: + +If the GNOME Shell built-in color picker is too basic for you, have a look at the [Color Picker extension](https://extensions.gnome.org/extension/3396/color-picker), which let's you select the pixel accurately, preview the color and adjust its values. Once installed and enabled, it will be transparently integrated into DrawOnYourScreen. + diff --git a/area.js b/area.js index dadec04..7420c5c 100644 --- a/area.js +++ b/area.js @@ -29,6 +29,7 @@ const GObject = imports.gi.GObject; const Gtk = imports.gi.Gtk; const Lang = imports.lang; const Pango = imports.gi.Pango; +const Shell = imports.gi.Shell; const St = imports.gi.St; const System = imports.system; @@ -49,6 +50,7 @@ const SVG_DEBUG_EXTENDS = false; const TEXT_CURSOR_TIME = 600; // ms const ELEMENT_GRABBER_TIME = 80; // ms, default is about 16 ms const GRID_TILES_HORIZONTAL_NUMBER = 30; +const COLOR_PICKER_EXTENSION_UUID = 'color-picker@tuberry'; const { Shapes, Transformations } = Elements; const { DisplayStrings } = Menu; @@ -62,15 +64,20 @@ var Tools = Object.assign({ }, Shapes, Manipulations); Object.defineProperty(Tools, 'getNameOf', { enumerable: false }); -const getClutterColorFromString = function(string, fallback) { - let [success, color] = Clutter.Color.from_string(string); - color.toString = () => string; +// 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 [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.toString = () => fallback.slice(0, 1).toUpperCase() + fallback.slice(1); + color.toJSON = () => fallback; + color.toString = () => fallback; return color; }; @@ -152,7 +159,7 @@ var DrawingArea = new Lang.Class({ set currentPalette(palette) { this._currentPalette = palette; - this.colors = palette[1].map(colorString => getClutterColorFromString(colorString, 'white')); + this.colors = palette[1].map(colorString => getColorFromString(colorString, 'White')); if (!this.colors[0]) this.colors.push(Clutter.Color.get_static(Clutter.StaticColor.WHITE)); }, @@ -254,9 +261,9 @@ var DrawingArea = new Lang.Class({ this.squareAreaSize = Me.drawingSettings.get_uint('square-area-size'); } - this.areaBackgroundColor = getClutterColorFromString(Me.drawingSettings.get_string('background-color'), 'black'); + this.areaBackgroundColor = getColorFromString(Me.drawingSettings.get_string('background-color'), 'Black'); - this.gridColor = getClutterColorFromString(Me.drawingSettings.get_string('grid-color'), 'gray'); + this.gridColor = getColorFromString(Me.drawingSettings.get_string('grid-color'), 'Gray'); if (Me.drawingSettings.get_boolean('grid-line-auto')) { this.gridLineSpacing = Math.round(this.monitor.width / (5 * GRID_TILES_HORIZONTAL_NUMBER)); this.gridLineWidth = this.gridLineSpacing / 20; @@ -596,7 +603,7 @@ var DrawingArea = new Lang.Class({ this._redisplay(); }, - _startDrawing: function(stageX, stageY, eraser) { + _startDrawing: function(stageX, stageY, shiftPressed) { let [success, startX, startY] = this.transform_stage_point(stageX, stageY); if (!success) @@ -610,7 +617,7 @@ var DrawingArea = new Lang.Class({ this.currentElement = new Elements.DrawingElement({ shape: this.currentTool, color: this.currentColor, - eraser: eraser, + eraser: shiftPressed, font: this.currentFont.copy(), // Translators: initial content of the text area text: pgettext("text-area-content", "Text"), @@ -621,16 +628,15 @@ var DrawingArea = new Lang.Class({ this.currentElement = new Elements.DrawingElement({ shape: this.currentTool, color: this.currentColor, - eraser: eraser, + colored: shiftPressed, image: this.currentImage, - operator: this.currentOperator, points: [] }); } else { this.currentElement = new Elements.DrawingElement({ shape: this.currentTool, color: this.currentColor, - eraser: eraser, + eraser: shiftPressed, fill: this.fill, fillRule: this.currentFillRule, line: { lineWidth: this.currentLineWidth, lineJoin: this.currentLineJoin, lineCap: this.currentLineCap }, @@ -1032,6 +1038,78 @@ var DrawingArea = new Lang.Class({ }); }, + _onColorPicked: function(color) { + if (color instanceof Clutter.Color) + color = color.to_string().slice(0, -2); + + this.currentColor = getColorFromString(color); + if (this.currentElement) { + this.currentElement.color = this.currentColor; + this._redisplay(); + } + this.emit('show-osd', Files.Icons.COLOR, String(this.currentColor), this.currentColor.to_string().slice(0, 7), -1, false); + this.initPointerCursor(); + }, + + pickColor: function() { + if (!Screenshot.PickPixel) + // GS 3.28- + return; + + // Translators: It is displayed in an OSD notification to ask the user to start picking, so it should use the imperative mood. + this.emit('show-osd', Files.Icons.COLOR_PICKER, pgettext("osd-notification", "Pick a color"), "", -1, false); + + let extension = Main.extensionManager && Main.extensionManager.lookup(COLOR_PICKER_EXTENSION_UUID); + if (extension && extension.state == ExtensionUtils.ExtensionState.ENABLED && extension.stateObj && extension.stateObj.pickAsync) { + extension.stateObj.pickAsync().then(result => { + if (typeof result == 'string') + this._onColorPicked(result); + else + this.initPointerCursor(); + }).catch(e => { + this.initPointerCursor(); + }); + + return; + } + + try { + let screenshot = new Shell.Screenshot(); + let pickPixel = new Screenshot.PickPixel(screenshot); + + if (pickPixel.pickAsync) { + pickPixel.pickAsync().then(result => { + if (result instanceof Clutter.Color) { + // GS 3.38+ + this._onColorPicked(result); + } else { + // GS 3.36 + let graphenePoint = result; + screenshot.pick_color(graphenePoint.x, graphenePoint.y, (o, res) => { + let [, color] = screenshot.pick_color_finish(res); + this._onColorPicked(color); + }); + } + }).catch(() => this.initPointerCursor()); + } else { + // GS 3.34- + pickPixel.show(); + pickPixel.connect('finished', (pickPixel, coords) => { + if (coords) + screenshot.pick_color(...coords, (o, res) => { + let [, color] = screenshot.pick_color_finish(res); + this._onColorPicked(color); + }); + else + this.initPointerCursor(); + }); + } + } catch(e) { + log(`${Me.metadata.uuid}: color picker failed: ${e.message}`); + this.initPointerCursor(); + } + }, + toggleHelp: function() { if (this.helper.visible) { this.helper.hideHelp(); @@ -1120,7 +1198,7 @@ var DrawingArea = new Lang.Class({ elements.push(...JSON.parse(json.contents).map(object => { if (object.color) - object.color = getClutterColorFromString(object.color, 'white'); + object.color = getColorFromString(object.color, 'White'); if (object.font && typeof object.font == 'string') object.font = Pango.FontDescription.from_string(object.font); if (object.image) @@ -1227,7 +1305,7 @@ var DrawingArea = new Lang.Class({ this.elements.push(...JSON.parse(json.contents).map(object => { if (object.color) - object.color = getClutterColorFromString(object.color, 'white'); + object.color = getColorFromString(object.color, 'White'); if (object.font && typeof object.font == 'string') object.font = Pango.FontDescription.from_string(object.font); if (object.image) diff --git a/elements.js b/elements.js index 82338dd..85a4387 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) { @@ -767,19 +767,13 @@ const ImageElement = new Lang.Class({ Name: 'DrawOnYourScreenImageElement', Extends: _DrawingElement, - _init: function(params) { - params.fill = false; - this.parent(params); - }, - toJSON: function() { return { shape: this.shape, - color: this.color.toString(), - fill: this.fill, - eraser: this.eraser, + color: this.color, + colored: this.colored, transformations: this.transformations, - image: this.image.toJson(), + image: this.image, preserveAspectRatio: this.preserveAspectRatio, points: this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100]) }; @@ -797,7 +791,7 @@ const ImageElement = new Lang.Class({ return; cr.save(); - this.image.setCairoSource(cr, x, y, width, height, this.preserveAspectRatio); + this.image.setCairoSource(cr, x, y, width, height, this.preserveAspectRatio, this.colored ? this.color.toJSON() : null); cr.rectangle(x, y, width, height); cr.fill(); cr.restore(); @@ -819,14 +813,15 @@ const ImageElement = new Lang.Class({ _drawSvg: function(transAttribute) { let points = this.points; let row = "\n "; - let attributes = this.eraser ? `class="eraser" ` : ''; + let attributes = ''; if (points.length == 2) { attributes += `fill="none"`; + let base64 = this.image.getBase64ForColor(this.colored ? this.color.toJSON() : null); row += ``; + `id="${this.image.displayName}" xlink:href="data:${this.image.contentType};base64,${base64}"/>`; } return row; diff --git a/extension.js b/extension.js index 1363f17..6a232fc 100644 --- a/extension.js +++ b/extension.js @@ -225,6 +225,7 @@ const AreaManager = new Lang.Class({ 'save-as-json': this.activeArea.saveAsJson.bind(this.activeArea, true, null), 'open-previous-json': this.activeArea.loadPreviousJson.bind(this.activeArea), 'open-next-json': this.activeArea.loadNextJson.bind(this.activeArea), + 'pick-color': this.activeArea.pickColor.bind(this.activeArea), 'toggle-background': this.activeArea.toggleBackground.bind(this.activeArea), 'toggle-grid': this.activeArea.toggleGrid.bind(this.activeArea), 'toggle-square-area': this.activeArea.toggleSquareArea.bind(this.activeArea), diff --git a/files.js b/files.js index c91b404..4f4b6bb 100644 --- a/files.js +++ b/files.js @@ -41,6 +41,7 @@ const ICON_NAMES = [ 'tool-ellipse', 'tool-line', 'tool-mirror', 'tool-move', 'tool-none', 'tool-polygon', 'tool-polyline', 'tool-rectangle', 'tool-resize', ]; const ThemedIconNames = { + COLOR_PICKER: 'color-select-symbolic', ENTER: 'applications-graphics', LEAVE: 'application-exit', GRAB: 'input-touchpad', UNGRAB: 'touchpad-disabled', OPEN: 'document-open', SAVE: 'document-save', @@ -49,7 +50,16 @@ const ThemedIconNames = { TOOL_IMAGE: 'insert-image', TOOL_TEXT: 'insert-text', }; -var Icons = {}; +var Icons = { + get FAKE() { + if (!this._fake) { + let bytes = new GLib.Bytes(''); + this._fake = Gio.BytesIcon.new(bytes); + } + + return this._fake; + } +}; ICON_NAMES.forEach(name => { Object.defineProperty(Icons, name.toUpperCase().replace(/-/gi, '_'), { @@ -73,6 +83,18 @@ Object.keys(ThemedIconNames).forEach(key => { }); }); +const replaceColor = function (contents, color) { + if (contents instanceof Uint8Array) + contents = ByteArray.toString(contents); + else + contents = contents.toString(); + + return contents.replace(/fill(?=="transparent"|="none"|:transparent|:none)/gi, 'filll') + .replace(/fill="[^"]+"/gi, `fill="${color}"`) + .replace(/fill:[^";]+/gi, `fill:${color};`) + .replace(/filll/gi, 'fill'); +}; + // Wrapper around image data. If not subclassed, it is used when loading in the area an image element for a drawing file (.json) // and it takes { displayName, contentType, base64, hash } as params. var Image = new Lang.Class({ @@ -87,7 +109,7 @@ var Image = new Lang.Class({ return this.displayName; }, - toJson: function() { + toJSON: function() { return { displayName: this.displayName, contentType: this.contentType, @@ -112,6 +134,14 @@ var Image = new Lang.Class({ this._base64 = base64; }, + getBase64ForColor: function(color) { + if (!color || this.contentType != 'image/svg+xml') + return this.base64; + + let contents = GLib.base64_decode(this.base64); + return GLib.base64_encode(replaceColor(contents, color)); + }, + // hash is not used get hash() { if (!this._hash) @@ -123,7 +153,22 @@ var Image = new Lang.Class({ this._hash = hash; }, - get pixbuf() { + getBytesForColor: function(color) { + if (!color || this.contentType != 'image/svg+xml') + return this.bytes; + + let contents = this.bytes.get_data(); + return new GLib.Bytes(replaceColor(contents, color)); + }, + + getPixbuf: function(color) { + if (color) { + let stream = Gio.MemoryInputStream.new_from_bytes(this.getBytesForColor(color)); + let pixbuf = GdkPixbuf.Pixbuf.new_from_stream(stream, null); + stream.close(null); + return pixbuf; + } + if (!this._pixbuf) { let stream = Gio.MemoryInputStream.new_from_bytes(this.bytes); this._pixbuf = GdkPixbuf.Pixbuf.new_from_stream(stream, null); @@ -132,16 +177,16 @@ var Image = new Lang.Class({ return this._pixbuf; }, - getPixbufAtScale: function(width, height) { - let stream = Gio.MemoryInputStream.new_from_bytes(this.bytes); + getPixbufAtScale: function(width, height, color) { + let stream = Gio.MemoryInputStream.new_from_bytes(this.getBytesForColor(color)); let pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale(stream, width, height, true, null); stream.close(null); return pixbuf; }, - setCairoSource: function(cr, x, y, width, height, preserveAspectRatio) { - let pixbuf = preserveAspectRatio ? this.getPixbufAtScale(width, height) - : this.pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR); + setCairoSource: function(cr, x, y, width, height, preserveAspectRatio, color) { + let pixbuf = preserveAspectRatio ? this.getPixbufAtScale(width, height, color) + : this.getPixbuf(color).scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR); Gdk.cairo_set_source_pixbuf(cr, pixbuf, x, y); } }); @@ -542,7 +587,7 @@ var Jsons = { var getDateString = function() { let date = GLib.DateTime.new_now_local(); - return `${date.format("%F")} ${date.format("%X")}`; + return `${date.format("%F")} ${date.format("%T")}`; }; var saveSvg = function(content) { 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/locale/draw-on-your-screen.pot b/locale/draw-on-your-screen.pot index 3b2aa41..3ad0099 100644 --- a/locale/draw-on-your-screen.pot +++ b/locale/draw-on-your-screen.pot @@ -10,7 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: Draw On Your Screen\n" "Report-Msgid-Bugs-To: https://framagit.org/abakkk/DrawOnYourScreen/issues\n" -"POT-Creation-Date: 2020-09-18 11:37+0200\n" +"POT-Creation-Date: 2020-09-19 15:32+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -47,6 +47,11 @@ msgstr "" msgid "Type your text and press %s" msgstr "" +#. Translators: It is displayed in an OSD notification to ask the user to start picking, so it should use the imperative mood. +msgctxt "osd-notification" +msgid "Pick a color" +msgstr "" + #. Translators: "released" as the opposite of "grabbed" msgid "Keyboard and pointer released" msgstr "" @@ -284,6 +289,10 @@ msgstr "" msgid "Color" msgstr "" +#. Translators: It is displayed in a menu button tooltip or as a shortcut action description, so it should NOT use the imperative mood. +msgid "Pick a color" +msgstr "" + msgid "Add to images" msgstr "" diff --git a/menu.js b/menu.js index 53364bf..ebad93b 100644 --- a/menu.js +++ b/menu.js @@ -315,8 +315,6 @@ var DrawingMenu = new Lang.Class({ this.lineSection.actor.visible = !isText && !isImage; this.fontSection.actor.visible = isText; this.imageSection.actor.visible = isImage; - this.colorItem.setSensitive(!isImage); - this.paletteItem.setSensitive(!isImage); this.fillItem.setSensitive(!isText && !isImage); this.fillSection.setSensitive(!isText && !isImage); @@ -483,6 +481,17 @@ var DrawingMenu = new Lang.Class({ item.icon.set_gicon(icon); item.icon.set_style(`color:${this.area.currentColor.to_string().slice(0, 7)};`); + if (GS_VERSION >= '3.30') { + let colorPickerCallback = () => { + this.close(); + this.area.pickColor(); + }; + // Translators: It is displayed in a menu button tooltip or as a shortcut action description, so it should NOT use the imperative mood. + let colorPickerButton = new ActionButton(_("Pick a color"), Files.Icons.COLOR_PICKER, colorPickerCallback, null, true); + let index = getActor(item).get_children().length - 1; + getActor(item).insert_child_at_index(colorPickerButton, index); + } + item.menu.itemActivated = item.menu.close; this._populateColorSubMenu(); @@ -512,6 +521,7 @@ var DrawingMenu = new Lang.Class({ item.icon.set_gicon(icon); item.menu.itemActivated = item.menu.close; + item.menu.actor.add_style_class_name('draw-on-your-screen-menu-ellipsized'); item.menu.openOld = item.menu.open; item.menu.open = (animate) => { @@ -521,8 +531,12 @@ var DrawingMenu = new Lang.Class({ item.label.set_text(DisplayStrings.getFontFamily(family)); this.area.currentFontFamily = family; }); + if (FONT_FAMILY_STYLE) - subItem.label.set_style(`font-family:${family}`); + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + subItem.label.set_style(`font-family:${family}`); + }); + getActor(subItem).connect('key-focus-in', updateSubMenuAdjustment); }); } @@ -541,6 +555,7 @@ var DrawingMenu = new Lang.Class({ item.update(); item.menu.itemActivated = item.menu.close; + item.menu.actor.add_style_class_name('draw-on-your-screen-menu-ellipsized'); item.menu.openOld = item.menu.open; item.menu.open = (animate) => { @@ -549,7 +564,13 @@ var DrawingMenu = new Lang.Class({ let subItem = item.menu.addAction(image.toString(), () => { this.area.currentImage = image; item.update(); - }, image.thumbnailGicon || undefined); + }, Files.Icons.FAKE); + + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + if (subItem.setIcon && image.thumbnailGicon) + subItem.setIcon(image.thumbnailGicon); + }); + getActor(subItem).connect('key-focus-in', updateSubMenuAdjustment); }); } @@ -563,6 +584,7 @@ var DrawingMenu = new Lang.Class({ _addDrawingNameItem: function(menu) { this.drawingNameMenuItem = new PopupMenu.PopupMenuItem('', { reactive: false, activate: false }); this.drawingNameMenuItem.setSensitive(false); + getActor(this.drawingNameMenuItem).add_style_class_name('draw-on-your-screen-menu-ellipsized'); menu.addMenuItem(this.drawingNameMenuItem); this._updateDrawingNameMenuItem(); }, @@ -584,6 +606,7 @@ var DrawingMenu = new Lang.Class({ item.icon.set_icon_name(icon); item.menu.itemActivated = item.menu.close; + item.menu.actor.add_style_class_name('draw-on-your-screen-menu-ellipsized'); item.menu.openOld = item.menu.open; item.menu.open = (animate) => { @@ -605,7 +628,12 @@ var DrawingMenu = new Lang.Class({ this.area.loadJson(json); this._updateDrawingNameMenuItem(); this._updateActionSensitivity(); - }, json.gicon); + }, Files.Icons.FAKE); + + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + if (subItem.setIcon) + subItem.setIcon(json.gicon); + }); subItem.label.get_clutter_text().set_use_markup(true); getActor(subItem).connect('key-focus-in', updateSubMenuAdjustment); diff --git a/metadata.json b/metadata.json index de88d13..f9d26b9 100644 --- a/metadata.json +++ b/metadata.json @@ -15,7 +15,8 @@ "3.30", "3.32", "3.34", - "3.36" + "3.36", + "3.38" ], - "version": 6.4 + "version": 7 } 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 76b17ef..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 d7a9158..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 @@ -164,6 +164,10 @@ ["<Primary>v"] Add images from the clipboard + + KP_0','0']]]> + Pick a color + ["<Primary><Shift>z"] Redo last brushstroke diff --git a/shortcuts.js b/shortcuts.js index df753d4..2b2d1ac 100644 --- a/shortcuts.js +++ b/shortcuts.js @@ -46,7 +46,7 @@ var INTERNAL_KEYBINDINGS = [ ['undo', 'redo', 'delete-last-element', 'smooth-last-element'], ['select-none-shape', 'select-line-shape', 'select-ellipse-shape', 'select-rectangle-shape', 'select-polygon-shape', 'select-polyline-shape', 'select-text-shape', 'select-image-shape', 'select-move-tool', 'select-resize-tool', 'select-mirror-tool'], - ['switch-fill', 'switch-fill-rule', 'switch-color-palette', 'switch-color-palette-reverse'], + ['switch-fill', 'switch-fill-rule', 'switch-color-palette', 'switch-color-palette-reverse', 'pick-color'], ['increment-line-width', 'increment-line-width-more', 'decrement-line-width', 'decrement-line-width-more', 'switch-linejoin', 'switch-linecap', 'switch-dash'], ['switch-font-family', 'switch-font-family-reverse', 'switch-font-weight', 'switch-font-style', 'switch-text-alignment'], @@ -55,6 +55,15 @@ var INTERNAL_KEYBINDINGS = [ ['open-next-json', 'open-previous-json', 'save-as-json', 'export-to-svg', 'open-preferences', 'toggle-help'], ]; +if (GS_VERSION < '3.30') { + // Remove 'pick-color' keybinding. + INTERNAL_KEYBINDINGS.forEach(settingKeys => { + let index = settingKeys.indexOf('pick-color'); + if (index != -1) + settingKeys.splice(index, 1); + }); +} + if (GS_VERSION < '3.36') { // Remove 'open-preferences' keybinding. INTERNAL_KEYBINDINGS.forEach(settingKeys => { diff --git a/stylesheet.css b/stylesheet.css index 768f8ca..edc6e7d 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -37,6 +37,7 @@ .draw-on-your-screen-menu { font-size: 0.97em; /* default: 1em */ + min-width: 21em; /* like the aggregate menu */ } .draw-on-your-screen-menu .popup-menu-item { @@ -49,6 +50,11 @@ padding-top: 0.03em; } +/* max size for uncontrolled contents (font, image and drawing names) */ +.draw-on-your-screen-menu-ellipsized { + max-width: 21em; +} + .draw-on-your-screen-menu .toggle-switch { height: 1.35em; /* default: 22px */ }