From 82aee30657a53bf5983ed154bf8f8e83091fbcee Mon Sep 17 00:00:00 2001 From: abakkk Date: Sun, 4 Oct 2020 10:01:55 +0200 Subject: [PATCH 1/8] fix unsanitized GType names With old GS/GJS, some GType names were generated from class names without sanitizing it (e.g. Menu.ActionButton). Instead of defining all GType names, sanitize all class name as it was GType name, and let GJS generated GType name securely when it is necessary. Fix #46. --- area.js | 3 ++- elements.js | 7 ++++--- extension.js | 9 +++++---- files.js | 9 +++++---- helper.js | 3 ++- menu.js | 7 ++++--- prefs.js | 36 +++++++++++------------------------- 7 files changed, 33 insertions(+), 41 deletions(-) diff --git a/area.js b/area.js index 3b5444c..f629462 100644 --- a/area.js +++ b/area.js @@ -51,6 +51,7 @@ 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 UUID = Me.uuid.replace(/@/gi, '_at_').replace(/[^a-z0-9+_-]/gi, '_'); const { Shapes, Transformations } = Elements; const { DisplayStrings } = Menu; @@ -85,7 +86,7 @@ const getColorFromString = function(string, fallback) { // It creates and manages a DrawingElement for each "brushstroke". // It handles pointer/mouse/(touch?) events and some keyboard events. var DrawingArea = new Lang.Class({ - Name: `${Me.uuid}.DrawingArea`, + Name: `${UUID}-DrawingArea`, Extends: St.DrawingArea, Signals: { 'show-osd': { param_types: [Gio.Icon.$gtype, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_DOUBLE, GObject.TYPE_BOOLEAN] }, 'update-action-mode': {}, diff --git a/elements.js b/elements.js index b0d1868..509e219 100644 --- a/elements.js +++ b/elements.js @@ -28,6 +28,7 @@ const Pango = imports.gi.Pango; const PangoCairo = imports.gi.PangoCairo; const Me = imports.misc.extensionUtils.getCurrentExtension(); +const UUID = Me.uuid.replace(/@/gi, '_at_').replace(/[^a-z0-9+_-]/gi, '_'); var Shapes = { NONE: 0, LINE: 1, ELLIPSE: 2, RECTANGLE: 3, TEXT: 4, POLYGON: 5, POLYLINE: 6, IMAGE: 7 }; var Transformations = { TRANSLATION: 0, ROTATION: 1, SCALE_PRESERVE: 2, STRETCH: 3, REFLECTION: 4, INVERSION: 5 }; @@ -72,7 +73,7 @@ var DrawingElement = function(params) { // It can be converted into a cairo path as well as a svg element. // See DrawingArea._startDrawing() to know its params. const _DrawingElement = new Lang.Class({ - Name: `${Me.uuid}.DrawingElement`, + Name: `${UUID}-DrawingElement`, _init: function(params) { for (let key in params) @@ -626,7 +627,7 @@ const _DrawingElement = new Lang.Class({ }); const TextElement = new Lang.Class({ - Name: `${Me.uuid}.TextElement`, + Name: `${UUID}-TextElement`, Extends: _DrawingElement, toJSON: function() { @@ -766,7 +767,7 @@ const TextElement = new Lang.Class({ }); const ImageElement = new Lang.Class({ - Name: `${Me.uuid}.ImageElement`, + Name: `${UUID}-ImageElement`, Extends: _DrawingElement, toJSON: function() { diff --git a/extension.js b/extension.js index 23360d7..a6d2744 100644 --- a/extension.js +++ b/extension.js @@ -41,6 +41,7 @@ const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; const GS_VERSION = Config.PACKAGE_VERSION; const HIDE_TIMEOUT_LONG = 2500; // ms, default is 1500 ms +const UUID = Me.uuid.replace(/@/gi, '_at_').replace(/[^a-z0-9+_-]/gi, '_'); // custom Shell.ActionMode, assuming that they are unused const DRAWING_ACTION_MODE = Math.pow(2,14); @@ -53,7 +54,7 @@ function init() { } const Extension = new Lang.Class({ - Name: `${Me.uuid}.Extension`, + Name: `${UUID}-Extension`, _init: function() { Convenience.initTranslations(); @@ -81,7 +82,7 @@ const Extension = new Lang.Class({ // distributes keybinding callbacks to the active area // and handles stylesheet and monitor changes. const AreaManager = new Lang.Class({ - Name: `${Me.uuid}.AreaManager`, + Name: `${UUID}-AreaManager`, _init: function() { this.areas = []; @@ -560,7 +561,7 @@ const AreaManager = new Lang.Class({ // The same as the original, without forcing a ratio of 1. const OsdWindowConstraint = new Lang.Class({ - Name: `${Me.uuid}.OsdWindowConstraint`, + Name: `${UUID}-OsdWindowConstraint`, Extends: OsdWindow.OsdWindowConstraint, vfunc_update_allocation: function(actor, actorBox) { @@ -582,7 +583,7 @@ const OsdWindowConstraint = new Lang.Class({ }); const DrawingIndicator = new Lang.Class({ - Name: `${Me.uuid}.Indicator`, + Name: `${UUID}-Indicator`, _init: function() { let [menuAlignment, dontCreateMenu] = [0, true]; diff --git a/files.js b/files.js index 8334601..1f31691 100644 --- a/files.js +++ b/files.js @@ -31,6 +31,7 @@ const St = imports.gi.St; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); +const UUID = Me.uuid.replace(/@/gi, '_at_').replace(/[^a-z0-9+_-]/gi, '_'); const EXAMPLE_IMAGE_DIRECTORY = Me.dir.get_child('data').get_child('images'); const DEFAULT_USER_IMAGE_LOCATION = GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir'], 'images']); const Clipboard = St.Clipboard.get_default(); @@ -98,7 +99,7 @@ const replaceColor = function (contents, color) { // 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({ - Name: `${Me.uuid}.Image`, + Name: `${UUID}-Image`, _init: function(params) { for (let key in params) @@ -193,7 +194,7 @@ var Image = new Lang.Class({ // Add a gicon generator to Image. It is used with image files and it takes { file, info } as params. const ImageWithGicon = new Lang.Class({ - Name: `${Me.uuid}.ImageWithGicon`, + Name: `${UUID}-ImageWithGicon`, Extends: Image, get displayName() { @@ -247,7 +248,7 @@ const ImageWithGicon = new Lang.Class({ // It is directly generated from a Json object, without an image file. It takes { bytes, displayName, gicon } as params. const ImageFromJson = new Lang.Class({ - Name: `${Me.uuid}.ImageFromJson`, + Name: `${UUID}-ImageFromJson`, Extends: Image, contentType: 'image/svg+xml', @@ -394,7 +395,7 @@ var Images = { // Wrapper around a json file (drawing saves). var Json = new Lang.Class({ - Name: `${Me.uuid}.Json`, + Name: `${UUID}-Json`, _init: function(params) { for (let key in params) diff --git a/helper.js b/helper.js index 70343e4..8616adb 100644 --- a/helper.js +++ b/helper.js @@ -40,11 +40,12 @@ const Tweener = GS_VERSION < '3.33.0' ? imports.ui.tweener : null; const HELPER_ANIMATION_TIME = 0.25; const MEDIA_KEYS_SCHEMA = 'org.gnome.settings-daemon.plugins.media-keys'; const MEDIA_KEYS_KEYS = ['screenshot', 'screenshot-clip', 'area-screenshot', 'area-screenshot-clip']; +const UUID = Me.uuid.replace(/@/gi, '_at_').replace(/[^a-z0-9+_-]/gi, '_'); // DrawingHelper provides the "help osd" (Ctrl + F1) // It uses the same texts as in prefs var DrawingHelper = new Lang.Class({ - Name: `${Me.uuid}.DrawingHelper`, + Name: `${UUID}-DrawingHelper`, Extends: St.ScrollView, _init: function(params, monitor) { diff --git a/menu.js b/menu.js index 2688f27..1753b82 100644 --- a/menu.js +++ b/menu.js @@ -46,6 +46,7 @@ const GS_VERSION = Config.PACKAGE_VERSION; const FONT_FAMILY_STYLE = true; // use 'login-dialog-message-warning' class in order to get GS theme warning color (default: #f57900) const WARNING_COLOR_STYLE_CLASS_NAME = 'login-dialog-message-warning'; +const UUID = Me.uuid.replace(/@/gi, '_at_').replace(/[^a-z0-9+_-]/gi, '_'); const getActor = function(object) { return GS_VERSION < '3.33.0' ? object.actor : object; @@ -139,7 +140,7 @@ var DisplayStrings = { }; var DrawingMenu = new Lang.Class({ - Name: `${Me.uuid}.DrawingMenu`, + Name: `${UUID}-DrawingMenu`, _init: function(area, monitor, drawingTools) { this.area = area; @@ -742,7 +743,7 @@ const updateSubMenuAdjustment = function(itemActor) { // An action button that uses upstream dash item tooltips. const ActionButton = new Lang.Class({ - Name: `${Me.uuid}.DrawingMenuActionButton`, + Name: `${UUID}-DrawingMenuActionButton`, Extends: St.Bin, _labelShowing: false, _resetHoverTimeoutId: 0, @@ -787,7 +788,7 @@ const ActionButton = new Lang.Class({ // based on searchItem.js, https://github.com/leonardo-bartoli/gnome-shell-extension-Recents const Entry = new Lang.Class({ - Name: `${Me.uuid}.DrawingMenuEntry`, + Name: `${UUID}-DrawingMenuEntry`, _init: function(params) { this.params = params; diff --git a/prefs.js b/prefs.js index eb01b24..434c9ab 100644 --- a/prefs.js +++ b/prefs.js @@ -43,11 +43,7 @@ const _GTK = imports.gettext.domain('gtk30').gettext; const MARGIN = 10; const ROWBOX_MARGIN_PARAMS = { margin_top: MARGIN / 2, margin_bottom: MARGIN / 2, margin_left: MARGIN, margin_right: MARGIN }; - -// GTypeName is not sanitized in GS 3.28- -const sanitizeGType = function(name) { - return `Gjs_${name.replace(/@/gi, '_at_').replace(/[^a-z0-9+_-]/gi, '_')}`; -} +const UUID = Me.uuid.replace(/@/gi, '_at_').replace(/[^a-z0-9+_-]/gi, '_'); function init() { Convenience.initTranslations(); @@ -68,8 +64,7 @@ function buildPrefsWidget() { } const TopStack = new GObject.Class({ - Name: `${Me.uuid}.TopStack`, - GTypeName: sanitizeGType(`${Me.uuid}-TopStack`), + Name: `${UUID}-TopStack`, Extends: Gtk.Stack, _init: function(params) { @@ -87,8 +82,7 @@ const TopStack = new GObject.Class({ }); const AboutPage = new GObject.Class({ - Name: `${Me.uuid}.AboutPage`, - GTypeName: sanitizeGType(`${Me.uuid}-AboutPage`), + Name: `${UUID}-AboutPage`, Extends: Gtk.ScrolledWindow, _init: function(params) { @@ -141,8 +135,7 @@ const AboutPage = new GObject.Class({ }); const DrawingPage = new GObject.Class({ - Name: `${Me.uuid}.DrawingPage`, - GTypeName: sanitizeGType(`${Me.uuid}-DrawingPage`), + Name: `${UUID}-DrawingPage`, Extends: Gtk.ScrolledWindow, _init: function(params) { @@ -394,8 +387,7 @@ const DrawingPage = new GObject.Class({ }); const PrefsPage = new GObject.Class({ - Name: `${Me.uuid}.PrefsPage`, - GTypeName: sanitizeGType(`${Me.uuid}-PrefsPage`), + Name: `${UUID}-PrefsPage`, Extends: Gtk.ScrolledWindow, _init: function(params) { @@ -508,8 +500,7 @@ const PrefsPage = new GObject.Class({ }); const Frame = new GObject.Class({ - Name: `${Me.uuid}.Frame`, - GTypeName: sanitizeGType(`${Me.uuid}-Frame`), + Name: `${UUID}-Frame`, Extends: Gtk.Frame, _init: function(params) { @@ -524,8 +515,7 @@ const Frame = new GObject.Class({ }); const PrefRow = new GObject.Class({ - Name: `${Me.uuid}.PrefRow`, - GTypeName: sanitizeGType(`${Me.uuid}-PrefRow`), + Name: `${UUID}-PrefRow`, Extends: Gtk.ListBoxRow, _init: function(params) { @@ -569,8 +559,7 @@ const PrefRow = new GObject.Class({ }); const PixelSpinButton = new GObject.Class({ - Name: `${Me.uuid}.PixelSpinButton`, - GTypeName: sanitizeGType(`${Me.uuid}-PixelSpinButton`), + Name: `${UUID}-PixelSpinButton`, Extends: Gtk.SpinButton, Properties: { 'range': GObject.param_spec_variant('range', 'range', 'GSettings range', @@ -609,8 +598,7 @@ const PixelSpinButton = new GObject.Class({ // A color button that can be easily bound with a color string setting. const ColorStringButton = new GObject.Class({ - Name: `${Me.uuid}.ColorStringButton`, - GTypeName: sanitizeGType(`${Me.uuid}-ColorStringButton`), + Name: `${UUID}-ColorStringButton`, Extends: Gtk.ColorButton, Properties: { 'color-string': GObject.ParamSpec.string('color-string', 'colorString', 'A string that describes the color', @@ -642,8 +630,7 @@ const ColorStringButton = new GObject.Class({ }); const FileChooserButton = new GObject.Class({ - Name: `${Me.uuid}.FileChooserButton`, - GTypeName: sanitizeGType(`${Me.uuid}-FileChooserButton`), + Name: `${UUID}-FileChooserButton`, Extends: Gtk.FileChooserButton, Properties: { 'location': GObject.ParamSpec.string('location', 'location', 'location', @@ -674,8 +661,7 @@ const FileChooserButton = new GObject.Class({ // this code comes from Sticky Notes View by Sam Bull, https://extensions.gnome.org/extension/568/notes/ const KeybindingsWidget = new GObject.Class({ - Name: `${Me.uuid}.KeybindingsWidget`, - GTypeName: sanitizeGType(`${Me.uuid}-KeybindingsWidget`), + Name: `${UUID}-KeybindingsWidget`, Extends: Gtk.Box, _init: function(settingKeys, settings) { From 6374cc8c47659a568b48f05d70707842a9edaa9e Mon Sep 17 00:00:00 2001 From: abakkk Date: Wed, 30 Sep 2020 19:16:55 +0200 Subject: [PATCH 2/8] motion timeout Add intermediate points to make quick free drawings smoother. Do not redisplay the area at this step (crashes). Use device.get_coords rather than global.get_pointer because the later return rounded (floor) values. Fix #45. --- area.js | 34 ++++++++++++++++++++++++++++++++++ elements.js | 12 ++++++++++++ 2 files changed, 46 insertions(+) diff --git a/area.js b/area.js index f629462..2956c0c 100644 --- a/area.js +++ b/area.js @@ -47,6 +47,7 @@ const pgettext = imports.gettext.domain(Me.metadata['gettext-domain']).pgettext; const CAIRO_DEBUG_EXTENDS = false; const SVG_DEBUG_EXTENDS = false; +const MOTION_TIME = 1; // ms, time accuracy for free drawing, max is about 33 ms. The lower it is, the smoother the drawing is. const TEXT_CURSOR_TIME = 600; // ms const ELEMENT_GRABBER_TIME = 80; // ms, default is about 16 ms const GRID_TILES_HORIZONTAL_NUMBER = 30; @@ -656,6 +657,11 @@ var DrawingArea = new Lang.Class({ } this.motionHandler = this.connect('motion-event', (actor, event) => { + if (this.motionTimeout) { + GLib.source_remove(this.motionTimeout); + this.motionTimeout = null; + } + if (this.spaceKeyPressed) return; @@ -665,6 +671,30 @@ var DrawingArea = new Lang.Class({ return; let controlPressed = event.has_control_modifier(); this._updateDrawing(x, y, controlPressed); + + if (this.currentTool == Shapes.NONE) { + let device = event.get_device(); + let sequence = event.get_event_sequence(); + + // Minimum time between two motion events is about 33 ms. + // Add intermediate points to make quick free drawings smoother. + this.motionTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, MOTION_TIME, () => { + let [success, coords] = device.get_coords(sequence); + if (!success) + return GLib.SOURCE_CONTINUE; + + let [s, x, y] = this.transform_stage_point(coords.x, coords.y); + if (!s) + return GLib.SOURCE_CONTINUE; + + // Important: do not call this._updateDrawing because the area MUST NOT BE REDISPLAYED at this step. + // It would lead to critical issues (bad performances and shell crashes). + // The area will be redisplayed, including the intermediate points, at the next motion event. + this.currentElement.addIntermediatePoint(x, y, controlPressed); + + return GLib.SOURCE_CONTINUE; + }); + } }); }, @@ -679,6 +709,10 @@ var DrawingArea = new Lang.Class({ }, _stopDrawing: function() { + if (this.motionTimeout) { + GLib.source_remove(this.motionTimeout); + this.motionTimeout = null; + } if (this.motionHandler) { this.disconnect(this.motionHandler); this.motionHandler = null; diff --git a/elements.js b/elements.js index 509e219..c750025 100644 --- a/elements.js +++ b/elements.js @@ -62,6 +62,7 @@ const MIN_REFLECTION_LINE_LENGTH = 10; // px const MIN_TRANSLATION_DISTANCE = 1; // px const MIN_ROTATION_ANGLE = Math.PI / 1000; // rad const MIN_DRAWING_SIZE = 3; // px +const MIN_INTERMEDIATE_POINT_DISTANCE = 1; // px, the higher it is, the fewer points there will be var DrawingElement = function(params) { return params.shape == Shapes.TEXT ? new TextElement(params) : @@ -422,6 +423,17 @@ const _DrawingElement = new Lang.Class({ } }, + // For free drawing only. + addIntermediatePoint: function(x, y, transform) { + let points = this.points; + if (getNearness(points[points.length - 1], [x, y], MIN_INTERMEDIATE_POINT_DISTANCE)) + return; + + points.push([x, y]); + if (transform) + this._smooth(points.length - 1); + }, + startDrawing: function(startX, startY) { this.points.push([startX, startY]); From e218819edd4038c28eca7ea2c0b18f39ce5b550e Mon Sep 17 00:00:00 2001 From: abakkk Date: Sun, 4 Oct 2020 17:20:03 +0200 Subject: [PATCH 3/8] make transformations undoable/redoable --- area.js | 26 ++++++++++++++++++++++++-- elements.js | 40 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/area.js b/area.js index 2956c0c..eaf318a 100644 --- a/area.js +++ b/area.js @@ -546,6 +546,10 @@ var DrawingArea = new Lang.Class({ this._redisplay(); } + if (duplicate) + // For undo, ignore both the transformations inherited from the duplicated element + // and the current transformation. + this.grabbedElement.makeAllTransformationsNotUndoable(); this.motionHandler = this.connect('motion-event', (actor, event) => { if (this.spaceKeyPressed) @@ -907,18 +911,36 @@ var DrawingArea = new Lang.Class({ deleteLastElement: function() { this._stopAll(); this.elements.pop(); + + if (this.elements.length) + this.elements[this.elements.length - 1].resetUndoneTransformations(); + this._redisplay(); }, undo: function() { - if (this.elements.length > 0) + if (!this.elements.length) + return; + + let success = this.elements[this.elements.length - 1].undoTransformation(); + if (!success) { this.undoneElements.push(this.elements.pop()); + if (this.elements.length) + this.elements[this.elements.length - 1].resetUndoneTransformations(); + } + this._redisplay(); }, redo: function() { - if (this.undoneElements.length > 0) + let success = false; + + if (this.elements.length) + success = this.elements[this.elements.length - 1].redoTransformation(); + + if (!success && this.undoneElements.length > 0) this.elements.push(this.undoneElements.pop()); + this._redisplay(); }, diff --git a/elements.js b/elements.js index c750025..ad80509 100644 --- a/elements.js +++ b/elements.js @@ -458,7 +458,7 @@ const _DrawingElement = new Lang.Class({ return; let center = this._getOriginalCenter(); - this.transformations[0] = { type: Transformations.ROTATION, + this.transformations[0] = { type: Transformations.ROTATION, notUndoable: true, angle: getAngle(center[0], center[1], points[points.length - 1][0], points[points.length - 1][1], x, y) }; } else if (this.shape == Shapes.ELLIPSE && transform) { @@ -467,7 +467,7 @@ const _DrawingElement = new Lang.Class({ points[2] = [x, y]; let center = this._getOriginalCenter(); - this.transformations[0] = { type: Transformations.ROTATION, + this.transformations[0] = { type: Transformations.ROTATION, notUndoable: true, angle: getAngle(center[0], center[1], center[0] + 1, center[1], x, y) }; } else if (this.shape == Shapes.POLYGON || this.shape == Shapes.POLYLINE) { @@ -586,6 +586,42 @@ const _DrawingElement = new Lang.Class({ } }, + undoTransformation: function() { + if (this.transformations && this.transformations.length) { + // Do not undo initial transformations (transformations made during the drawing step). + if (this.lastTransformation.notUndoable) + return false; + + if (!this._undoneTransformations) + this._undoneTransformations = []; + this._undoneTransformations.push(this.transformations.pop()); + + return true; + } + + return false; + }, + + redoTransformation: function() { + if (this._undoneTransformations && this._undoneTransformations.length) { + if (!this.transformations) + this.transformations = []; + this.transformations.push(this._undoneTransformations.pop()); + + return true; + } + + return false; + }, + + resetUndoneTransformations: function() { + delete this._undoneTransformations; + }, + + makeAllTransformationsNotUndoable: function() { + this.transformations.forEach(transformation => transformation.notUndoable = true); + }, + // The figure rotation center before transformations (original). // this.textWidth is computed during Cairo building. _getOriginalCenter: function() { From ccf928c04803e8b0073ec8d995b40ca433a0f032 Mon Sep 17 00:00:00 2001 From: abakkk Date: Sun, 4 Oct 2020 20:55:06 +0200 Subject: [PATCH 4/8] make smooth transformations undoable/redoable --- elements.js | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/elements.js b/elements.js index ad80509..1d3e8cb 100644 --- a/elements.js +++ b/elements.js @@ -31,7 +31,7 @@ const Me = imports.misc.extensionUtils.getCurrentExtension(); const UUID = Me.uuid.replace(/@/gi, '_at_').replace(/[^a-z0-9+_-]/gi, '_'); var Shapes = { NONE: 0, LINE: 1, ELLIPSE: 2, RECTANGLE: 3, TEXT: 4, POLYGON: 5, POLYLINE: 6, IMAGE: 7 }; -var Transformations = { TRANSLATION: 0, ROTATION: 1, SCALE_PRESERVE: 2, STRETCH: 3, REFLECTION: 4, INVERSION: 5 }; +var Transformations = { TRANSLATION: 0, ROTATION: 1, SCALE_PRESERVE: 2, STRETCH: 3, REFLECTION: 4, INVERSION: 5, SMOOTH: 100 }; var getAllFontFamilies = function() { return PangoCairo.font_map_get_default().list_families().map(fontFamily => fontFamily.get_name()).sort((a,b) => a.localeCompare(b)); @@ -123,7 +123,7 @@ const _DrawingElement = new Lang.Class({ fill: this.fill, fillRule: this.fillRule, eraser: this.eraser, - transformations: this.transformations, + transformations: this.transformations.filter(transformation => transformation.type != Transformations.SMOOTH), points: this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100]) }; }, @@ -402,9 +402,16 @@ const _DrawingElement = new Lang.Class({ }, smoothAll: function() { - for (let i = 0; i < this.points.length; i++) { + let oldPoints = this.points.slice(); + + for (let i = 0; i < this.points.length; i++) this._smooth(i); - } + + let newPoints = this.points.slice(); + + this.transformations.push({ type: Transformations.SMOOTH, + undo: () => this.points = oldPoints, + redo: () => this.points = newPoints }); }, addPoint: function() { @@ -594,7 +601,12 @@ const _DrawingElement = new Lang.Class({ if (!this._undoneTransformations) this._undoneTransformations = []; - this._undoneTransformations.push(this.transformations.pop()); + + let transformation = this.transformations.pop(); + if (transformation.type == Transformations.SMOOTH) + transformation.undo(); + + this._undoneTransformations.push(transformation); return true; } @@ -606,7 +618,12 @@ const _DrawingElement = new Lang.Class({ if (this._undoneTransformations && this._undoneTransformations.length) { if (!this.transformations) this.transformations = []; - this.transformations.push(this._undoneTransformations.pop()); + + let transformation = this._undoneTransformations.pop(); + if (transformation.type == Transformations.SMOOTH) + transformation.redo(); + + this.transformations.push(transformation); return true; } From 236e4db2360d0a241fdcf55e8078dc0d0335438f Mon Sep 17 00:00:00 2001 From: abakkk Date: Sun, 4 Oct 2020 22:17:13 +0200 Subject: [PATCH 5/8] transformation notUndoable => undoable The `undoable` transformation property is not preserved when the element is "stringified". So transformations cannot be undone, once the element is loaded from JSON. --- area.js | 27 +++++++++++++-------------- elements.js | 27 ++++++++++++--------------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/area.js b/area.js index eaf318a..70e2175 100644 --- a/area.js +++ b/area.js @@ -537,20 +537,17 @@ var DrawingArea = new Lang.Class({ this.grabbedElement = copy; } + let undoable = !duplicate; + if (this.currentTool == Manipulations.MOVE) - this.grabbedElement.startTransformation(startX, startY, controlPressed ? Transformations.ROTATION : Transformations.TRANSLATION); + this.grabbedElement.startTransformation(startX, startY, controlPressed ? Transformations.ROTATION : Transformations.TRANSLATION, undoable); else if (this.currentTool == Manipulations.RESIZE) - this.grabbedElement.startTransformation(startX, startY, controlPressed ? Transformations.STRETCH : Transformations.SCALE_PRESERVE); + this.grabbedElement.startTransformation(startX, startY, controlPressed ? Transformations.STRETCH : Transformations.SCALE_PRESERVE, undoable); else if (this.currentTool == Manipulations.MIRROR) { - this.grabbedElement.startTransformation(startX, startY, controlPressed ? Transformations.INVERSION : Transformations.REFLECTION); + this.grabbedElement.startTransformation(startX, startY, controlPressed ? Transformations.INVERSION : Transformations.REFLECTION, undoable); this._redisplay(); } - if (duplicate) - // For undo, ignore both the transformations inherited from the duplicated element - // and the current transformation. - this.grabbedElement.makeAllTransformationsNotUndoable(); - this.motionHandler = this.connect('motion-event', (actor, event) => { if (this.spaceKeyPressed) return; @@ -565,28 +562,30 @@ var DrawingArea = new Lang.Class({ }, _updateTransforming: function(x, y, controlPressed) { + let undoable = this.grabbedElement.lastTransformation.undoable || false; + if (controlPressed && this.grabbedElement.lastTransformation.type == Transformations.TRANSLATION) { this.grabbedElement.stopTransformation(); - this.grabbedElement.startTransformation(x, y, Transformations.ROTATION); + this.grabbedElement.startTransformation(x, y, Transformations.ROTATION, undoable); } else if (!controlPressed && this.grabbedElement.lastTransformation.type == Transformations.ROTATION) { this.grabbedElement.stopTransformation(); - this.grabbedElement.startTransformation(x, y, Transformations.TRANSLATION); + this.grabbedElement.startTransformation(x, y, Transformations.TRANSLATION, undoable); } if (controlPressed && this.grabbedElement.lastTransformation.type == Transformations.SCALE_PRESERVE) { this.grabbedElement.stopTransformation(); - this.grabbedElement.startTransformation(x, y, Transformations.STRETCH); + this.grabbedElement.startTransformation(x, y, Transformations.STRETCH, undoable); } else if (!controlPressed && this.grabbedElement.lastTransformation.type == Transformations.STRETCH) { this.grabbedElement.stopTransformation(); - this.grabbedElement.startTransformation(x, y, Transformations.SCALE_PRESERVE); + this.grabbedElement.startTransformation(x, y, Transformations.SCALE_PRESERVE, undoable); } if (controlPressed && this.grabbedElement.lastTransformation.type == Transformations.REFLECTION) { this.grabbedElement.transformations.pop(); - this.grabbedElement.startTransformation(x, y, Transformations.INVERSION); + this.grabbedElement.startTransformation(x, y, Transformations.INVERSION, undoable); } else if (!controlPressed && this.grabbedElement.lastTransformation.type == Transformations.INVERSION) { this.grabbedElement.transformations.pop(); - this.grabbedElement.startTransformation(x, y, Transformations.REFLECTION); + this.grabbedElement.startTransformation(x, y, Transformations.REFLECTION, undoable); } this.grabbedElement.updateTransformation(x, y); diff --git a/elements.js b/elements.js index 1d3e8cb..71599be 100644 --- a/elements.js +++ b/elements.js @@ -123,7 +123,8 @@ const _DrawingElement = new Lang.Class({ fill: this.fill, fillRule: this.fillRule, eraser: this.eraser, - transformations: this.transformations.filter(transformation => transformation.type != Transformations.SMOOTH), + transformations: this.transformations.filter(transformation => transformation.type != Transformations.SMOOTH) + .map(transformation => Object.assign({}, transformation, { undoable: undefined })), points: this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100]) }; }, @@ -409,7 +410,7 @@ const _DrawingElement = new Lang.Class({ let newPoints = this.points.slice(); - this.transformations.push({ type: Transformations.SMOOTH, + this.transformations.push({ type: Transformations.SMOOTH, undoable: true, undo: () => this.points = oldPoints, redo: () => this.points = newPoints }); }, @@ -465,7 +466,7 @@ const _DrawingElement = new Lang.Class({ return; let center = this._getOriginalCenter(); - this.transformations[0] = { type: Transformations.ROTATION, notUndoable: true, + this.transformations[0] = { type: Transformations.ROTATION, angle: getAngle(center[0], center[1], points[points.length - 1][0], points[points.length - 1][1], x, y) }; } else if (this.shape == Shapes.ELLIPSE && transform) { @@ -474,7 +475,7 @@ const _DrawingElement = new Lang.Class({ points[2] = [x, y]; let center = this._getOriginalCenter(); - this.transformations[0] = { type: Transformations.ROTATION, notUndoable: true, + this.transformations[0] = { type: Transformations.ROTATION, angle: getAngle(center[0], center[1], center[0] + 1, center[1], x, y) }; } else if (this.shape == Shapes.POLYGON || this.shape == Shapes.POLYLINE) { @@ -500,18 +501,18 @@ const _DrawingElement = new Lang.Class({ this.transformations.shift(); }, - startTransformation: function(startX, startY, type) { + startTransformation: function(startX, startY, type, undoable) { if (type == Transformations.TRANSLATION) - this.transformations.push({ startX: startX, startY: startY, type: type, slideX: 0, slideY: 0 }); + this.transformations.push({ startX, startY, type, undoable, slideX: 0, slideY: 0 }); else if (type == Transformations.ROTATION) - this.transformations.push({ startX: startX, startY: startY, type: type, angle: 0 }); + this.transformations.push({ startX, startY, type, undoable, angle: 0 }); else if (type == Transformations.SCALE_PRESERVE || type == Transformations.STRETCH) - this.transformations.push({ startX: startX, startY: startY, type: type, scaleX: 1, scaleY: 1, angle: 0 }); + this.transformations.push({ startX, startY, type, undoable, scaleX: 1, scaleY: 1, angle: 0 }); else if (type == Transformations.REFLECTION) - this.transformations.push({ startX: startX, startY: startY, endX: startX, endY: startY, type: type, + this.transformations.push({ startX, startY, endX: startX, endY: startY, type, undoable, scaleX: 1, scaleY: 1, slideX: 0, slideY: 0, angle: 0 }); else if (type == Transformations.INVERSION) - this.transformations.push({ startX: startX, startY: startY, endX: startX, endY: startY, type: type, + this.transformations.push({ startX, startY, endX: startX, endY: startY, type, undoable, scaleX: -1, scaleY: -1, slideX: startX, slideY: startY, angle: Math.PI + Math.atan(startY / (startX || 1)) }); @@ -596,7 +597,7 @@ const _DrawingElement = new Lang.Class({ undoTransformation: function() { if (this.transformations && this.transformations.length) { // Do not undo initial transformations (transformations made during the drawing step). - if (this.lastTransformation.notUndoable) + if (!this.lastTransformation.undoable) return false; if (!this._undoneTransformations) @@ -635,10 +636,6 @@ const _DrawingElement = new Lang.Class({ delete this._undoneTransformations; }, - makeAllTransformationsNotUndoable: function() { - this.transformations.forEach(transformation => transformation.notUndoable = true); - }, - // The figure rotation center before transformations (original). // this.textWidth is computed during Cairo building. _getOriginalCenter: function() { From 28def0059d1991a7e0d23054954a47cc48a742ea Mon Sep 17 00:00:00 2001 From: abakkk Date: Sun, 4 Oct 2020 22:48:40 +0200 Subject: [PATCH 6/8] finish undo/redo rework * "Undo(Redo) last brushstroke" -> "Undo(Redo)". * Sync `_updateActionSensitivity` menu method with the new undo/redo behavior. * Smooth button action is no longer destructive. * Clean undone smooth transformations when doing a new smooth transformation. --- elements.js | 7 +++++++ locale/draw-on-your-screen.pot | 12 +++--------- menu.js | 7 +++---- ....shell.extensions.draw-on-your-screen.gschema.xml | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/elements.js b/elements.js index 71599be..2bbcaeb 100644 --- a/elements.js +++ b/elements.js @@ -413,6 +413,9 @@ const _DrawingElement = new Lang.Class({ this.transformations.push({ type: Transformations.SMOOTH, undoable: true, undo: () => this.points = oldPoints, redo: () => this.points = newPoints }); + + if (this._undoneTransformations) + this._undoneTransformations = this._undoneTransformations.filter(transformation => transformation.type != Transformations.SMOOTH); }, addPoint: function() { @@ -636,6 +639,10 @@ const _DrawingElement = new Lang.Class({ delete this._undoneTransformations; }, + get canUndo() { + return this._undoneTransformations && this._undoneTransformations.length ? true : false; + }, + // The figure rotation center before transformations (original). // this.textWidth is computed during Cairo building. _getOriginalCenter: function() { diff --git a/locale/draw-on-your-screen.pot b/locale/draw-on-your-screen.pot index 3ad0099..5e87946 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-19 15:32+0200\n" +"POT-Creation-Date: 2020-10-04 22:45+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -265,12 +265,6 @@ msgctxt "drawing-tool" msgid "Mirror" msgstr "" -msgid "Undo" -msgstr "" - -msgid "Redo" -msgstr "" - msgid "Erase" msgstr "" @@ -586,7 +580,7 @@ msgstr "" msgid "Add images from the clipboard" msgstr "" -msgid "Redo last brushstroke" +msgid "Redo" msgstr "" msgid "Save drawing" @@ -710,5 +704,5 @@ msgstr "" msgid "Square drawing area" msgstr "" -msgid "Undo last brushstroke" +msgid "Undo" msgstr "" diff --git a/menu.js b/menu.js index 1753b82..4c1c8ea 100644 --- a/menu.js +++ b/menu.js @@ -230,12 +230,11 @@ var DrawingMenu = new Lang.Class({ this.menu.removeAll(); let groupItem = new PopupMenu.PopupBaseMenuItem({ reactive: false, can_focus: false, style_class: 'draw-on-your-screen-menu-group-item' }); - this.undoButton = new ActionButton(_("Undo"), 'edit-undo-symbolic', this.area.undo.bind(this.area), this._updateActionSensitivity.bind(this)); - this.redoButton = new ActionButton(_("Redo"), 'edit-redo-symbolic', this.area.redo.bind(this.area), this._updateActionSensitivity.bind(this)); + this.undoButton = new ActionButton(getSummary('undo'), 'edit-undo-symbolic', this.area.undo.bind(this.area), this._updateActionSensitivity.bind(this)); + this.redoButton = new ActionButton(getSummary('redo'), 'edit-redo-symbolic', this.area.redo.bind(this.area), this._updateActionSensitivity.bind(this)); this.eraseButton = new ActionButton(_("Erase"), 'edit-clear-all-symbolic', this.area.deleteLastElement.bind(this.area), this._updateActionSensitivity.bind(this)); this.smoothButton = new ActionButton(_("Smooth"), Files.Icons.SMOOTH, this.area.smoothLastElement.bind(this.area), this._updateActionSensitivity.bind(this)); this.eraseButton.child.add_style_class_name('draw-on-your-screen-menu-destructive-button'); - this.smoothButton.child.add_style_class_name('draw-on-your-screen-menu-destructive-button'); getActor(groupItem).add_child(this.undoButton); getActor(groupItem).add_child(this.redoButton); getActor(groupItem).add_child(this.eraseButton); @@ -309,7 +308,7 @@ var DrawingMenu = new Lang.Class({ _updateActionSensitivity: function() { this.undoButton.child.reactive = this.area.elements.length > 0; - this.redoButton.child.reactive = this.area.undoneElements.length > 0; + this.redoButton.child.reactive = this.area.undoneElements.length > 0 || (this.area.elements.length && this.area.elements[this.area.elements.length - 1].canUndo); this.eraseButton.child.reactive = this.area.elements.length > 0; this.smoothButton.child.reactive = this.area.elements.length > 0 && this.area.elements[this.area.elements.length - 1].shape == this.drawingTools.NONE; this.saveButton.child.reactive = this.area.elements.length > 0; 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 e13b393..1b26430 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 @@ -170,7 +170,7 @@ ["<Primary><Shift>z"] - Redo last brushstroke + Redo ["<Primary>s"] @@ -340,7 +340,7 @@ ["<Primary>z"] - Undo last brushstroke + Undo From ff350130a4f3d3cdc5db2cbbb68e9efa8cd40b5e Mon Sep 17 00:00:00 2001 From: abakkk Date: Mon, 5 Oct 2020 16:46:16 +0200 Subject: [PATCH 7/8] rework of key event handling * Handle stage key events only when the area is reactive. Any reason to handle key events in other cases? * Move `Escape` key handling to the stage `key-press-event` handler. So leaving the drawing mode with `Escape` is still possible when the overview mode has been entered inadvertently. --- area.js | 75 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/area.js b/area.js index 70e2175..4eb284b 100644 --- a/area.js +++ b/area.js @@ -395,8 +395,14 @@ var DrawingArea = new Lang.Class({ }, _onStageKeyPressed: function(actor, event) { - if (event.get_key_symbol() == Clutter.KEY_space) + if (event.get_key_symbol() == Clutter.KEY_Escape) { + if (this.helper.visible) + this.toggleHelp(); + else + this.emit('leave-drawing-mode'); + } else if (event.get_key_symbol() == Clutter.KEY_space) { this.spaceKeyPressed = true; + } return Clutter.EVENT_PROPAGATE; }, @@ -409,38 +415,28 @@ var DrawingArea = new Lang.Class({ }, _onKeyPressed: function(actor, event) { - if (this.currentElement && this.currentElement.shape == Shapes.LINE) { - if (event.get_key_symbol() == Clutter.KEY_Return || - event.get_key_symbol() == Clutter.KEY_KP_Enter || - event.get_key_symbol() == Clutter.KEY_Control_L) { - if (this.currentElement.points.length == 2) - // Translators: %s is a key label - this.emit('show-osd', Files.Icons.ARC, _("Press %s to get\na fourth control point") - .format(Gtk.accelerator_get_label(Clutter.KEY_Return, 0)), "", -1, true); - this.currentElement.addPoint(); - this.updatePointerCursor(true); - this._redisplay(); - return Clutter.EVENT_STOP; - } else { - return Clutter.EVENT_PROPAGATE; - } - + if (this.currentElement && this.currentElement.shape == Shapes.LINE && + (event.get_key_symbol() == Clutter.KEY_Return || + event.get_key_symbol() == Clutter.KEY_KP_Enter || + event.get_key_symbol() == Clutter.KEY_Control_L)) { + + if (this.currentElement.points.length == 2) + // Translators: %s is a key label + this.emit('show-osd', Files.Icons.ARC, _("Press %s to get\na fourth control point") + .format(Gtk.accelerator_get_label(Clutter.KEY_Return, 0)), "", -1, true); + this.currentElement.addPoint(); + this.updatePointerCursor(true); + this._redisplay(); + return Clutter.EVENT_STOP; } else if (this.currentElement && (this.currentElement.shape == Shapes.POLYGON || this.currentElement.shape == Shapes.POLYLINE) && (event.get_key_symbol() == Clutter.KEY_Return || event.get_key_symbol() == Clutter.KEY_KP_Enter)) { + this.currentElement.addPoint(); return Clutter.EVENT_STOP; - - } else if (event.get_key_symbol() == Clutter.KEY_Escape) { - if (this.helper.visible) - this.toggleHelp(); - else - this.emit('leave-drawing-mode'); - return Clutter.EVENT_STOP; - - } else { - return Clutter.EVENT_PROPAGATE; } + + return Clutter.EVENT_PROPAGATE; }, _onScroll: function(actor, event) { @@ -1186,6 +1182,21 @@ var DrawingArea = new Lang.Class({ this.toggleHelp(); if (this.textEntry && this.reactive) this.textEntry.grab_key_focus(); + + if (this.reactive) { + this.stageKeyPressedHandler = global.stage.connect('key-press-event', this._onStageKeyPressed.bind(this)); + this.stageKeyReleasedHandler = global.stage.connect('key-release-event', this._onStageKeyReleased.bind(this)); + } else { + if (this.stageKeyPressedHandler) { + global.stage.disconnect(this.stageKeyPressedHandler); + this.stageKeyPressedHandler = null; + } + if (this.stageKeyReleasedHandler) { + global.stage.disconnect(this.stageKeyReleasedHandler); + this.stageKeyReleasedHandler = null; + } + this.spaceKeyPressed = false; + } }, _onDestroy: function() { @@ -1200,8 +1211,6 @@ var DrawingArea = new Lang.Class({ }, enterDrawingMode: function() { - this.stageKeyPressedHandler = global.stage.connect('key-press-event', this._onStageKeyPressed.bind(this)); - this.stageKeyReleasedHandler = global.stage.connect('key-release-event', this._onStageKeyReleased.bind(this)); this.keyPressedHandler = this.connect('key-press-event', this._onKeyPressed.bind(this)); this.buttonPressedHandler = this.connect('button-press-event', this._onButtonPressed.bind(this)); this.keyboardPopupMenuHandler = this.connect('popup-menu', this._onKeyboardPopupMenu.bind(this)); @@ -1210,14 +1219,6 @@ var DrawingArea = new Lang.Class({ }, leaveDrawingMode: function(save, erase) { - if (this.stageKeyPressedHandler) { - global.stage.disconnect(this.stageKeyPressedHandler); - this.stageKeyPressedHandler = null; - } - if (this.stageKeyReleasedHandler) { - global.stage.disconnect(this.stageKeyReleasedHandler); - this.stageKeyReleasedHandler = null; - } if (this.keyPressedHandler) { this.disconnect(this.keyPressedHandler); this.keyPressedHandler = null; From 42eff0fa054fe5159c7972777af1579e1ff74c9f Mon Sep 17 00:00:00 2001 From: abakkk Date: Tue, 6 Oct 2020 17:20:59 +0200 Subject: [PATCH 8/8] version -> 8.1 --- NEWS | 7 +++++++ metadata.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 0e706ab..65de969 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,10 @@ +v8.1 - October 2020 +=================== + +* Fix unsanitized GType names #46 +* Quick free drawings are smoother #45 +* Transformations are undoable/redoable + v8 - September 2020 =================== diff --git a/metadata.json b/metadata.json index fd0f6ae..7846d9c 100644 --- a/metadata.json +++ b/metadata.json @@ -18,5 +18,5 @@ "3.36", "3.38" ], - "version": 8 + "version": 8.1 }