diff --git a/NEWS b/NEWS index 65de969..408c2eb 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,9 @@ +v9 - October 2020 +================= + +* Split the drawing area in several layers (performance) +* Start a new line with "Shift + Enter" (text tool) + v8.1 - October 2020 =================== diff --git a/area.js b/area.js index 4eb284b..380da2f 100644 --- a/area.js +++ b/area.js @@ -45,8 +45,6 @@ const Menu = Me.imports.menu; const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; 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 @@ -83,12 +81,47 @@ const getColorFromString = function(string, fallback) { return color; }; -// DrawingArea is the widget in which we draw, thanks to Cairo. -// It creates and manages a DrawingElement for each "brushstroke". +// Drawing layers are the proper drawing area widgets (painted thanks to Cairo). +const DrawingLayer = new Lang.Class({ + Name: `${UUID}-DrawingLayer`, + Extends: St.DrawingArea, + + _init: function(repaintFunction, getHasImageFunction) { + this._repaint = repaintFunction; + this._getHasImage = getHasImageFunction || (() => false); + this.parent(); + }, + + // Bind the size of layers and layer container. + vfunc_parent_set: function() { + this.clear_constraints(); + + if (this.get_parent()) + this.add_constraint(new Clutter.BindConstraint({ coordinate: Clutter.BindCoordinate.SIZE, source: this.get_parent() })); + }, + + vfunc_repaint: function() { + let cr = this.get_context(); + + try { + this._repaint(cr); + } catch(e) { + logError(e, "An error occured while painting"); + } + + cr.$dispose(); + if (this._getHasImage()) + System.gc(); + } +}); + +// Darwing area is a container that manages drawing elements and drawing layers. +// There is a drawing element for each "brushstroke". +// There is a separated layer for the current element so only the current element is redisplayed when drawing. // It handles pointer/mouse/(touch?) events and some keyboard events. var DrawingArea = new Lang.Class({ Name: `${UUID}-DrawingArea`, - Extends: St.DrawingArea, + Extends: St.Widget, Signals: { 'show-osd': { param_types: [Gio.Icon.$gtype, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_DOUBLE, GObject.TYPE_BOOLEAN] }, 'update-action-mode': {}, 'leave-drawing-mode': {} }, @@ -98,6 +131,18 @@ var DrawingArea = new Lang.Class({ this.monitor = monitor; this.helper = helper; + this.layerContainer = new St.Widget({ width: monitor.width, height: monitor.height }); + this.add_child(this.layerContainer); + this.add_child(this.helper); + + this.backLayer = new DrawingLayer(this._repaintBack.bind(this), this._getHasImageBack.bind(this)); + this.layerContainer.add_child(this.backLayer); + this.foreLayer = new DrawingLayer(this._repaintFore.bind(this), this._getHasImageFore.bind(this)); + this.layerContainer.add_child(this.foreLayer); + this.gridLayer = new DrawingLayer(this._repaintGrid.bind(this)); + this.gridLayer.hide(); + this.layerContainer.add_child(this.gridLayer); + this.elements = []; this.undoneElements = []; this.currentElement = null; @@ -113,7 +158,6 @@ var DrawingArea = new Lang.Class({ this.currentLineCap = Cairo.LineCap.ROUND; this.currentFillRule = Cairo.FillRule.WINDING; this.isSquareArea = false; - this.hasGrid = false; this.hasBackground = false; this.textHasCursor = false; this.dashedLine = false; @@ -225,25 +269,6 @@ var DrawingArea = new Lang.Class({ return this._fontFamilies; }, - vfunc_repaint: function() { - let cr = this.get_context(); - - try { - this._repaint(cr); - } catch(e) { - logError(e, "An error occured while painting"); - } - - cr.$dispose(); - if (this.elements.some(element => element.shape == Shapes.IMAGE) || this.currentElement && this.currentElement.shape == Shapes.IMAGE) - System.gc(); - }, - - _redisplay: function() { - // force area to emit 'repaint' - this.queue_repaint(); - }, - _onDrawingSettingsChanged: function() { this.palettes = Me.drawingSettings.get_value('palettes').deep_unpack(); if (!this.colors) { @@ -284,12 +309,7 @@ var DrawingArea = new Lang.Class({ } }, - _repaint: function(cr) { - if (CAIRO_DEBUG_EXTENDS) { - cr.scale(0.5, 0.5); - cr.translate(this.monitor.width, this.monitor.height); - } - + _repaintBack: function(cr) { for (let i = 0; i < this.elements.length; i++) { cr.save(); @@ -309,41 +329,72 @@ var DrawingArea = new Lang.Class({ cr.restore(); } - if (this.currentElement) { - cr.save(); + if (this.currentElement && this.currentElement.eraser) { this.currentElement.buildCairo(cr, { showTextCursor: this.textHasCursor, showTextRectangle: this.currentElement.shape != Shapes.TEXT || !this.isWriting, dummyStroke: this.currentElement.fill && this.currentElement.line.lineWidth == 0 }); - cr.stroke(); - cr.restore(); } + }, + + _repaintFore: function(cr) { + if (!this.currentElement || this.currentElement.eraser) + return; - if (this.reactive && this.hasGrid) { - cr.save(); - Clutter.cairo_set_source_color(cr, this.gridColor); - - let [gridX, gridY] = [0, 0]; - while (gridX < this.monitor.width / 2) { - cr.setLineWidth((gridX / this.gridLineSpacing) % 5 ? this.gridLineWidth / 2 : this.gridLineWidth); - cr.moveTo(this.monitor.width / 2 + gridX, 0); - cr.lineTo(this.monitor.width / 2 + gridX, this.monitor.height); - cr.moveTo(this.monitor.width / 2 - gridX, 0); - cr.lineTo(this.monitor.width / 2 - gridX, this.monitor.height); - gridX += this.gridLineSpacing; - cr.stroke(); - } - while (gridY < this.monitor.height / 2) { - cr.setLineWidth((gridY / this.gridLineSpacing) % 5 ? this.gridLineWidth / 2 : this.gridLineWidth); - cr.moveTo(0, this.monitor.height / 2 + gridY); - cr.lineTo(this.monitor.width, this.monitor.height / 2 + gridY); - cr.moveTo(0, this.monitor.height / 2 - gridY); - cr.lineTo(this.monitor.width, this.monitor.height / 2 - gridY); - gridY += this.gridLineSpacing; - cr.stroke(); - } - cr.restore(); + this.currentElement.buildCairo(cr, { showTextCursor: this.textHasCursor, + showTextRectangle: this.currentElement.shape != Shapes.TEXT || !this.isWriting, + dummyStroke: this.currentElement.fill && this.currentElement.line.lineWidth == 0 }); + cr.stroke(); + }, + + _repaintGrid: function(cr) { + if (!this.reactive) + return; + + Clutter.cairo_set_source_color(cr, this.gridColor); + + let [gridX, gridY] = [0, 0]; + while (gridX < this.monitor.width / 2) { + cr.setLineWidth((gridX / this.gridLineSpacing) % 5 ? this.gridLineWidth / 2 : this.gridLineWidth); + cr.moveTo(this.monitor.width / 2 + gridX, 0); + cr.lineTo(this.monitor.width / 2 + gridX, this.monitor.height); + cr.moveTo(this.monitor.width / 2 - gridX, 0); + cr.lineTo(this.monitor.width / 2 - gridX, this.monitor.height); + gridX += this.gridLineSpacing; + cr.stroke(); } + while (gridY < this.monitor.height / 2) { + cr.setLineWidth((gridY / this.gridLineSpacing) % 5 ? this.gridLineWidth / 2 : this.gridLineWidth); + cr.moveTo(0, this.monitor.height / 2 + gridY); + cr.lineTo(this.monitor.width, this.monitor.height / 2 + gridY); + cr.moveTo(0, this.monitor.height / 2 - gridY); + cr.lineTo(this.monitor.width, this.monitor.height / 2 - gridY); + gridY += this.gridLineSpacing; + cr.stroke(); + } + }, + + _getHasImageBack: function() { + return this.elements.some(element => element.shape == Shapes.IMAGE); + }, + + _getHasImageFore: function() { + return this.currentElement && this.currentElement.shape == Shapes.IMAGE || false; + }, + + _redisplay: function() { + // force area to emit 'repaint' + this.backLayer.queue_repaint(); + this.foreLayer.queue_repaint(); + if (this.hasGrid) + this.gridLayer.queue_repaint(); + }, + + _transformStagePoint: function(x, y) { + if (!this.layerContainer.get_allocation_box().contains(x, y)) + return [false, 0, 0]; + + return this.layerContainer.transform_stage_point(x, y); }, _onButtonPressed: function(actor, event) { @@ -479,7 +530,7 @@ var DrawingArea = new Lang.Class({ this.elementGrabberTimestamp = event.get_time(); let coords = event.get_coords(); - let [s, x, y] = this.transform_stage_point(coords[0], coords[1]); + let [s, x, y] = this._transformStagePoint(coords[0], coords[1]); if (!s) return; @@ -499,7 +550,7 @@ var DrawingArea = new Lang.Class({ }, _startTransforming: function(stageX, stageY, controlPressed, duplicate) { - let [success, startX, startY] = this.transform_stage_point(stageX, stageY); + let [success, startX, startY] = this._transformStagePoint(stageX, stageY); if (!success) return; @@ -549,7 +600,7 @@ var DrawingArea = new Lang.Class({ return; let coords = event.get_coords(); - let [s, x, y] = this.transform_stage_point(coords[0], coords[1]); + let [s, x, y] = this._transformStagePoint(coords[0], coords[1]); if (!s) return; let controlPressed = event.has_control_modifier(); @@ -605,8 +656,7 @@ var DrawingArea = new Lang.Class({ }, _startDrawing: function(stageX, stageY, shiftPressed) { - let [success, startX, startY] = this.transform_stage_point(stageX, stageY); - + let [success, startX, startY] = this._transformStagePoint(stageX, stageY); if (!success) return; @@ -665,9 +715,10 @@ var DrawingArea = new Lang.Class({ return; let coords = event.get_coords(); - let [s, x, y] = this.transform_stage_point(coords[0], coords[1]); + let [s, x, y] = this._transformStagePoint(coords[0], coords[1]); if (!s) return; + let controlPressed = event.has_control_modifier(); this._updateDrawing(x, y, controlPressed); @@ -682,7 +733,7 @@ var DrawingArea = new Lang.Class({ if (!success) return GLib.SOURCE_CONTINUE; - let [s, x, y] = this.transform_stage_point(coords.x, coords.y); + let [s, x, y] = this._transformStagePoint(coords.x, coords.y); if (!s) return GLib.SOURCE_CONTINUE; @@ -703,7 +754,10 @@ var DrawingArea = new Lang.Class({ this.currentElement.updateDrawing(x, y, controlPressed); - this._redisplay(); + if (this.currentElement.eraser) + this._redisplay(); + else + this.foreLayer.queue_repaint(); this.updatePointerCursor(controlPressed); }, @@ -748,15 +802,15 @@ var DrawingArea = new Lang.Class({ this.currentElement.text = ''; this.currentElement.cursorPosition = 0; // Translators: %s is a key label - this.emit('show-osd', Files.Icons.TOOL_TEXT, _("Type your text and press %s") - .format(Gtk.accelerator_get_label(Clutter.KEY_Escape, 0)), "", -1, true); + this.emit('show-osd', Files.Icons.TOOL_TEXT, _("Press %s\nto start a new line") + .format(Gtk.accelerator_get_label(Clutter.KEY_Return, 1)), "", -1, true); this._updateTextCursorTimeout(); this.textHasCursor = true; this._redisplay(); // Do not hide and do not set opacity to 0 because ibusCandidatePopup need a mapped text entry to init correctly its position. this.textEntry = new St.Entry({ opacity: 1, x: stageX + x, y: stageY + y }); - this.get_parent().insert_child_below(this.textEntry, this); + this.insert_child_below(this.textEntry, null); this.textEntry.grab_key_focus(); this.updateActionMode(); this.updatePointerCursor(); @@ -766,7 +820,7 @@ var DrawingArea = new Lang.Class({ if (ibusCandidatePopup) { this.ibusHandler = ibusCandidatePopup.connect('notify::visible', () => { if (ibusCandidatePopup.visible) { - this.get_parent().set_child_above_sibling(this.textEntry, this); + this.set_child_above_sibling(this.textEntry, null); this.textEntry.opacity = 255; } }); @@ -774,9 +828,7 @@ var DrawingArea = new Lang.Class({ } this.textEntry.clutterText.connect('activate', (clutterText) => { - let startNewLine = true; - this._stopWriting(startNewLine); - clutterText.text = ""; + this._stopWriting(); }); this.textEntry.clutterText.connect('text-changed', (clutterText) => { @@ -790,8 +842,16 @@ var DrawingArea = new Lang.Class({ this.textEntry.clutterText.connect('key-press-event', (clutterText, event) => { if (event.get_key_symbol() == Clutter.KEY_Escape) { + this.currentElement.text = ""; this._stopWriting(); return Clutter.EVENT_STOP; + } else if (event.has_shift_modifier() && + (event.get_key_symbol() == Clutter.KEY_Return || + event.get_key_symbol() == Clutter.KEY_KP_Enter)) { + let startNewLine = true; + this._stopWriting(startNewLine); + clutterText.text = ""; + return Clutter.EVENT_STOP; } // 'cursor-changed' signal is not emitted if the text entry is not visible. @@ -884,8 +944,12 @@ var DrawingArea = new Lang.Class({ // A priori there is nothing to stop, except transformations, if there is no current element. // 'force' argument is passed when leaving drawing mode to ensure all is clean, as a workaround for possible bugs. _stopAll: function(force) { - if (this.grabbedElement) + if (this.grabbedElement) { this._stopTransforming(); + this.grabbedElement = null; + this.grabbedElementLocked = null; + this.updatePointerCursor(); + } if (!this.currentElement && !force) return; @@ -948,24 +1012,28 @@ var DrawingArea = new Lang.Class({ toggleBackground: function() { this.hasBackground = !this.hasBackground; - this.get_parent().set_background_color(this.hasBackground ? this.areaBackgroundColor : null); + this.set_background_color(this.hasBackground ? this.areaBackgroundColor : null); + }, + + get hasGrid() { + return this.gridLayer.visible; }, toggleGrid: function() { - this.hasGrid = !this.hasGrid; - this._redisplay(); + // The grid layer is repainted when the visibility changes. + this.gridLayer.visible = !this.gridLayer.visible; }, toggleSquareArea: function() { this.isSquareArea = !this.isSquareArea; if (this.isSquareArea) { - this.set_position((this.monitor.width - this.squareAreaSize) / 2, (this.monitor.height - this.squareAreaSize) / 2); - this.set_size(this.squareAreaSize, this.squareAreaSize); - this.add_style_class_name('draw-on-your-screen-square-area'); + this.layerContainer.set_position((this.monitor.width - this.squareAreaSize) / 2, (this.monitor.height - this.squareAreaSize) / 2); + this.layerContainer.set_size(this.squareAreaSize, this.squareAreaSize); + this.layerContainer.add_style_class_name('draw-on-your-screen-square-area'); } else { - this.set_position(0, 0); - this.set_size(this.monitor.width, this.monitor.height); - this.remove_style_class_name('draw-on-your-screen-square-area'); + this.layerContainer.set_position(0, 0); + this.layerContainer.set_size(this.monitor.width, this.monitor.height); + this.layerContainer.remove_style_class_name('draw-on-your-screen-square-area'); } }, @@ -1215,7 +1283,7 @@ var DrawingArea = new Lang.Class({ this.buttonPressedHandler = this.connect('button-press-event', this._onButtonPressed.bind(this)); this.keyboardPopupMenuHandler = this.connect('popup-menu', this._onKeyboardPopupMenu.bind(this)); this.scrollHandler = this.connect('scroll-event', this._onScroll.bind(this)); - this.get_parent().set_background_color(this.reactive && this.hasBackground ? this.areaBackgroundColor : null); + this.set_background_color(this.reactive && this.hasBackground ? this.areaBackgroundColor : null); }, leaveDrawingMode: function(save, erase) { @@ -1242,7 +1310,7 @@ var DrawingArea = new Lang.Class({ this.erase(); this.closeMenu(); - this.get_parent().set_background_color(null); + this.set_background_color(null); Files.Images.reset(); if (save) this.savePersistent(); @@ -1273,7 +1341,7 @@ var DrawingArea = new Lang.Class({ }; let getImageSvgContent = () => { - return `${elementsContent}\n`; + return `${elementsContent}\n`; }; return [getGiconSvgContent, getImageSvgContent]; @@ -1285,23 +1353,15 @@ var DrawingArea = new Lang.Class({ let prefixes = 'xmlns="http://www.w3.org/2000/svg"'; if (this.elements.some(element => element.shape == Shapes.IMAGE)) prefixes += ' xmlns:xlink="http://www.w3.org/1999/xlink"'; - let content = ``; - if (SVG_DEBUG_EXTENDS) - content = ``; + let content = ``; let backgroundColorString = this.hasBackground ? String(this.areaBackgroundColor) : 'transparent'; - if (backgroundColorString != 'transparent') { + if (backgroundColorString != 'transparent') content += `\n `; - } - if (SVG_DEBUG_EXTENDS) { - content += `\n `; - content += `\n `; - } this.elements.forEach(element => content += element.buildSVG(backgroundColorString)); content += "\n"; if (Files.saveSvg(content)) { - // pass the parent (bgContainer) to Flashspot because coords of this are relative - let flashspot = new Screenshot.Flashspot(this.get_parent()); + let flashspot = new Screenshot.Flashspot(this); flashspot.fire(); if (global.play_theme_sound) { global.play_theme_sound(0, 'screen-capture', "Save as SVG", null); diff --git a/elements.js b/elements.js index 2bbcaeb..5890187 100644 --- a/elements.js +++ b/elements.js @@ -577,11 +577,12 @@ const _DrawingElement = new Lang.Class({ }, stopTransformation: function() { + this.showSymmetryElement = false; + // Clean transformations let transformation = this.lastTransformation; - - if (transformation.type == Transformations.REFLECTION || transformation.type == Transformations.INVERSION) - this.showSymmetryElement = false; + if (!transformation) + return; if (transformation.type == Transformations.REFLECTION && getNearness([transformation.startX, transformation.startY], [transformation.endX, transformation.endY], MIN_REFLECTION_LINE_LENGTH) || diff --git a/extension.js b/extension.js index a6d2744..3240084 100644 --- a/extension.js +++ b/extension.js @@ -131,9 +131,9 @@ const AreaManager = new Lang.Class({ onDesktopSettingChanged: function() { if (this.onDesktop) - this.areas.forEach(area => area.get_parent().show()); + this.areas.forEach(area => area.show()); else - this.areas.forEach(area => area.get_parent().hide()); + this.areas.forEach(area => area.hide()); }, onPersistentOverRestartsSettingChanged: function() { @@ -167,19 +167,15 @@ const AreaManager = new Lang.Class({ for (let i = 0; i < this.monitors.length; i++) { let monitor = this.monitors[i]; - let container = new St.Widget({ name: 'drawOnYourSreenContainer' + i }); let helper = new Helper.DrawingHelper({ name: 'drawOnYourSreenHelper' + i }, monitor); let loadPersistent = i == Main.layoutManager.primaryIndex && this.persistentOverRestarts; let area = new Area.DrawingArea({ name: 'drawOnYourSreenArea' + i }, monitor, helper, loadPersistent); - container.add_child(area); - container.add_child(helper); - Main.layoutManager._backgroundGroup.insert_child_above(container, Main.layoutManager._bgManagers[i].backgroundActor); + Main.layoutManager._backgroundGroup.insert_child_above(area, Main.layoutManager._bgManagers[i].backgroundActor); if (!this.onDesktop) - container.hide(); + area.hide(); - container.set_position(monitor.x, monitor.y); - container.set_size(monitor.width, monitor.height); + area.set_position(monitor.x, monitor.y); area.set_size(monitor.width, monitor.height); area.leaveDrawingHandler = area.connect('leave-drawing-mode', this.toggleDrawing.bind(this)); area.updateActionModeHandler = area.connect('update-action-mode', this.updateActionMode.bind(this)); @@ -333,25 +329,24 @@ const AreaManager = new Lang.Class({ } }, - toggleContainer: function() { + toggleArea: function() { if (!this.activeArea) return; - let activeContainer = this.activeArea.get_parent(); let activeIndex = this.areas.indexOf(this.activeArea); - if (activeContainer.get_parent() == Main.uiGroup) { + if (this.activeArea.get_parent() == Main.uiGroup) { Main.uiGroup.set_child_at_index(Main.layoutManager.keyboardBox, this.oldKeyboardIndex); - Main.uiGroup.remove_actor(activeContainer); - Main.layoutManager._backgroundGroup.insert_child_above(activeContainer, Main.layoutManager._bgManagers[activeIndex].backgroundActor); + Main.uiGroup.remove_actor(this.activeArea); + Main.layoutManager._backgroundGroup.insert_child_above(this.activeArea, Main.layoutManager._bgManagers[activeIndex].backgroundActor); if (!this.onDesktop) - activeContainer.hide(); + this.activeArea.hide(); } else { - Main.layoutManager._backgroundGroup.remove_actor(activeContainer); - Main.uiGroup.add_child(activeContainer); + Main.layoutManager._backgroundGroup.remove_actor(this.activeArea); + Main.uiGroup.add_child(this.activeArea); // move the keyboard above the area to make it available with text entries this.oldKeyboardIndex = Main.uiGroup.get_children().indexOf(Main.layoutManager.keyboardBox); - Main.uiGroup.set_child_above_sibling(Main.layoutManager.keyboardBox, activeContainer); + Main.uiGroup.set_child_above_sibling(Main.layoutManager.keyboardBox, this.activeArea); } }, @@ -397,23 +392,24 @@ const AreaManager = new Lang.Class({ if (Main._findModal(this.activeArea) != -1) this.toggleModal(); - this.toggleContainer(); + this.toggleArea(); this.activeArea = null; } else { // avoid to deal with Meta changes (global.display/global.screen) let currentIndex = Main.layoutManager.monitors.indexOf(Main.layoutManager.currentMonitor); this.activeArea = this.areas[currentIndex]; - this.toggleContainer(); + this.toggleArea(); if (!this.toggleModal()) { - this.toggleContainer(); + this.toggleArea(); this.activeArea = null; return; } this.activeArea.enterDrawingMode(); this.osdDisabled = Me.settings.get_boolean('osd-disabled'); + // is a clutter/mutter 3.38 bug workaround: https://gitlab.gnome.org/GNOME/mutter/-/issues/1467 // Translators: %s is a key label - let label = "" + _("Press %s for help").format(this.activeArea.helper.helpKeyLabel) + "\n\n" + _("Entering drawing mode"); + let label = `${_("Press %s for help").format(this.activeArea.helper.helpKeyLabel)}\n\n${_("Entering drawing mode")}`; this.showOsd(null, Files.Icons.ENTER, label, null, null, true); } @@ -517,9 +513,7 @@ const AreaManager = new Lang.Class({ area.disconnect(area.leaveDrawingHandler); area.disconnect(area.updateActionModeHandler); area.disconnect(area.showOsdHandler); - let container = area.get_parent(); - container.get_parent().remove_actor(container); - container.destroy(); + area.destroy(); } this.areas = []; }, diff --git a/locale/draw-on-your-screen.pot b/locale/draw-on-your-screen.pot index 5e87946..5de5eaf 100644 --- a/locale/draw-on-your-screen.pot +++ b/locale/draw-on-your-screen.pot @@ -44,7 +44,7 @@ msgstr "" #. Translators: %s is a key label #, javascript-format -msgid "Type your text and press %s" +msgid "Press %s\nto start a new line" msgstr "" #. Translators: It is displayed in an OSD notification to ask the user to start picking, so it should use the imperative mood. diff --git a/metadata.json b/metadata.json index 7846d9c..e620616 100644 --- a/metadata.json +++ b/metadata.json @@ -18,5 +18,5 @@ "3.36", "3.38" ], - "version": 8.1 + "version": 9 }