From 80163fcca84d575911c1ea78f7850715e0583c7e Mon Sep 17 00:00:00 2001 From: abakkk Date: Tue, 30 Jun 2020 23:40:15 +0200 Subject: [PATCH 01/31] draw-to-menu 1 --- draw.js => menu.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename draw.js => menu.js (100%) diff --git a/draw.js b/menu.js similarity index 100% rename from draw.js rename to menu.js From 9b7e7cc3cb280eeb4b6fae13a593575c42cb2132 Mon Sep 17 00:00:00 2001 From: abakkk Date: Tue, 30 Jun 2020 23:41:54 +0200 Subject: [PATCH 02/31] draw-to-menu 2 --- menu.js | 1942 ------------------------------------------------------- 1 file changed, 1942 deletions(-) diff --git a/menu.js b/menu.js index 087f504..a283e8d 100644 --- a/menu.js +++ b/menu.js @@ -124,1948 +124,6 @@ const getJsonFiles = function() { return jsonFiles; }; -// DrawingArea is the widget in which we draw, thanks to Cairo. -// 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: 'DrawOnYourScreenDrawingArea', - Extends: St.DrawingArea, - Signals: { 'show-osd': { param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_DOUBLE, GObject.TYPE_BOOLEAN] }, - 'update-action-mode': {}, - 'leave-drawing-mode': {} }, - - _init: function(params, monitor, helper, loadPersistent) { - this.parent({ style_class: 'draw-on-your-screen', name: params.name}); - - this.connect('destroy', this._onDestroy.bind(this)); - this.reactiveHandler = this.connect('notify::reactive', this._onReactiveChanged.bind(this)); - - this.settings = Convenience.getSettings(); - this.monitor = monitor; - this.helper = helper; - - this.elements = []; - this.undoneElements = []; - this.currentElement = null; - this.currentTool = Shapes.NONE; - this.currentFontGeneric = 0; - this.isSquareArea = false; - this.hasGrid = false; - this.hasBackground = false; - this.textHasCursor = false; - this.dashedLine = false; - this.fill = false; - this.colors = [Clutter.Color.new(0, 0, 0, 255)]; - this.newThemeAttributes = {}; - this.oldThemeAttributes = {}; - - if (loadPersistent) - this._loadPersistent(); - }, - - get menu() { - if (!this._menu) - this._menu = new DrawingMenu(this, this.monitor); - return this._menu; - }, - - closeMenu: function() { - if (this._menu) - this._menu.close(); - }, - - get isWriting() { - return this.textEntry ? true : false; - }, - - get currentTool() { - return this._currentTool; - }, - - set currentTool(tool) { - this._currentTool = tool; - if (this.hasManipulationTool) - this._startElementGrabber(); - else - this._stopElementGrabber(); - }, - - get hasManipulationTool() { - // No Object.values method in GS 3.24. - return Object.keys(Manipulations).map(key => Manipulations[key]).indexOf(this.currentTool) != -1; - }, - - // Boolean wrapper for switch menu item. - get currentEvenodd() { - return this.currentFillRule == Cairo.FillRule.EVEN_ODD; - }, - - set currentEvenodd(evenodd) { - this.currentFillRule = evenodd ? Cairo.FillRule.EVEN_ODD : Cairo.FillRule.WINDING; - }, - - vfunc_repaint: function() { - let cr = this.get_context(); - - try { - this._repaint(cr); - } catch(e) { - logError(e, "An error occured while painting"); - } - - cr.$dispose(); - }, - - _redisplay: function() { - // force area to emit 'repaint' - this.queue_repaint(); - }, - - _updateStyle: function() { - try { - let themeNode = this.get_theme_node(); - for (let i = 1; i < 10; i++) { - this.colors[i] = themeNode.get_color('-drawing-color' + i); - } - let font = themeNode.get_font(); - this.newThemeAttributes.ThemeFontFamily = font.get_family(); - try { this.newThemeAttributes.FontWeight = font.get_weight(); } catch(e) { this.newThemeAttributes.FontWeight = Pango.Weight.NORMAL; } - this.newThemeAttributes.FontStyle = font.get_style(); - this.newThemeAttributes.FontStretch = font.get_stretch(); - this.newThemeAttributes.FontVariant = font.get_variant(); - this.newThemeAttributes.TextRightAligned = themeNode.get_text_align() == St.TextAlign.RIGHT; - this.newThemeAttributes.LineWidth = themeNode.get_length('-drawing-line-width'); - this.newThemeAttributes.LineJoin = themeNode.get_double('-drawing-line-join'); - this.newThemeAttributes.LineCap = themeNode.get_double('-drawing-line-cap'); - this.newThemeAttributes.FillRule = themeNode.get_double('-drawing-fill-rule'); - this.dashArray = [Math.abs(themeNode.get_length('-drawing-dash-array-on')), Math.abs(themeNode.get_length('-drawing-dash-array-off'))]; - this.dashOffset = themeNode.get_length('-drawing-dash-offset'); - this.gridGap = themeNode.get_length('-grid-overlay-gap'); - this.gridLineWidth = themeNode.get_length('-grid-overlay-line-width'); - this.gridInterlineWidth = themeNode.get_length('-grid-overlay-interline-width'); - this.gridColor = themeNode.get_color('-grid-overlay-color'); - this.squareAreaWidth = themeNode.get_length('-drawing-square-area-width'); - this.squareAreaHeight = themeNode.get_length('-drawing-square-area-height'); - this.activeBackgroundColor = themeNode.get_color('-drawing-background-color'); - } catch(e) { - logError(e); - } - - for (let i = 1; i < 10; i++) { - this.colors[i] = this.colors[i].alpha ? this.colors[i] : this.colors[0]; - } - this.currentColor = this.currentColor || this.colors[1]; - // SVG does not support 'Ultra-heavy' weight (1000) - this.newThemeAttributes.FontWeight = Math.min(this.newThemeAttributes.FontWeight, 900); - this.newThemeAttributes.LineWidth = (this.newThemeAttributes.LineWidth > 0) ? this.newThemeAttributes.LineWidth : 3; - this.newThemeAttributes.LineJoin = ([0, 1, 2].indexOf(this.newThemeAttributes.LineJoin) != -1) ? this.newThemeAttributes.LineJoin : Cairo.LineJoin.ROUND; - this.newThemeAttributes.LineCap = ([0, 1, 2].indexOf(this.newThemeAttributes.LineCap) != -1) ? this.newThemeAttributes.LineCap : Cairo.LineCap.ROUND; - this.newThemeAttributes.FillRule = ([0, 1].indexOf(this.newThemeAttributes.FillRule) != -1) ? this.newThemeAttributes.FillRule : Cairo.FillRule.WINDING; - for (let attributeName in this.newThemeAttributes) { - if (this.newThemeAttributes[attributeName] != this.oldThemeAttributes[attributeName]) { - this.oldThemeAttributes[attributeName] = this.newThemeAttributes[attributeName]; - this[`current${attributeName}`] = this.newThemeAttributes[attributeName]; - } - } - this.gridGap = this.gridGap && this.gridGap >= 1 ? this.gridGap : 10; - this.gridLineWidth = this.gridLineWidth || 0.4; - this.gridInterlineWidth = this.gridInterlineWidth || 0.2; - this.gridColor = this.gridColor && this.gridColor.alpha ? this.gridColor : Clutter.Color.new(127, 127, 127, 255); - }, - - _repaint: function(cr) { - if (CAIRO_DEBUG_EXTENDS) { - cr.scale(0.5, 0.5); - cr.translate(this.monitor.width, this.monitor.height); - } - - for (let i = 0; i < this.elements.length; i++) { - cr.save(); - - this.elements[i].buildCairo(cr, { showTextRectangle: this.grabbedElement && this.grabbedElement == this.elements[i], - drawTextRectangle: this.grabPoint ? true : false }); - - if (this.grabPoint) - this._searchElementToGrab(cr, this.elements[i]); - - if (this.elements[i].fill && !this.elements[i].isStraightLine) { - cr.fillPreserve(); - if (this.elements[i].shape == Shapes.NONE || this.elements[i].shape == Shapes.LINE) - cr.closePath(); - } - - cr.stroke(); - cr.restore(); - } - - if (this.currentElement) { - cr.save(); - 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(); - } - - if (this.reactive && this.hasGrid && this.gridGap && this.gridGap >= 1) { - cr.save(); - Clutter.cairo_set_source_color(cr, this.gridColor); - - let [gridX, gridY] = [this.gridGap, this.gridGap]; - while (gridX < this.monitor.width) { - cr.setLineWidth((gridX / this.gridGap) % 5 ? this.gridInterlineWidth : this.gridLineWidth); - cr.moveTo(gridX, 0); - cr.lineTo(gridX, this.monitor.height); - gridX += this.gridGap; - cr.stroke(); - } - while (gridY < this.monitor.height) { - cr.setLineWidth((gridY / this.gridGap) % 5 ? this.gridInterlineWidth : this.gridLineWidth); - cr.moveTo(0, gridY); - cr.lineTo(this.monitor.width, gridY); - gridY += this.gridGap; - cr.stroke(); - } - cr.restore(); - } - }, - - _onButtonPressed: function(actor, event) { - if (this.spaceKeyPressed) - return Clutter.EVENT_PROPAGATE; - - let button = event.get_button(); - let [x, y] = event.get_coords(); - let controlPressed = event.has_control_modifier(); - let shiftPressed = event.has_shift_modifier(); - - if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) - // finish writing - this._stopWriting(); - - if (this.helper.visible) { - // hide helper - this.toggleHelp(); - return Clutter.EVENT_STOP; - } - - if (button == 1) { - if (this.hasManipulationTool) { - if (this.grabbedElement) - this._startTransforming(x, y, controlPressed, shiftPressed); - } else { - this._startDrawing(x, y, shiftPressed); - } - return Clutter.EVENT_STOP; - } else if (button == 2) { - this.toggleFill(); - } else if (button == 3) { - this._stopDrawing(); - this.menu.open(x, y); - return Clutter.EVENT_STOP; - } - - return Clutter.EVENT_PROPAGATE; - }, - - _onKeyboardPopupMenu: function() { - this._stopDrawing(); - if (this.helper.visible) - this.toggleHelp(); - this.menu.popup(); - return Clutter.EVENT_STOP; - }, - - _onStageKeyPressed: function(actor, event) { - if (event.get_key_symbol() == Clutter.KEY_space) - this.spaceKeyPressed = true; - - return Clutter.EVENT_PROPAGATE; - }, - - _onStageKeyReleased: function(actor, event) { - if (event.get_key_symbol() == Clutter.KEY_space) - this.spaceKeyPressed = false; - - return Clutter.EVENT_PROPAGATE; - }, - - _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) - this.emit('show-osd', null, _("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; - } - - } 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; - } - }, - - _onScroll: function(actor, event) { - if (this.helper.visible) - return Clutter.EVENT_PROPAGATE; - let direction = event.get_scroll_direction(); - if (direction == Clutter.ScrollDirection.UP) - this.incrementLineWidth(1); - else if (direction == Clutter.ScrollDirection.DOWN) - this.incrementLineWidth(-1); - else - return Clutter.EVENT_PROPAGATE; - return Clutter.EVENT_STOP; - }, - - _searchElementToGrab: function(cr, element) { - if (element.getContainsPoint(cr, this.grabPoint[0], this.grabPoint[1])) - this.grabbedElement = element; - else if (this.grabbedElement == element) - this.grabbedElement = null; - - if (element == this.elements[this.elements.length - 1]) - // All elements have been tested, the winner is the last. - this.updatePointerCursor(); - }, - - _startElementGrabber: function() { - if (this.elementGrabberHandler) - return; - - this.elementGrabberHandler = this.connect('motion-event', (actor, event) => { - if (this.motionHandler || this.grabbedElementLocked) { - this.grabPoint = null; - return; - } - - // Reduce computing without notable effect. - if (Math.random() <= 0.75) - return; - - let coords = event.get_coords(); - let [s, x, y] = this.transform_stage_point(coords[0], coords[1]); - if (!s) - return; - - this.grabPoint = [x, y]; - this.grabbedElement = null; - // this._redisplay calls this._searchElementToGrab. - this._redisplay(); - }); - }, - - _stopElementGrabber: function() { - if (this.elementGrabberHandler) { - this.disconnect(this.elementGrabberHandler); - this.grabPoint = null; - this.elementGrabberHandler = null; - } - }, - - _startTransforming: function(stageX, stageY, controlPressed, duplicate) { - let [success, startX, startY] = this.transform_stage_point(stageX, stageY); - - if (!success) - return; - - if (this.currentTool == Manipulations.MIRROR) { - this.grabbedElementLocked = !this.grabbedElementLocked; - if (this.grabbedElementLocked) { - this.updatePointerCursor(); - let label = controlPressed ? _("Mark a point of symmetry") : _("Draw a line of symmetry"); - this.emit('show-osd', null, label, "", -1, true); - return; - } - } - - this.grabPoint = null; - - this.buttonReleasedHandler = this.connect('button-release-event', (actor, event) => { - this._stopTransforming(); - }); - - if (duplicate) { - // deep cloning - let copy = new DrawingElement(JSON.parse(JSON.stringify(this.grabbedElement))); - this.elements.push(copy); - this.grabbedElement = copy; - } - - if (this.currentTool == Manipulations.MOVE) - this.grabbedElement.startTransformation(startX, startY, controlPressed ? Transformations.ROTATION : Transformations.TRANSLATION); - else if (this.currentTool == Manipulations.RESIZE) - this.grabbedElement.startTransformation(startX, startY, controlPressed ? Transformations.STRETCH : Transformations.SCALE_PRESERVE); - else if (this.currentTool == Manipulations.MIRROR) { - this.grabbedElement.startTransformation(startX, startY, controlPressed ? Transformations.INVERSION : Transformations.REFLECTION); - this._redisplay(); - } - - - this.motionHandler = this.connect('motion-event', (actor, event) => { - if (this.spaceKeyPressed) - return; - - let coords = event.get_coords(); - let [s, x, y] = this.transform_stage_point(coords[0], coords[1]); - if (!s) - return; - let controlPressed = event.has_control_modifier(); - this._updateTransforming(x, y, controlPressed); - }); - }, - - _updateTransforming: function(x, y, controlPressed) { - if (controlPressed && this.grabbedElement.lastTransformation.type == Transformations.TRANSLATION) { - this.grabbedElement.stopTransformation(); - this.grabbedElement.startTransformation(x, y, Transformations.ROTATION); - } else if (!controlPressed && this.grabbedElement.lastTransformation.type == Transformations.ROTATION) { - this.grabbedElement.stopTransformation(); - this.grabbedElement.startTransformation(x, y, Transformations.TRANSLATION); - } - - if (controlPressed && this.grabbedElement.lastTransformation.type == Transformations.SCALE_PRESERVE) { - this.grabbedElement.stopTransformation(); - this.grabbedElement.startTransformation(x, y, Transformations.STRETCH); - } else if (!controlPressed && this.grabbedElement.lastTransformation.type == Transformations.STRETCH) { - this.grabbedElement.stopTransformation(); - this.grabbedElement.startTransformation(x, y, Transformations.SCALE_PRESERVE); - } - - if (controlPressed && this.grabbedElement.lastTransformation.type == Transformations.REFLECTION) { - this.grabbedElement.transformations.pop(); - this.grabbedElement.startTransformation(x, y, Transformations.INVERSION); - } else if (!controlPressed && this.grabbedElement.lastTransformation.type == Transformations.INVERSION) { - this.grabbedElement.transformations.pop(); - this.grabbedElement.startTransformation(x, y, Transformations.REFLECTION); - } - - this.grabbedElement.updateTransformation(x, y); - this._redisplay(); - }, - - _stopTransforming: function() { - if (this.motionHandler) { - this.disconnect(this.motionHandler); - this.motionHandler = null; - } - if (this.buttonReleasedHandler) { - this.disconnect(this.buttonReleasedHandler); - this.buttonReleasedHandler = null; - } - - this.grabbedElement.stopTransformation(); - this.grabbedElement = null; - this.grabbedElementLocked = false; - this._redisplay(); - }, - - _startDrawing: function(stageX, stageY, eraser) { - let [success, startX, startY] = this.transform_stage_point(stageX, stageY); - - if (!success) - return; - - this.buttonReleasedHandler = this.connect('button-release-event', (actor, event) => { - this._stopDrawing(); - }); - - this.currentElement = new DrawingElement ({ - shape: this.currentTool, - color: this.currentColor.to_string(), - line: { lineWidth: this.currentLineWidth, lineJoin: this.currentLineJoin, lineCap: this.currentLineCap }, - dash: { active: this.dashedLine, array: this.dashedLine ? [this.dashArray[0] || this.currentLineWidth, this.dashArray[1] || this.currentLineWidth * 3] : [0, 0] , offset: this.dashOffset }, - fill: this.fill, - fillRule: this.currentFillRule, - eraser: eraser, - transform: { active: false, center: [0, 0], angle: 0, startAngle: 0, ratio: 1 }, - points: [] - }); - - if (this.currentTool == Shapes.TEXT) { - this.currentElement.fill = false; - this.currentElement.font = { - family: (this.currentFontGeneric == 0 ? this.currentThemeFontFamily : FontGenericNames[this.currentFontGeneric]), - weight: this.currentFontWeight, - style: this.currentFontStyle, - stretch: this.currentFontStretch, - variant: this.currentFontVariant }; - this.currentElement.text = _("Text"); - this.currentElement.textRightAligned = this.currentTextRightAligned; - } - - this.currentElement.startDrawing(startX, startY); - - if (this.currentTool == Shapes.POLYGON || this.currentTool == Shapes.POLYLINE) - this.emit('show-osd', null, _("Press %s to mark vertices") - .format(Gtk.accelerator_get_label(Clutter.KEY_Return, 0)), "", -1, true); - - this.motionHandler = this.connect('motion-event', (actor, event) => { - if (this.spaceKeyPressed) - return; - - let coords = event.get_coords(); - let [s, x, y] = this.transform_stage_point(coords[0], coords[1]); - if (!s) - return; - let controlPressed = event.has_control_modifier(); - this._updateDrawing(x, y, controlPressed); - }); - }, - - _updateDrawing: function(x, y, controlPressed) { - if (!this.currentElement) - return; - - this.currentElement.updateDrawing(x, y, controlPressed); - - this._redisplay(); - this.updatePointerCursor(controlPressed); - }, - - _stopDrawing: function() { - if (this.motionHandler) { - this.disconnect(this.motionHandler); - this.motionHandler = null; - } - if (this.buttonReleasedHandler) { - this.disconnect(this.buttonReleasedHandler); - this.buttonReleasedHandler = null; - } - - // skip when a polygon has not at least 3 points - if (this.currentElement && this.currentElement.shape == Shapes.POLYGON && this.currentElement.points.length < 3) - this.currentElement = null; - - if (this.currentElement) - this.currentElement.stopDrawing(); - - if (this.currentElement && this.currentElement.points.length >= 2) { - if (this.currentElement.shape == Shapes.TEXT && !this.isWriting) { - this._startWriting(); - return; - } - - this.elements.push(this.currentElement); - } - - this.currentElement = null; - this._redisplay(); - this.updatePointerCursor(); - }, - - _startWriting: function() { - this.currentElement.text = ''; - this.currentElement.cursorPosition = 0; - this.emit('show-osd', null, _("Type your text and press %s") - .format(Gtk.accelerator_get_label(Clutter.KEY_Escape, 0)), "", -1, true); - this._updateTextCursorTimeout(); - this.textHasCursor = true; - this._redisplay(); - - this.textEntry = new St.Entry({ visible: false }); - this.get_parent().add_child(this.textEntry); - this.textEntry.grab_key_focus(); - this.updateActionMode(); - this.updatePointerCursor(); - - this.textEntry.clutterText.connect('activate', (clutterText) => { - let startNewLine = true; - this._stopWriting(startNewLine); - clutterText.text = ""; - }); - - this.textEntry.clutterText.connect('text-changed', (clutterText) => { - GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { - this.currentElement.text = clutterText.text; - this.currentElement.cursorPosition = clutterText.cursorPosition; - this._updateTextCursorTimeout(); - this._redisplay(); - }); - }); - - this.textEntry.clutterText.connect('key-press-event', (clutterText, event) => { - if (event.get_key_symbol() == Clutter.KEY_Escape) { - this._stopWriting(); - return Clutter.EVENT_STOP; - } - - // 'cursor-changed' signal is not emitted if the text entry is not visible. - // So key events related to the cursor must be listened. - if (event.get_key_symbol() == Clutter.KEY_Left || event.get_key_symbol() == Clutter.KEY_Right || - event.get_key_symbol() == Clutter.KEY_Home || event.get_key_symbol() == Clutter.KEY_End) { - GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { - this.currentElement.cursorPosition = clutterText.cursorPosition; - this._updateTextCursorTimeout(); - this.textHasCursor = true; - this._redisplay(); - }); - } - - return Clutter.EVENT_PROPAGATE; - }); - }, - - _stopWriting: function(startNewLine) { - if (this.currentElement.text.length > 0) - this.elements.push(this.currentElement); - - if (startNewLine && this.currentElement.points.length == 2) { - this.currentElement.lineIndex = this.currentElement.lineIndex || 0; - // copy object, the original keep existing in this.elements - this.currentElement = Object.create(this.currentElement); - this.currentElement.lineIndex ++; - let height = Math.abs(this.currentElement.points[1][1] - this.currentElement.points[0][1]); - // define a new 'points' array, the original keep existing in this.elements - this.currentElement.points = [ - [this.currentElement.points[0][0], this.currentElement.points[0][1] + height], - [this.currentElement.points[1][0], this.currentElement.points[1][1] + height] - ]; - this.currentElement.text = ""; - } else { - this.currentElement = null; - this._stopTextCursorTimeout(); - this.textEntry.destroy(); - delete this.textEntry; - this.grab_key_focus(); - this.updateActionMode(); - this.updatePointerCursor(); - } - - this._redisplay(); - }, - - setPointerCursor: function(pointerCursorName) { - if (!this.currentPointerCursorName || this.currentPointerCursorName != pointerCursorName) { - this.currentPointerCursorName = pointerCursorName; - Extension.setCursor(pointerCursorName); - } - }, - - updatePointerCursor: function(controlPressed) { - if (this.currentTool == Manipulations.MIRROR && this.grabbedElementLocked) - this.setPointerCursor('CROSSHAIR'); - else if (this.hasManipulationTool) - this.setPointerCursor(this.grabbedElement ? 'MOVE_OR_RESIZE_WINDOW' : 'DEFAULT'); - else if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) - this.setPointerCursor('IBEAM'); - else if (!this.currentElement) - this.setPointerCursor(this.currentTool == Shapes.NONE ? 'POINTING_HAND' : 'CROSSHAIR'); - else if (this.currentElement.shape != Shapes.NONE && controlPressed) - this.setPointerCursor('MOVE_OR_RESIZE_WINDOW'); - }, - - initPointerCursor: function() { - this.currentPointerCursorName = null; - this.updatePointerCursor(); - }, - - _stopTextCursorTimeout: function() { - if (this.textCursorTimeoutId) { - GLib.source_remove(this.textCursorTimeoutId); - this.textCursorTimeoutId = null; - } - this.textHasCursor = false; - }, - - _updateTextCursorTimeout: function() { - this._stopTextCursorTimeout(); - this.textCursorTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, TEXT_CURSOR_TIME, () => { - this.textHasCursor = !this.textHasCursor; - this._redisplay(); - return GLib.SOURCE_CONTINUE; - }); - }, - - erase: function() { - this.deleteLastElement(); - this.elements = []; - this.undoneElements = []; - this._redisplay(); - }, - - deleteLastElement: function() { - if (this.currentElement) { - if (this.motionHandler) { - this.disconnect(this.motionHandler); - this.motionHandler = null; - } - if (this.buttonReleasedHandler) { - this.disconnect(this.buttonReleasedHandler); - this.buttonReleasedHandler = null; - } - if (this.isWriting) - this._stopWriting(); - this.currentElement = null; - } else { - this.elements.pop(); - } - this._redisplay(); - }, - - undo: function() { - if (this.elements.length > 0) - this.undoneElements.push(this.elements.pop()); - this._redisplay(); - }, - - redo: function() { - if (this.undoneElements.length > 0) - this.elements.push(this.undoneElements.pop()); - this._redisplay(); - }, - - smoothLastElement: function() { - if (this.elements.length > 0 && this.elements[this.elements.length - 1].shape == Shapes.NONE) { - this.elements[this.elements.length - 1].smoothAll(); - this._redisplay(); - } - }, - - toggleBackground: function() { - this.hasBackground = !this.hasBackground; - this.get_parent().set_background_color(this.hasBackground ? this.activeBackgroundColor : null); - }, - - toggleGrid: function() { - this.hasGrid = !this.hasGrid; - this._redisplay(); - }, - - toggleSquareArea: function() { - this.isSquareArea = !this.isSquareArea; - if (this.isSquareArea) { - let width = this.squareAreaWidth || this.squareAreaHeight || Math.min(this.monitor.width, this.monitor.height) * 3 / 4; - let height = this.squareAreaHeight || this.squareAreaWidth || Math.min(this.monitor.width, this.monitor.height) * 3 / 4; - this.set_position(Math.floor(this.monitor.width / 2 - width / 2), Math.floor(this.monitor.height / 2 - height / 2)); - this.set_size(width, height); - this.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'); - } - }, - - toggleColor: function() { - this.selectColor((this.currentColor == this.colors[1]) ? 2 : 1); - }, - - selectColor: function(index) { - this.currentColor = this.colors[index]; - if (this.currentElement) { - this.currentElement.color = this.currentColor.to_string(); - this._redisplay(); - } - // Foreground color markup is not displayed since 3.36, use style instead but the transparency is lost. - this.emit('show-osd', null, this.currentColor.to_string(), this.currentColor.to_string().slice(0, 7), -1, false); - }, - - selectTool: function(tool) { - this.currentTool = tool; - this.emit('show-osd', null, _(ToolNames[tool]), "", -1, false); - this.updatePointerCursor(); - }, - - toggleFill: function() { - this.fill = !this.fill; - this.emit('show-osd', null, this.fill ? _("Fill") : _("Outline"), "", -1, false); - }, - - toggleDash: function() { - this.dashedLine = !this.dashedLine; - this.emit('show-osd', null, this.dashedLine ? _("Dashed line") : _("Full line"), "", -1, false); - }, - - incrementLineWidth: function(increment) { - this.currentLineWidth = Math.max(this.currentLineWidth + increment, 0); - this.emit('show-osd', null, _("%d px").format(this.currentLineWidth), "", 2 * this.currentLineWidth, false); - }, - - toggleLineJoin: function() { - this.currentLineJoin = this.currentLineJoin == 2 ? 0 : this.currentLineJoin + 1; - this.emit('show-osd', null, _(LineJoinNames[this.currentLineJoin]), "", -1, false); - }, - - toggleLineCap: function() { - this.currentLineCap = this.currentLineCap == 2 ? 0 : this.currentLineCap + 1; - this.emit('show-osd', null, _(LineCapNames[this.currentLineCap]), "", -1, false); - }, - - toggleFillRule: function() { - this.currentFillRule = this.currentFillRule == 1 ? 0 : this.currentFillRule + 1; - this.emit('show-osd', null, _(FillRuleNames[this.currentFillRule]), "", -1, false); - }, - - toggleFontWeight: function() { - let fontWeights = Object.keys(FontWeightNames).map(key => Number(key)); - let index = fontWeights.indexOf(this.currentFontWeight); - this.currentFontWeight = index == fontWeights.length - 1 ? fontWeights[0] : fontWeights[index + 1]; - if (this.currentElement && this.currentElement.font) { - this.currentElement.font.weight = this.currentFontWeight; - this._redisplay(); - } - this.emit('show-osd', null, `` + - `${_(FontWeightNames[this.currentFontWeight])}`, "", -1, false); - }, - - toggleFontStyle: function() { - this.currentFontStyle = this.currentFontStyle == 2 ? 0 : this.currentFontStyle + 1; - if (this.currentElement && this.currentElement.font) { - this.currentElement.font.style = this.currentFontStyle; - this._redisplay(); - } - this.emit('show-osd', null, `` + - `${_(FontStyleNames[this.currentFontStyle])}`, "", -1, false); - }, - - toggleFontFamily: function() { - this.currentFontGeneric = this.currentFontGeneric == 5 ? 0 : this.currentFontGeneric + 1; - let currentFontFamily = this.currentFontGeneric == 0 ? this.currentThemeFontFamily : FontGenericNames[this.currentFontGeneric]; - if (this.currentElement && this.currentElement.font) { - this.currentElement.font.family = currentFontFamily; - this._redisplay(); - } - this.emit('show-osd', null, `${_(currentFontFamily)}`, "", -1, false); - }, - - toggleTextAlignment: function() { - this.currentTextRightAligned = !this.currentTextRightAligned; - if (this.currentElement && this.currentElement.textRightAligned !== undefined) { - this.currentElement.textRightAligned = this.currentTextRightAligned; - this._redisplay(); - } - this.emit('show-osd', null, this.currentTextRightAligned ? _("Right aligned") : _("Left aligned"), "", -1, false); - }, - - toggleHelp: function() { - if (this.helper.visible) { - this.helper.hideHelp(); - if (this.textEntry) - this.textEntry.grab_key_focus(); - } else { - this.helper.showHelp(); - this.grab_key_focus(); - } - - }, - - // The area is reactive when it is modal. - _onReactiveChanged: function() { - if (this.hasGrid) - this._redisplay(); - if (this.helper.visible) - this.toggleHelp(); - if (this.textEntry && this.reactive) - this.textEntry.grab_key_focus(); - }, - - _onDestroy: function() { - this.disconnect(this.reactiveHandler); - this.erase(); - if (this._menu) - this._menu.disable(); - }, - - updateActionMode: function() { - this.emit('update-action-mode'); - }, - - 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._onKeyboardPopupMenuHandler = 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.activeBackgroundColor : null); - this._updateStyle(); - }, - - leaveDrawingMode: function(save) { - 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; - } - if (this.buttonPressedHandler) { - this.disconnect(this.buttonPressedHandler); - this.buttonPressedHandler = null; - } - if (this._onKeyboardPopupMenuHandler) { - this.disconnect(this._onKeyboardPopupMenuHandler); - this._onKeyboardPopupMenuHandler = null; - } - if (this.motionHandler) { - this.disconnect(this.motionHandler); - this.motionHandler = null; - } - if (this.buttonReleasedHandler) { - this.disconnect(this.buttonReleasedHandler); - this.buttonReleasedHandler = null; - } - if (this.scrollHandler) { - this.disconnect(this.scrollHandler); - this.scrollHandler = null; - } - - this.currentElement = null; - this._stopTextCursorTimeout(); - this._redisplay(); - this.closeMenu(); - this.get_parent().set_background_color(null); - if (save) - this.savePersistent(); - }, - - saveAsSvg: function() { - // stop drawing or writing - if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) { - this._stopWriting(); - } else if (this.currentElement && this.currentElement.shape != Shapes.TEXT) { - this._stopDrawing(); - } - - let content = ``; - if (SVG_DEBUG_EXTENDS) - content = ``; - let backgroundColorString = this.hasBackground ? this.activeBackgroundColor.to_string() : 'transparent'; - if (backgroundColorString != 'transparent') { - content += `\n `; - } - if (SVG_DEBUG_EXTENDS) { - content += `\n `; - content += `\n `; - } - for (let i = 0; i < this.elements.length; i++) { - content += this.elements[i].buildSVG(backgroundColorString); - } - content += "\n"; - - let filename = `${Me.metadata['svg-file-name']} ${getDateString()}.svg`; - let dir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES); - let path = GLib.build_filenamev([dir, filename]); - if (GLib.file_test(path, GLib.FileTest.EXISTS)) - return false; - let success = GLib.file_set_contents(path, content); - - if (success) { - // pass the parent (bgContainer) to Flashspot because coords of this are relative - let flashspot = new Screenshot.Flashspot(this.get_parent()); - flashspot.fire(); - if (global.play_theme_sound) { - global.play_theme_sound(0, 'screen-capture', "Save as SVG", null); - } else if (global.display && global.display.get_sound_player) { - let player = global.display.get_sound_player(); - player.play_from_theme('screen-capture', "Save as SVG", null); - } - } - }, - - _saveAsJson: function(name, notify) { - // stop drawing or writing - if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) { - this._stopWriting(); - } else if (this.currentElement && this.currentElement.shape != Shapes.TEXT) { - this._stopDrawing(); - } - - let dir = GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir']]); - if (!GLib.file_test(dir, GLib.FileTest.EXISTS)) - GLib.mkdir_with_parents(dir, 0o700); - let path = GLib.build_filenamev([dir, `${name}.json`]); - - let oldContents; - - if (name == Me.metadata['persistent-file-name']) { - if (GLib.file_test(path, GLib.FileTest.EXISTS)) { - oldContents = GLib.file_get_contents(path)[1]; - if (oldContents instanceof Uint8Array) - oldContents = ByteArray.toString(oldContents); - } - - // do not create a file to write just an empty array - if (!oldContents && this.elements.length == 0) - return; - } - - // do not use "content = JSON.stringify(this.elements, null, 2);", neither "content = JSON.stringify(this.elements);" - // because of compromise between disk usage and human readability - let contents = `[\n ` + new Array(...this.elements.map(element => JSON.stringify(element))).join(`,\n\n `) + `\n]`; - - if (name == Me.metadata['persistent-file-name'] && contents == oldContents) - return; - - GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { - GLib.file_set_contents(path, contents); - if (notify) - this.emit('show-osd', 'document-save-symbolic', name, "", -1, false); - if (name != Me.metadata['persistent-file-name']) { - this.jsonName = name; - this.lastJsonContents = contents; - } - }); - }, - - saveAsJsonWithName: function(name) { - this._saveAsJson(name); - }, - - saveAsJson: function() { - this._saveAsJson(getDateString(), true); - }, - - savePersistent: function() { - this._saveAsJson(Me.metadata['persistent-file-name']); - }, - - syncPersistent: function() { - // do not override peristent.json with an empty drawing when changing persistency setting - if (!this.elements.length) - this._loadPersistent(); - else - this.savePersistent(); - - }, - - _loadJson: function(name, notify) { - // stop drawing or writing - if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) { - this._stopWriting(); - } else if (this.currentElement && this.currentElement.shape != Shapes.TEXT) { - this._stopDrawing(); - } - this.elements = []; - this.currentElement = null; - - let dir = GLib.get_user_data_dir(); - let path = GLib.build_filenamev([dir, Me.metadata['data-dir'], `${name}.json`]); - - if (!GLib.file_test(path, GLib.FileTest.EXISTS)) - return; - let [success, contents] = GLib.file_get_contents(path); - if (!success) - return; - if (contents instanceof Uint8Array) - contents = ByteArray.toString(contents); - this.elements.push(...JSON.parse(contents).map(object => new DrawingElement(object))); - - if (notify) - this.emit('show-osd', 'document-open-symbolic', name, "", -1, false); - if (name != Me.metadata['persistent-file-name']) { - this.jsonName = name; - this.lastJsonContents = contents; - } - }, - - _loadPersistent: function() { - this._loadJson(Me.metadata['persistent-file-name']); - }, - - loadJson: function(name, notify) { - this._loadJson(name, notify); - this._redisplay(); - }, - - loadNextJson: function() { - let names = getJsonFiles().map(file => file.name); - - if (!names.length) - return; - - let nextName = names[this.jsonName && names.indexOf(this.jsonName) != names.length - 1 ? names.indexOf(this.jsonName) + 1 : 0]; - this.loadJson(nextName, true); - }, - - loadPreviousJson: function() { - let names = getJsonFiles().map(file => file.name); - - if (!names.length) - return; - - let previousName = names[this.jsonName && names.indexOf(this.jsonName) > 0 ? names.indexOf(this.jsonName) - 1 : names.length - 1]; - this.loadJson(previousName, true); - }, - - get drawingContentsHasChanged() { - let contents = `[\n ` + new Array(...this.elements.map(element => JSON.stringify(element))).join(`,\n\n `) + `\n]`; - return contents != this.lastJsonContents; - } -}); - -const RADIAN = 180 / Math.PI; // degree -const INVERSION_CIRCLE_RADIUS = 12; // px -const REFLECTION_TOLERANCE = 5; // px, to select vertical and horizontal directions -const STRETCH_TOLERANCE = Math.PI / 8; // rad, to select vertical and horizontal directions -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 - -// DrawingElement represents a "brushstroke". -// 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: 'DrawOnYourScreenDrawingElement', - - _init: function(params) { - for (let key in params) - this[key] = params[key]; - - // compatibility with json generated by old extension versions - - if (params.fillRule === undefined) - this.fillRule = Cairo.FillRule.WINDING; - if (params.transformations === undefined) - this.transformations = []; - if (params.shape == Shapes.TEXT) { - if (params.font && params.font.weight === 0) - this.font.weight = 400; - if (params.font && params.font.weight === 1) - this.font.weight = 700; - } - - if (params.transform && params.transform.center) { - let angle = (params.transform.angle || 0) + (params.transform.startAngle || 0); - if (angle) - this.transformations.push({ type: Transformations.ROTATION, angle: angle }); - } - if (params.shape == Shapes.ELLIPSE && params.transform && params.transform.ratio && params.transform.ratio != 1 && params.points.length >= 2) { - let [ratio, p0, p1] = [params.transform.ratio, params.points[0], params.points[1]]; - // Add a fake point that will give the right ellipse ratio when building the element. - this.points.push([ratio * (p1[0] - p0[0]) + p0[0], ratio * (p1[1] - p0[1]) + p0[1]]); - } - delete this.transform; - }, - - // toJSON is called by JSON.stringify - toJSON: function() { - return { - shape: this.shape, - color: this.color, - line: this.line, - dash: this.dash, - fill: this.fill, - fillRule: this.fillRule, - eraser: this.eraser, - transformations: this.transformations, - text: this.text, - lineIndex: this.lineIndex !== undefined ? this.lineIndex : undefined, - textRightAligned: this.textRightAligned, - font: this.font, - points: this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100]) - }; - }, - - buildCairo: function(cr, params) { - let [success, color] = Clutter.Color.from_string(this.color); - if (success) - Clutter.cairo_set_source_color(cr, color); - - if (this.showSymmetryElement) { - let transformation = this.lastTransformation; - setDummyStroke(cr); - if (transformation.type == Transformations.REFLECTION) { - cr.moveTo(transformation.startX, transformation.startY); - cr.lineTo(transformation.endX, transformation.endY); - } else { - cr.arc(transformation.endX, transformation.endY, INVERSION_CIRCLE_RADIUS, 0, 2 * Math.PI); - } - cr.stroke(); - } - - cr.setLineCap(this.line.lineCap); - cr.setLineJoin(this.line.lineJoin); - cr.setLineWidth(this.line.lineWidth); - if (this.fillRule) - cr.setFillRule(this.fillRule); - - if (this.dash && this.dash.active && this.dash.array && this.dash.array[0] && this.dash.array[1]) - cr.setDash(this.dash.array, this.dash.offset); - - if (this.eraser) - cr.setOperator(Cairo.Operator.CLEAR); - else - cr.setOperator(Cairo.Operator.OVER); - - if (params.dummyStroke) - setDummyStroke(cr); - - if (SVG_DEBUG_SUPERPOSES_CAIRO) { - Clutter.cairo_set_source_color(cr, Clutter.Color.new(255, 0, 0, 255)); - cr.setLineWidth(this.line.lineWidth / 2 || 1); - } - - this.transformations.slice(0).reverse().forEach(transformation => { - if (transformation.type == Transformations.TRANSLATION) { - cr.translate(transformation.slideX, transformation.slideY); - } else if (transformation.type == Transformations.ROTATION) { - let center = this._getTransformedCenter(transformation); - cr.translate(center[0], center[1]); - cr.rotate(transformation.angle); - cr.translate(-center[0], -center[1]); - } else if (transformation.type == Transformations.SCALE_PRESERVE || transformation.type == Transformations.STRETCH) { - let center = this._getTransformedCenter(transformation); - cr.translate(center[0], center[1]); - cr.rotate(transformation.angle); - cr.scale(transformation.scaleX, transformation.scaleY); - cr.rotate(-transformation.angle); - cr.translate(-center[0], -center[1]); - } else if (transformation.type == Transformations.REFLECTION || transformation.type == Transformations.INVERSION) { - cr.translate(transformation.slideX, transformation.slideY); - cr.rotate(transformation.angle); - cr.scale(transformation.scaleX, transformation.scaleY); - cr.rotate(-transformation.angle); - cr.translate(-transformation.slideX, -transformation.slideY); - } - }); - - let [points, shape] = [this.points, this.shape]; - - if (shape == Shapes.LINE && points.length == 3) { - cr.moveTo(points[0][0], points[0][1]); - cr.curveTo(points[0][0], points[0][1], points[1][0], points[1][1], points[2][0], points[2][1]); - - } else if (shape == Shapes.LINE && points.length == 4) { - cr.moveTo(points[0][0], points[0][1]); - cr.curveTo(points[1][0], points[1][1], points[2][0], points[2][1], points[3][0], points[3][1]); - - } else if (shape == Shapes.NONE || shape == Shapes.LINE) { - cr.moveTo(points[0][0], points[0][1]); - for (let j = 1; j < points.length; j++) { - cr.lineTo(points[j][0], points[j][1]); - } - - } else if (shape == Shapes.ELLIPSE && points.length >= 2) { - let radius = Math.hypot(points[1][0] - points[0][0], points[1][1] - points[0][1]); - let ratio = 1; - - if (points[2]) { - ratio = Math.hypot(points[2][0] - points[0][0], points[2][1] - points[0][1]) / radius; - cr.translate(points[0][0], points[0][1]); - cr.scale(ratio, 1); - cr.translate(-points[0][0], -points[0][1]); - cr.arc(points[0][0], points[0][1], radius, 0, 2 * Math.PI); - cr.translate(points[0][0], points[0][1]); - cr.scale(1 / ratio, 1); - cr.translate(-points[0][0], -points[0][1]); - } else - cr.arc(points[0][0], points[0][1], radius, 0, 2 * Math.PI); - - } else if (shape == Shapes.RECTANGLE && points.length == 2) { - cr.rectangle(points[0][0], points[0][1], points[1][0] - points[0][0], points[1][1] - points[0][1]); - - } else if ((shape == Shapes.POLYGON || shape == Shapes.POLYLINE) && points.length >= 2) { - cr.moveTo(points[0][0], points[0][1]); - for (let j = 1; j < points.length; j++) { - cr.lineTo(points[j][0], points[j][1]); - } - if (shape == Shapes.POLYGON) - cr.closePath(); - - } else if (shape == Shapes.TEXT && points.length == 2) { - let layout = PangoCairo.create_layout(cr); - let fontSize = Math.abs(points[1][1] - points[0][1]) * Pango.SCALE; - let fontDescription = new Pango.FontDescription(); - fontDescription.set_absolute_size(fontSize); - ['family', 'weight', 'style', 'stretch', 'variant'].forEach(attribute => { - if (this.font[attribute] !== undefined) - try { - fontDescription[`set_${attribute}`](this.font[attribute]); - } catch(e) {} - }); - layout.set_font_description(fontDescription); - layout.set_text(this.text, -1); - this.textWidth = layout.get_pixel_size()[0]; - cr.moveTo(points[1][0] - (this.textRightAligned ? this.textWidth : 0), Math.max(points[0][1],points[1][1]) - layout.get_baseline() / Pango.SCALE); - layout.set_text(this.text, -1); - PangoCairo.show_layout(cr, layout); - - if (params.showTextCursor) { - let cursorPosition = this.cursorPosition == -1 ? this.text.length : this.cursorPosition; - layout.set_text(this.text.slice(0, cursorPosition), -1); - let width = layout.get_pixel_size()[0]; - cr.rectangle(points[1][0] - (this.textRightAligned ? this.textWidth : 0) + width, Math.max(points[0][1],points[1][1]), - Math.abs(points[1][1] - points[0][1]) / 25, - Math.abs(points[1][1] - points[0][1])); - cr.fill(); - } - - if (params.showTextRectangle || params.drawTextRectangle) { - cr.rectangle(points[1][0] - (this.textRightAligned ? this.textWidth : 0), Math.max(points[0][1], points[1][1]), - this.textWidth, - Math.abs(points[1][1] - points[0][1])); - if (params.showTextRectangle) - setDummyStroke(cr); - else - // Only draw the rectangle to find the element, not to show it. - cr.setLineWidth(0); - } - } - - cr.identityMatrix(); - }, - - getContainsPoint: function(cr, x, y) { - if (this.shape == Shapes.TEXT) - return cr.inFill(x, y); - - cr.save(); - cr.setLineWidth(Math.max(this.line.lineWidth, 25)); - cr.setDash([], 0); - - // Check whether the point is inside/on/near the element. - let inElement = cr.inStroke(x, y) || this.fill && cr.inFill(x, y); - cr.restore(); - return inElement; - }, - - buildSVG: function(bgColor) { - 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 ? bgColor : this.color; - let fill = this.fill && !this.isStraightLine; - let attributes = ''; - - if (fill) { - attributes = `fill="${color}"`; - if (this.fillRule) - attributes += ` fill-rule="${FillRuleNames[this.fillRule].toLowerCase()}"`; - } else { - attributes = `fill="none"`; - } - - if (this.line && this.line.lineWidth) { - attributes += ` stroke="${color}"` + - ` stroke-width="${this.line.lineWidth}"`; - if (this.line.lineCap) - attributes += ` stroke-linecap="${LineCapNames[this.line.lineCap].toLowerCase()}"`; - if (this.line.lineJoin && !this.isStraightLine) - attributes += ` stroke-linejoin="${LineJoinNames[this.line.lineJoin].toLowerCase()}"`; - if (this.dash && this.dash.active && this.dash.array && this.dash.array[0] && this.dash.array[1]) - attributes += ` stroke-dasharray="${this.dash.array[0]} ${this.dash.array[1]}" stroke-dashoffset="${this.dash.offset}"`; - } else { - attributes += ` stroke="none"`; - } - - let transAttribute = ''; - this.transformations.slice(0).reverse().forEach(transformation => { - transAttribute += transAttribute ? ' ' : ' transform="'; - let center = this._getTransformedCenter(transformation); - - if (transformation.type == Transformations.TRANSLATION) { - transAttribute += `translate(${transformation.slideX},${transformation.slideY})`; - } else if (transformation.type == Transformations.ROTATION) { - transAttribute += `translate(${center[0]},${center[1]}) `; - transAttribute += `rotate(${transformation.angle * RADIAN}) `; - transAttribute += `translate(${-center[0]},${-center[1]})`; - } else if (transformation.type == Transformations.SCALE_PRESERVE || transformation.type == Transformations.STRETCH) { - transAttribute += `translate(${center[0]},${center[1]}) `; - transAttribute += `rotate(${transformation.angle * RADIAN}) `; - transAttribute += `scale(${transformation.scaleX},${transformation.scaleY}) `; - transAttribute += `rotate(${-transformation.angle * RADIAN}) `; - transAttribute += `translate(${-center[0]},${-center[1]})`; - } else if (transformation.type == Transformations.REFLECTION || transformation.type == Transformations.INVERSION) { - transAttribute += `translate(${transformation.slideX}, ${transformation.slideY}) `; - transAttribute += `rotate(${transformation.angle * RADIAN}) `; - transAttribute += `scale(${transformation.scaleX}, ${transformation.scaleY}) `; - transAttribute += `rotate(${-transformation.angle * RADIAN}) `; - transAttribute += `translate(${-transformation.slideX}, ${-transformation.slideY})`; - } - }); - transAttribute += transAttribute ? '"' : ''; - - if (this.shape == Shapes.LINE && points.length == 4) { - row += ``; - - } else if (this.shape == Shapes.LINE && points.length == 3) { - row += ``; - - } else if (this.shape == Shapes.LINE) { - row += ``; - - } else if (this.shape == Shapes.NONE) { - row += ``; - - } else if (this.shape == Shapes.ELLIPSE && points.length == 3) { - let ry = Math.hypot(points[1][0] - points[0][0], points[1][1] - points[0][1]); - let rx = Math.hypot(points[2][0] - points[0][0], points[2][1] - points[0][1]); - row += ``; - - } else if (this.shape == Shapes.ELLIPSE && points.length == 2) { - let r = Math.hypot(points[1][0] - points[0][0], points[1][1] - points[0][1]); - row += ``; - - } else if (this.shape == Shapes.RECTANGLE && points.length == 2) { - row += ``; - - } else if (this.shape == Shapes.POLYGON && points.length >= 3) { - row += ``; - - } else if (this.shape == Shapes.POLYLINE && points.length >= 2) { - row += ``; - - } else if (this.shape == Shapes.TEXT && points.length == 2) { - attributes = `fill="${color}" ` + - `stroke="transparent" ` + - `stroke-opacity="0" ` + - `font-size="${Math.abs(points[1][1] - points[0][1])}"`; - - if (this.font.family) - attributes += ` font-family="${this.font.family}"`; - if (this.font.weight && this.font.weight != Pango.Weight.NORMAL) - attributes += ` font-weight="${this.font.weight}"`; - if (this.font.style && FontStyleNames[this.font.style]) - attributes += ` font-style="${FontStyleNames[this.font.style].toLowerCase()}"`; - if (FontStretchNames[this.font.stretch] && this.font.stretch != Pango.Stretch.NORMAL) - attributes += ` font-stretch="${FontStretchNames[this.font.stretch].toLowerCase()}"`; - if (this.font.variant && FontVariantNames[this.font.variant]) - attributes += ` font-variant="${FontVariantNames[this.font.variant].toLowerCase()}"`; - - // this.textWidth is computed during Cairo building. - row += `${this.text}`; - } - - return row; - }, - - get lastTransformation() { - if (!this.transformations.length) - return null; - - return this.transformations[this.transformations.length - 1]; - }, - - get isStraightLine() { - return this.shape == Shapes.LINE && this.points.length == 2; - }, - - smoothAll: function() { - for (let i = 0; i < this.points.length; i++) { - this._smooth(i); - } - }, - - addPoint: function() { - if (this.shape == Shapes.POLYGON || this.shape == Shapes.POLYLINE) { - // copy last point - let [lastPoint, secondToLastPoint] = [this.points[this.points.length - 1], this.points[this.points.length - 2]]; - if (!getNearness(secondToLastPoint, lastPoint, MIN_DRAWING_SIZE)) - this.points.push([lastPoint[0], lastPoint[1]]); - } else if (this.shape == Shapes.LINE) { - if (this.points.length == 2) { - this.points[2] = this.points[1]; - } else if (this.points.length == 3) { - this.points[3] = this.points[2]; - this.points[2] = this.points[1]; - } - } - }, - - startDrawing: function(startX, startY) { - this.points.push([startX, startY]); - - if (this.shape == Shapes.POLYGON || this.shape == Shapes.POLYLINE) - this.points.push([startX, startY]); - }, - - updateDrawing: function(x, y, transform) { - let points = this.points; - if (x == points[points.length - 1][0] && y == points[points.length - 1][1]) - return; - - transform = transform || this.transformations.length >= 1; - - if (this.shape == Shapes.NONE) { - points.push([x, y]); - if (transform) - this._smooth(points.length - 1); - - } else if ((this.shape == Shapes.RECTANGLE || this.shape == Shapes.POLYGON || this.shape == Shapes.POLYLINE) && transform) { - if (points.length < 2) - return; - - let center = this._getOriginalCenter(); - 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) { - if (points.length < 2) - return; - - points[2] = [x, y]; - let center = this._getOriginalCenter(); - 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) { - points[points.length - 1] = [x, y]; - - } else if (this.shape == Shapes.TEXT && transform) { - if (points.length < 2) - return; - - let [slideX, slideY] = [x - points[1][0], y - points[1][1]]; - points[0] = [points[0][0] + slideX, points[0][1] + slideY]; - points[1] = [x, y]; - - } else { - points[1] = [x, y]; - - } - }, - - stopDrawing: function() { - // skip when the size is too small to be visible (3px) (except for free drawing) - if (this.shape != Shapes.NONE && this.points.length >= 2) { - let lastPoint = this.points[this.points.length - 1]; - let secondToLastPoint = this.points[this.points.length - 2]; - if (getNearness(secondToLastPoint, lastPoint, MIN_DRAWING_SIZE)) - this.points.pop(); - } - - if (this.transformations[0] && this.transformations[0].type == Transformations.ROTATION && - Math.abs(this.transformations[0].angle) < MIN_ROTATION_ANGLE) - this.transformations.shift(); - }, - - startTransformation: function(startX, startY, type) { - if (type == Transformations.TRANSLATION) - this.transformations.push({ startX: startX, startY: startY, type: type, slideX: 0, slideY: 0 }); - else if (type == Transformations.ROTATION) - this.transformations.push({ startX: startX, startY: startY, type: type, 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 }); - else if (type == Transformations.REFLECTION) - this.transformations.push({ startX: startX, startY: startY, endX: startX, endY: startY, type: type, - 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, - scaleX: -1, scaleY: -1, slideX: startX, slideY: startY, - angle: Math.PI + Math.atan(startY / (startX || 1)) }); - - if (type == Transformations.REFLECTION || type == Transformations.INVERSION) - this.showSymmetryElement = true; - }, - - updateTransformation: function(x, y) { - let transformation = this.lastTransformation; - - if (transformation.type == Transformations.TRANSLATION) { - transformation.slideX = x - transformation.startX; - transformation.slideY = y - transformation.startY; - } else if (transformation.type == Transformations.ROTATION) { - let center = this._getTransformedCenter(transformation); - transformation.angle = getAngle(center[0], center[1], transformation.startX, transformation.startY, x, y); - } else if (transformation.type == Transformations.SCALE_PRESERVE) { - let center = this._getTransformedCenter(transformation); - let scale = Math.hypot(x - center[0], y - center[1]) / Math.hypot(transformation.startX - center[0], transformation.startY - center[1]) || 1; - [transformation.scaleX, transformation.scaleY] = [scale, scale]; - } else if (transformation.type == Transformations.STRETCH) { - let center = this._getTransformedCenter(transformation); - let startAngle = getAngle(center[0], center[1], center[0] + 1, center[1], transformation.startX, transformation.startY); - let vertical = Math.abs(Math.sin(startAngle)) >= Math.sin(Math.PI / 2 - STRETCH_TOLERANCE); - let horizontal = Math.abs(Math.cos(startAngle)) >= Math.cos(STRETCH_TOLERANCE); - let scale = Math.hypot(x - center[0], y - center[1]) / Math.hypot(transformation.startX - center[0], transformation.startY - center[1]) || 1; - transformation.scaleX = vertical ? 1 : scale; - transformation.scaleY = !vertical ? 1 : scale; - transformation.angle = vertical || horizontal ? 0 : getAngle(center[0], center[1], center[0] + 1, center[1], x, y); - } else if (transformation.type == Transformations.REFLECTION) { - [transformation.endX, transformation.endY] = [x, y]; - if (getNearness([transformation.startX, transformation.startY], [x, y], MIN_REFLECTION_LINE_LENGTH)) { - // do nothing to avoid jumps (no transformation at starting and locked transformation after) - } else if (Math.abs(y - transformation.startY) <= REFLECTION_TOLERANCE && Math.abs(x - transformation.startX) > REFLECTION_TOLERANCE) { - [transformation.scaleX, transformation.scaleY] = [1, -1]; - [transformation.slideX, transformation.slideY] = [0, transformation.startY]; - transformation.angle = Math.PI; - } else if (Math.abs(x - transformation.startX) <= REFLECTION_TOLERANCE && Math.abs(y - transformation.startY) > REFLECTION_TOLERANCE) { - [transformation.scaleX, transformation.scaleY] = [-1, 1]; - [transformation.slideX, transformation.slideY] = [transformation.startX, 0]; - transformation.angle = Math.PI; - } else if (x != transformation.startX) { - let tan = (y - transformation.startY) / (x - transformation.startX); - [transformation.scaleX, transformation.scaleY] = [1, -1]; - [transformation.slideX, transformation.slideY] = [0, transformation.startY - transformation.startX * tan]; - transformation.angle = Math.PI + Math.atan(tan); - } else if (y != transformation.startY) { - let tan = (x - transformation.startX) / (y - transformation.startY); - [transformation.scaleX, transformation.scaleY] = [-1, 1]; - [transformation.slideX, transformation.slideY] = [transformation.startX - transformation.startY * tan, 0]; - transformation.angle = Math.PI - Math.atan(tan); - } - } else if (transformation.type == Transformations.INVERSION) { - [transformation.endX, transformation.endY] = [x, y]; - [transformation.scaleX, transformation.scaleY] = [-1, -1]; - [transformation.slideX, transformation.slideY] = [x, y]; - transformation.angle = Math.PI + Math.atan(y / (x || 1)); - } - }, - - stopTransformation: function() { - // Clean transformations - let transformation = this.lastTransformation; - - if (transformation.type == Transformations.REFLECTION || transformation.type == Transformations.INVERSION) - this.showSymmetryElement = false; - - if (transformation.type == Transformations.REFLECTION && - getNearness([transformation.startX, transformation.startY], [transformation.endX, transformation.endY], MIN_REFLECTION_LINE_LENGTH) || - transformation.type == Transformations.TRANSLATION && Math.hypot(transformation.slideX, transformation.slideY) < MIN_TRANSLATION_DISTANCE || - transformation.type == Transformations.ROTATION && Math.abs(transformation.angle) < MIN_ROTATION_ANGLE) { - - this.transformations.pop(); - } else { - delete transformation.startX; - delete transformation.startY; - delete transformation.endX; - delete transformation.endY; - } - }, - - // When rotating grouped lines, lineOffset is used to retrieve the rotation center of the first line. - _getLineOffset: function() { - return (this.lineIndex || 0) * Math.abs(this.points[1][1] - this.points[0][1]); - }, - - // The figure rotation center before transformations (original). - // this.textWidth is computed during Cairo building. - _getOriginalCenter: function() { - if (!this._originalCenter) { - let points = this.points; - this._originalCenter = this.shape == Shapes.ELLIPSE ? [points[0][0], points[0][1]] : - this.shape == Shapes.LINE && points.length == 4 ? getCurveCenter(points[0], points[1], points[2], points[3]) : - this.shape == Shapes.LINE && points.length == 3 ? getCurveCenter(points[0], points[0], points[1], points[2]) : - this.shape == Shapes.TEXT && this.textWidth ? [points[1][0], Math.max(points[0][1], points[1][1]) - this._getLineOffset()] : - points.length >= 3 ? getCentroid(points) : - getNaiveCenter(points); - } - - return this._originalCenter; - }, - - // The figure rotation center, whose position is affected by all transformations done before 'transformation'. - _getTransformedCenter: function(transformation) { - if (!transformation.elementTransformedCenter) { - let matrix = new Pango.Matrix({ xx: 1, xy: 0, yx: 0, yy: 1, x0: 0, y0: 0 }); - - // Apply transformations to the matrice in reverse order - // because Pango multiply matrices by the left when applying a transformation - this.transformations.slice(0, this.transformations.indexOf(transformation)).reverse().forEach(transformation => { - if (transformation.type == Transformations.TRANSLATION) { - matrix.translate(transformation.slideX, transformation.slideY); - } else if (transformation.type == Transformations.ROTATION) { - // nothing, the center position is preserved. - } else if (transformation.type == Transformations.SCALE_PRESERVE || transformation.type == Transformations.STRETCH) { - // nothing, the center position is preserved. - } else if (transformation.type == Transformations.REFLECTION || transformation.type == Transformations.INVERSION) { - matrix.translate(transformation.slideX, transformation.slideY); - matrix.rotate(-transformation.angle * RADIAN); - matrix.scale(transformation.scaleX, transformation.scaleY); - matrix.rotate(transformation.angle * RADIAN); - matrix.translate(-transformation.slideX, -transformation.slideY); - } - }); - - let originalCenter = this._getOriginalCenter(); - transformation.elementTransformedCenter = matrix.transform_point(originalCenter[0], originalCenter[1]); - } - - return transformation.elementTransformedCenter; - }, - - _smooth: function(i) { - if (i < 2) - return; - this.points[i-1] = [(this.points[i-2][0] + this.points[i][0]) / 2, (this.points[i-2][1] + this.points[i][1]) / 2]; - } -}); - -const setDummyStroke = function(cr) { - cr.setLineWidth(2); - cr.setLineCap(0); - cr.setLineJoin(0); - cr.setDash([1, 2], 0); -}; - -/* - Some geometric utils -*/ - -const getNearness = function(pointA, pointB, distance) { - return Math.hypot(pointB[0] - pointA[0], pointB[1] - pointA[1]) < distance; -}; - -// mean of the vertices, ok for regular polygons -const getNaiveCenter = function(points) { - return points.reduce((accumulator, point) => accumulator = [accumulator[0] + point[0], accumulator[1] + point[1]]) - .map(coord => coord / points.length); -}; - -// https://en.wikipedia.org/wiki/Centroid#Of_a_polygon -const getCentroid = function(points) { - let n = points.length; - points.push(points[0]); - - let [sA, sX, sY] = [0, 0, 0]; - for (let i = 0; i <= n-1; i++) { - let a = points[i][0]*points[i+1][1] - points[i+1][0]*points[i][1]; - sA += a; - sX += (points[i][0] + points[i+1][0]) * a; - sY += (points[i][1] + points[i+1][1]) * a; - } - - points.pop(); - if (sA == 0) - return getNaiveCenter(points); - return [sX / (3 * sA), sY / (3 * sA)]; -}; - -/* -Cubic Bézier: -[0, 1] -> ℝ², P(t) = (1-t)³P₀ + 3t(1-t)²P₁ + 3t²(1-t)P₂ + t³P₃ - -general case: - -const cubicBezierCoord = function(x0, x1, x2, x3, t) { - return (1-t)**3*x0 + 3*t*(1-t)**2*x1 + 3*t**2*(1-t)*x2 + t**3*x3; -} - -const cubicBezierPoint = function(p0, p1, p2, p3, t) { - return [cubicBezier(p0[0], p1[0], p2[0], p3[0], t), cubicBezier(p0[1], p1[1], p2[1], p3[1], t)]; -} - -Approximatively: -control point: p0 ---- p1 ---- p2 ---- p3 (p2 is not on the curve) - t: 0 ---- 1/3 ---- 2/3 ---- 1 -*/ - -// If the curve has a symmetry axis, it is truly a center (the intersection of the curve and the axis). -// In other cases, it is not a notable point, just a visual approximation. -const getCurveCenter = function(p0, p1, p2, p3) { - if (p0[0] == p1[0] && p0[1] == p1[1]) - // p0 = p1, t = 2/3 - return [(p1[0] + 6*p1[0] + 12*p2[0] + 8*p3[0]) / 27, (p1[1] + 6*p1[1] + 12*p2[1] + 8*p3[1]) / 27]; - else - // t = 1/2 - return [(p0[0] + 3*p1[0] + 3*p2[0] + p3[0]) / 8, (p0[1] + 3*p1[1] + 3*p2[1] + p3[1]) / 8]; -}; - -const getAngle = function(xO, yO, xA, yA, xB, yB) { - // calculate angle of rotation in absolute value - // cos(AOB) = (OA.OB)/(||OA||*||OB||) where OA.OB = (xA-xO)*(xB-xO) + (yA-yO)*(yB-yO) - let cos = ((xA - xO)*(xB - xO) + (yA - yO)*(yB - yO)) / (Math.hypot(xA - xO, yA - yO) * Math.hypot(xB - xO, yB - yO)); - - // acos is defined on [-1, 1] but - // with A == B and imperfect computer calculations, cos may be equal to 1.00000001. - cos = Math.min(Math.max(-1, cos), 1); - let angle = Math.acos( cos ); - - // determine the sign of the angle - if (xA == xO) { - if (xB > xO) - angle = -angle; - } else { - // equation of OA: y = ax + b - let a = (yA - yO) / (xA - xO); - let b = yA - a*xA; - if (yB < a*xB + b) - angle = - angle; - if (xA < xO) - angle = - angle; - } - - return angle; -}; - -const HELPER_ANIMATION_TIME = 0.25; -const MEDIA_KEYS_SCHEMA = 'org.gnome.settings-daemon.plugins.media-keys'; -const MEDIA_KEYS_KEYS = { - 'screenshot': "Screenshot", - 'screenshot-clip': "Screenshot to clipboard", - 'area-screenshot': "Area screenshot", - 'area-screenshot-clip': "Area screenshot to clipboard" -}; - -// DrawingHelper provides the "help osd" (Ctrl + F1) -// It uses the same texts as in prefs -var DrawingHelper = new Lang.Class({ - Name: 'DrawOnYourScreenDrawingHelper', - Extends: St.ScrollView, - - _init: function(params, monitor) { - params.style_class = 'osd-window draw-on-your-screen-helper'; - this.parent(params); - this.monitor = monitor; - this.hide(); - this.settings = Convenience.getSettings(); - - this.settingHandler = this.settings.connect('changed', this._onSettingChanged.bind(this)); - this.connect('destroy', () => this.settings.disconnect(this.settingHandler)); - }, - - _onSettingChanged: function(settings, key) { - if (key == 'toggle-help') - this._updateHelpKeyLabel(); - - if (this.vbox) { - this.vbox.destroy(); - this.vbox = null; - } - }, - - _updateHelpKeyLabel: function() { - let [keyval, mods] = Gtk.accelerator_parse(this.settings.get_strv('toggle-help')[0]); - this._helpKeyLabel = Gtk.accelerator_get_label(keyval, mods); - }, - - get helpKeyLabel() { - if (!this._helpKeyLabel) - this._updateHelpKeyLabel(); - - return this._helpKeyLabel; - }, - - _populate: function() { - this.vbox = new St.BoxLayout({ vertical: true }); - this.add_actor(this.vbox); - this.vbox.add_child(new St.Label({ text: _("Global") })); - - for (let settingKey in Prefs.GLOBAL_KEYBINDINGS) { - let hbox = new St.BoxLayout({ vertical: false }); - if (settingKey.indexOf('-separator-') != -1) { - this.vbox.add_child(hbox); - continue; - } - if (!this.settings.get_strv(settingKey)[0]) - continue; - let [keyval, mods] = Gtk.accelerator_parse(this.settings.get_strv(settingKey)[0]); - hbox.add_child(new St.Label({ text: _(Prefs.GLOBAL_KEYBINDINGS[settingKey]) })); - hbox.add_child(new St.Label({ text: Gtk.accelerator_get_label(keyval, mods), x_expand: true })); - this.vbox.add_child(hbox); - } - - this.vbox.add_child(new St.Label({ text: _("Internal") })); - - for (let i = 0; i < Prefs.OTHER_SHORTCUTS.length; i++) { - if (Prefs.OTHER_SHORTCUTS[i].desc.indexOf('-separator-') != -1) { - this.vbox.add_child(new St.BoxLayout({ vertical: false, style_class: 'draw-on-your-screen-helper-separator' })); - continue; - } - let hbox = new St.BoxLayout({ vertical: false }); - hbox.add_child(new St.Label({ text: _(Prefs.OTHER_SHORTCUTS[i].desc) })); - hbox.add_child(new St.Label({ text: Prefs.OTHER_SHORTCUTS[i].shortcut, x_expand: true })); - hbox.get_children()[0].get_clutter_text().set_use_markup(true); - this.vbox.add_child(hbox); - } - - this.vbox.add_child(new St.BoxLayout({ vertical: false, style_class: 'draw-on-your-screen-helper-separator' })); - - for (let settingKey in Prefs.INTERNAL_KEYBINDINGS) { - if (settingKey.indexOf('-separator-') != -1) { - this.vbox.add_child(new St.BoxLayout({ vertical: false, style_class: 'draw-on-your-screen-helper-separator' })); - continue; - } - let hbox = new St.BoxLayout({ vertical: false }); - if (!this.settings.get_strv(settingKey)[0]) - continue; - let [keyval, mods] = Gtk.accelerator_parse(this.settings.get_strv(settingKey)[0]); - hbox.add_child(new St.Label({ text: _(Prefs.INTERNAL_KEYBINDINGS[settingKey]) })); - hbox.add_child(new St.Label({ text: Gtk.accelerator_get_label(keyval, mods), x_expand: true })); - this.vbox.add_child(hbox); - } - - let mediaKeysSettings; - try { mediaKeysSettings = Convenience.getSettings(MEDIA_KEYS_SCHEMA); } catch(e) { return; } - this.vbox.add_child(new St.Label({ text: _("System") })); - - for (let settingKey in MEDIA_KEYS_KEYS) { - if (!mediaKeysSettings.settings_schema.has_key(settingKey)) - continue; - let shortcut = GS_VERSION < '3.33.0' ? mediaKeysSettings.get_string(settingKey) : mediaKeysSettings.get_strv(settingKey)[0]; - if (!shortcut) - continue; - let [keyval, mods] = Gtk.accelerator_parse(shortcut); - let hbox = new St.BoxLayout({ vertical: false }); - hbox.add_child(new St.Label({ text: _(MEDIA_KEYS_KEYS[settingKey]) })); - hbox.add_child(new St.Label({ text: Gtk.accelerator_get_label(keyval, mods), x_expand: true })); - this.vbox.add_child(hbox); - } - }, - - showHelp: function() { - if (!this.vbox) - this._populate(); - - this.opacity = 0; - this.show(); - - let maxHeight = this.monitor.height * 3 / 4; - this.set_height(Math.min(this.height, maxHeight)); - this.set_position(Math.floor(this.monitor.width / 2 - this.width / 2), - Math.floor(this.monitor.height / 2 - this.height / 2)); - - if (this.height == maxHeight) - this.vscrollbar_policy = Gtk.PolicyType.ALWAYS; - else - this.vscrollbar_policy = Gtk.PolicyType.NEVER; - - Tweener.removeTweens(this); - Tweener.addTween(this, { opacity: 255, - time: HELPER_ANIMATION_TIME, - transition: 'easeOutQuad', - onComplete: null }); - }, - - hideHelp: function() { - Tweener.removeTweens(this); - Tweener.addTween(this, { opacity: 0, - time: HELPER_ANIMATION_TIME, - transition: 'easeOutQuad', - onComplete: this.hide.bind(this) }); - - } -}); - const getActor = function(object) { return GS_VERSION < '3.33.0' ? object.actor : object; }; From 1abdb528a20b2ddcc59c687c72c5fb13930a87d8 Mon Sep 17 00:00:00 2001 From: abakkk Date: Tue, 30 Jun 2020 23:43:52 +0200 Subject: [PATCH 03/31] draw-to-menu 3 --- draw.js | 501 -------------------------------------------------------- 1 file changed, 501 deletions(-) diff --git a/draw.js b/draw.js index 087f504..b3853bb 100644 --- a/draw.js +++ b/draw.js @@ -2066,504 +2066,3 @@ var DrawingHelper = new Lang.Class({ } }); -const getActor = function(object) { - return GS_VERSION < '3.33.0' ? object.actor : object; -}; - -const DrawingMenu = new Lang.Class({ - Name: 'DrawOnYourScreenDrawingMenu', - - _init: function(area, monitor) { - this.area = area; - let side = Clutter.get_default_text_direction() == Clutter.TextDirection.RTL ? St.Side.RIGHT : St.Side.LEFT; - this.menu = new PopupMenu.PopupMenu(Main.layoutManager.dummyCursor, 0.25, side); - this.menuManager = new PopupMenu.PopupMenuManager(GS_VERSION < '3.33.0' ? { actor: this.area } : this.area); - this.menuManager.addMenu(this.menu); - - Main.layoutManager.uiGroup.add_actor(this.menu.actor); - this.menu.actor.add_style_class_name('background-menu draw-on-your-screen-menu'); - this.menu.actor.set_style('max-height:' + monitor.height + 'px;'); - this.menu.actor.hide(); - this.hasSeparators = monitor.height >= 750; - - // do not close the menu on item activated - this.menu.itemActivated = () => {}; - this.menu.connect('open-state-changed', this._onMenuOpenStateChanged.bind(this)); - - // Case where the menu is closed (escape key) while the save entry clutter_text is active: - // St.Entry clutter_text set the DEFAULT cursor on leave event with a delay and - // overrides the cursor set by area.updatePointerCursor(). - // In order to update drawing cursor on menu closed, we need to leave the saveEntry before closing menu. - // Since escape key press event can't be captured easily, the job is done in the menu close function. - let menuCloseFunc = this.menu.close; - this.menu.close = (animate) => { - if (this.saveDrawingSubMenu && this.saveDrawingSubMenu.isOpen) - this.saveDrawingSubMenu.close(); - menuCloseFunc.bind(this.menu)(animate); - }; - - this.colorIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(COLOR_ICON_PATH) }); - this.strokeIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(STROKE_ICON_PATH) }); - this.fillIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(FILL_ICON_PATH) }); - this.fillRuleNonzeroIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(FILLRULE_NONZERO_ICON_PATH) }); - this.fillRuleEvenoddIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(FILLRULE_EVENODD_ICON_PATH) }); - this.linejoinIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(LINEJOIN_ICON_PATH) }); - this.linecapIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(LINECAP_ICON_PATH) }); - this.fullLineIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(FULL_LINE_ICON_PATH) }); - this.dashedLineIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(DASHED_LINE_ICON_PATH) }); - }, - - disable: function() { - this.menuManager.removeMenu(this.menu); - Main.layoutManager.uiGroup.remove_actor(this.menu.actor); - this.menu.destroy(); - }, - - _onMenuOpenStateChanged: function(menu, open) { - if (open) { - this.area.setPointerCursor('DEFAULT'); - } else { - this.area.updatePointerCursor(); - // actionMode has changed, set previous actionMode in order to keep internal shortcuts working - this.area.updateActionMode(); - this.area.grab_key_focus(); - } - }, - - popup: function() { - if (this.menu.isOpen) { - this.close(); - } else { - this.open(); - this.menu.actor.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false); - } - }, - - open: function(x, y) { - if (this.menu.isOpen) - return; - if (x === undefined || y === undefined) - [x, y] = [this.area.monitor.x + this.area.monitor.width / 2, this.area.monitor.y + this.area.monitor.height / 2]; - this._redisplay(); - Main.layoutManager.setDummyCursorGeometry(x, y, 0, 0); - let monitor = this.area.monitor; - this.menu._arrowAlignment = (y - monitor.y) / monitor.height; - this.menu.open(BoxPointer.PopupAnimation.NONE); - this.menuManager.ignoreRelease(); - }, - - close: function() { - if (this.menu.isOpen) - this.menu.close(); - }, - - _redisplay: function() { - this.menu.removeAll(); - - this.menu.addAction(_("Undo"), this.area.undo.bind(this.area), 'edit-undo-symbolic'); - this.menu.addAction(_("Redo"), this.area.redo.bind(this.area), 'edit-redo-symbolic'); - this.menu.addAction(_("Erase"), this.area.deleteLastElement.bind(this.area), 'edit-clear-all-symbolic'); - this.menu.addAction(_("Smooth"), this.area.smoothLastElement.bind(this.area), 'format-text-strikethrough-symbolic'); - this._addSeparator(this.menu); - - this._addSubMenuItem(this.menu, 'document-edit-symbolic', ToolNames, this.area, 'currentTool', this._updateSectionVisibility.bind(this)); - this._addColorSubMenuItem(this.menu); - this.fillItem = this._addSwitchItem(this.menu, _("Fill"), this.strokeIcon, this.fillIcon, this.area, 'fill', this._updateSectionVisibility.bind(this)); - this.fillSection = new PopupMenu.PopupMenuSection(); - this.fillSection.itemActivated = () => {}; - this.fillRuleItem = this._addSwitchItem(this.fillSection, _("Evenodd"), this.fillRuleNonzeroIcon, this.fillRuleEvenoddIcon, this.area, 'currentEvenodd'); - this.menu.addMenuItem(this.fillSection); - this._addSeparator(this.menu); - - let lineSection = new PopupMenu.PopupMenuSection(); - this._addSliderItem(lineSection, this.area, 'currentLineWidth'); - this._addSubMenuItem(lineSection, this.linejoinIcon, LineJoinNames, this.area, 'currentLineJoin'); - this._addSubMenuItem(lineSection, this.linecapIcon, LineCapNames, this.area, 'currentLineCap'); - this._addSwitchItem(lineSection, _("Dashed"), this.fullLineIcon, this.dashedLineIcon, this.area, 'dashedLine'); - this._addSeparator(lineSection); - this.menu.addMenuItem(lineSection); - lineSection.itemActivated = () => {}; - this.lineSection = lineSection; - - let fontSection = new PopupMenu.PopupMenuSection(); - let FontGenericNamesCopy = Object.create(FontGenericNames); - FontGenericNamesCopy[0] = this.area.currentThemeFontFamily; - this._addSubMenuItem(fontSection, 'font-x-generic-symbolic', FontGenericNamesCopy, this.area, 'currentFontGeneric'); - this._addSubMenuItem(fontSection, 'format-text-bold-symbolic', FontWeightNames, this.area, 'currentFontWeight'); - this._addSubMenuItem(fontSection, 'format-text-italic-symbolic', FontStyleNames, this.area, 'currentFontStyle'); - this._addSwitchItem(fontSection, _("Right aligned"), 'format-justify-left-symbolic', 'format-justify-right-symbolic', this.area, 'currentTextRightAligned'); - this._addSeparator(fontSection); - this.menu.addMenuItem(fontSection); - fontSection.itemActivated = () => {}; - this.fontSection = fontSection; - - let manager = Extension.manager; - this._addSimpleSwitchItem(this.menu, _("Hide panel and dock"), manager.hiddenList ? true : false, manager.togglePanelAndDockOpacity.bind(manager)); - this._addSimpleSwitchItem(this.menu, _("Add a drawing background"), this.area.hasBackground, this.area.toggleBackground.bind(this.area)); - this._addSimpleSwitchItem(this.menu, _("Add a grid overlay"), this.area.hasGrid, this.area.toggleGrid.bind(this.area)); - this._addSimpleSwitchItem(this.menu, _("Square drawing area"), this.area.isSquareArea, this.area.toggleSquareArea.bind(this.area)); - this._addSeparator(this.menu); - - this._addDrawingNameItem(this.menu); - this._addOpenDrawingSubMenuItem(this.menu); - this._addSaveDrawingSubMenuItem(this.menu); - - this.menu.addAction(_("Save drawing as a SVG file"), this.area.saveAsSvg.bind(this.area), 'image-x-generic-symbolic'); - this.menu.addAction(_("Edit style"), manager.openUserStyleFile.bind(manager), 'document-page-setup-symbolic'); - this.menu.addAction(_("Show help"), () => { this.close(); this.area.toggleHelp(); }, 'preferences-desktop-keyboard-shortcuts-symbolic'); - - this._updateSectionVisibility(); - }, - - _updateSectionVisibility: function() { - if (this.area.currentTool != Shapes.TEXT) { - this.lineSection.actor.show(); - this.fontSection.actor.hide(); - this.fillItem.setSensitive(true); - this.fillSection.setSensitive(true); - } else { - this.lineSection.actor.hide(); - this.fontSection.actor.show(); - this.fillItem.setSensitive(false); - this.fillSection.setSensitive(false); - } - - if (this.area.fill) - this.fillSection.actor.show(); - else - this.fillSection.actor.hide(); - }, - - _addSwitchItem: function(menu, label, iconFalse, iconTrue, target, targetProperty, onToggled) { - let item = new PopupMenu.PopupSwitchMenuItem(label, target[targetProperty]); - - item.icon = new St.Icon({ style_class: 'popup-menu-icon' }); - getActor(item).insert_child_at_index(item.icon, 1); - let icon = target[targetProperty] ? iconTrue : iconFalse; - if (icon && icon instanceof GObject.Object && GObject.type_is_a(icon, Gio.Icon)) - item.icon.set_gicon(icon); - else if (icon) - item.icon.set_icon_name(icon); - - item.connect('toggled', (item, state) => { - target[targetProperty] = state; - let icon = target[targetProperty] ? iconTrue : iconFalse; - if (icon && icon instanceof GObject.Object && GObject.type_is_a(icon, Gio.Icon)) - item.icon.set_gicon(icon); - else if (icon) - item.icon.set_icon_name(icon); - if (onToggled) - onToggled(); - }); - menu.addMenuItem(item); - return item; - }, - - _addSimpleSwitchItem: function(menu, label, active, onToggled) { - let item = new PopupMenu.PopupSwitchMenuItem(label, active); - item.connect('toggled', onToggled); - menu.addMenuItem(item); - }, - - _addSliderItem: function(menu, target, targetProperty) { - let item = new PopupMenu.PopupBaseMenuItem({ activate: false }); - let label = new St.Label({ text: _("%d px").format(target[targetProperty]), style_class: 'draw-on-your-screen-menu-slider-label' }); - let slider = new Slider.Slider(target[targetProperty] / 50); - - if (GS_VERSION < '3.33.0') { - slider.connect('value-changed', (slider, value, property) => { - target[targetProperty] = Math.max(Math.round(value * 50), 0); - label.set_text(target[targetProperty] + " px"); - if (target[targetProperty] === 0) - label.add_style_class_name(Extension.WARNING_COLOR_STYLE_CLASS_NAME); - else - label.remove_style_class_name(Extension.WARNING_COLOR_STYLE_CLASS_NAME); - }); - } else { - slider.connect('notify::value', () => { - target[targetProperty] = Math.max(Math.round(slider.value * 50), 0); - label.set_text(target[targetProperty] + " px"); - if (target[targetProperty] === 0) - label.add_style_class_name(Extension.WARNING_COLOR_STYLE_CLASS_NAME); - else - label.remove_style_class_name(Extension.WARNING_COLOR_STYLE_CLASS_NAME); - }); - } - - getActor(slider).x_expand = true; - getActor(item).add_child(getActor(slider)); - getActor(item).add_child(label); - if (slider.onKeyPressEvent) - getActor(item).connect('key-press-event', slider.onKeyPressEvent.bind(slider)); - menu.addMenuItem(item); - }, - - _addSubMenuItem: function(menu, icon, obj, target, targetProperty, callback) { - let item = new PopupMenu.PopupSubMenuMenuItem(_(obj[target[targetProperty]]), icon ? true : false); - if (icon && icon instanceof GObject.Object && GObject.type_is_a(icon, Gio.Icon)) - item.icon.set_gicon(icon); - else if (icon) - item.icon.set_icon_name(icon); - - item.menu.itemActivated = () => { - item.menu.close(); - }; - - GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { - for (let i in obj) { - let text; - if (targetProperty == 'currentFontGeneric') - text = `${_(obj[i])}`; - else if (targetProperty == 'currentFontWeight') - text = `${_(obj[i])}`; - else if (targetProperty == 'currentFontStyle') - text = `${_(obj[i])}`; - else - text = _(obj[i]); - - let iCaptured = Number(i); - let subItem = item.menu.addAction(text, () => { - item.label.set_text(_(obj[iCaptured])); - target[targetProperty] = iCaptured; - if (callback) - callback(); - }); - - subItem.label.get_clutter_text().set_use_markup(true); - - // change the display order of tools - if (obj == ToolNames && i == Shapes.POLYGON) - item.menu.moveMenuItem(subItem, 4); - else if (obj == ToolNames && i == Shapes.POLYLINE) - item.menu.moveMenuItem(subItem, 5); - } - return GLib.SOURCE_REMOVE; - }); - menu.addMenuItem(item); - }, - - _addColorSubMenuItem: function(menu) { - let item = new PopupMenu.PopupSubMenuMenuItem(_("Color"), true); - item.icon.set_gicon(this.colorIcon); - item.icon.set_style(`color:${this.area.currentColor.to_string().slice(0, 7)};`); - - item.menu.itemActivated = () => { - item.menu.close(); - }; - - GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { - for (let i = 1; i < this.area.colors.length; i++) { - let text = this.area.colors[i].to_string(); - let iCaptured = i; - let colorItem = item.menu.addAction(text, () => { - this.area.currentColor = this.area.colors[iCaptured]; - item.icon.set_style(`color:${this.area.currentColor.to_string().slice(0, 7)};`); - }); - colorItem.label.get_clutter_text().set_use_markup(true); - // Foreground color markup is not displayed since 3.36, use style instead but the transparency is lost. - colorItem.label.set_style(`color:${this.area.colors[i].to_string().slice(0, 7)};`); - } - return GLib.SOURCE_REMOVE; - }); - menu.addMenuItem(item); - }, - - _addDrawingNameItem: function(menu) { - this.drawingNameMenuItem = new PopupMenu.PopupMenuItem('', { reactive: false, activate: false }); - this.drawingNameMenuItem.setSensitive(false); - menu.addMenuItem(this.drawingNameMenuItem); - this._updateDrawingNameMenuItem(); - }, - - _updateDrawingNameMenuItem: function() { - getActor(this.drawingNameMenuItem).visible = this.area.jsonName ? true : false; - if (this.area.jsonName) { - let prefix = this.area.drawingContentsHasChanged ? "* " : ""; - this.drawingNameMenuItem.label.set_text(`${prefix}${this.area.jsonName}`); - this.drawingNameMenuItem.label.get_clutter_text().set_use_markup(true); - } - }, - - _addOpenDrawingSubMenuItem: function(menu) { - let item = new PopupMenu.PopupSubMenuMenuItem(_("Open drawing"), true); - this.openDrawingSubMenuItem = item; - this.openDrawingSubMenu = item.menu; - item.icon.set_icon_name('document-open-symbolic'); - - item.menu.itemActivated = () => { - item.menu.close(); - }; - - item.menu.openOld = item.menu.open; - item.menu.open = (animate) => { - if (!item.menu.isOpen) - this._populateOpenDrawingSubMenu(); - item.menu.openOld(); - }; - - menu.addMenuItem(item); - }, - - _populateOpenDrawingSubMenu: function() { - this.openDrawingSubMenu.removeAll(); - let jsonFiles = getJsonFiles(); - jsonFiles.forEach(file => { - let item = this.openDrawingSubMenu.addAction(`${file.displayName}`, () => { - this.area.loadJson(file.name); - this._updateDrawingNameMenuItem(); - this._updateSaveDrawingSubMenuItemSensitivity(); - }); - item.label.get_clutter_text().set_use_markup(true); - - let expander = new St.Bin({ - style_class: 'popup-menu-item-expander', - x_expand: true, - }); - getActor(item).add_child(expander); - - let deleteButton = new St.Button({ style_class: 'draw-on-your-screen-menu-delete-button', - child: new St.Icon({ icon_name: 'edit-delete-symbolic', - style_class: 'popup-menu-icon', - x_align: Clutter.ActorAlign.END }) }); - getActor(item).add_child(deleteButton); - - deleteButton.connect('clicked', () => { - file.delete(); - item.destroy(); - }); - }); - - this.openDrawingSubMenuItem.setSensitive(!this.openDrawingSubMenu.isEmpty()); - }, - - _addSaveDrawingSubMenuItem: function(menu) { - let item = new PopupMenu.PopupSubMenuMenuItem(_("Save drawing"), true); - this.saveDrawingSubMenuItem = item; - this._updateSaveDrawingSubMenuItemSensitivity(); - this.saveDrawingSubMenu = item.menu; - item.icon.set_icon_name('document-save-symbolic'); - - item.menu.itemActivated = () => { - item.menu.close(); - }; - - item.menu.openOld = item.menu.open; - item.menu.open = (animate) => { - if (!item.menu.isOpen) - this._populateSaveDrawingSubMenu(); - item.menu.openOld(); - }; - menu.addMenuItem(item); - }, - - _updateSaveDrawingSubMenuItemSensitivity: function() { - this.saveDrawingSubMenuItem.setSensitive(this.area.elements.length > 0); - }, - - _populateSaveDrawingSubMenu: function() { - this.saveDrawingSubMenu.removeAll(); - let saveEntry = new DrawingMenuEntry({ initialTextGetter: getDateString, - entryActivateCallback: (text) => { - this.area.saveAsJsonWithName(text); - this.saveDrawingSubMenu.toggle(); - this._updateDrawingNameMenuItem(); - }, - invalidStrings: [Me.metadata['persistent-file-name'], '/'], - primaryIconName: 'insert-text' }); - this.saveDrawingSubMenu.addMenuItem(saveEntry.item); - }, - - _addSeparator: function(menu) { - if (this.hasSeparators) { - let separatorItem = new PopupMenu.PopupSeparatorMenuItem(' '); - getActor(separatorItem).add_style_class_name('draw-on-your-screen-menu-separator-item'); - menu.addMenuItem(separatorItem); - } - } -}); - -// based on searchItem.js, https://github.com/leonardo-bartoli/gnome-shell-extension-Recents -const DrawingMenuEntry = new Lang.Class({ - Name: 'DrawOnYourScreenDrawingMenuEntry', - - _init: function(params) { - this.params = params; - this.item = new PopupMenu.PopupBaseMenuItem({ style_class: 'draw-on-your-screen-menu-entry-item', - activate: false, - reactive: true, - can_focus: false }); - - this.itemActor = GS_VERSION < '3.33.0' ? this.item.actor : this.item; - - this.entry = new St.Entry({ - style_class: 'search-entry draw-on-your-screen-menu-entry', - track_hover: true, - reactive: true, - can_focus: true, - x_expand: true - }); - - this.entry.set_primary_icon(new St.Icon({ style_class: 'search-entry-icon', - icon_name: this.params.primaryIconName })); - - this.entry.clutter_text.connect('text-changed', this._onTextChanged.bind(this)); - this.entry.clutter_text.connect('activate', this._onTextActivated.bind(this)); - - this.clearIcon = new St.Icon({ - style_class: 'search-entry-icon', - icon_name: 'edit-clear-symbolic' - }); - this.entry.connect('secondary-icon-clicked', this._reset.bind(this)); - - getActor(this.item).add_child(this.entry); - getActor(this.item).connect('notify::mapped', (actor) => { - if (actor.mapped) { - this.entry.set_text(this.params.initialTextGetter()); - this.entry.clutter_text.grab_key_focus(); - } - }); - }, - - _setError: function(hasError) { - if (hasError) - this.entry.add_style_class_name('draw-on-your-screen-menu-entry-error'); - else - this.entry.remove_style_class_name('draw-on-your-screen-menu-entry-error'); - }, - - _reset: function() { - this.entry.text = ''; - this.entry.clutter_text.set_cursor_visible(true); - this.entry.clutter_text.set_selection(0, 0); - this._setError(false); - }, - - _onTextActivated: function(clutterText) { - let text = clutterText.get_text(); - if (text.length == 0) - return; - if (this._getIsInvalid()) - return; - this._reset(); - this.params.entryActivateCallback(text); - }, - - _onTextChanged: function(clutterText) { - let text = clutterText.get_text(); - this.entry.set_secondary_icon(text.length ? this.clearIcon : null); - - if (text.length) - this._setError(this._getIsInvalid()); - }, - - _getIsInvalid: function() { - for (let i = 0; i < this.params.invalidStrings.length; i++) { - if (this.entry.text.indexOf(this.params.invalidStrings[i]) != -1) - return true; - } - - return false; - } -}); - - From ef51bb8bf92ffe75391a0a6312ac61696825461d Mon Sep 17 00:00:00 2001 From: abakkk Date: Fri, 3 Jul 2020 22:51:23 +0200 Subject: [PATCH 04/31] draw-to-menu 5 --- draw.js | 42 +++++++++---------------- menu.js | 97 ++++++++------------------------------------------------- 2 files changed, 27 insertions(+), 112 deletions(-) diff --git a/draw.js b/draw.js index b3853bb..f2a520b 100644 --- a/draw.js +++ b/draw.js @@ -32,11 +32,7 @@ const Pango = imports.gi.Pango; const PangoCairo = imports.gi.PangoCairo; const St = imports.gi.St; -const BoxPointer = imports.ui.boxpointer; const Config = imports.misc.config; -const Main = imports.ui.main; -const PopupMenu = imports.ui.popupMenu; -const Slider = imports.ui.slider; const Screenshot = imports.ui.screenshot; const Tweener = imports.ui.tweener; @@ -44,6 +40,7 @@ const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Convenience = ExtensionUtils.getSettings ? ExtensionUtils : Me.imports.convenience; const Extension = Me.imports.extension; +const Menu = Me.imports.menu; const Prefs = Me.imports.prefs; const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; @@ -53,17 +50,6 @@ const SVG_DEBUG_EXTENDS = false; const SVG_DEBUG_SUPERPOSES_CAIRO = false; const TEXT_CURSOR_TIME = 600; // ms -const ICON_DIR = Me.dir.get_child('data').get_child('icons'); -const COLOR_ICON_PATH = ICON_DIR.get_child('color-symbolic.svg').get_path(); -const FILL_ICON_PATH = ICON_DIR.get_child('fill-symbolic.svg').get_path(); -const STROKE_ICON_PATH = ICON_DIR.get_child('stroke-symbolic.svg').get_path(); -const LINEJOIN_ICON_PATH = ICON_DIR.get_child('linejoin-symbolic.svg').get_path(); -const LINECAP_ICON_PATH = ICON_DIR.get_child('linecap-symbolic.svg').get_path(); -const FILLRULE_NONZERO_ICON_PATH = ICON_DIR.get_child('fillrule-nonzero-symbolic.svg').get_path(); -const FILLRULE_EVENODD_ICON_PATH = ICON_DIR.get_child('fillrule-evenodd-symbolic.svg').get_path(); -const DASHED_LINE_ICON_PATH = ICON_DIR.get_child('dashed-line-symbolic.svg').get_path(); -const FULL_LINE_ICON_PATH = ICON_DIR.get_child('full-line-symbolic.svg').get_path(); - const reverseEnumeration = function(obj) { let reversed = {}; Object.keys(obj).forEach(key => { @@ -74,25 +60,25 @@ const reverseEnumeration = function(obj) { const Shapes = { NONE: 0, LINE: 1, ELLIPSE: 2, RECTANGLE: 3, TEXT: 4, POLYGON: 5, POLYLINE: 6 }; const Manipulations = { MOVE: 100, RESIZE: 101, MIRROR: 102 }; -var Tools = Object.assign({}, Shapes, Manipulations); const Transformations = { TRANSLATION: 0, ROTATION: 1, SCALE_PRESERVE: 2, STRETCH: 3, REFLECTION: 4, INVERSION: 5 }; -const ToolNames = { 0: "Free drawing", 1: "Line", 2: "Ellipse", 3: "Rectangle", 4: "Text", 5: "Polygon", 6: "Polyline", 100: "Move", 101: "Resize", 102: "Mirror" }; -const LineCapNames = Object.assign(reverseEnumeration(Cairo.LineCap), { 2: 'Square' }); -const LineJoinNames = reverseEnumeration(Cairo.LineJoin); -const FillRuleNames = { 0: 'Nonzero', 1: 'Evenodd' }; -const FontGenericNames = { 0: 'Theme', 1: 'Sans-Serif', 2: 'Serif', 3: 'Monospace', 4: 'Cursive', 5: 'Fantasy' }; -const FontWeightNames = Object.assign(reverseEnumeration(Pango.Weight), { 200: "Ultra-light", 350: "Semi-light", 600: "Semi-bold", 800: "Ultra-bold" }); +var Tools = Object.assign({}, Shapes, Manipulations); +var ToolNames = { 0: "Free drawing", 1: "Line", 2: "Ellipse", 3: "Rectangle", 4: "Text", 5: "Polygon", 6: "Polyline", 100: "Move", 101: "Resize", 102: "Mirror" }; +var LineCapNames = Object.assign(reverseEnumeration(Cairo.LineCap), { 2: 'Square' }); +var LineJoinNames = reverseEnumeration(Cairo.LineJoin); +var FillRuleNames = { 0: 'Nonzero', 1: 'Evenodd' }; +var FontGenericNames = { 0: 'Theme', 1: 'Sans-Serif', 2: 'Serif', 3: 'Monospace', 4: 'Cursive', 5: 'Fantasy' }; +var FontWeightNames = Object.assign(reverseEnumeration(Pango.Weight), { 200: "Ultra-light", 350: "Semi-light", 600: "Semi-bold", 800: "Ultra-bold" }); delete FontWeightNames[Pango.Weight.ULTRAHEAVY]; -const FontStyleNames = reverseEnumeration(Pango.Style); -const FontStretchNames = reverseEnumeration(Pango.Stretch); -const FontVariantNames = reverseEnumeration(Pango.Variant); +var FontStyleNames = reverseEnumeration(Pango.Style); +var FontStretchNames = reverseEnumeration(Pango.Stretch); +var FontVariantNames = reverseEnumeration(Pango.Variant); -const getDateString = function() { +var getDateString = function() { let date = GLib.DateTime.new_now_local(); return `${date.format("%F")} ${date.format("%X")}`; }; -const getJsonFiles = function() { +var getJsonFiles = function() { let directory = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir']])); let enumerator; @@ -165,7 +151,7 @@ var DrawingArea = new Lang.Class({ get menu() { if (!this._menu) - this._menu = new DrawingMenu(this, this.monitor); + this._menu = new Menu.DrawingMenu(this, this.monitor); return this._menu; }, diff --git a/menu.js b/menu.js index a283e8d..ea83845 100644 --- a/menu.js +++ b/menu.js @@ -20,16 +20,12 @@ * along with this program. If not, see . */ -const ByteArray = imports.byteArray; -const Cairo = imports.cairo; const Clutter = imports.gi.Clutter; const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const GObject = imports.gi.GObject; const Gtk = imports.gi.Gtk; const Lang = imports.lang; -const Pango = imports.gi.Pango; -const PangoCairo = imports.gi.PangoCairo; const St = imports.gi.St; const BoxPointer = imports.ui.boxpointer; @@ -37,21 +33,14 @@ const Config = imports.misc.config; const Main = imports.ui.main; const PopupMenu = imports.ui.popupMenu; const Slider = imports.ui.slider; -const Screenshot = imports.ui.screenshot; -const Tweener = imports.ui.tweener; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); -const Convenience = ExtensionUtils.getSettings ? ExtensionUtils : Me.imports.convenience; +const Draw = Me.imports.draw; const Extension = Me.imports.extension; -const Prefs = Me.imports.prefs; const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; const GS_VERSION = Config.PACKAGE_VERSION; -const CAIRO_DEBUG_EXTENDS = false; -const SVG_DEBUG_EXTENDS = false; -const SVG_DEBUG_SUPERPOSES_CAIRO = false; -const TEXT_CURSOR_TIME = 600; // ms const ICON_DIR = Me.dir.get_child('data').get_child('icons'); const COLOR_ICON_PATH = ICON_DIR.get_child('color-symbolic.svg').get_path(); @@ -64,71 +53,11 @@ const FILLRULE_EVENODD_ICON_PATH = ICON_DIR.get_child('fillrule-evenodd-symbolic const DASHED_LINE_ICON_PATH = ICON_DIR.get_child('dashed-line-symbolic.svg').get_path(); const FULL_LINE_ICON_PATH = ICON_DIR.get_child('full-line-symbolic.svg').get_path(); -const reverseEnumeration = function(obj) { - let reversed = {}; - Object.keys(obj).forEach(key => { - reversed[obj[key]] = key.slice(0,1) + key.slice(1).toLowerCase().replace('_', '-'); - }); - return reversed; -}; - -const Shapes = { NONE: 0, LINE: 1, ELLIPSE: 2, RECTANGLE: 3, TEXT: 4, POLYGON: 5, POLYLINE: 6 }; -const Manipulations = { MOVE: 100, RESIZE: 101, MIRROR: 102 }; -var Tools = Object.assign({}, Shapes, Manipulations); -const Transformations = { TRANSLATION: 0, ROTATION: 1, SCALE_PRESERVE: 2, STRETCH: 3, REFLECTION: 4, INVERSION: 5 }; -const ToolNames = { 0: "Free drawing", 1: "Line", 2: "Ellipse", 3: "Rectangle", 4: "Text", 5: "Polygon", 6: "Polyline", 100: "Move", 101: "Resize", 102: "Mirror" }; -const LineCapNames = Object.assign(reverseEnumeration(Cairo.LineCap), { 2: 'Square' }); -const LineJoinNames = reverseEnumeration(Cairo.LineJoin); -const FillRuleNames = { 0: 'Nonzero', 1: 'Evenodd' }; -const FontGenericNames = { 0: 'Theme', 1: 'Sans-Serif', 2: 'Serif', 3: 'Monospace', 4: 'Cursive', 5: 'Fantasy' }; -const FontWeightNames = Object.assign(reverseEnumeration(Pango.Weight), { 200: "Ultra-light", 350: "Semi-light", 600: "Semi-bold", 800: "Ultra-bold" }); -delete FontWeightNames[Pango.Weight.ULTRAHEAVY]; -const FontStyleNames = reverseEnumeration(Pango.Style); -const FontStretchNames = reverseEnumeration(Pango.Stretch); -const FontVariantNames = reverseEnumeration(Pango.Variant); - -const getDateString = function() { - let date = GLib.DateTime.new_now_local(); - return `${date.format("%F")} ${date.format("%X")}`; -}; - -const getJsonFiles = function() { - let directory = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir']])); - - let enumerator; - try { - enumerator = directory.enumerate_children('standard::name,standard::display-name,standard::content-type,time::modified', Gio.FileQueryInfoFlags.NONE, null); - } catch(e) { - return []; - } - - let jsonFiles = []; - let fileInfo = enumerator.next_file(null); - while (fileInfo) { - if (fileInfo.get_content_type().indexOf('json') != -1 && fileInfo.get_name() != `${Me.metadata['persistent-file-name']}.json`) { - let file = enumerator.get_child(fileInfo); - jsonFiles.push({ name: fileInfo.get_name().slice(0, -5), - displayName: fileInfo.get_display_name().slice(0, -5), - // fileInfo.get_modification_date_time: Gio 2.62+ - modificationUnixTime: fileInfo.get_attribute_uint64('time::modified'), - delete: () => file.delete(null) }); - } - fileInfo = enumerator.next_file(null); - } - enumerator.close(null); - - jsonFiles.sort((a, b) => { - return b.modificationUnixTime - a.modificationUnixTime; - }); - - return jsonFiles; -}; - const getActor = function(object) { return GS_VERSION < '3.33.0' ? object.actor : object; }; -const DrawingMenu = new Lang.Class({ +var DrawingMenu = new Lang.Class({ Name: 'DrawOnYourScreenDrawingMenu', _init: function(area, monitor) { @@ -224,7 +153,7 @@ const DrawingMenu = new Lang.Class({ this.menu.addAction(_("Smooth"), this.area.smoothLastElement.bind(this.area), 'format-text-strikethrough-symbolic'); this._addSeparator(this.menu); - this._addSubMenuItem(this.menu, 'document-edit-symbolic', ToolNames, this.area, 'currentTool', this._updateSectionVisibility.bind(this)); + this._addSubMenuItem(this.menu, 'document-edit-symbolic', Draw.ToolNames, this.area, 'currentTool', this._updateSectionVisibility.bind(this)); this._addColorSubMenuItem(this.menu); this.fillItem = this._addSwitchItem(this.menu, _("Fill"), this.strokeIcon, this.fillIcon, this.area, 'fill', this._updateSectionVisibility.bind(this)); this.fillSection = new PopupMenu.PopupMenuSection(); @@ -235,8 +164,8 @@ const DrawingMenu = new Lang.Class({ let lineSection = new PopupMenu.PopupMenuSection(); this._addSliderItem(lineSection, this.area, 'currentLineWidth'); - this._addSubMenuItem(lineSection, this.linejoinIcon, LineJoinNames, this.area, 'currentLineJoin'); - this._addSubMenuItem(lineSection, this.linecapIcon, LineCapNames, this.area, 'currentLineCap'); + this._addSubMenuItem(lineSection, this.linejoinIcon, Draw.LineJoinNames, this.area, 'currentLineJoin'); + this._addSubMenuItem(lineSection, this.linecapIcon, Draw.LineCapNames, this.area, 'currentLineCap'); this._addSwitchItem(lineSection, _("Dashed"), this.fullLineIcon, this.dashedLineIcon, this.area, 'dashedLine'); this._addSeparator(lineSection); this.menu.addMenuItem(lineSection); @@ -244,11 +173,11 @@ const DrawingMenu = new Lang.Class({ this.lineSection = lineSection; let fontSection = new PopupMenu.PopupMenuSection(); - let FontGenericNamesCopy = Object.create(FontGenericNames); + let FontGenericNamesCopy = Object.create(Draw.FontGenericNames); FontGenericNamesCopy[0] = this.area.currentThemeFontFamily; this._addSubMenuItem(fontSection, 'font-x-generic-symbolic', FontGenericNamesCopy, this.area, 'currentFontGeneric'); - this._addSubMenuItem(fontSection, 'format-text-bold-symbolic', FontWeightNames, this.area, 'currentFontWeight'); - this._addSubMenuItem(fontSection, 'format-text-italic-symbolic', FontStyleNames, this.area, 'currentFontStyle'); + this._addSubMenuItem(fontSection, 'format-text-bold-symbolic', Draw.FontWeightNames, this.area, 'currentFontWeight'); + this._addSubMenuItem(fontSection, 'format-text-italic-symbolic', Draw.FontStyleNames, this.area, 'currentFontStyle'); this._addSwitchItem(fontSection, _("Right aligned"), 'format-justify-left-symbolic', 'format-justify-right-symbolic', this.area, 'currentTextRightAligned'); this._addSeparator(fontSection); this.menu.addMenuItem(fontSection); @@ -274,7 +203,7 @@ const DrawingMenu = new Lang.Class({ }, _updateSectionVisibility: function() { - if (this.area.currentTool != Shapes.TEXT) { + if (this.area.currentTool != Draw.Tools.TEXT) { this.lineSection.actor.show(); this.fontSection.actor.hide(); this.fillItem.setSensitive(true); @@ -390,9 +319,9 @@ const DrawingMenu = new Lang.Class({ subItem.label.get_clutter_text().set_use_markup(true); // change the display order of tools - if (obj == ToolNames && i == Shapes.POLYGON) + if (obj == Draw.ToolNames && i == Draw.Tools.POLYGON) item.menu.moveMenuItem(subItem, 4); - else if (obj == ToolNames && i == Shapes.POLYLINE) + else if (obj == Draw.ToolNames && i == Draw.Tools.POLYLINE) item.menu.moveMenuItem(subItem, 5); } return GLib.SOURCE_REMOVE; @@ -464,7 +393,7 @@ const DrawingMenu = new Lang.Class({ _populateOpenDrawingSubMenu: function() { this.openDrawingSubMenu.removeAll(); - let jsonFiles = getJsonFiles(); + let jsonFiles = Draw.getJsonFiles(); jsonFiles.forEach(file => { let item = this.openDrawingSubMenu.addAction(`${file.displayName}`, () => { this.area.loadJson(file.name); @@ -520,7 +449,7 @@ const DrawingMenu = new Lang.Class({ _populateSaveDrawingSubMenu: function() { this.saveDrawingSubMenu.removeAll(); - let saveEntry = new DrawingMenuEntry({ initialTextGetter: getDateString, + let saveEntry = new DrawingMenuEntry({ initialTextGetter: Draw.getDateString, entryActivateCallback: (text) => { this.area.saveAsJsonWithName(text); this.saveDrawingSubMenu.toggle(); From da7ad4069a1eeda83d73ebb75f0c3cb93153abfb Mon Sep 17 00:00:00 2001 From: abakkk Date: Fri, 3 Jul 2020 22:59:54 +0200 Subject: [PATCH 05/31] draw-to-osd 1 --- draw.js => osd.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename draw.js => osd.js (100%) diff --git a/draw.js b/osd.js similarity index 100% rename from draw.js rename to osd.js From 27872b84dbafc1f478b9941b4b9e5e62e8c9703f Mon Sep 17 00:00:00 2001 From: abakkk Date: Fri, 3 Jul 2020 23:03:09 +0200 Subject: [PATCH 06/31] draw-to-osd 2 --- osd.js | 1868 -------------------------------------------------------- 1 file changed, 1868 deletions(-) diff --git a/osd.js b/osd.js index f2a520b..e3bfe2e 100644 --- a/osd.js +++ b/osd.js @@ -20,1888 +20,20 @@ * along with this program. If not, see . */ -const ByteArray = imports.byteArray; -const Cairo = imports.cairo; -const Clutter = imports.gi.Clutter; -const Gio = imports.gi.Gio; -const GLib = imports.gi.GLib; -const GObject = imports.gi.GObject; const Gtk = imports.gi.Gtk; const Lang = imports.lang; -const Pango = imports.gi.Pango; -const PangoCairo = imports.gi.PangoCairo; const St = imports.gi.St; const Config = imports.misc.config; -const Screenshot = imports.ui.screenshot; const Tweener = imports.ui.tweener; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Convenience = ExtensionUtils.getSettings ? ExtensionUtils : Me.imports.convenience; -const Extension = Me.imports.extension; -const Menu = Me.imports.menu; const Prefs = Me.imports.prefs; const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; const GS_VERSION = Config.PACKAGE_VERSION; -const CAIRO_DEBUG_EXTENDS = false; -const SVG_DEBUG_EXTENDS = false; -const SVG_DEBUG_SUPERPOSES_CAIRO = false; -const TEXT_CURSOR_TIME = 600; // ms - -const reverseEnumeration = function(obj) { - let reversed = {}; - Object.keys(obj).forEach(key => { - reversed[obj[key]] = key.slice(0,1) + key.slice(1).toLowerCase().replace('_', '-'); - }); - return reversed; -}; - -const Shapes = { NONE: 0, LINE: 1, ELLIPSE: 2, RECTANGLE: 3, TEXT: 4, POLYGON: 5, POLYLINE: 6 }; -const Manipulations = { MOVE: 100, RESIZE: 101, MIRROR: 102 }; -const Transformations = { TRANSLATION: 0, ROTATION: 1, SCALE_PRESERVE: 2, STRETCH: 3, REFLECTION: 4, INVERSION: 5 }; -var Tools = Object.assign({}, Shapes, Manipulations); -var ToolNames = { 0: "Free drawing", 1: "Line", 2: "Ellipse", 3: "Rectangle", 4: "Text", 5: "Polygon", 6: "Polyline", 100: "Move", 101: "Resize", 102: "Mirror" }; -var LineCapNames = Object.assign(reverseEnumeration(Cairo.LineCap), { 2: 'Square' }); -var LineJoinNames = reverseEnumeration(Cairo.LineJoin); -var FillRuleNames = { 0: 'Nonzero', 1: 'Evenodd' }; -var FontGenericNames = { 0: 'Theme', 1: 'Sans-Serif', 2: 'Serif', 3: 'Monospace', 4: 'Cursive', 5: 'Fantasy' }; -var FontWeightNames = Object.assign(reverseEnumeration(Pango.Weight), { 200: "Ultra-light", 350: "Semi-light", 600: "Semi-bold", 800: "Ultra-bold" }); -delete FontWeightNames[Pango.Weight.ULTRAHEAVY]; -var FontStyleNames = reverseEnumeration(Pango.Style); -var FontStretchNames = reverseEnumeration(Pango.Stretch); -var FontVariantNames = reverseEnumeration(Pango.Variant); - -var getDateString = function() { - let date = GLib.DateTime.new_now_local(); - return `${date.format("%F")} ${date.format("%X")}`; -}; - -var getJsonFiles = function() { - let directory = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir']])); - - let enumerator; - try { - enumerator = directory.enumerate_children('standard::name,standard::display-name,standard::content-type,time::modified', Gio.FileQueryInfoFlags.NONE, null); - } catch(e) { - return []; - } - - let jsonFiles = []; - let fileInfo = enumerator.next_file(null); - while (fileInfo) { - if (fileInfo.get_content_type().indexOf('json') != -1 && fileInfo.get_name() != `${Me.metadata['persistent-file-name']}.json`) { - let file = enumerator.get_child(fileInfo); - jsonFiles.push({ name: fileInfo.get_name().slice(0, -5), - displayName: fileInfo.get_display_name().slice(0, -5), - // fileInfo.get_modification_date_time: Gio 2.62+ - modificationUnixTime: fileInfo.get_attribute_uint64('time::modified'), - delete: () => file.delete(null) }); - } - fileInfo = enumerator.next_file(null); - } - enumerator.close(null); - - jsonFiles.sort((a, b) => { - return b.modificationUnixTime - a.modificationUnixTime; - }); - - return jsonFiles; -}; - -// DrawingArea is the widget in which we draw, thanks to Cairo. -// 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: 'DrawOnYourScreenDrawingArea', - Extends: St.DrawingArea, - Signals: { 'show-osd': { param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_DOUBLE, GObject.TYPE_BOOLEAN] }, - 'update-action-mode': {}, - 'leave-drawing-mode': {} }, - - _init: function(params, monitor, helper, loadPersistent) { - this.parent({ style_class: 'draw-on-your-screen', name: params.name}); - - this.connect('destroy', this._onDestroy.bind(this)); - this.reactiveHandler = this.connect('notify::reactive', this._onReactiveChanged.bind(this)); - - this.settings = Convenience.getSettings(); - this.monitor = monitor; - this.helper = helper; - - this.elements = []; - this.undoneElements = []; - this.currentElement = null; - this.currentTool = Shapes.NONE; - this.currentFontGeneric = 0; - this.isSquareArea = false; - this.hasGrid = false; - this.hasBackground = false; - this.textHasCursor = false; - this.dashedLine = false; - this.fill = false; - this.colors = [Clutter.Color.new(0, 0, 0, 255)]; - this.newThemeAttributes = {}; - this.oldThemeAttributes = {}; - - if (loadPersistent) - this._loadPersistent(); - }, - - get menu() { - if (!this._menu) - this._menu = new Menu.DrawingMenu(this, this.monitor); - return this._menu; - }, - - closeMenu: function() { - if (this._menu) - this._menu.close(); - }, - - get isWriting() { - return this.textEntry ? true : false; - }, - - get currentTool() { - return this._currentTool; - }, - - set currentTool(tool) { - this._currentTool = tool; - if (this.hasManipulationTool) - this._startElementGrabber(); - else - this._stopElementGrabber(); - }, - - get hasManipulationTool() { - // No Object.values method in GS 3.24. - return Object.keys(Manipulations).map(key => Manipulations[key]).indexOf(this.currentTool) != -1; - }, - - // Boolean wrapper for switch menu item. - get currentEvenodd() { - return this.currentFillRule == Cairo.FillRule.EVEN_ODD; - }, - - set currentEvenodd(evenodd) { - this.currentFillRule = evenodd ? Cairo.FillRule.EVEN_ODD : Cairo.FillRule.WINDING; - }, - - vfunc_repaint: function() { - let cr = this.get_context(); - - try { - this._repaint(cr); - } catch(e) { - logError(e, "An error occured while painting"); - } - - cr.$dispose(); - }, - - _redisplay: function() { - // force area to emit 'repaint' - this.queue_repaint(); - }, - - _updateStyle: function() { - try { - let themeNode = this.get_theme_node(); - for (let i = 1; i < 10; i++) { - this.colors[i] = themeNode.get_color('-drawing-color' + i); - } - let font = themeNode.get_font(); - this.newThemeAttributes.ThemeFontFamily = font.get_family(); - try { this.newThemeAttributes.FontWeight = font.get_weight(); } catch(e) { this.newThemeAttributes.FontWeight = Pango.Weight.NORMAL; } - this.newThemeAttributes.FontStyle = font.get_style(); - this.newThemeAttributes.FontStretch = font.get_stretch(); - this.newThemeAttributes.FontVariant = font.get_variant(); - this.newThemeAttributes.TextRightAligned = themeNode.get_text_align() == St.TextAlign.RIGHT; - this.newThemeAttributes.LineWidth = themeNode.get_length('-drawing-line-width'); - this.newThemeAttributes.LineJoin = themeNode.get_double('-drawing-line-join'); - this.newThemeAttributes.LineCap = themeNode.get_double('-drawing-line-cap'); - this.newThemeAttributes.FillRule = themeNode.get_double('-drawing-fill-rule'); - this.dashArray = [Math.abs(themeNode.get_length('-drawing-dash-array-on')), Math.abs(themeNode.get_length('-drawing-dash-array-off'))]; - this.dashOffset = themeNode.get_length('-drawing-dash-offset'); - this.gridGap = themeNode.get_length('-grid-overlay-gap'); - this.gridLineWidth = themeNode.get_length('-grid-overlay-line-width'); - this.gridInterlineWidth = themeNode.get_length('-grid-overlay-interline-width'); - this.gridColor = themeNode.get_color('-grid-overlay-color'); - this.squareAreaWidth = themeNode.get_length('-drawing-square-area-width'); - this.squareAreaHeight = themeNode.get_length('-drawing-square-area-height'); - this.activeBackgroundColor = themeNode.get_color('-drawing-background-color'); - } catch(e) { - logError(e); - } - - for (let i = 1; i < 10; i++) { - this.colors[i] = this.colors[i].alpha ? this.colors[i] : this.colors[0]; - } - this.currentColor = this.currentColor || this.colors[1]; - // SVG does not support 'Ultra-heavy' weight (1000) - this.newThemeAttributes.FontWeight = Math.min(this.newThemeAttributes.FontWeight, 900); - this.newThemeAttributes.LineWidth = (this.newThemeAttributes.LineWidth > 0) ? this.newThemeAttributes.LineWidth : 3; - this.newThemeAttributes.LineJoin = ([0, 1, 2].indexOf(this.newThemeAttributes.LineJoin) != -1) ? this.newThemeAttributes.LineJoin : Cairo.LineJoin.ROUND; - this.newThemeAttributes.LineCap = ([0, 1, 2].indexOf(this.newThemeAttributes.LineCap) != -1) ? this.newThemeAttributes.LineCap : Cairo.LineCap.ROUND; - this.newThemeAttributes.FillRule = ([0, 1].indexOf(this.newThemeAttributes.FillRule) != -1) ? this.newThemeAttributes.FillRule : Cairo.FillRule.WINDING; - for (let attributeName in this.newThemeAttributes) { - if (this.newThemeAttributes[attributeName] != this.oldThemeAttributes[attributeName]) { - this.oldThemeAttributes[attributeName] = this.newThemeAttributes[attributeName]; - this[`current${attributeName}`] = this.newThemeAttributes[attributeName]; - } - } - this.gridGap = this.gridGap && this.gridGap >= 1 ? this.gridGap : 10; - this.gridLineWidth = this.gridLineWidth || 0.4; - this.gridInterlineWidth = this.gridInterlineWidth || 0.2; - this.gridColor = this.gridColor && this.gridColor.alpha ? this.gridColor : Clutter.Color.new(127, 127, 127, 255); - }, - - _repaint: function(cr) { - if (CAIRO_DEBUG_EXTENDS) { - cr.scale(0.5, 0.5); - cr.translate(this.monitor.width, this.monitor.height); - } - - for (let i = 0; i < this.elements.length; i++) { - cr.save(); - - this.elements[i].buildCairo(cr, { showTextRectangle: this.grabbedElement && this.grabbedElement == this.elements[i], - drawTextRectangle: this.grabPoint ? true : false }); - - if (this.grabPoint) - this._searchElementToGrab(cr, this.elements[i]); - - if (this.elements[i].fill && !this.elements[i].isStraightLine) { - cr.fillPreserve(); - if (this.elements[i].shape == Shapes.NONE || this.elements[i].shape == Shapes.LINE) - cr.closePath(); - } - - cr.stroke(); - cr.restore(); - } - - if (this.currentElement) { - cr.save(); - 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(); - } - - if (this.reactive && this.hasGrid && this.gridGap && this.gridGap >= 1) { - cr.save(); - Clutter.cairo_set_source_color(cr, this.gridColor); - - let [gridX, gridY] = [this.gridGap, this.gridGap]; - while (gridX < this.monitor.width) { - cr.setLineWidth((gridX / this.gridGap) % 5 ? this.gridInterlineWidth : this.gridLineWidth); - cr.moveTo(gridX, 0); - cr.lineTo(gridX, this.monitor.height); - gridX += this.gridGap; - cr.stroke(); - } - while (gridY < this.monitor.height) { - cr.setLineWidth((gridY / this.gridGap) % 5 ? this.gridInterlineWidth : this.gridLineWidth); - cr.moveTo(0, gridY); - cr.lineTo(this.monitor.width, gridY); - gridY += this.gridGap; - cr.stroke(); - } - cr.restore(); - } - }, - - _onButtonPressed: function(actor, event) { - if (this.spaceKeyPressed) - return Clutter.EVENT_PROPAGATE; - - let button = event.get_button(); - let [x, y] = event.get_coords(); - let controlPressed = event.has_control_modifier(); - let shiftPressed = event.has_shift_modifier(); - - if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) - // finish writing - this._stopWriting(); - - if (this.helper.visible) { - // hide helper - this.toggleHelp(); - return Clutter.EVENT_STOP; - } - - if (button == 1) { - if (this.hasManipulationTool) { - if (this.grabbedElement) - this._startTransforming(x, y, controlPressed, shiftPressed); - } else { - this._startDrawing(x, y, shiftPressed); - } - return Clutter.EVENT_STOP; - } else if (button == 2) { - this.toggleFill(); - } else if (button == 3) { - this._stopDrawing(); - this.menu.open(x, y); - return Clutter.EVENT_STOP; - } - - return Clutter.EVENT_PROPAGATE; - }, - - _onKeyboardPopupMenu: function() { - this._stopDrawing(); - if (this.helper.visible) - this.toggleHelp(); - this.menu.popup(); - return Clutter.EVENT_STOP; - }, - - _onStageKeyPressed: function(actor, event) { - if (event.get_key_symbol() == Clutter.KEY_space) - this.spaceKeyPressed = true; - - return Clutter.EVENT_PROPAGATE; - }, - - _onStageKeyReleased: function(actor, event) { - if (event.get_key_symbol() == Clutter.KEY_space) - this.spaceKeyPressed = false; - - return Clutter.EVENT_PROPAGATE; - }, - - _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) - this.emit('show-osd', null, _("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; - } - - } 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; - } - }, - - _onScroll: function(actor, event) { - if (this.helper.visible) - return Clutter.EVENT_PROPAGATE; - let direction = event.get_scroll_direction(); - if (direction == Clutter.ScrollDirection.UP) - this.incrementLineWidth(1); - else if (direction == Clutter.ScrollDirection.DOWN) - this.incrementLineWidth(-1); - else - return Clutter.EVENT_PROPAGATE; - return Clutter.EVENT_STOP; - }, - - _searchElementToGrab: function(cr, element) { - if (element.getContainsPoint(cr, this.grabPoint[0], this.grabPoint[1])) - this.grabbedElement = element; - else if (this.grabbedElement == element) - this.grabbedElement = null; - - if (element == this.elements[this.elements.length - 1]) - // All elements have been tested, the winner is the last. - this.updatePointerCursor(); - }, - - _startElementGrabber: function() { - if (this.elementGrabberHandler) - return; - - this.elementGrabberHandler = this.connect('motion-event', (actor, event) => { - if (this.motionHandler || this.grabbedElementLocked) { - this.grabPoint = null; - return; - } - - // Reduce computing without notable effect. - if (Math.random() <= 0.75) - return; - - let coords = event.get_coords(); - let [s, x, y] = this.transform_stage_point(coords[0], coords[1]); - if (!s) - return; - - this.grabPoint = [x, y]; - this.grabbedElement = null; - // this._redisplay calls this._searchElementToGrab. - this._redisplay(); - }); - }, - - _stopElementGrabber: function() { - if (this.elementGrabberHandler) { - this.disconnect(this.elementGrabberHandler); - this.grabPoint = null; - this.elementGrabberHandler = null; - } - }, - - _startTransforming: function(stageX, stageY, controlPressed, duplicate) { - let [success, startX, startY] = this.transform_stage_point(stageX, stageY); - - if (!success) - return; - - if (this.currentTool == Manipulations.MIRROR) { - this.grabbedElementLocked = !this.grabbedElementLocked; - if (this.grabbedElementLocked) { - this.updatePointerCursor(); - let label = controlPressed ? _("Mark a point of symmetry") : _("Draw a line of symmetry"); - this.emit('show-osd', null, label, "", -1, true); - return; - } - } - - this.grabPoint = null; - - this.buttonReleasedHandler = this.connect('button-release-event', (actor, event) => { - this._stopTransforming(); - }); - - if (duplicate) { - // deep cloning - let copy = new DrawingElement(JSON.parse(JSON.stringify(this.grabbedElement))); - this.elements.push(copy); - this.grabbedElement = copy; - } - - if (this.currentTool == Manipulations.MOVE) - this.grabbedElement.startTransformation(startX, startY, controlPressed ? Transformations.ROTATION : Transformations.TRANSLATION); - else if (this.currentTool == Manipulations.RESIZE) - this.grabbedElement.startTransformation(startX, startY, controlPressed ? Transformations.STRETCH : Transformations.SCALE_PRESERVE); - else if (this.currentTool == Manipulations.MIRROR) { - this.grabbedElement.startTransformation(startX, startY, controlPressed ? Transformations.INVERSION : Transformations.REFLECTION); - this._redisplay(); - } - - - this.motionHandler = this.connect('motion-event', (actor, event) => { - if (this.spaceKeyPressed) - return; - - let coords = event.get_coords(); - let [s, x, y] = this.transform_stage_point(coords[0], coords[1]); - if (!s) - return; - let controlPressed = event.has_control_modifier(); - this._updateTransforming(x, y, controlPressed); - }); - }, - - _updateTransforming: function(x, y, controlPressed) { - if (controlPressed && this.grabbedElement.lastTransformation.type == Transformations.TRANSLATION) { - this.grabbedElement.stopTransformation(); - this.grabbedElement.startTransformation(x, y, Transformations.ROTATION); - } else if (!controlPressed && this.grabbedElement.lastTransformation.type == Transformations.ROTATION) { - this.grabbedElement.stopTransformation(); - this.grabbedElement.startTransformation(x, y, Transformations.TRANSLATION); - } - - if (controlPressed && this.grabbedElement.lastTransformation.type == Transformations.SCALE_PRESERVE) { - this.grabbedElement.stopTransformation(); - this.grabbedElement.startTransformation(x, y, Transformations.STRETCH); - } else if (!controlPressed && this.grabbedElement.lastTransformation.type == Transformations.STRETCH) { - this.grabbedElement.stopTransformation(); - this.grabbedElement.startTransformation(x, y, Transformations.SCALE_PRESERVE); - } - - if (controlPressed && this.grabbedElement.lastTransformation.type == Transformations.REFLECTION) { - this.grabbedElement.transformations.pop(); - this.grabbedElement.startTransformation(x, y, Transformations.INVERSION); - } else if (!controlPressed && this.grabbedElement.lastTransformation.type == Transformations.INVERSION) { - this.grabbedElement.transformations.pop(); - this.grabbedElement.startTransformation(x, y, Transformations.REFLECTION); - } - - this.grabbedElement.updateTransformation(x, y); - this._redisplay(); - }, - - _stopTransforming: function() { - if (this.motionHandler) { - this.disconnect(this.motionHandler); - this.motionHandler = null; - } - if (this.buttonReleasedHandler) { - this.disconnect(this.buttonReleasedHandler); - this.buttonReleasedHandler = null; - } - - this.grabbedElement.stopTransformation(); - this.grabbedElement = null; - this.grabbedElementLocked = false; - this._redisplay(); - }, - - _startDrawing: function(stageX, stageY, eraser) { - let [success, startX, startY] = this.transform_stage_point(stageX, stageY); - - if (!success) - return; - - this.buttonReleasedHandler = this.connect('button-release-event', (actor, event) => { - this._stopDrawing(); - }); - - this.currentElement = new DrawingElement ({ - shape: this.currentTool, - color: this.currentColor.to_string(), - line: { lineWidth: this.currentLineWidth, lineJoin: this.currentLineJoin, lineCap: this.currentLineCap }, - dash: { active: this.dashedLine, array: this.dashedLine ? [this.dashArray[0] || this.currentLineWidth, this.dashArray[1] || this.currentLineWidth * 3] : [0, 0] , offset: this.dashOffset }, - fill: this.fill, - fillRule: this.currentFillRule, - eraser: eraser, - transform: { active: false, center: [0, 0], angle: 0, startAngle: 0, ratio: 1 }, - points: [] - }); - - if (this.currentTool == Shapes.TEXT) { - this.currentElement.fill = false; - this.currentElement.font = { - family: (this.currentFontGeneric == 0 ? this.currentThemeFontFamily : FontGenericNames[this.currentFontGeneric]), - weight: this.currentFontWeight, - style: this.currentFontStyle, - stretch: this.currentFontStretch, - variant: this.currentFontVariant }; - this.currentElement.text = _("Text"); - this.currentElement.textRightAligned = this.currentTextRightAligned; - } - - this.currentElement.startDrawing(startX, startY); - - if (this.currentTool == Shapes.POLYGON || this.currentTool == Shapes.POLYLINE) - this.emit('show-osd', null, _("Press %s to mark vertices") - .format(Gtk.accelerator_get_label(Clutter.KEY_Return, 0)), "", -1, true); - - this.motionHandler = this.connect('motion-event', (actor, event) => { - if (this.spaceKeyPressed) - return; - - let coords = event.get_coords(); - let [s, x, y] = this.transform_stage_point(coords[0], coords[1]); - if (!s) - return; - let controlPressed = event.has_control_modifier(); - this._updateDrawing(x, y, controlPressed); - }); - }, - - _updateDrawing: function(x, y, controlPressed) { - if (!this.currentElement) - return; - - this.currentElement.updateDrawing(x, y, controlPressed); - - this._redisplay(); - this.updatePointerCursor(controlPressed); - }, - - _stopDrawing: function() { - if (this.motionHandler) { - this.disconnect(this.motionHandler); - this.motionHandler = null; - } - if (this.buttonReleasedHandler) { - this.disconnect(this.buttonReleasedHandler); - this.buttonReleasedHandler = null; - } - - // skip when a polygon has not at least 3 points - if (this.currentElement && this.currentElement.shape == Shapes.POLYGON && this.currentElement.points.length < 3) - this.currentElement = null; - - if (this.currentElement) - this.currentElement.stopDrawing(); - - if (this.currentElement && this.currentElement.points.length >= 2) { - if (this.currentElement.shape == Shapes.TEXT && !this.isWriting) { - this._startWriting(); - return; - } - - this.elements.push(this.currentElement); - } - - this.currentElement = null; - this._redisplay(); - this.updatePointerCursor(); - }, - - _startWriting: function() { - this.currentElement.text = ''; - this.currentElement.cursorPosition = 0; - this.emit('show-osd', null, _("Type your text and press %s") - .format(Gtk.accelerator_get_label(Clutter.KEY_Escape, 0)), "", -1, true); - this._updateTextCursorTimeout(); - this.textHasCursor = true; - this._redisplay(); - - this.textEntry = new St.Entry({ visible: false }); - this.get_parent().add_child(this.textEntry); - this.textEntry.grab_key_focus(); - this.updateActionMode(); - this.updatePointerCursor(); - - this.textEntry.clutterText.connect('activate', (clutterText) => { - let startNewLine = true; - this._stopWriting(startNewLine); - clutterText.text = ""; - }); - - this.textEntry.clutterText.connect('text-changed', (clutterText) => { - GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { - this.currentElement.text = clutterText.text; - this.currentElement.cursorPosition = clutterText.cursorPosition; - this._updateTextCursorTimeout(); - this._redisplay(); - }); - }); - - this.textEntry.clutterText.connect('key-press-event', (clutterText, event) => { - if (event.get_key_symbol() == Clutter.KEY_Escape) { - this._stopWriting(); - return Clutter.EVENT_STOP; - } - - // 'cursor-changed' signal is not emitted if the text entry is not visible. - // So key events related to the cursor must be listened. - if (event.get_key_symbol() == Clutter.KEY_Left || event.get_key_symbol() == Clutter.KEY_Right || - event.get_key_symbol() == Clutter.KEY_Home || event.get_key_symbol() == Clutter.KEY_End) { - GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { - this.currentElement.cursorPosition = clutterText.cursorPosition; - this._updateTextCursorTimeout(); - this.textHasCursor = true; - this._redisplay(); - }); - } - - return Clutter.EVENT_PROPAGATE; - }); - }, - - _stopWriting: function(startNewLine) { - if (this.currentElement.text.length > 0) - this.elements.push(this.currentElement); - - if (startNewLine && this.currentElement.points.length == 2) { - this.currentElement.lineIndex = this.currentElement.lineIndex || 0; - // copy object, the original keep existing in this.elements - this.currentElement = Object.create(this.currentElement); - this.currentElement.lineIndex ++; - let height = Math.abs(this.currentElement.points[1][1] - this.currentElement.points[0][1]); - // define a new 'points' array, the original keep existing in this.elements - this.currentElement.points = [ - [this.currentElement.points[0][0], this.currentElement.points[0][1] + height], - [this.currentElement.points[1][0], this.currentElement.points[1][1] + height] - ]; - this.currentElement.text = ""; - } else { - this.currentElement = null; - this._stopTextCursorTimeout(); - this.textEntry.destroy(); - delete this.textEntry; - this.grab_key_focus(); - this.updateActionMode(); - this.updatePointerCursor(); - } - - this._redisplay(); - }, - - setPointerCursor: function(pointerCursorName) { - if (!this.currentPointerCursorName || this.currentPointerCursorName != pointerCursorName) { - this.currentPointerCursorName = pointerCursorName; - Extension.setCursor(pointerCursorName); - } - }, - - updatePointerCursor: function(controlPressed) { - if (this.currentTool == Manipulations.MIRROR && this.grabbedElementLocked) - this.setPointerCursor('CROSSHAIR'); - else if (this.hasManipulationTool) - this.setPointerCursor(this.grabbedElement ? 'MOVE_OR_RESIZE_WINDOW' : 'DEFAULT'); - else if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) - this.setPointerCursor('IBEAM'); - else if (!this.currentElement) - this.setPointerCursor(this.currentTool == Shapes.NONE ? 'POINTING_HAND' : 'CROSSHAIR'); - else if (this.currentElement.shape != Shapes.NONE && controlPressed) - this.setPointerCursor('MOVE_OR_RESIZE_WINDOW'); - }, - - initPointerCursor: function() { - this.currentPointerCursorName = null; - this.updatePointerCursor(); - }, - - _stopTextCursorTimeout: function() { - if (this.textCursorTimeoutId) { - GLib.source_remove(this.textCursorTimeoutId); - this.textCursorTimeoutId = null; - } - this.textHasCursor = false; - }, - - _updateTextCursorTimeout: function() { - this._stopTextCursorTimeout(); - this.textCursorTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, TEXT_CURSOR_TIME, () => { - this.textHasCursor = !this.textHasCursor; - this._redisplay(); - return GLib.SOURCE_CONTINUE; - }); - }, - - erase: function() { - this.deleteLastElement(); - this.elements = []; - this.undoneElements = []; - this._redisplay(); - }, - - deleteLastElement: function() { - if (this.currentElement) { - if (this.motionHandler) { - this.disconnect(this.motionHandler); - this.motionHandler = null; - } - if (this.buttonReleasedHandler) { - this.disconnect(this.buttonReleasedHandler); - this.buttonReleasedHandler = null; - } - if (this.isWriting) - this._stopWriting(); - this.currentElement = null; - } else { - this.elements.pop(); - } - this._redisplay(); - }, - - undo: function() { - if (this.elements.length > 0) - this.undoneElements.push(this.elements.pop()); - this._redisplay(); - }, - - redo: function() { - if (this.undoneElements.length > 0) - this.elements.push(this.undoneElements.pop()); - this._redisplay(); - }, - - smoothLastElement: function() { - if (this.elements.length > 0 && this.elements[this.elements.length - 1].shape == Shapes.NONE) { - this.elements[this.elements.length - 1].smoothAll(); - this._redisplay(); - } - }, - - toggleBackground: function() { - this.hasBackground = !this.hasBackground; - this.get_parent().set_background_color(this.hasBackground ? this.activeBackgroundColor : null); - }, - - toggleGrid: function() { - this.hasGrid = !this.hasGrid; - this._redisplay(); - }, - - toggleSquareArea: function() { - this.isSquareArea = !this.isSquareArea; - if (this.isSquareArea) { - let width = this.squareAreaWidth || this.squareAreaHeight || Math.min(this.monitor.width, this.monitor.height) * 3 / 4; - let height = this.squareAreaHeight || this.squareAreaWidth || Math.min(this.monitor.width, this.monitor.height) * 3 / 4; - this.set_position(Math.floor(this.monitor.width / 2 - width / 2), Math.floor(this.monitor.height / 2 - height / 2)); - this.set_size(width, height); - this.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'); - } - }, - - toggleColor: function() { - this.selectColor((this.currentColor == this.colors[1]) ? 2 : 1); - }, - - selectColor: function(index) { - this.currentColor = this.colors[index]; - if (this.currentElement) { - this.currentElement.color = this.currentColor.to_string(); - this._redisplay(); - } - // Foreground color markup is not displayed since 3.36, use style instead but the transparency is lost. - this.emit('show-osd', null, this.currentColor.to_string(), this.currentColor.to_string().slice(0, 7), -1, false); - }, - - selectTool: function(tool) { - this.currentTool = tool; - this.emit('show-osd', null, _(ToolNames[tool]), "", -1, false); - this.updatePointerCursor(); - }, - - toggleFill: function() { - this.fill = !this.fill; - this.emit('show-osd', null, this.fill ? _("Fill") : _("Outline"), "", -1, false); - }, - - toggleDash: function() { - this.dashedLine = !this.dashedLine; - this.emit('show-osd', null, this.dashedLine ? _("Dashed line") : _("Full line"), "", -1, false); - }, - - incrementLineWidth: function(increment) { - this.currentLineWidth = Math.max(this.currentLineWidth + increment, 0); - this.emit('show-osd', null, _("%d px").format(this.currentLineWidth), "", 2 * this.currentLineWidth, false); - }, - - toggleLineJoin: function() { - this.currentLineJoin = this.currentLineJoin == 2 ? 0 : this.currentLineJoin + 1; - this.emit('show-osd', null, _(LineJoinNames[this.currentLineJoin]), "", -1, false); - }, - - toggleLineCap: function() { - this.currentLineCap = this.currentLineCap == 2 ? 0 : this.currentLineCap + 1; - this.emit('show-osd', null, _(LineCapNames[this.currentLineCap]), "", -1, false); - }, - - toggleFillRule: function() { - this.currentFillRule = this.currentFillRule == 1 ? 0 : this.currentFillRule + 1; - this.emit('show-osd', null, _(FillRuleNames[this.currentFillRule]), "", -1, false); - }, - - toggleFontWeight: function() { - let fontWeights = Object.keys(FontWeightNames).map(key => Number(key)); - let index = fontWeights.indexOf(this.currentFontWeight); - this.currentFontWeight = index == fontWeights.length - 1 ? fontWeights[0] : fontWeights[index + 1]; - if (this.currentElement && this.currentElement.font) { - this.currentElement.font.weight = this.currentFontWeight; - this._redisplay(); - } - this.emit('show-osd', null, `` + - `${_(FontWeightNames[this.currentFontWeight])}`, "", -1, false); - }, - - toggleFontStyle: function() { - this.currentFontStyle = this.currentFontStyle == 2 ? 0 : this.currentFontStyle + 1; - if (this.currentElement && this.currentElement.font) { - this.currentElement.font.style = this.currentFontStyle; - this._redisplay(); - } - this.emit('show-osd', null, `` + - `${_(FontStyleNames[this.currentFontStyle])}`, "", -1, false); - }, - - toggleFontFamily: function() { - this.currentFontGeneric = this.currentFontGeneric == 5 ? 0 : this.currentFontGeneric + 1; - let currentFontFamily = this.currentFontGeneric == 0 ? this.currentThemeFontFamily : FontGenericNames[this.currentFontGeneric]; - if (this.currentElement && this.currentElement.font) { - this.currentElement.font.family = currentFontFamily; - this._redisplay(); - } - this.emit('show-osd', null, `${_(currentFontFamily)}`, "", -1, false); - }, - - toggleTextAlignment: function() { - this.currentTextRightAligned = !this.currentTextRightAligned; - if (this.currentElement && this.currentElement.textRightAligned !== undefined) { - this.currentElement.textRightAligned = this.currentTextRightAligned; - this._redisplay(); - } - this.emit('show-osd', null, this.currentTextRightAligned ? _("Right aligned") : _("Left aligned"), "", -1, false); - }, - - toggleHelp: function() { - if (this.helper.visible) { - this.helper.hideHelp(); - if (this.textEntry) - this.textEntry.grab_key_focus(); - } else { - this.helper.showHelp(); - this.grab_key_focus(); - } - - }, - - // The area is reactive when it is modal. - _onReactiveChanged: function() { - if (this.hasGrid) - this._redisplay(); - if (this.helper.visible) - this.toggleHelp(); - if (this.textEntry && this.reactive) - this.textEntry.grab_key_focus(); - }, - - _onDestroy: function() { - this.disconnect(this.reactiveHandler); - this.erase(); - if (this._menu) - this._menu.disable(); - }, - - updateActionMode: function() { - this.emit('update-action-mode'); - }, - - 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._onKeyboardPopupMenuHandler = 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.activeBackgroundColor : null); - this._updateStyle(); - }, - - leaveDrawingMode: function(save) { - 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; - } - if (this.buttonPressedHandler) { - this.disconnect(this.buttonPressedHandler); - this.buttonPressedHandler = null; - } - if (this._onKeyboardPopupMenuHandler) { - this.disconnect(this._onKeyboardPopupMenuHandler); - this._onKeyboardPopupMenuHandler = null; - } - if (this.motionHandler) { - this.disconnect(this.motionHandler); - this.motionHandler = null; - } - if (this.buttonReleasedHandler) { - this.disconnect(this.buttonReleasedHandler); - this.buttonReleasedHandler = null; - } - if (this.scrollHandler) { - this.disconnect(this.scrollHandler); - this.scrollHandler = null; - } - - this.currentElement = null; - this._stopTextCursorTimeout(); - this._redisplay(); - this.closeMenu(); - this.get_parent().set_background_color(null); - if (save) - this.savePersistent(); - }, - - saveAsSvg: function() { - // stop drawing or writing - if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) { - this._stopWriting(); - } else if (this.currentElement && this.currentElement.shape != Shapes.TEXT) { - this._stopDrawing(); - } - - let content = ``; - if (SVG_DEBUG_EXTENDS) - content = ``; - let backgroundColorString = this.hasBackground ? this.activeBackgroundColor.to_string() : 'transparent'; - if (backgroundColorString != 'transparent') { - content += `\n `; - } - if (SVG_DEBUG_EXTENDS) { - content += `\n `; - content += `\n `; - } - for (let i = 0; i < this.elements.length; i++) { - content += this.elements[i].buildSVG(backgroundColorString); - } - content += "\n"; - - let filename = `${Me.metadata['svg-file-name']} ${getDateString()}.svg`; - let dir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES); - let path = GLib.build_filenamev([dir, filename]); - if (GLib.file_test(path, GLib.FileTest.EXISTS)) - return false; - let success = GLib.file_set_contents(path, content); - - if (success) { - // pass the parent (bgContainer) to Flashspot because coords of this are relative - let flashspot = new Screenshot.Flashspot(this.get_parent()); - flashspot.fire(); - if (global.play_theme_sound) { - global.play_theme_sound(0, 'screen-capture', "Save as SVG", null); - } else if (global.display && global.display.get_sound_player) { - let player = global.display.get_sound_player(); - player.play_from_theme('screen-capture', "Save as SVG", null); - } - } - }, - - _saveAsJson: function(name, notify) { - // stop drawing or writing - if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) { - this._stopWriting(); - } else if (this.currentElement && this.currentElement.shape != Shapes.TEXT) { - this._stopDrawing(); - } - - let dir = GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir']]); - if (!GLib.file_test(dir, GLib.FileTest.EXISTS)) - GLib.mkdir_with_parents(dir, 0o700); - let path = GLib.build_filenamev([dir, `${name}.json`]); - - let oldContents; - - if (name == Me.metadata['persistent-file-name']) { - if (GLib.file_test(path, GLib.FileTest.EXISTS)) { - oldContents = GLib.file_get_contents(path)[1]; - if (oldContents instanceof Uint8Array) - oldContents = ByteArray.toString(oldContents); - } - - // do not create a file to write just an empty array - if (!oldContents && this.elements.length == 0) - return; - } - - // do not use "content = JSON.stringify(this.elements, null, 2);", neither "content = JSON.stringify(this.elements);" - // because of compromise between disk usage and human readability - let contents = `[\n ` + new Array(...this.elements.map(element => JSON.stringify(element))).join(`,\n\n `) + `\n]`; - - if (name == Me.metadata['persistent-file-name'] && contents == oldContents) - return; - - GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { - GLib.file_set_contents(path, contents); - if (notify) - this.emit('show-osd', 'document-save-symbolic', name, "", -1, false); - if (name != Me.metadata['persistent-file-name']) { - this.jsonName = name; - this.lastJsonContents = contents; - } - }); - }, - - saveAsJsonWithName: function(name) { - this._saveAsJson(name); - }, - - saveAsJson: function() { - this._saveAsJson(getDateString(), true); - }, - - savePersistent: function() { - this._saveAsJson(Me.metadata['persistent-file-name']); - }, - - syncPersistent: function() { - // do not override peristent.json with an empty drawing when changing persistency setting - if (!this.elements.length) - this._loadPersistent(); - else - this.savePersistent(); - - }, - - _loadJson: function(name, notify) { - // stop drawing or writing - if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) { - this._stopWriting(); - } else if (this.currentElement && this.currentElement.shape != Shapes.TEXT) { - this._stopDrawing(); - } - this.elements = []; - this.currentElement = null; - - let dir = GLib.get_user_data_dir(); - let path = GLib.build_filenamev([dir, Me.metadata['data-dir'], `${name}.json`]); - - if (!GLib.file_test(path, GLib.FileTest.EXISTS)) - return; - let [success, contents] = GLib.file_get_contents(path); - if (!success) - return; - if (contents instanceof Uint8Array) - contents = ByteArray.toString(contents); - this.elements.push(...JSON.parse(contents).map(object => new DrawingElement(object))); - - if (notify) - this.emit('show-osd', 'document-open-symbolic', name, "", -1, false); - if (name != Me.metadata['persistent-file-name']) { - this.jsonName = name; - this.lastJsonContents = contents; - } - }, - - _loadPersistent: function() { - this._loadJson(Me.metadata['persistent-file-name']); - }, - - loadJson: function(name, notify) { - this._loadJson(name, notify); - this._redisplay(); - }, - - loadNextJson: function() { - let names = getJsonFiles().map(file => file.name); - - if (!names.length) - return; - - let nextName = names[this.jsonName && names.indexOf(this.jsonName) != names.length - 1 ? names.indexOf(this.jsonName) + 1 : 0]; - this.loadJson(nextName, true); - }, - - loadPreviousJson: function() { - let names = getJsonFiles().map(file => file.name); - - if (!names.length) - return; - - let previousName = names[this.jsonName && names.indexOf(this.jsonName) > 0 ? names.indexOf(this.jsonName) - 1 : names.length - 1]; - this.loadJson(previousName, true); - }, - - get drawingContentsHasChanged() { - let contents = `[\n ` + new Array(...this.elements.map(element => JSON.stringify(element))).join(`,\n\n `) + `\n]`; - return contents != this.lastJsonContents; - } -}); - -const RADIAN = 180 / Math.PI; // degree -const INVERSION_CIRCLE_RADIUS = 12; // px -const REFLECTION_TOLERANCE = 5; // px, to select vertical and horizontal directions -const STRETCH_TOLERANCE = Math.PI / 8; // rad, to select vertical and horizontal directions -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 - -// DrawingElement represents a "brushstroke". -// 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: 'DrawOnYourScreenDrawingElement', - - _init: function(params) { - for (let key in params) - this[key] = params[key]; - - // compatibility with json generated by old extension versions - - if (params.fillRule === undefined) - this.fillRule = Cairo.FillRule.WINDING; - if (params.transformations === undefined) - this.transformations = []; - if (params.shape == Shapes.TEXT) { - if (params.font && params.font.weight === 0) - this.font.weight = 400; - if (params.font && params.font.weight === 1) - this.font.weight = 700; - } - - if (params.transform && params.transform.center) { - let angle = (params.transform.angle || 0) + (params.transform.startAngle || 0); - if (angle) - this.transformations.push({ type: Transformations.ROTATION, angle: angle }); - } - if (params.shape == Shapes.ELLIPSE && params.transform && params.transform.ratio && params.transform.ratio != 1 && params.points.length >= 2) { - let [ratio, p0, p1] = [params.transform.ratio, params.points[0], params.points[1]]; - // Add a fake point that will give the right ellipse ratio when building the element. - this.points.push([ratio * (p1[0] - p0[0]) + p0[0], ratio * (p1[1] - p0[1]) + p0[1]]); - } - delete this.transform; - }, - - // toJSON is called by JSON.stringify - toJSON: function() { - return { - shape: this.shape, - color: this.color, - line: this.line, - dash: this.dash, - fill: this.fill, - fillRule: this.fillRule, - eraser: this.eraser, - transformations: this.transformations, - text: this.text, - lineIndex: this.lineIndex !== undefined ? this.lineIndex : undefined, - textRightAligned: this.textRightAligned, - font: this.font, - points: this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100]) - }; - }, - - buildCairo: function(cr, params) { - let [success, color] = Clutter.Color.from_string(this.color); - if (success) - Clutter.cairo_set_source_color(cr, color); - - if (this.showSymmetryElement) { - let transformation = this.lastTransformation; - setDummyStroke(cr); - if (transformation.type == Transformations.REFLECTION) { - cr.moveTo(transformation.startX, transformation.startY); - cr.lineTo(transformation.endX, transformation.endY); - } else { - cr.arc(transformation.endX, transformation.endY, INVERSION_CIRCLE_RADIUS, 0, 2 * Math.PI); - } - cr.stroke(); - } - - cr.setLineCap(this.line.lineCap); - cr.setLineJoin(this.line.lineJoin); - cr.setLineWidth(this.line.lineWidth); - if (this.fillRule) - cr.setFillRule(this.fillRule); - - if (this.dash && this.dash.active && this.dash.array && this.dash.array[0] && this.dash.array[1]) - cr.setDash(this.dash.array, this.dash.offset); - - if (this.eraser) - cr.setOperator(Cairo.Operator.CLEAR); - else - cr.setOperator(Cairo.Operator.OVER); - - if (params.dummyStroke) - setDummyStroke(cr); - - if (SVG_DEBUG_SUPERPOSES_CAIRO) { - Clutter.cairo_set_source_color(cr, Clutter.Color.new(255, 0, 0, 255)); - cr.setLineWidth(this.line.lineWidth / 2 || 1); - } - - this.transformations.slice(0).reverse().forEach(transformation => { - if (transformation.type == Transformations.TRANSLATION) { - cr.translate(transformation.slideX, transformation.slideY); - } else if (transformation.type == Transformations.ROTATION) { - let center = this._getTransformedCenter(transformation); - cr.translate(center[0], center[1]); - cr.rotate(transformation.angle); - cr.translate(-center[0], -center[1]); - } else if (transformation.type == Transformations.SCALE_PRESERVE || transformation.type == Transformations.STRETCH) { - let center = this._getTransformedCenter(transformation); - cr.translate(center[0], center[1]); - cr.rotate(transformation.angle); - cr.scale(transformation.scaleX, transformation.scaleY); - cr.rotate(-transformation.angle); - cr.translate(-center[0], -center[1]); - } else if (transformation.type == Transformations.REFLECTION || transformation.type == Transformations.INVERSION) { - cr.translate(transformation.slideX, transformation.slideY); - cr.rotate(transformation.angle); - cr.scale(transformation.scaleX, transformation.scaleY); - cr.rotate(-transformation.angle); - cr.translate(-transformation.slideX, -transformation.slideY); - } - }); - - let [points, shape] = [this.points, this.shape]; - - if (shape == Shapes.LINE && points.length == 3) { - cr.moveTo(points[0][0], points[0][1]); - cr.curveTo(points[0][0], points[0][1], points[1][0], points[1][1], points[2][0], points[2][1]); - - } else if (shape == Shapes.LINE && points.length == 4) { - cr.moveTo(points[0][0], points[0][1]); - cr.curveTo(points[1][0], points[1][1], points[2][0], points[2][1], points[3][0], points[3][1]); - - } else if (shape == Shapes.NONE || shape == Shapes.LINE) { - cr.moveTo(points[0][0], points[0][1]); - for (let j = 1; j < points.length; j++) { - cr.lineTo(points[j][0], points[j][1]); - } - - } else if (shape == Shapes.ELLIPSE && points.length >= 2) { - let radius = Math.hypot(points[1][0] - points[0][0], points[1][1] - points[0][1]); - let ratio = 1; - - if (points[2]) { - ratio = Math.hypot(points[2][0] - points[0][0], points[2][1] - points[0][1]) / radius; - cr.translate(points[0][0], points[0][1]); - cr.scale(ratio, 1); - cr.translate(-points[0][0], -points[0][1]); - cr.arc(points[0][0], points[0][1], radius, 0, 2 * Math.PI); - cr.translate(points[0][0], points[0][1]); - cr.scale(1 / ratio, 1); - cr.translate(-points[0][0], -points[0][1]); - } else - cr.arc(points[0][0], points[0][1], radius, 0, 2 * Math.PI); - - } else if (shape == Shapes.RECTANGLE && points.length == 2) { - cr.rectangle(points[0][0], points[0][1], points[1][0] - points[0][0], points[1][1] - points[0][1]); - - } else if ((shape == Shapes.POLYGON || shape == Shapes.POLYLINE) && points.length >= 2) { - cr.moveTo(points[0][0], points[0][1]); - for (let j = 1; j < points.length; j++) { - cr.lineTo(points[j][0], points[j][1]); - } - if (shape == Shapes.POLYGON) - cr.closePath(); - - } else if (shape == Shapes.TEXT && points.length == 2) { - let layout = PangoCairo.create_layout(cr); - let fontSize = Math.abs(points[1][1] - points[0][1]) * Pango.SCALE; - let fontDescription = new Pango.FontDescription(); - fontDescription.set_absolute_size(fontSize); - ['family', 'weight', 'style', 'stretch', 'variant'].forEach(attribute => { - if (this.font[attribute] !== undefined) - try { - fontDescription[`set_${attribute}`](this.font[attribute]); - } catch(e) {} - }); - layout.set_font_description(fontDescription); - layout.set_text(this.text, -1); - this.textWidth = layout.get_pixel_size()[0]; - cr.moveTo(points[1][0] - (this.textRightAligned ? this.textWidth : 0), Math.max(points[0][1],points[1][1]) - layout.get_baseline() / Pango.SCALE); - layout.set_text(this.text, -1); - PangoCairo.show_layout(cr, layout); - - if (params.showTextCursor) { - let cursorPosition = this.cursorPosition == -1 ? this.text.length : this.cursorPosition; - layout.set_text(this.text.slice(0, cursorPosition), -1); - let width = layout.get_pixel_size()[0]; - cr.rectangle(points[1][0] - (this.textRightAligned ? this.textWidth : 0) + width, Math.max(points[0][1],points[1][1]), - Math.abs(points[1][1] - points[0][1]) / 25, - Math.abs(points[1][1] - points[0][1])); - cr.fill(); - } - - if (params.showTextRectangle || params.drawTextRectangle) { - cr.rectangle(points[1][0] - (this.textRightAligned ? this.textWidth : 0), Math.max(points[0][1], points[1][1]), - this.textWidth, - Math.abs(points[1][1] - points[0][1])); - if (params.showTextRectangle) - setDummyStroke(cr); - else - // Only draw the rectangle to find the element, not to show it. - cr.setLineWidth(0); - } - } - - cr.identityMatrix(); - }, - - getContainsPoint: function(cr, x, y) { - if (this.shape == Shapes.TEXT) - return cr.inFill(x, y); - - cr.save(); - cr.setLineWidth(Math.max(this.line.lineWidth, 25)); - cr.setDash([], 0); - - // Check whether the point is inside/on/near the element. - let inElement = cr.inStroke(x, y) || this.fill && cr.inFill(x, y); - cr.restore(); - return inElement; - }, - - buildSVG: function(bgColor) { - 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 ? bgColor : this.color; - let fill = this.fill && !this.isStraightLine; - let attributes = ''; - - if (fill) { - attributes = `fill="${color}"`; - if (this.fillRule) - attributes += ` fill-rule="${FillRuleNames[this.fillRule].toLowerCase()}"`; - } else { - attributes = `fill="none"`; - } - - if (this.line && this.line.lineWidth) { - attributes += ` stroke="${color}"` + - ` stroke-width="${this.line.lineWidth}"`; - if (this.line.lineCap) - attributes += ` stroke-linecap="${LineCapNames[this.line.lineCap].toLowerCase()}"`; - if (this.line.lineJoin && !this.isStraightLine) - attributes += ` stroke-linejoin="${LineJoinNames[this.line.lineJoin].toLowerCase()}"`; - if (this.dash && this.dash.active && this.dash.array && this.dash.array[0] && this.dash.array[1]) - attributes += ` stroke-dasharray="${this.dash.array[0]} ${this.dash.array[1]}" stroke-dashoffset="${this.dash.offset}"`; - } else { - attributes += ` stroke="none"`; - } - - let transAttribute = ''; - this.transformations.slice(0).reverse().forEach(transformation => { - transAttribute += transAttribute ? ' ' : ' transform="'; - let center = this._getTransformedCenter(transformation); - - if (transformation.type == Transformations.TRANSLATION) { - transAttribute += `translate(${transformation.slideX},${transformation.slideY})`; - } else if (transformation.type == Transformations.ROTATION) { - transAttribute += `translate(${center[0]},${center[1]}) `; - transAttribute += `rotate(${transformation.angle * RADIAN}) `; - transAttribute += `translate(${-center[0]},${-center[1]})`; - } else if (transformation.type == Transformations.SCALE_PRESERVE || transformation.type == Transformations.STRETCH) { - transAttribute += `translate(${center[0]},${center[1]}) `; - transAttribute += `rotate(${transformation.angle * RADIAN}) `; - transAttribute += `scale(${transformation.scaleX},${transformation.scaleY}) `; - transAttribute += `rotate(${-transformation.angle * RADIAN}) `; - transAttribute += `translate(${-center[0]},${-center[1]})`; - } else if (transformation.type == Transformations.REFLECTION || transformation.type == Transformations.INVERSION) { - transAttribute += `translate(${transformation.slideX}, ${transformation.slideY}) `; - transAttribute += `rotate(${transformation.angle * RADIAN}) `; - transAttribute += `scale(${transformation.scaleX}, ${transformation.scaleY}) `; - transAttribute += `rotate(${-transformation.angle * RADIAN}) `; - transAttribute += `translate(${-transformation.slideX}, ${-transformation.slideY})`; - } - }); - transAttribute += transAttribute ? '"' : ''; - - if (this.shape == Shapes.LINE && points.length == 4) { - row += ``; - - } else if (this.shape == Shapes.LINE && points.length == 3) { - row += ``; - - } else if (this.shape == Shapes.LINE) { - row += ``; - - } else if (this.shape == Shapes.NONE) { - row += ``; - - } else if (this.shape == Shapes.ELLIPSE && points.length == 3) { - let ry = Math.hypot(points[1][0] - points[0][0], points[1][1] - points[0][1]); - let rx = Math.hypot(points[2][0] - points[0][0], points[2][1] - points[0][1]); - row += ``; - - } else if (this.shape == Shapes.ELLIPSE && points.length == 2) { - let r = Math.hypot(points[1][0] - points[0][0], points[1][1] - points[0][1]); - row += ``; - - } else if (this.shape == Shapes.RECTANGLE && points.length == 2) { - row += ``; - - } else if (this.shape == Shapes.POLYGON && points.length >= 3) { - row += ``; - - } else if (this.shape == Shapes.POLYLINE && points.length >= 2) { - row += ``; - - } else if (this.shape == Shapes.TEXT && points.length == 2) { - attributes = `fill="${color}" ` + - `stroke="transparent" ` + - `stroke-opacity="0" ` + - `font-size="${Math.abs(points[1][1] - points[0][1])}"`; - - if (this.font.family) - attributes += ` font-family="${this.font.family}"`; - if (this.font.weight && this.font.weight != Pango.Weight.NORMAL) - attributes += ` font-weight="${this.font.weight}"`; - if (this.font.style && FontStyleNames[this.font.style]) - attributes += ` font-style="${FontStyleNames[this.font.style].toLowerCase()}"`; - if (FontStretchNames[this.font.stretch] && this.font.stretch != Pango.Stretch.NORMAL) - attributes += ` font-stretch="${FontStretchNames[this.font.stretch].toLowerCase()}"`; - if (this.font.variant && FontVariantNames[this.font.variant]) - attributes += ` font-variant="${FontVariantNames[this.font.variant].toLowerCase()}"`; - - // this.textWidth is computed during Cairo building. - row += `${this.text}`; - } - - return row; - }, - - get lastTransformation() { - if (!this.transformations.length) - return null; - - return this.transformations[this.transformations.length - 1]; - }, - - get isStraightLine() { - return this.shape == Shapes.LINE && this.points.length == 2; - }, - - smoothAll: function() { - for (let i = 0; i < this.points.length; i++) { - this._smooth(i); - } - }, - - addPoint: function() { - if (this.shape == Shapes.POLYGON || this.shape == Shapes.POLYLINE) { - // copy last point - let [lastPoint, secondToLastPoint] = [this.points[this.points.length - 1], this.points[this.points.length - 2]]; - if (!getNearness(secondToLastPoint, lastPoint, MIN_DRAWING_SIZE)) - this.points.push([lastPoint[0], lastPoint[1]]); - } else if (this.shape == Shapes.LINE) { - if (this.points.length == 2) { - this.points[2] = this.points[1]; - } else if (this.points.length == 3) { - this.points[3] = this.points[2]; - this.points[2] = this.points[1]; - } - } - }, - - startDrawing: function(startX, startY) { - this.points.push([startX, startY]); - - if (this.shape == Shapes.POLYGON || this.shape == Shapes.POLYLINE) - this.points.push([startX, startY]); - }, - - updateDrawing: function(x, y, transform) { - let points = this.points; - if (x == points[points.length - 1][0] && y == points[points.length - 1][1]) - return; - - transform = transform || this.transformations.length >= 1; - - if (this.shape == Shapes.NONE) { - points.push([x, y]); - if (transform) - this._smooth(points.length - 1); - - } else if ((this.shape == Shapes.RECTANGLE || this.shape == Shapes.POLYGON || this.shape == Shapes.POLYLINE) && transform) { - if (points.length < 2) - return; - - let center = this._getOriginalCenter(); - 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) { - if (points.length < 2) - return; - - points[2] = [x, y]; - let center = this._getOriginalCenter(); - 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) { - points[points.length - 1] = [x, y]; - - } else if (this.shape == Shapes.TEXT && transform) { - if (points.length < 2) - return; - - let [slideX, slideY] = [x - points[1][0], y - points[1][1]]; - points[0] = [points[0][0] + slideX, points[0][1] + slideY]; - points[1] = [x, y]; - - } else { - points[1] = [x, y]; - - } - }, - - stopDrawing: function() { - // skip when the size is too small to be visible (3px) (except for free drawing) - if (this.shape != Shapes.NONE && this.points.length >= 2) { - let lastPoint = this.points[this.points.length - 1]; - let secondToLastPoint = this.points[this.points.length - 2]; - if (getNearness(secondToLastPoint, lastPoint, MIN_DRAWING_SIZE)) - this.points.pop(); - } - - if (this.transformations[0] && this.transformations[0].type == Transformations.ROTATION && - Math.abs(this.transformations[0].angle) < MIN_ROTATION_ANGLE) - this.transformations.shift(); - }, - - startTransformation: function(startX, startY, type) { - if (type == Transformations.TRANSLATION) - this.transformations.push({ startX: startX, startY: startY, type: type, slideX: 0, slideY: 0 }); - else if (type == Transformations.ROTATION) - this.transformations.push({ startX: startX, startY: startY, type: type, 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 }); - else if (type == Transformations.REFLECTION) - this.transformations.push({ startX: startX, startY: startY, endX: startX, endY: startY, type: type, - 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, - scaleX: -1, scaleY: -1, slideX: startX, slideY: startY, - angle: Math.PI + Math.atan(startY / (startX || 1)) }); - - if (type == Transformations.REFLECTION || type == Transformations.INVERSION) - this.showSymmetryElement = true; - }, - - updateTransformation: function(x, y) { - let transformation = this.lastTransformation; - - if (transformation.type == Transformations.TRANSLATION) { - transformation.slideX = x - transformation.startX; - transformation.slideY = y - transformation.startY; - } else if (transformation.type == Transformations.ROTATION) { - let center = this._getTransformedCenter(transformation); - transformation.angle = getAngle(center[0], center[1], transformation.startX, transformation.startY, x, y); - } else if (transformation.type == Transformations.SCALE_PRESERVE) { - let center = this._getTransformedCenter(transformation); - let scale = Math.hypot(x - center[0], y - center[1]) / Math.hypot(transformation.startX - center[0], transformation.startY - center[1]) || 1; - [transformation.scaleX, transformation.scaleY] = [scale, scale]; - } else if (transformation.type == Transformations.STRETCH) { - let center = this._getTransformedCenter(transformation); - let startAngle = getAngle(center[0], center[1], center[0] + 1, center[1], transformation.startX, transformation.startY); - let vertical = Math.abs(Math.sin(startAngle)) >= Math.sin(Math.PI / 2 - STRETCH_TOLERANCE); - let horizontal = Math.abs(Math.cos(startAngle)) >= Math.cos(STRETCH_TOLERANCE); - let scale = Math.hypot(x - center[0], y - center[1]) / Math.hypot(transformation.startX - center[0], transformation.startY - center[1]) || 1; - transformation.scaleX = vertical ? 1 : scale; - transformation.scaleY = !vertical ? 1 : scale; - transformation.angle = vertical || horizontal ? 0 : getAngle(center[0], center[1], center[0] + 1, center[1], x, y); - } else if (transformation.type == Transformations.REFLECTION) { - [transformation.endX, transformation.endY] = [x, y]; - if (getNearness([transformation.startX, transformation.startY], [x, y], MIN_REFLECTION_LINE_LENGTH)) { - // do nothing to avoid jumps (no transformation at starting and locked transformation after) - } else if (Math.abs(y - transformation.startY) <= REFLECTION_TOLERANCE && Math.abs(x - transformation.startX) > REFLECTION_TOLERANCE) { - [transformation.scaleX, transformation.scaleY] = [1, -1]; - [transformation.slideX, transformation.slideY] = [0, transformation.startY]; - transformation.angle = Math.PI; - } else if (Math.abs(x - transformation.startX) <= REFLECTION_TOLERANCE && Math.abs(y - transformation.startY) > REFLECTION_TOLERANCE) { - [transformation.scaleX, transformation.scaleY] = [-1, 1]; - [transformation.slideX, transformation.slideY] = [transformation.startX, 0]; - transformation.angle = Math.PI; - } else if (x != transformation.startX) { - let tan = (y - transformation.startY) / (x - transformation.startX); - [transformation.scaleX, transformation.scaleY] = [1, -1]; - [transformation.slideX, transformation.slideY] = [0, transformation.startY - transformation.startX * tan]; - transformation.angle = Math.PI + Math.atan(tan); - } else if (y != transformation.startY) { - let tan = (x - transformation.startX) / (y - transformation.startY); - [transformation.scaleX, transformation.scaleY] = [-1, 1]; - [transformation.slideX, transformation.slideY] = [transformation.startX - transformation.startY * tan, 0]; - transformation.angle = Math.PI - Math.atan(tan); - } - } else if (transformation.type == Transformations.INVERSION) { - [transformation.endX, transformation.endY] = [x, y]; - [transformation.scaleX, transformation.scaleY] = [-1, -1]; - [transformation.slideX, transformation.slideY] = [x, y]; - transformation.angle = Math.PI + Math.atan(y / (x || 1)); - } - }, - - stopTransformation: function() { - // Clean transformations - let transformation = this.lastTransformation; - - if (transformation.type == Transformations.REFLECTION || transformation.type == Transformations.INVERSION) - this.showSymmetryElement = false; - - if (transformation.type == Transformations.REFLECTION && - getNearness([transformation.startX, transformation.startY], [transformation.endX, transformation.endY], MIN_REFLECTION_LINE_LENGTH) || - transformation.type == Transformations.TRANSLATION && Math.hypot(transformation.slideX, transformation.slideY) < MIN_TRANSLATION_DISTANCE || - transformation.type == Transformations.ROTATION && Math.abs(transformation.angle) < MIN_ROTATION_ANGLE) { - - this.transformations.pop(); - } else { - delete transformation.startX; - delete transformation.startY; - delete transformation.endX; - delete transformation.endY; - } - }, - - // When rotating grouped lines, lineOffset is used to retrieve the rotation center of the first line. - _getLineOffset: function() { - return (this.lineIndex || 0) * Math.abs(this.points[1][1] - this.points[0][1]); - }, - - // The figure rotation center before transformations (original). - // this.textWidth is computed during Cairo building. - _getOriginalCenter: function() { - if (!this._originalCenter) { - let points = this.points; - this._originalCenter = this.shape == Shapes.ELLIPSE ? [points[0][0], points[0][1]] : - this.shape == Shapes.LINE && points.length == 4 ? getCurveCenter(points[0], points[1], points[2], points[3]) : - this.shape == Shapes.LINE && points.length == 3 ? getCurveCenter(points[0], points[0], points[1], points[2]) : - this.shape == Shapes.TEXT && this.textWidth ? [points[1][0], Math.max(points[0][1], points[1][1]) - this._getLineOffset()] : - points.length >= 3 ? getCentroid(points) : - getNaiveCenter(points); - } - - return this._originalCenter; - }, - - // The figure rotation center, whose position is affected by all transformations done before 'transformation'. - _getTransformedCenter: function(transformation) { - if (!transformation.elementTransformedCenter) { - let matrix = new Pango.Matrix({ xx: 1, xy: 0, yx: 0, yy: 1, x0: 0, y0: 0 }); - - // Apply transformations to the matrice in reverse order - // because Pango multiply matrices by the left when applying a transformation - this.transformations.slice(0, this.transformations.indexOf(transformation)).reverse().forEach(transformation => { - if (transformation.type == Transformations.TRANSLATION) { - matrix.translate(transformation.slideX, transformation.slideY); - } else if (transformation.type == Transformations.ROTATION) { - // nothing, the center position is preserved. - } else if (transformation.type == Transformations.SCALE_PRESERVE || transformation.type == Transformations.STRETCH) { - // nothing, the center position is preserved. - } else if (transformation.type == Transformations.REFLECTION || transformation.type == Transformations.INVERSION) { - matrix.translate(transformation.slideX, transformation.slideY); - matrix.rotate(-transformation.angle * RADIAN); - matrix.scale(transformation.scaleX, transformation.scaleY); - matrix.rotate(transformation.angle * RADIAN); - matrix.translate(-transformation.slideX, -transformation.slideY); - } - }); - - let originalCenter = this._getOriginalCenter(); - transformation.elementTransformedCenter = matrix.transform_point(originalCenter[0], originalCenter[1]); - } - - return transformation.elementTransformedCenter; - }, - - _smooth: function(i) { - if (i < 2) - return; - this.points[i-1] = [(this.points[i-2][0] + this.points[i][0]) / 2, (this.points[i-2][1] + this.points[i][1]) / 2]; - } -}); - -const setDummyStroke = function(cr) { - cr.setLineWidth(2); - cr.setLineCap(0); - cr.setLineJoin(0); - cr.setDash([1, 2], 0); -}; - -/* - Some geometric utils -*/ - -const getNearness = function(pointA, pointB, distance) { - return Math.hypot(pointB[0] - pointA[0], pointB[1] - pointA[1]) < distance; -}; - -// mean of the vertices, ok for regular polygons -const getNaiveCenter = function(points) { - return points.reduce((accumulator, point) => accumulator = [accumulator[0] + point[0], accumulator[1] + point[1]]) - .map(coord => coord / points.length); -}; - -// https://en.wikipedia.org/wiki/Centroid#Of_a_polygon -const getCentroid = function(points) { - let n = points.length; - points.push(points[0]); - - let [sA, sX, sY] = [0, 0, 0]; - for (let i = 0; i <= n-1; i++) { - let a = points[i][0]*points[i+1][1] - points[i+1][0]*points[i][1]; - sA += a; - sX += (points[i][0] + points[i+1][0]) * a; - sY += (points[i][1] + points[i+1][1]) * a; - } - - points.pop(); - if (sA == 0) - return getNaiveCenter(points); - return [sX / (3 * sA), sY / (3 * sA)]; -}; - -/* -Cubic Bézier: -[0, 1] -> ℝ², P(t) = (1-t)³P₀ + 3t(1-t)²P₁ + 3t²(1-t)P₂ + t³P₃ - -general case: - -const cubicBezierCoord = function(x0, x1, x2, x3, t) { - return (1-t)**3*x0 + 3*t*(1-t)**2*x1 + 3*t**2*(1-t)*x2 + t**3*x3; -} - -const cubicBezierPoint = function(p0, p1, p2, p3, t) { - return [cubicBezier(p0[0], p1[0], p2[0], p3[0], t), cubicBezier(p0[1], p1[1], p2[1], p3[1], t)]; -} - -Approximatively: -control point: p0 ---- p1 ---- p2 ---- p3 (p2 is not on the curve) - t: 0 ---- 1/3 ---- 2/3 ---- 1 -*/ - -// If the curve has a symmetry axis, it is truly a center (the intersection of the curve and the axis). -// In other cases, it is not a notable point, just a visual approximation. -const getCurveCenter = function(p0, p1, p2, p3) { - if (p0[0] == p1[0] && p0[1] == p1[1]) - // p0 = p1, t = 2/3 - return [(p1[0] + 6*p1[0] + 12*p2[0] + 8*p3[0]) / 27, (p1[1] + 6*p1[1] + 12*p2[1] + 8*p3[1]) / 27]; - else - // t = 1/2 - return [(p0[0] + 3*p1[0] + 3*p2[0] + p3[0]) / 8, (p0[1] + 3*p1[1] + 3*p2[1] + p3[1]) / 8]; -}; - -const getAngle = function(xO, yO, xA, yA, xB, yB) { - // calculate angle of rotation in absolute value - // cos(AOB) = (OA.OB)/(||OA||*||OB||) where OA.OB = (xA-xO)*(xB-xO) + (yA-yO)*(yB-yO) - let cos = ((xA - xO)*(xB - xO) + (yA - yO)*(yB - yO)) / (Math.hypot(xA - xO, yA - yO) * Math.hypot(xB - xO, yB - yO)); - - // acos is defined on [-1, 1] but - // with A == B and imperfect computer calculations, cos may be equal to 1.00000001. - cos = Math.min(Math.max(-1, cos), 1); - let angle = Math.acos( cos ); - - // determine the sign of the angle - if (xA == xO) { - if (xB > xO) - angle = -angle; - } else { - // equation of OA: y = ax + b - let a = (yA - yO) / (xA - xO); - let b = yA - a*xA; - if (yB < a*xB + b) - angle = - angle; - if (xA < xO) - angle = - angle; - } - - return angle; -}; const HELPER_ANIMATION_TIME = 0.25; const MEDIA_KEYS_SCHEMA = 'org.gnome.settings-daemon.plugins.media-keys'; From 9269022d0f5408932ed3e6698608a3c1cb29841e Mon Sep 17 00:00:00 2001 From: abakkk Date: Fri, 3 Jul 2020 23:08:50 +0200 Subject: [PATCH 07/31] draw-to-osd 3 --- draw.js | 153 -------------------------------------------------------- 1 file changed, 153 deletions(-) diff --git a/draw.js b/draw.js index f2a520b..eef5cd8 100644 --- a/draw.js +++ b/draw.js @@ -32,19 +32,15 @@ const Pango = imports.gi.Pango; const PangoCairo = imports.gi.PangoCairo; const St = imports.gi.St; -const Config = imports.misc.config; const Screenshot = imports.ui.screenshot; -const Tweener = imports.ui.tweener; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Convenience = ExtensionUtils.getSettings ? ExtensionUtils : Me.imports.convenience; const Extension = Me.imports.extension; const Menu = Me.imports.menu; -const Prefs = Me.imports.prefs; const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; -const GS_VERSION = Config.PACKAGE_VERSION; const CAIRO_DEBUG_EXTENDS = false; const SVG_DEBUG_EXTENDS = false; const SVG_DEBUG_SUPERPOSES_CAIRO = false; @@ -1903,152 +1899,3 @@ const getAngle = function(xO, yO, xA, yA, xB, yB) { return angle; }; -const HELPER_ANIMATION_TIME = 0.25; -const MEDIA_KEYS_SCHEMA = 'org.gnome.settings-daemon.plugins.media-keys'; -const MEDIA_KEYS_KEYS = { - 'screenshot': "Screenshot", - 'screenshot-clip': "Screenshot to clipboard", - 'area-screenshot': "Area screenshot", - 'area-screenshot-clip': "Area screenshot to clipboard" -}; - -// DrawingHelper provides the "help osd" (Ctrl + F1) -// It uses the same texts as in prefs -var DrawingHelper = new Lang.Class({ - Name: 'DrawOnYourScreenDrawingHelper', - Extends: St.ScrollView, - - _init: function(params, monitor) { - params.style_class = 'osd-window draw-on-your-screen-helper'; - this.parent(params); - this.monitor = monitor; - this.hide(); - this.settings = Convenience.getSettings(); - - this.settingHandler = this.settings.connect('changed', this._onSettingChanged.bind(this)); - this.connect('destroy', () => this.settings.disconnect(this.settingHandler)); - }, - - _onSettingChanged: function(settings, key) { - if (key == 'toggle-help') - this._updateHelpKeyLabel(); - - if (this.vbox) { - this.vbox.destroy(); - this.vbox = null; - } - }, - - _updateHelpKeyLabel: function() { - let [keyval, mods] = Gtk.accelerator_parse(this.settings.get_strv('toggle-help')[0]); - this._helpKeyLabel = Gtk.accelerator_get_label(keyval, mods); - }, - - get helpKeyLabel() { - if (!this._helpKeyLabel) - this._updateHelpKeyLabel(); - - return this._helpKeyLabel; - }, - - _populate: function() { - this.vbox = new St.BoxLayout({ vertical: true }); - this.add_actor(this.vbox); - this.vbox.add_child(new St.Label({ text: _("Global") })); - - for (let settingKey in Prefs.GLOBAL_KEYBINDINGS) { - let hbox = new St.BoxLayout({ vertical: false }); - if (settingKey.indexOf('-separator-') != -1) { - this.vbox.add_child(hbox); - continue; - } - if (!this.settings.get_strv(settingKey)[0]) - continue; - let [keyval, mods] = Gtk.accelerator_parse(this.settings.get_strv(settingKey)[0]); - hbox.add_child(new St.Label({ text: _(Prefs.GLOBAL_KEYBINDINGS[settingKey]) })); - hbox.add_child(new St.Label({ text: Gtk.accelerator_get_label(keyval, mods), x_expand: true })); - this.vbox.add_child(hbox); - } - - this.vbox.add_child(new St.Label({ text: _("Internal") })); - - for (let i = 0; i < Prefs.OTHER_SHORTCUTS.length; i++) { - if (Prefs.OTHER_SHORTCUTS[i].desc.indexOf('-separator-') != -1) { - this.vbox.add_child(new St.BoxLayout({ vertical: false, style_class: 'draw-on-your-screen-helper-separator' })); - continue; - } - let hbox = new St.BoxLayout({ vertical: false }); - hbox.add_child(new St.Label({ text: _(Prefs.OTHER_SHORTCUTS[i].desc) })); - hbox.add_child(new St.Label({ text: Prefs.OTHER_SHORTCUTS[i].shortcut, x_expand: true })); - hbox.get_children()[0].get_clutter_text().set_use_markup(true); - this.vbox.add_child(hbox); - } - - this.vbox.add_child(new St.BoxLayout({ vertical: false, style_class: 'draw-on-your-screen-helper-separator' })); - - for (let settingKey in Prefs.INTERNAL_KEYBINDINGS) { - if (settingKey.indexOf('-separator-') != -1) { - this.vbox.add_child(new St.BoxLayout({ vertical: false, style_class: 'draw-on-your-screen-helper-separator' })); - continue; - } - let hbox = new St.BoxLayout({ vertical: false }); - if (!this.settings.get_strv(settingKey)[0]) - continue; - let [keyval, mods] = Gtk.accelerator_parse(this.settings.get_strv(settingKey)[0]); - hbox.add_child(new St.Label({ text: _(Prefs.INTERNAL_KEYBINDINGS[settingKey]) })); - hbox.add_child(new St.Label({ text: Gtk.accelerator_get_label(keyval, mods), x_expand: true })); - this.vbox.add_child(hbox); - } - - let mediaKeysSettings; - try { mediaKeysSettings = Convenience.getSettings(MEDIA_KEYS_SCHEMA); } catch(e) { return; } - this.vbox.add_child(new St.Label({ text: _("System") })); - - for (let settingKey in MEDIA_KEYS_KEYS) { - if (!mediaKeysSettings.settings_schema.has_key(settingKey)) - continue; - let shortcut = GS_VERSION < '3.33.0' ? mediaKeysSettings.get_string(settingKey) : mediaKeysSettings.get_strv(settingKey)[0]; - if (!shortcut) - continue; - let [keyval, mods] = Gtk.accelerator_parse(shortcut); - let hbox = new St.BoxLayout({ vertical: false }); - hbox.add_child(new St.Label({ text: _(MEDIA_KEYS_KEYS[settingKey]) })); - hbox.add_child(new St.Label({ text: Gtk.accelerator_get_label(keyval, mods), x_expand: true })); - this.vbox.add_child(hbox); - } - }, - - showHelp: function() { - if (!this.vbox) - this._populate(); - - this.opacity = 0; - this.show(); - - let maxHeight = this.monitor.height * 3 / 4; - this.set_height(Math.min(this.height, maxHeight)); - this.set_position(Math.floor(this.monitor.width / 2 - this.width / 2), - Math.floor(this.monitor.height / 2 - this.height / 2)); - - if (this.height == maxHeight) - this.vscrollbar_policy = Gtk.PolicyType.ALWAYS; - else - this.vscrollbar_policy = Gtk.PolicyType.NEVER; - - Tweener.removeTweens(this); - Tweener.addTween(this, { opacity: 255, - time: HELPER_ANIMATION_TIME, - transition: 'easeOutQuad', - onComplete: null }); - }, - - hideHelp: function() { - Tweener.removeTweens(this); - Tweener.addTween(this, { opacity: 0, - time: HELPER_ANIMATION_TIME, - transition: 'easeOutQuad', - onComplete: this.hide.bind(this) }); - - } -}); - From 0c311ae477d0182d12fa66eb399599b172604d61 Mon Sep 17 00:00:00 2001 From: abakkk Date: Fri, 3 Jul 2020 23:16:43 +0200 Subject: [PATCH 08/31] draw-to-osd 5 --- extension.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extension.js b/extension.js index 2f42a55..906be7c 100644 --- a/extension.js +++ b/extension.js @@ -36,6 +36,7 @@ const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Convenience = ExtensionUtils.getSettings && ExtensionUtils.initTranslations ? ExtensionUtils : Me.imports.convenience; const Draw = Me.imports.draw; +const Osd = Me.imports.osd; const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; const GS_VERSION = Config.PACKAGE_VERSION; @@ -154,7 +155,7 @@ var 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 Draw.DrawingHelper({ name: 'drawOnYourSreenHelper' + i }, monitor); + let helper = new Osd.DrawingHelper({ name: 'drawOnYourSreenHelper' + i }, monitor); let loadPersistent = i == Main.layoutManager.primaryIndex && this.settings.get_boolean('persistent-drawing'); let area = new Draw.DrawingArea({ name: 'drawOnYourSreenArea' + i }, monitor, helper, loadPersistent); container.add_child(area); From 92e1c0d2993daa6486faea1c1acc33af93d89de8 Mon Sep 17 00:00:00 2001 From: abakkk Date: Fri, 3 Jul 2020 23:25:20 +0200 Subject: [PATCH 09/31] osd.js -> helper.js --- osd.js => helper.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename osd.js => helper.js (100%) diff --git a/osd.js b/helper.js similarity index 100% rename from osd.js rename to helper.js From b2b6a6a8feb8baddd98f2c076fbc0035237b9ae4 Mon Sep 17 00:00:00 2001 From: abakkk Date: Fri, 3 Jul 2020 23:27:52 +0200 Subject: [PATCH 10/31] draw-to-elements 1 --- draw.js => elements.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename draw.js => elements.js (100%) diff --git a/draw.js b/elements.js similarity index 100% rename from draw.js rename to elements.js From 35980726fe9647fd1b0bfa322552b67594053dc6 Mon Sep 17 00:00:00 2001 From: abakkk Date: Fri, 3 Jul 2020 23:32:26 +0200 Subject: [PATCH 11/31] draw-to-elements 2 --- elements.js | 1147 --------------------------------------------------- 1 file changed, 1147 deletions(-) diff --git a/elements.js b/elements.js index eef5cd8..9e531d2 100644 --- a/elements.js +++ b/elements.js @@ -20,31 +20,17 @@ * along with this program. If not, see . */ -const ByteArray = imports.byteArray; const Cairo = imports.cairo; const Clutter = imports.gi.Clutter; -const Gio = imports.gi.Gio; -const GLib = imports.gi.GLib; -const GObject = imports.gi.GObject; -const Gtk = imports.gi.Gtk; const Lang = imports.lang; const Pango = imports.gi.Pango; const PangoCairo = imports.gi.PangoCairo; -const St = imports.gi.St; - -const Screenshot = imports.ui.screenshot; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); -const Convenience = ExtensionUtils.getSettings ? ExtensionUtils : Me.imports.convenience; -const Extension = Me.imports.extension; -const Menu = Me.imports.menu; const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; -const CAIRO_DEBUG_EXTENDS = false; -const SVG_DEBUG_EXTENDS = false; const SVG_DEBUG_SUPERPOSES_CAIRO = false; -const TEXT_CURSOR_TIME = 600; // ms const reverseEnumeration = function(obj) { let reversed = {}; @@ -69,1139 +55,6 @@ var FontStyleNames = reverseEnumeration(Pango.Style); var FontStretchNames = reverseEnumeration(Pango.Stretch); var FontVariantNames = reverseEnumeration(Pango.Variant); -var getDateString = function() { - let date = GLib.DateTime.new_now_local(); - return `${date.format("%F")} ${date.format("%X")}`; -}; - -var getJsonFiles = function() { - let directory = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir']])); - - let enumerator; - try { - enumerator = directory.enumerate_children('standard::name,standard::display-name,standard::content-type,time::modified', Gio.FileQueryInfoFlags.NONE, null); - } catch(e) { - return []; - } - - let jsonFiles = []; - let fileInfo = enumerator.next_file(null); - while (fileInfo) { - if (fileInfo.get_content_type().indexOf('json') != -1 && fileInfo.get_name() != `${Me.metadata['persistent-file-name']}.json`) { - let file = enumerator.get_child(fileInfo); - jsonFiles.push({ name: fileInfo.get_name().slice(0, -5), - displayName: fileInfo.get_display_name().slice(0, -5), - // fileInfo.get_modification_date_time: Gio 2.62+ - modificationUnixTime: fileInfo.get_attribute_uint64('time::modified'), - delete: () => file.delete(null) }); - } - fileInfo = enumerator.next_file(null); - } - enumerator.close(null); - - jsonFiles.sort((a, b) => { - return b.modificationUnixTime - a.modificationUnixTime; - }); - - return jsonFiles; -}; - -// DrawingArea is the widget in which we draw, thanks to Cairo. -// 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: 'DrawOnYourScreenDrawingArea', - Extends: St.DrawingArea, - Signals: { 'show-osd': { param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_DOUBLE, GObject.TYPE_BOOLEAN] }, - 'update-action-mode': {}, - 'leave-drawing-mode': {} }, - - _init: function(params, monitor, helper, loadPersistent) { - this.parent({ style_class: 'draw-on-your-screen', name: params.name}); - - this.connect('destroy', this._onDestroy.bind(this)); - this.reactiveHandler = this.connect('notify::reactive', this._onReactiveChanged.bind(this)); - - this.settings = Convenience.getSettings(); - this.monitor = monitor; - this.helper = helper; - - this.elements = []; - this.undoneElements = []; - this.currentElement = null; - this.currentTool = Shapes.NONE; - this.currentFontGeneric = 0; - this.isSquareArea = false; - this.hasGrid = false; - this.hasBackground = false; - this.textHasCursor = false; - this.dashedLine = false; - this.fill = false; - this.colors = [Clutter.Color.new(0, 0, 0, 255)]; - this.newThemeAttributes = {}; - this.oldThemeAttributes = {}; - - if (loadPersistent) - this._loadPersistent(); - }, - - get menu() { - if (!this._menu) - this._menu = new Menu.DrawingMenu(this, this.monitor); - return this._menu; - }, - - closeMenu: function() { - if (this._menu) - this._menu.close(); - }, - - get isWriting() { - return this.textEntry ? true : false; - }, - - get currentTool() { - return this._currentTool; - }, - - set currentTool(tool) { - this._currentTool = tool; - if (this.hasManipulationTool) - this._startElementGrabber(); - else - this._stopElementGrabber(); - }, - - get hasManipulationTool() { - // No Object.values method in GS 3.24. - return Object.keys(Manipulations).map(key => Manipulations[key]).indexOf(this.currentTool) != -1; - }, - - // Boolean wrapper for switch menu item. - get currentEvenodd() { - return this.currentFillRule == Cairo.FillRule.EVEN_ODD; - }, - - set currentEvenodd(evenodd) { - this.currentFillRule = evenodd ? Cairo.FillRule.EVEN_ODD : Cairo.FillRule.WINDING; - }, - - vfunc_repaint: function() { - let cr = this.get_context(); - - try { - this._repaint(cr); - } catch(e) { - logError(e, "An error occured while painting"); - } - - cr.$dispose(); - }, - - _redisplay: function() { - // force area to emit 'repaint' - this.queue_repaint(); - }, - - _updateStyle: function() { - try { - let themeNode = this.get_theme_node(); - for (let i = 1; i < 10; i++) { - this.colors[i] = themeNode.get_color('-drawing-color' + i); - } - let font = themeNode.get_font(); - this.newThemeAttributes.ThemeFontFamily = font.get_family(); - try { this.newThemeAttributes.FontWeight = font.get_weight(); } catch(e) { this.newThemeAttributes.FontWeight = Pango.Weight.NORMAL; } - this.newThemeAttributes.FontStyle = font.get_style(); - this.newThemeAttributes.FontStretch = font.get_stretch(); - this.newThemeAttributes.FontVariant = font.get_variant(); - this.newThemeAttributes.TextRightAligned = themeNode.get_text_align() == St.TextAlign.RIGHT; - this.newThemeAttributes.LineWidth = themeNode.get_length('-drawing-line-width'); - this.newThemeAttributes.LineJoin = themeNode.get_double('-drawing-line-join'); - this.newThemeAttributes.LineCap = themeNode.get_double('-drawing-line-cap'); - this.newThemeAttributes.FillRule = themeNode.get_double('-drawing-fill-rule'); - this.dashArray = [Math.abs(themeNode.get_length('-drawing-dash-array-on')), Math.abs(themeNode.get_length('-drawing-dash-array-off'))]; - this.dashOffset = themeNode.get_length('-drawing-dash-offset'); - this.gridGap = themeNode.get_length('-grid-overlay-gap'); - this.gridLineWidth = themeNode.get_length('-grid-overlay-line-width'); - this.gridInterlineWidth = themeNode.get_length('-grid-overlay-interline-width'); - this.gridColor = themeNode.get_color('-grid-overlay-color'); - this.squareAreaWidth = themeNode.get_length('-drawing-square-area-width'); - this.squareAreaHeight = themeNode.get_length('-drawing-square-area-height'); - this.activeBackgroundColor = themeNode.get_color('-drawing-background-color'); - } catch(e) { - logError(e); - } - - for (let i = 1; i < 10; i++) { - this.colors[i] = this.colors[i].alpha ? this.colors[i] : this.colors[0]; - } - this.currentColor = this.currentColor || this.colors[1]; - // SVG does not support 'Ultra-heavy' weight (1000) - this.newThemeAttributes.FontWeight = Math.min(this.newThemeAttributes.FontWeight, 900); - this.newThemeAttributes.LineWidth = (this.newThemeAttributes.LineWidth > 0) ? this.newThemeAttributes.LineWidth : 3; - this.newThemeAttributes.LineJoin = ([0, 1, 2].indexOf(this.newThemeAttributes.LineJoin) != -1) ? this.newThemeAttributes.LineJoin : Cairo.LineJoin.ROUND; - this.newThemeAttributes.LineCap = ([0, 1, 2].indexOf(this.newThemeAttributes.LineCap) != -1) ? this.newThemeAttributes.LineCap : Cairo.LineCap.ROUND; - this.newThemeAttributes.FillRule = ([0, 1].indexOf(this.newThemeAttributes.FillRule) != -1) ? this.newThemeAttributes.FillRule : Cairo.FillRule.WINDING; - for (let attributeName in this.newThemeAttributes) { - if (this.newThemeAttributes[attributeName] != this.oldThemeAttributes[attributeName]) { - this.oldThemeAttributes[attributeName] = this.newThemeAttributes[attributeName]; - this[`current${attributeName}`] = this.newThemeAttributes[attributeName]; - } - } - this.gridGap = this.gridGap && this.gridGap >= 1 ? this.gridGap : 10; - this.gridLineWidth = this.gridLineWidth || 0.4; - this.gridInterlineWidth = this.gridInterlineWidth || 0.2; - this.gridColor = this.gridColor && this.gridColor.alpha ? this.gridColor : Clutter.Color.new(127, 127, 127, 255); - }, - - _repaint: function(cr) { - if (CAIRO_DEBUG_EXTENDS) { - cr.scale(0.5, 0.5); - cr.translate(this.monitor.width, this.monitor.height); - } - - for (let i = 0; i < this.elements.length; i++) { - cr.save(); - - this.elements[i].buildCairo(cr, { showTextRectangle: this.grabbedElement && this.grabbedElement == this.elements[i], - drawTextRectangle: this.grabPoint ? true : false }); - - if (this.grabPoint) - this._searchElementToGrab(cr, this.elements[i]); - - if (this.elements[i].fill && !this.elements[i].isStraightLine) { - cr.fillPreserve(); - if (this.elements[i].shape == Shapes.NONE || this.elements[i].shape == Shapes.LINE) - cr.closePath(); - } - - cr.stroke(); - cr.restore(); - } - - if (this.currentElement) { - cr.save(); - 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(); - } - - if (this.reactive && this.hasGrid && this.gridGap && this.gridGap >= 1) { - cr.save(); - Clutter.cairo_set_source_color(cr, this.gridColor); - - let [gridX, gridY] = [this.gridGap, this.gridGap]; - while (gridX < this.monitor.width) { - cr.setLineWidth((gridX / this.gridGap) % 5 ? this.gridInterlineWidth : this.gridLineWidth); - cr.moveTo(gridX, 0); - cr.lineTo(gridX, this.monitor.height); - gridX += this.gridGap; - cr.stroke(); - } - while (gridY < this.monitor.height) { - cr.setLineWidth((gridY / this.gridGap) % 5 ? this.gridInterlineWidth : this.gridLineWidth); - cr.moveTo(0, gridY); - cr.lineTo(this.monitor.width, gridY); - gridY += this.gridGap; - cr.stroke(); - } - cr.restore(); - } - }, - - _onButtonPressed: function(actor, event) { - if (this.spaceKeyPressed) - return Clutter.EVENT_PROPAGATE; - - let button = event.get_button(); - let [x, y] = event.get_coords(); - let controlPressed = event.has_control_modifier(); - let shiftPressed = event.has_shift_modifier(); - - if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) - // finish writing - this._stopWriting(); - - if (this.helper.visible) { - // hide helper - this.toggleHelp(); - return Clutter.EVENT_STOP; - } - - if (button == 1) { - if (this.hasManipulationTool) { - if (this.grabbedElement) - this._startTransforming(x, y, controlPressed, shiftPressed); - } else { - this._startDrawing(x, y, shiftPressed); - } - return Clutter.EVENT_STOP; - } else if (button == 2) { - this.toggleFill(); - } else if (button == 3) { - this._stopDrawing(); - this.menu.open(x, y); - return Clutter.EVENT_STOP; - } - - return Clutter.EVENT_PROPAGATE; - }, - - _onKeyboardPopupMenu: function() { - this._stopDrawing(); - if (this.helper.visible) - this.toggleHelp(); - this.menu.popup(); - return Clutter.EVENT_STOP; - }, - - _onStageKeyPressed: function(actor, event) { - if (event.get_key_symbol() == Clutter.KEY_space) - this.spaceKeyPressed = true; - - return Clutter.EVENT_PROPAGATE; - }, - - _onStageKeyReleased: function(actor, event) { - if (event.get_key_symbol() == Clutter.KEY_space) - this.spaceKeyPressed = false; - - return Clutter.EVENT_PROPAGATE; - }, - - _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) - this.emit('show-osd', null, _("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; - } - - } 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; - } - }, - - _onScroll: function(actor, event) { - if (this.helper.visible) - return Clutter.EVENT_PROPAGATE; - let direction = event.get_scroll_direction(); - if (direction == Clutter.ScrollDirection.UP) - this.incrementLineWidth(1); - else if (direction == Clutter.ScrollDirection.DOWN) - this.incrementLineWidth(-1); - else - return Clutter.EVENT_PROPAGATE; - return Clutter.EVENT_STOP; - }, - - _searchElementToGrab: function(cr, element) { - if (element.getContainsPoint(cr, this.grabPoint[0], this.grabPoint[1])) - this.grabbedElement = element; - else if (this.grabbedElement == element) - this.grabbedElement = null; - - if (element == this.elements[this.elements.length - 1]) - // All elements have been tested, the winner is the last. - this.updatePointerCursor(); - }, - - _startElementGrabber: function() { - if (this.elementGrabberHandler) - return; - - this.elementGrabberHandler = this.connect('motion-event', (actor, event) => { - if (this.motionHandler || this.grabbedElementLocked) { - this.grabPoint = null; - return; - } - - // Reduce computing without notable effect. - if (Math.random() <= 0.75) - return; - - let coords = event.get_coords(); - let [s, x, y] = this.transform_stage_point(coords[0], coords[1]); - if (!s) - return; - - this.grabPoint = [x, y]; - this.grabbedElement = null; - // this._redisplay calls this._searchElementToGrab. - this._redisplay(); - }); - }, - - _stopElementGrabber: function() { - if (this.elementGrabberHandler) { - this.disconnect(this.elementGrabberHandler); - this.grabPoint = null; - this.elementGrabberHandler = null; - } - }, - - _startTransforming: function(stageX, stageY, controlPressed, duplicate) { - let [success, startX, startY] = this.transform_stage_point(stageX, stageY); - - if (!success) - return; - - if (this.currentTool == Manipulations.MIRROR) { - this.grabbedElementLocked = !this.grabbedElementLocked; - if (this.grabbedElementLocked) { - this.updatePointerCursor(); - let label = controlPressed ? _("Mark a point of symmetry") : _("Draw a line of symmetry"); - this.emit('show-osd', null, label, "", -1, true); - return; - } - } - - this.grabPoint = null; - - this.buttonReleasedHandler = this.connect('button-release-event', (actor, event) => { - this._stopTransforming(); - }); - - if (duplicate) { - // deep cloning - let copy = new DrawingElement(JSON.parse(JSON.stringify(this.grabbedElement))); - this.elements.push(copy); - this.grabbedElement = copy; - } - - if (this.currentTool == Manipulations.MOVE) - this.grabbedElement.startTransformation(startX, startY, controlPressed ? Transformations.ROTATION : Transformations.TRANSLATION); - else if (this.currentTool == Manipulations.RESIZE) - this.grabbedElement.startTransformation(startX, startY, controlPressed ? Transformations.STRETCH : Transformations.SCALE_PRESERVE); - else if (this.currentTool == Manipulations.MIRROR) { - this.grabbedElement.startTransformation(startX, startY, controlPressed ? Transformations.INVERSION : Transformations.REFLECTION); - this._redisplay(); - } - - - this.motionHandler = this.connect('motion-event', (actor, event) => { - if (this.spaceKeyPressed) - return; - - let coords = event.get_coords(); - let [s, x, y] = this.transform_stage_point(coords[0], coords[1]); - if (!s) - return; - let controlPressed = event.has_control_modifier(); - this._updateTransforming(x, y, controlPressed); - }); - }, - - _updateTransforming: function(x, y, controlPressed) { - if (controlPressed && this.grabbedElement.lastTransformation.type == Transformations.TRANSLATION) { - this.grabbedElement.stopTransformation(); - this.grabbedElement.startTransformation(x, y, Transformations.ROTATION); - } else if (!controlPressed && this.grabbedElement.lastTransformation.type == Transformations.ROTATION) { - this.grabbedElement.stopTransformation(); - this.grabbedElement.startTransformation(x, y, Transformations.TRANSLATION); - } - - if (controlPressed && this.grabbedElement.lastTransformation.type == Transformations.SCALE_PRESERVE) { - this.grabbedElement.stopTransformation(); - this.grabbedElement.startTransformation(x, y, Transformations.STRETCH); - } else if (!controlPressed && this.grabbedElement.lastTransformation.type == Transformations.STRETCH) { - this.grabbedElement.stopTransformation(); - this.grabbedElement.startTransformation(x, y, Transformations.SCALE_PRESERVE); - } - - if (controlPressed && this.grabbedElement.lastTransformation.type == Transformations.REFLECTION) { - this.grabbedElement.transformations.pop(); - this.grabbedElement.startTransformation(x, y, Transformations.INVERSION); - } else if (!controlPressed && this.grabbedElement.lastTransformation.type == Transformations.INVERSION) { - this.grabbedElement.transformations.pop(); - this.grabbedElement.startTransformation(x, y, Transformations.REFLECTION); - } - - this.grabbedElement.updateTransformation(x, y); - this._redisplay(); - }, - - _stopTransforming: function() { - if (this.motionHandler) { - this.disconnect(this.motionHandler); - this.motionHandler = null; - } - if (this.buttonReleasedHandler) { - this.disconnect(this.buttonReleasedHandler); - this.buttonReleasedHandler = null; - } - - this.grabbedElement.stopTransformation(); - this.grabbedElement = null; - this.grabbedElementLocked = false; - this._redisplay(); - }, - - _startDrawing: function(stageX, stageY, eraser) { - let [success, startX, startY] = this.transform_stage_point(stageX, stageY); - - if (!success) - return; - - this.buttonReleasedHandler = this.connect('button-release-event', (actor, event) => { - this._stopDrawing(); - }); - - this.currentElement = new DrawingElement ({ - shape: this.currentTool, - color: this.currentColor.to_string(), - line: { lineWidth: this.currentLineWidth, lineJoin: this.currentLineJoin, lineCap: this.currentLineCap }, - dash: { active: this.dashedLine, array: this.dashedLine ? [this.dashArray[0] || this.currentLineWidth, this.dashArray[1] || this.currentLineWidth * 3] : [0, 0] , offset: this.dashOffset }, - fill: this.fill, - fillRule: this.currentFillRule, - eraser: eraser, - transform: { active: false, center: [0, 0], angle: 0, startAngle: 0, ratio: 1 }, - points: [] - }); - - if (this.currentTool == Shapes.TEXT) { - this.currentElement.fill = false; - this.currentElement.font = { - family: (this.currentFontGeneric == 0 ? this.currentThemeFontFamily : FontGenericNames[this.currentFontGeneric]), - weight: this.currentFontWeight, - style: this.currentFontStyle, - stretch: this.currentFontStretch, - variant: this.currentFontVariant }; - this.currentElement.text = _("Text"); - this.currentElement.textRightAligned = this.currentTextRightAligned; - } - - this.currentElement.startDrawing(startX, startY); - - if (this.currentTool == Shapes.POLYGON || this.currentTool == Shapes.POLYLINE) - this.emit('show-osd', null, _("Press %s to mark vertices") - .format(Gtk.accelerator_get_label(Clutter.KEY_Return, 0)), "", -1, true); - - this.motionHandler = this.connect('motion-event', (actor, event) => { - if (this.spaceKeyPressed) - return; - - let coords = event.get_coords(); - let [s, x, y] = this.transform_stage_point(coords[0], coords[1]); - if (!s) - return; - let controlPressed = event.has_control_modifier(); - this._updateDrawing(x, y, controlPressed); - }); - }, - - _updateDrawing: function(x, y, controlPressed) { - if (!this.currentElement) - return; - - this.currentElement.updateDrawing(x, y, controlPressed); - - this._redisplay(); - this.updatePointerCursor(controlPressed); - }, - - _stopDrawing: function() { - if (this.motionHandler) { - this.disconnect(this.motionHandler); - this.motionHandler = null; - } - if (this.buttonReleasedHandler) { - this.disconnect(this.buttonReleasedHandler); - this.buttonReleasedHandler = null; - } - - // skip when a polygon has not at least 3 points - if (this.currentElement && this.currentElement.shape == Shapes.POLYGON && this.currentElement.points.length < 3) - this.currentElement = null; - - if (this.currentElement) - this.currentElement.stopDrawing(); - - if (this.currentElement && this.currentElement.points.length >= 2) { - if (this.currentElement.shape == Shapes.TEXT && !this.isWriting) { - this._startWriting(); - return; - } - - this.elements.push(this.currentElement); - } - - this.currentElement = null; - this._redisplay(); - this.updatePointerCursor(); - }, - - _startWriting: function() { - this.currentElement.text = ''; - this.currentElement.cursorPosition = 0; - this.emit('show-osd', null, _("Type your text and press %s") - .format(Gtk.accelerator_get_label(Clutter.KEY_Escape, 0)), "", -1, true); - this._updateTextCursorTimeout(); - this.textHasCursor = true; - this._redisplay(); - - this.textEntry = new St.Entry({ visible: false }); - this.get_parent().add_child(this.textEntry); - this.textEntry.grab_key_focus(); - this.updateActionMode(); - this.updatePointerCursor(); - - this.textEntry.clutterText.connect('activate', (clutterText) => { - let startNewLine = true; - this._stopWriting(startNewLine); - clutterText.text = ""; - }); - - this.textEntry.clutterText.connect('text-changed', (clutterText) => { - GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { - this.currentElement.text = clutterText.text; - this.currentElement.cursorPosition = clutterText.cursorPosition; - this._updateTextCursorTimeout(); - this._redisplay(); - }); - }); - - this.textEntry.clutterText.connect('key-press-event', (clutterText, event) => { - if (event.get_key_symbol() == Clutter.KEY_Escape) { - this._stopWriting(); - return Clutter.EVENT_STOP; - } - - // 'cursor-changed' signal is not emitted if the text entry is not visible. - // So key events related to the cursor must be listened. - if (event.get_key_symbol() == Clutter.KEY_Left || event.get_key_symbol() == Clutter.KEY_Right || - event.get_key_symbol() == Clutter.KEY_Home || event.get_key_symbol() == Clutter.KEY_End) { - GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { - this.currentElement.cursorPosition = clutterText.cursorPosition; - this._updateTextCursorTimeout(); - this.textHasCursor = true; - this._redisplay(); - }); - } - - return Clutter.EVENT_PROPAGATE; - }); - }, - - _stopWriting: function(startNewLine) { - if (this.currentElement.text.length > 0) - this.elements.push(this.currentElement); - - if (startNewLine && this.currentElement.points.length == 2) { - this.currentElement.lineIndex = this.currentElement.lineIndex || 0; - // copy object, the original keep existing in this.elements - this.currentElement = Object.create(this.currentElement); - this.currentElement.lineIndex ++; - let height = Math.abs(this.currentElement.points[1][1] - this.currentElement.points[0][1]); - // define a new 'points' array, the original keep existing in this.elements - this.currentElement.points = [ - [this.currentElement.points[0][0], this.currentElement.points[0][1] + height], - [this.currentElement.points[1][0], this.currentElement.points[1][1] + height] - ]; - this.currentElement.text = ""; - } else { - this.currentElement = null; - this._stopTextCursorTimeout(); - this.textEntry.destroy(); - delete this.textEntry; - this.grab_key_focus(); - this.updateActionMode(); - this.updatePointerCursor(); - } - - this._redisplay(); - }, - - setPointerCursor: function(pointerCursorName) { - if (!this.currentPointerCursorName || this.currentPointerCursorName != pointerCursorName) { - this.currentPointerCursorName = pointerCursorName; - Extension.setCursor(pointerCursorName); - } - }, - - updatePointerCursor: function(controlPressed) { - if (this.currentTool == Manipulations.MIRROR && this.grabbedElementLocked) - this.setPointerCursor('CROSSHAIR'); - else if (this.hasManipulationTool) - this.setPointerCursor(this.grabbedElement ? 'MOVE_OR_RESIZE_WINDOW' : 'DEFAULT'); - else if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) - this.setPointerCursor('IBEAM'); - else if (!this.currentElement) - this.setPointerCursor(this.currentTool == Shapes.NONE ? 'POINTING_HAND' : 'CROSSHAIR'); - else if (this.currentElement.shape != Shapes.NONE && controlPressed) - this.setPointerCursor('MOVE_OR_RESIZE_WINDOW'); - }, - - initPointerCursor: function() { - this.currentPointerCursorName = null; - this.updatePointerCursor(); - }, - - _stopTextCursorTimeout: function() { - if (this.textCursorTimeoutId) { - GLib.source_remove(this.textCursorTimeoutId); - this.textCursorTimeoutId = null; - } - this.textHasCursor = false; - }, - - _updateTextCursorTimeout: function() { - this._stopTextCursorTimeout(); - this.textCursorTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, TEXT_CURSOR_TIME, () => { - this.textHasCursor = !this.textHasCursor; - this._redisplay(); - return GLib.SOURCE_CONTINUE; - }); - }, - - erase: function() { - this.deleteLastElement(); - this.elements = []; - this.undoneElements = []; - this._redisplay(); - }, - - deleteLastElement: function() { - if (this.currentElement) { - if (this.motionHandler) { - this.disconnect(this.motionHandler); - this.motionHandler = null; - } - if (this.buttonReleasedHandler) { - this.disconnect(this.buttonReleasedHandler); - this.buttonReleasedHandler = null; - } - if (this.isWriting) - this._stopWriting(); - this.currentElement = null; - } else { - this.elements.pop(); - } - this._redisplay(); - }, - - undo: function() { - if (this.elements.length > 0) - this.undoneElements.push(this.elements.pop()); - this._redisplay(); - }, - - redo: function() { - if (this.undoneElements.length > 0) - this.elements.push(this.undoneElements.pop()); - this._redisplay(); - }, - - smoothLastElement: function() { - if (this.elements.length > 0 && this.elements[this.elements.length - 1].shape == Shapes.NONE) { - this.elements[this.elements.length - 1].smoothAll(); - this._redisplay(); - } - }, - - toggleBackground: function() { - this.hasBackground = !this.hasBackground; - this.get_parent().set_background_color(this.hasBackground ? this.activeBackgroundColor : null); - }, - - toggleGrid: function() { - this.hasGrid = !this.hasGrid; - this._redisplay(); - }, - - toggleSquareArea: function() { - this.isSquareArea = !this.isSquareArea; - if (this.isSquareArea) { - let width = this.squareAreaWidth || this.squareAreaHeight || Math.min(this.monitor.width, this.monitor.height) * 3 / 4; - let height = this.squareAreaHeight || this.squareAreaWidth || Math.min(this.monitor.width, this.monitor.height) * 3 / 4; - this.set_position(Math.floor(this.monitor.width / 2 - width / 2), Math.floor(this.monitor.height / 2 - height / 2)); - this.set_size(width, height); - this.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'); - } - }, - - toggleColor: function() { - this.selectColor((this.currentColor == this.colors[1]) ? 2 : 1); - }, - - selectColor: function(index) { - this.currentColor = this.colors[index]; - if (this.currentElement) { - this.currentElement.color = this.currentColor.to_string(); - this._redisplay(); - } - // Foreground color markup is not displayed since 3.36, use style instead but the transparency is lost. - this.emit('show-osd', null, this.currentColor.to_string(), this.currentColor.to_string().slice(0, 7), -1, false); - }, - - selectTool: function(tool) { - this.currentTool = tool; - this.emit('show-osd', null, _(ToolNames[tool]), "", -1, false); - this.updatePointerCursor(); - }, - - toggleFill: function() { - this.fill = !this.fill; - this.emit('show-osd', null, this.fill ? _("Fill") : _("Outline"), "", -1, false); - }, - - toggleDash: function() { - this.dashedLine = !this.dashedLine; - this.emit('show-osd', null, this.dashedLine ? _("Dashed line") : _("Full line"), "", -1, false); - }, - - incrementLineWidth: function(increment) { - this.currentLineWidth = Math.max(this.currentLineWidth + increment, 0); - this.emit('show-osd', null, _("%d px").format(this.currentLineWidth), "", 2 * this.currentLineWidth, false); - }, - - toggleLineJoin: function() { - this.currentLineJoin = this.currentLineJoin == 2 ? 0 : this.currentLineJoin + 1; - this.emit('show-osd', null, _(LineJoinNames[this.currentLineJoin]), "", -1, false); - }, - - toggleLineCap: function() { - this.currentLineCap = this.currentLineCap == 2 ? 0 : this.currentLineCap + 1; - this.emit('show-osd', null, _(LineCapNames[this.currentLineCap]), "", -1, false); - }, - - toggleFillRule: function() { - this.currentFillRule = this.currentFillRule == 1 ? 0 : this.currentFillRule + 1; - this.emit('show-osd', null, _(FillRuleNames[this.currentFillRule]), "", -1, false); - }, - - toggleFontWeight: function() { - let fontWeights = Object.keys(FontWeightNames).map(key => Number(key)); - let index = fontWeights.indexOf(this.currentFontWeight); - this.currentFontWeight = index == fontWeights.length - 1 ? fontWeights[0] : fontWeights[index + 1]; - if (this.currentElement && this.currentElement.font) { - this.currentElement.font.weight = this.currentFontWeight; - this._redisplay(); - } - this.emit('show-osd', null, `` + - `${_(FontWeightNames[this.currentFontWeight])}`, "", -1, false); - }, - - toggleFontStyle: function() { - this.currentFontStyle = this.currentFontStyle == 2 ? 0 : this.currentFontStyle + 1; - if (this.currentElement && this.currentElement.font) { - this.currentElement.font.style = this.currentFontStyle; - this._redisplay(); - } - this.emit('show-osd', null, `` + - `${_(FontStyleNames[this.currentFontStyle])}`, "", -1, false); - }, - - toggleFontFamily: function() { - this.currentFontGeneric = this.currentFontGeneric == 5 ? 0 : this.currentFontGeneric + 1; - let currentFontFamily = this.currentFontGeneric == 0 ? this.currentThemeFontFamily : FontGenericNames[this.currentFontGeneric]; - if (this.currentElement && this.currentElement.font) { - this.currentElement.font.family = currentFontFamily; - this._redisplay(); - } - this.emit('show-osd', null, `${_(currentFontFamily)}`, "", -1, false); - }, - - toggleTextAlignment: function() { - this.currentTextRightAligned = !this.currentTextRightAligned; - if (this.currentElement && this.currentElement.textRightAligned !== undefined) { - this.currentElement.textRightAligned = this.currentTextRightAligned; - this._redisplay(); - } - this.emit('show-osd', null, this.currentTextRightAligned ? _("Right aligned") : _("Left aligned"), "", -1, false); - }, - - toggleHelp: function() { - if (this.helper.visible) { - this.helper.hideHelp(); - if (this.textEntry) - this.textEntry.grab_key_focus(); - } else { - this.helper.showHelp(); - this.grab_key_focus(); - } - - }, - - // The area is reactive when it is modal. - _onReactiveChanged: function() { - if (this.hasGrid) - this._redisplay(); - if (this.helper.visible) - this.toggleHelp(); - if (this.textEntry && this.reactive) - this.textEntry.grab_key_focus(); - }, - - _onDestroy: function() { - this.disconnect(this.reactiveHandler); - this.erase(); - if (this._menu) - this._menu.disable(); - }, - - updateActionMode: function() { - this.emit('update-action-mode'); - }, - - 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._onKeyboardPopupMenuHandler = 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.activeBackgroundColor : null); - this._updateStyle(); - }, - - leaveDrawingMode: function(save) { - 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; - } - if (this.buttonPressedHandler) { - this.disconnect(this.buttonPressedHandler); - this.buttonPressedHandler = null; - } - if (this._onKeyboardPopupMenuHandler) { - this.disconnect(this._onKeyboardPopupMenuHandler); - this._onKeyboardPopupMenuHandler = null; - } - if (this.motionHandler) { - this.disconnect(this.motionHandler); - this.motionHandler = null; - } - if (this.buttonReleasedHandler) { - this.disconnect(this.buttonReleasedHandler); - this.buttonReleasedHandler = null; - } - if (this.scrollHandler) { - this.disconnect(this.scrollHandler); - this.scrollHandler = null; - } - - this.currentElement = null; - this._stopTextCursorTimeout(); - this._redisplay(); - this.closeMenu(); - this.get_parent().set_background_color(null); - if (save) - this.savePersistent(); - }, - - saveAsSvg: function() { - // stop drawing or writing - if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) { - this._stopWriting(); - } else if (this.currentElement && this.currentElement.shape != Shapes.TEXT) { - this._stopDrawing(); - } - - let content = ``; - if (SVG_DEBUG_EXTENDS) - content = ``; - let backgroundColorString = this.hasBackground ? this.activeBackgroundColor.to_string() : 'transparent'; - if (backgroundColorString != 'transparent') { - content += `\n `; - } - if (SVG_DEBUG_EXTENDS) { - content += `\n `; - content += `\n `; - } - for (let i = 0; i < this.elements.length; i++) { - content += this.elements[i].buildSVG(backgroundColorString); - } - content += "\n"; - - let filename = `${Me.metadata['svg-file-name']} ${getDateString()}.svg`; - let dir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES); - let path = GLib.build_filenamev([dir, filename]); - if (GLib.file_test(path, GLib.FileTest.EXISTS)) - return false; - let success = GLib.file_set_contents(path, content); - - if (success) { - // pass the parent (bgContainer) to Flashspot because coords of this are relative - let flashspot = new Screenshot.Flashspot(this.get_parent()); - flashspot.fire(); - if (global.play_theme_sound) { - global.play_theme_sound(0, 'screen-capture', "Save as SVG", null); - } else if (global.display && global.display.get_sound_player) { - let player = global.display.get_sound_player(); - player.play_from_theme('screen-capture', "Save as SVG", null); - } - } - }, - - _saveAsJson: function(name, notify) { - // stop drawing or writing - if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) { - this._stopWriting(); - } else if (this.currentElement && this.currentElement.shape != Shapes.TEXT) { - this._stopDrawing(); - } - - let dir = GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir']]); - if (!GLib.file_test(dir, GLib.FileTest.EXISTS)) - GLib.mkdir_with_parents(dir, 0o700); - let path = GLib.build_filenamev([dir, `${name}.json`]); - - let oldContents; - - if (name == Me.metadata['persistent-file-name']) { - if (GLib.file_test(path, GLib.FileTest.EXISTS)) { - oldContents = GLib.file_get_contents(path)[1]; - if (oldContents instanceof Uint8Array) - oldContents = ByteArray.toString(oldContents); - } - - // do not create a file to write just an empty array - if (!oldContents && this.elements.length == 0) - return; - } - - // do not use "content = JSON.stringify(this.elements, null, 2);", neither "content = JSON.stringify(this.elements);" - // because of compromise between disk usage and human readability - let contents = `[\n ` + new Array(...this.elements.map(element => JSON.stringify(element))).join(`,\n\n `) + `\n]`; - - if (name == Me.metadata['persistent-file-name'] && contents == oldContents) - return; - - GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { - GLib.file_set_contents(path, contents); - if (notify) - this.emit('show-osd', 'document-save-symbolic', name, "", -1, false); - if (name != Me.metadata['persistent-file-name']) { - this.jsonName = name; - this.lastJsonContents = contents; - } - }); - }, - - saveAsJsonWithName: function(name) { - this._saveAsJson(name); - }, - - saveAsJson: function() { - this._saveAsJson(getDateString(), true); - }, - - savePersistent: function() { - this._saveAsJson(Me.metadata['persistent-file-name']); - }, - - syncPersistent: function() { - // do not override peristent.json with an empty drawing when changing persistency setting - if (!this.elements.length) - this._loadPersistent(); - else - this.savePersistent(); - - }, - - _loadJson: function(name, notify) { - // stop drawing or writing - if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) { - this._stopWriting(); - } else if (this.currentElement && this.currentElement.shape != Shapes.TEXT) { - this._stopDrawing(); - } - this.elements = []; - this.currentElement = null; - - let dir = GLib.get_user_data_dir(); - let path = GLib.build_filenamev([dir, Me.metadata['data-dir'], `${name}.json`]); - - if (!GLib.file_test(path, GLib.FileTest.EXISTS)) - return; - let [success, contents] = GLib.file_get_contents(path); - if (!success) - return; - if (contents instanceof Uint8Array) - contents = ByteArray.toString(contents); - this.elements.push(...JSON.parse(contents).map(object => new DrawingElement(object))); - - if (notify) - this.emit('show-osd', 'document-open-symbolic', name, "", -1, false); - if (name != Me.metadata['persistent-file-name']) { - this.jsonName = name; - this.lastJsonContents = contents; - } - }, - - _loadPersistent: function() { - this._loadJson(Me.metadata['persistent-file-name']); - }, - - loadJson: function(name, notify) { - this._loadJson(name, notify); - this._redisplay(); - }, - - loadNextJson: function() { - let names = getJsonFiles().map(file => file.name); - - if (!names.length) - return; - - let nextName = names[this.jsonName && names.indexOf(this.jsonName) != names.length - 1 ? names.indexOf(this.jsonName) + 1 : 0]; - this.loadJson(nextName, true); - }, - - loadPreviousJson: function() { - let names = getJsonFiles().map(file => file.name); - - if (!names.length) - return; - - let previousName = names[this.jsonName && names.indexOf(this.jsonName) > 0 ? names.indexOf(this.jsonName) - 1 : names.length - 1]; - this.loadJson(previousName, true); - }, - - get drawingContentsHasChanged() { - let contents = `[\n ` + new Array(...this.elements.map(element => JSON.stringify(element))).join(`,\n\n `) + `\n]`; - return contents != this.lastJsonContents; - } -}); - const RADIAN = 180 / Math.PI; // degree const INVERSION_CIRCLE_RADIUS = 12; // px const REFLECTION_TOLERANCE = 5; // px, to select vertical and horizontal directions From f4909e021db02ec39e5de2632b747821a97b8f0e Mon Sep 17 00:00:00 2001 From: abakkk Date: Fri, 3 Jul 2020 23:37:09 +0200 Subject: [PATCH 12/31] draw-to-elements 3 --- draw.js | 706 +------------------------------------------------------- 1 file changed, 4 insertions(+), 702 deletions(-) diff --git a/draw.js b/draw.js index eef5cd8..4594f73 100644 --- a/draw.js +++ b/draw.js @@ -29,7 +29,6 @@ const GObject = imports.gi.GObject; const Gtk = imports.gi.Gtk; const Lang = imports.lang; const Pango = imports.gi.Pango; -const PangoCairo = imports.gi.PangoCairo; const St = imports.gi.St; const Screenshot = imports.ui.screenshot; @@ -38,12 +37,12 @@ const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Convenience = ExtensionUtils.getSettings ? ExtensionUtils : Me.imports.convenience; const Extension = Me.imports.extension; +const Elements = Me.imports.elements; const Menu = Me.imports.menu; const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; const CAIRO_DEBUG_EXTENDS = false; const SVG_DEBUG_EXTENDS = false; -const SVG_DEBUG_SUPERPOSES_CAIRO = false; const TEXT_CURSOR_TIME = 600; // ms const reverseEnumeration = function(obj) { @@ -489,7 +488,7 @@ var DrawingArea = new Lang.Class({ if (duplicate) { // deep cloning - let copy = new DrawingElement(JSON.parse(JSON.stringify(this.grabbedElement))); + let copy = new Elements.DrawingElement(JSON.parse(JSON.stringify(this.grabbedElement))); this.elements.push(copy); this.grabbedElement = copy; } @@ -572,7 +571,7 @@ var DrawingArea = new Lang.Class({ this._stopDrawing(); }); - this.currentElement = new DrawingElement ({ + this.currentElement = new Elements.DrawingElement ({ shape: this.currentTool, color: this.currentColor.to_string(), line: { lineWidth: this.currentLineWidth, lineJoin: this.currentLineJoin, lineCap: this.currentLineCap }, @@ -1157,7 +1156,7 @@ var DrawingArea = new Lang.Class({ return; if (contents instanceof Uint8Array) contents = ByteArray.toString(contents); - this.elements.push(...JSON.parse(contents).map(object => new DrawingElement(object))); + this.elements.push(...JSON.parse(contents).map(object => new Elements.DrawingElement(object))); if (notify) this.emit('show-osd', 'document-open-symbolic', name, "", -1, false); @@ -1202,700 +1201,3 @@ var DrawingArea = new Lang.Class({ } }); -const RADIAN = 180 / Math.PI; // degree -const INVERSION_CIRCLE_RADIUS = 12; // px -const REFLECTION_TOLERANCE = 5; // px, to select vertical and horizontal directions -const STRETCH_TOLERANCE = Math.PI / 8; // rad, to select vertical and horizontal directions -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 - -// DrawingElement represents a "brushstroke". -// 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: 'DrawOnYourScreenDrawingElement', - - _init: function(params) { - for (let key in params) - this[key] = params[key]; - - // compatibility with json generated by old extension versions - - if (params.fillRule === undefined) - this.fillRule = Cairo.FillRule.WINDING; - if (params.transformations === undefined) - this.transformations = []; - if (params.shape == Shapes.TEXT) { - if (params.font && params.font.weight === 0) - this.font.weight = 400; - if (params.font && params.font.weight === 1) - this.font.weight = 700; - } - - if (params.transform && params.transform.center) { - let angle = (params.transform.angle || 0) + (params.transform.startAngle || 0); - if (angle) - this.transformations.push({ type: Transformations.ROTATION, angle: angle }); - } - if (params.shape == Shapes.ELLIPSE && params.transform && params.transform.ratio && params.transform.ratio != 1 && params.points.length >= 2) { - let [ratio, p0, p1] = [params.transform.ratio, params.points[0], params.points[1]]; - // Add a fake point that will give the right ellipse ratio when building the element. - this.points.push([ratio * (p1[0] - p0[0]) + p0[0], ratio * (p1[1] - p0[1]) + p0[1]]); - } - delete this.transform; - }, - - // toJSON is called by JSON.stringify - toJSON: function() { - return { - shape: this.shape, - color: this.color, - line: this.line, - dash: this.dash, - fill: this.fill, - fillRule: this.fillRule, - eraser: this.eraser, - transformations: this.transformations, - text: this.text, - lineIndex: this.lineIndex !== undefined ? this.lineIndex : undefined, - textRightAligned: this.textRightAligned, - font: this.font, - points: this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100]) - }; - }, - - buildCairo: function(cr, params) { - let [success, color] = Clutter.Color.from_string(this.color); - if (success) - Clutter.cairo_set_source_color(cr, color); - - if (this.showSymmetryElement) { - let transformation = this.lastTransformation; - setDummyStroke(cr); - if (transformation.type == Transformations.REFLECTION) { - cr.moveTo(transformation.startX, transformation.startY); - cr.lineTo(transformation.endX, transformation.endY); - } else { - cr.arc(transformation.endX, transformation.endY, INVERSION_CIRCLE_RADIUS, 0, 2 * Math.PI); - } - cr.stroke(); - } - - cr.setLineCap(this.line.lineCap); - cr.setLineJoin(this.line.lineJoin); - cr.setLineWidth(this.line.lineWidth); - if (this.fillRule) - cr.setFillRule(this.fillRule); - - if (this.dash && this.dash.active && this.dash.array && this.dash.array[0] && this.dash.array[1]) - cr.setDash(this.dash.array, this.dash.offset); - - if (this.eraser) - cr.setOperator(Cairo.Operator.CLEAR); - else - cr.setOperator(Cairo.Operator.OVER); - - if (params.dummyStroke) - setDummyStroke(cr); - - if (SVG_DEBUG_SUPERPOSES_CAIRO) { - Clutter.cairo_set_source_color(cr, Clutter.Color.new(255, 0, 0, 255)); - cr.setLineWidth(this.line.lineWidth / 2 || 1); - } - - this.transformations.slice(0).reverse().forEach(transformation => { - if (transformation.type == Transformations.TRANSLATION) { - cr.translate(transformation.slideX, transformation.slideY); - } else if (transformation.type == Transformations.ROTATION) { - let center = this._getTransformedCenter(transformation); - cr.translate(center[0], center[1]); - cr.rotate(transformation.angle); - cr.translate(-center[0], -center[1]); - } else if (transformation.type == Transformations.SCALE_PRESERVE || transformation.type == Transformations.STRETCH) { - let center = this._getTransformedCenter(transformation); - cr.translate(center[0], center[1]); - cr.rotate(transformation.angle); - cr.scale(transformation.scaleX, transformation.scaleY); - cr.rotate(-transformation.angle); - cr.translate(-center[0], -center[1]); - } else if (transformation.type == Transformations.REFLECTION || transformation.type == Transformations.INVERSION) { - cr.translate(transformation.slideX, transformation.slideY); - cr.rotate(transformation.angle); - cr.scale(transformation.scaleX, transformation.scaleY); - cr.rotate(-transformation.angle); - cr.translate(-transformation.slideX, -transformation.slideY); - } - }); - - let [points, shape] = [this.points, this.shape]; - - if (shape == Shapes.LINE && points.length == 3) { - cr.moveTo(points[0][0], points[0][1]); - cr.curveTo(points[0][0], points[0][1], points[1][0], points[1][1], points[2][0], points[2][1]); - - } else if (shape == Shapes.LINE && points.length == 4) { - cr.moveTo(points[0][0], points[0][1]); - cr.curveTo(points[1][0], points[1][1], points[2][0], points[2][1], points[3][0], points[3][1]); - - } else if (shape == Shapes.NONE || shape == Shapes.LINE) { - cr.moveTo(points[0][0], points[0][1]); - for (let j = 1; j < points.length; j++) { - cr.lineTo(points[j][0], points[j][1]); - } - - } else if (shape == Shapes.ELLIPSE && points.length >= 2) { - let radius = Math.hypot(points[1][0] - points[0][0], points[1][1] - points[0][1]); - let ratio = 1; - - if (points[2]) { - ratio = Math.hypot(points[2][0] - points[0][0], points[2][1] - points[0][1]) / radius; - cr.translate(points[0][0], points[0][1]); - cr.scale(ratio, 1); - cr.translate(-points[0][0], -points[0][1]); - cr.arc(points[0][0], points[0][1], radius, 0, 2 * Math.PI); - cr.translate(points[0][0], points[0][1]); - cr.scale(1 / ratio, 1); - cr.translate(-points[0][0], -points[0][1]); - } else - cr.arc(points[0][0], points[0][1], radius, 0, 2 * Math.PI); - - } else if (shape == Shapes.RECTANGLE && points.length == 2) { - cr.rectangle(points[0][0], points[0][1], points[1][0] - points[0][0], points[1][1] - points[0][1]); - - } else if ((shape == Shapes.POLYGON || shape == Shapes.POLYLINE) && points.length >= 2) { - cr.moveTo(points[0][0], points[0][1]); - for (let j = 1; j < points.length; j++) { - cr.lineTo(points[j][0], points[j][1]); - } - if (shape == Shapes.POLYGON) - cr.closePath(); - - } else if (shape == Shapes.TEXT && points.length == 2) { - let layout = PangoCairo.create_layout(cr); - let fontSize = Math.abs(points[1][1] - points[0][1]) * Pango.SCALE; - let fontDescription = new Pango.FontDescription(); - fontDescription.set_absolute_size(fontSize); - ['family', 'weight', 'style', 'stretch', 'variant'].forEach(attribute => { - if (this.font[attribute] !== undefined) - try { - fontDescription[`set_${attribute}`](this.font[attribute]); - } catch(e) {} - }); - layout.set_font_description(fontDescription); - layout.set_text(this.text, -1); - this.textWidth = layout.get_pixel_size()[0]; - cr.moveTo(points[1][0] - (this.textRightAligned ? this.textWidth : 0), Math.max(points[0][1],points[1][1]) - layout.get_baseline() / Pango.SCALE); - layout.set_text(this.text, -1); - PangoCairo.show_layout(cr, layout); - - if (params.showTextCursor) { - let cursorPosition = this.cursorPosition == -1 ? this.text.length : this.cursorPosition; - layout.set_text(this.text.slice(0, cursorPosition), -1); - let width = layout.get_pixel_size()[0]; - cr.rectangle(points[1][0] - (this.textRightAligned ? this.textWidth : 0) + width, Math.max(points[0][1],points[1][1]), - Math.abs(points[1][1] - points[0][1]) / 25, - Math.abs(points[1][1] - points[0][1])); - cr.fill(); - } - - if (params.showTextRectangle || params.drawTextRectangle) { - cr.rectangle(points[1][0] - (this.textRightAligned ? this.textWidth : 0), Math.max(points[0][1], points[1][1]), - this.textWidth, - Math.abs(points[1][1] - points[0][1])); - if (params.showTextRectangle) - setDummyStroke(cr); - else - // Only draw the rectangle to find the element, not to show it. - cr.setLineWidth(0); - } - } - - cr.identityMatrix(); - }, - - getContainsPoint: function(cr, x, y) { - if (this.shape == Shapes.TEXT) - return cr.inFill(x, y); - - cr.save(); - cr.setLineWidth(Math.max(this.line.lineWidth, 25)); - cr.setDash([], 0); - - // Check whether the point is inside/on/near the element. - let inElement = cr.inStroke(x, y) || this.fill && cr.inFill(x, y); - cr.restore(); - return inElement; - }, - - buildSVG: function(bgColor) { - 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 ? bgColor : this.color; - let fill = this.fill && !this.isStraightLine; - let attributes = ''; - - if (fill) { - attributes = `fill="${color}"`; - if (this.fillRule) - attributes += ` fill-rule="${FillRuleNames[this.fillRule].toLowerCase()}"`; - } else { - attributes = `fill="none"`; - } - - if (this.line && this.line.lineWidth) { - attributes += ` stroke="${color}"` + - ` stroke-width="${this.line.lineWidth}"`; - if (this.line.lineCap) - attributes += ` stroke-linecap="${LineCapNames[this.line.lineCap].toLowerCase()}"`; - if (this.line.lineJoin && !this.isStraightLine) - attributes += ` stroke-linejoin="${LineJoinNames[this.line.lineJoin].toLowerCase()}"`; - if (this.dash && this.dash.active && this.dash.array && this.dash.array[0] && this.dash.array[1]) - attributes += ` stroke-dasharray="${this.dash.array[0]} ${this.dash.array[1]}" stroke-dashoffset="${this.dash.offset}"`; - } else { - attributes += ` stroke="none"`; - } - - let transAttribute = ''; - this.transformations.slice(0).reverse().forEach(transformation => { - transAttribute += transAttribute ? ' ' : ' transform="'; - let center = this._getTransformedCenter(transformation); - - if (transformation.type == Transformations.TRANSLATION) { - transAttribute += `translate(${transformation.slideX},${transformation.slideY})`; - } else if (transformation.type == Transformations.ROTATION) { - transAttribute += `translate(${center[0]},${center[1]}) `; - transAttribute += `rotate(${transformation.angle * RADIAN}) `; - transAttribute += `translate(${-center[0]},${-center[1]})`; - } else if (transformation.type == Transformations.SCALE_PRESERVE || transformation.type == Transformations.STRETCH) { - transAttribute += `translate(${center[0]},${center[1]}) `; - transAttribute += `rotate(${transformation.angle * RADIAN}) `; - transAttribute += `scale(${transformation.scaleX},${transformation.scaleY}) `; - transAttribute += `rotate(${-transformation.angle * RADIAN}) `; - transAttribute += `translate(${-center[0]},${-center[1]})`; - } else if (transformation.type == Transformations.REFLECTION || transformation.type == Transformations.INVERSION) { - transAttribute += `translate(${transformation.slideX}, ${transformation.slideY}) `; - transAttribute += `rotate(${transformation.angle * RADIAN}) `; - transAttribute += `scale(${transformation.scaleX}, ${transformation.scaleY}) `; - transAttribute += `rotate(${-transformation.angle * RADIAN}) `; - transAttribute += `translate(${-transformation.slideX}, ${-transformation.slideY})`; - } - }); - transAttribute += transAttribute ? '"' : ''; - - if (this.shape == Shapes.LINE && points.length == 4) { - row += ``; - - } else if (this.shape == Shapes.LINE && points.length == 3) { - row += ``; - - } else if (this.shape == Shapes.LINE) { - row += ``; - - } else if (this.shape == Shapes.NONE) { - row += ``; - - } else if (this.shape == Shapes.ELLIPSE && points.length == 3) { - let ry = Math.hypot(points[1][0] - points[0][0], points[1][1] - points[0][1]); - let rx = Math.hypot(points[2][0] - points[0][0], points[2][1] - points[0][1]); - row += ``; - - } else if (this.shape == Shapes.ELLIPSE && points.length == 2) { - let r = Math.hypot(points[1][0] - points[0][0], points[1][1] - points[0][1]); - row += ``; - - } else if (this.shape == Shapes.RECTANGLE && points.length == 2) { - row += ``; - - } else if (this.shape == Shapes.POLYGON && points.length >= 3) { - row += ``; - - } else if (this.shape == Shapes.POLYLINE && points.length >= 2) { - row += ``; - - } else if (this.shape == Shapes.TEXT && points.length == 2) { - attributes = `fill="${color}" ` + - `stroke="transparent" ` + - `stroke-opacity="0" ` + - `font-size="${Math.abs(points[1][1] - points[0][1])}"`; - - if (this.font.family) - attributes += ` font-family="${this.font.family}"`; - if (this.font.weight && this.font.weight != Pango.Weight.NORMAL) - attributes += ` font-weight="${this.font.weight}"`; - if (this.font.style && FontStyleNames[this.font.style]) - attributes += ` font-style="${FontStyleNames[this.font.style].toLowerCase()}"`; - if (FontStretchNames[this.font.stretch] && this.font.stretch != Pango.Stretch.NORMAL) - attributes += ` font-stretch="${FontStretchNames[this.font.stretch].toLowerCase()}"`; - if (this.font.variant && FontVariantNames[this.font.variant]) - attributes += ` font-variant="${FontVariantNames[this.font.variant].toLowerCase()}"`; - - // this.textWidth is computed during Cairo building. - row += `${this.text}`; - } - - return row; - }, - - get lastTransformation() { - if (!this.transformations.length) - return null; - - return this.transformations[this.transformations.length - 1]; - }, - - get isStraightLine() { - return this.shape == Shapes.LINE && this.points.length == 2; - }, - - smoothAll: function() { - for (let i = 0; i < this.points.length; i++) { - this._smooth(i); - } - }, - - addPoint: function() { - if (this.shape == Shapes.POLYGON || this.shape == Shapes.POLYLINE) { - // copy last point - let [lastPoint, secondToLastPoint] = [this.points[this.points.length - 1], this.points[this.points.length - 2]]; - if (!getNearness(secondToLastPoint, lastPoint, MIN_DRAWING_SIZE)) - this.points.push([lastPoint[0], lastPoint[1]]); - } else if (this.shape == Shapes.LINE) { - if (this.points.length == 2) { - this.points[2] = this.points[1]; - } else if (this.points.length == 3) { - this.points[3] = this.points[2]; - this.points[2] = this.points[1]; - } - } - }, - - startDrawing: function(startX, startY) { - this.points.push([startX, startY]); - - if (this.shape == Shapes.POLYGON || this.shape == Shapes.POLYLINE) - this.points.push([startX, startY]); - }, - - updateDrawing: function(x, y, transform) { - let points = this.points; - if (x == points[points.length - 1][0] && y == points[points.length - 1][1]) - return; - - transform = transform || this.transformations.length >= 1; - - if (this.shape == Shapes.NONE) { - points.push([x, y]); - if (transform) - this._smooth(points.length - 1); - - } else if ((this.shape == Shapes.RECTANGLE || this.shape == Shapes.POLYGON || this.shape == Shapes.POLYLINE) && transform) { - if (points.length < 2) - return; - - let center = this._getOriginalCenter(); - 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) { - if (points.length < 2) - return; - - points[2] = [x, y]; - let center = this._getOriginalCenter(); - 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) { - points[points.length - 1] = [x, y]; - - } else if (this.shape == Shapes.TEXT && transform) { - if (points.length < 2) - return; - - let [slideX, slideY] = [x - points[1][0], y - points[1][1]]; - points[0] = [points[0][0] + slideX, points[0][1] + slideY]; - points[1] = [x, y]; - - } else { - points[1] = [x, y]; - - } - }, - - stopDrawing: function() { - // skip when the size is too small to be visible (3px) (except for free drawing) - if (this.shape != Shapes.NONE && this.points.length >= 2) { - let lastPoint = this.points[this.points.length - 1]; - let secondToLastPoint = this.points[this.points.length - 2]; - if (getNearness(secondToLastPoint, lastPoint, MIN_DRAWING_SIZE)) - this.points.pop(); - } - - if (this.transformations[0] && this.transformations[0].type == Transformations.ROTATION && - Math.abs(this.transformations[0].angle) < MIN_ROTATION_ANGLE) - this.transformations.shift(); - }, - - startTransformation: function(startX, startY, type) { - if (type == Transformations.TRANSLATION) - this.transformations.push({ startX: startX, startY: startY, type: type, slideX: 0, slideY: 0 }); - else if (type == Transformations.ROTATION) - this.transformations.push({ startX: startX, startY: startY, type: type, 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 }); - else if (type == Transformations.REFLECTION) - this.transformations.push({ startX: startX, startY: startY, endX: startX, endY: startY, type: type, - 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, - scaleX: -1, scaleY: -1, slideX: startX, slideY: startY, - angle: Math.PI + Math.atan(startY / (startX || 1)) }); - - if (type == Transformations.REFLECTION || type == Transformations.INVERSION) - this.showSymmetryElement = true; - }, - - updateTransformation: function(x, y) { - let transformation = this.lastTransformation; - - if (transformation.type == Transformations.TRANSLATION) { - transformation.slideX = x - transformation.startX; - transformation.slideY = y - transformation.startY; - } else if (transformation.type == Transformations.ROTATION) { - let center = this._getTransformedCenter(transformation); - transformation.angle = getAngle(center[0], center[1], transformation.startX, transformation.startY, x, y); - } else if (transformation.type == Transformations.SCALE_PRESERVE) { - let center = this._getTransformedCenter(transformation); - let scale = Math.hypot(x - center[0], y - center[1]) / Math.hypot(transformation.startX - center[0], transformation.startY - center[1]) || 1; - [transformation.scaleX, transformation.scaleY] = [scale, scale]; - } else if (transformation.type == Transformations.STRETCH) { - let center = this._getTransformedCenter(transformation); - let startAngle = getAngle(center[0], center[1], center[0] + 1, center[1], transformation.startX, transformation.startY); - let vertical = Math.abs(Math.sin(startAngle)) >= Math.sin(Math.PI / 2 - STRETCH_TOLERANCE); - let horizontal = Math.abs(Math.cos(startAngle)) >= Math.cos(STRETCH_TOLERANCE); - let scale = Math.hypot(x - center[0], y - center[1]) / Math.hypot(transformation.startX - center[0], transformation.startY - center[1]) || 1; - transformation.scaleX = vertical ? 1 : scale; - transformation.scaleY = !vertical ? 1 : scale; - transformation.angle = vertical || horizontal ? 0 : getAngle(center[0], center[1], center[0] + 1, center[1], x, y); - } else if (transformation.type == Transformations.REFLECTION) { - [transformation.endX, transformation.endY] = [x, y]; - if (getNearness([transformation.startX, transformation.startY], [x, y], MIN_REFLECTION_LINE_LENGTH)) { - // do nothing to avoid jumps (no transformation at starting and locked transformation after) - } else if (Math.abs(y - transformation.startY) <= REFLECTION_TOLERANCE && Math.abs(x - transformation.startX) > REFLECTION_TOLERANCE) { - [transformation.scaleX, transformation.scaleY] = [1, -1]; - [transformation.slideX, transformation.slideY] = [0, transformation.startY]; - transformation.angle = Math.PI; - } else if (Math.abs(x - transformation.startX) <= REFLECTION_TOLERANCE && Math.abs(y - transformation.startY) > REFLECTION_TOLERANCE) { - [transformation.scaleX, transformation.scaleY] = [-1, 1]; - [transformation.slideX, transformation.slideY] = [transformation.startX, 0]; - transformation.angle = Math.PI; - } else if (x != transformation.startX) { - let tan = (y - transformation.startY) / (x - transformation.startX); - [transformation.scaleX, transformation.scaleY] = [1, -1]; - [transformation.slideX, transformation.slideY] = [0, transformation.startY - transformation.startX * tan]; - transformation.angle = Math.PI + Math.atan(tan); - } else if (y != transformation.startY) { - let tan = (x - transformation.startX) / (y - transformation.startY); - [transformation.scaleX, transformation.scaleY] = [-1, 1]; - [transformation.slideX, transformation.slideY] = [transformation.startX - transformation.startY * tan, 0]; - transformation.angle = Math.PI - Math.atan(tan); - } - } else if (transformation.type == Transformations.INVERSION) { - [transformation.endX, transformation.endY] = [x, y]; - [transformation.scaleX, transformation.scaleY] = [-1, -1]; - [transformation.slideX, transformation.slideY] = [x, y]; - transformation.angle = Math.PI + Math.atan(y / (x || 1)); - } - }, - - stopTransformation: function() { - // Clean transformations - let transformation = this.lastTransformation; - - if (transformation.type == Transformations.REFLECTION || transformation.type == Transformations.INVERSION) - this.showSymmetryElement = false; - - if (transformation.type == Transformations.REFLECTION && - getNearness([transformation.startX, transformation.startY], [transformation.endX, transformation.endY], MIN_REFLECTION_LINE_LENGTH) || - transformation.type == Transformations.TRANSLATION && Math.hypot(transformation.slideX, transformation.slideY) < MIN_TRANSLATION_DISTANCE || - transformation.type == Transformations.ROTATION && Math.abs(transformation.angle) < MIN_ROTATION_ANGLE) { - - this.transformations.pop(); - } else { - delete transformation.startX; - delete transformation.startY; - delete transformation.endX; - delete transformation.endY; - } - }, - - // When rotating grouped lines, lineOffset is used to retrieve the rotation center of the first line. - _getLineOffset: function() { - return (this.lineIndex || 0) * Math.abs(this.points[1][1] - this.points[0][1]); - }, - - // The figure rotation center before transformations (original). - // this.textWidth is computed during Cairo building. - _getOriginalCenter: function() { - if (!this._originalCenter) { - let points = this.points; - this._originalCenter = this.shape == Shapes.ELLIPSE ? [points[0][0], points[0][1]] : - this.shape == Shapes.LINE && points.length == 4 ? getCurveCenter(points[0], points[1], points[2], points[3]) : - this.shape == Shapes.LINE && points.length == 3 ? getCurveCenter(points[0], points[0], points[1], points[2]) : - this.shape == Shapes.TEXT && this.textWidth ? [points[1][0], Math.max(points[0][1], points[1][1]) - this._getLineOffset()] : - points.length >= 3 ? getCentroid(points) : - getNaiveCenter(points); - } - - return this._originalCenter; - }, - - // The figure rotation center, whose position is affected by all transformations done before 'transformation'. - _getTransformedCenter: function(transformation) { - if (!transformation.elementTransformedCenter) { - let matrix = new Pango.Matrix({ xx: 1, xy: 0, yx: 0, yy: 1, x0: 0, y0: 0 }); - - // Apply transformations to the matrice in reverse order - // because Pango multiply matrices by the left when applying a transformation - this.transformations.slice(0, this.transformations.indexOf(transformation)).reverse().forEach(transformation => { - if (transformation.type == Transformations.TRANSLATION) { - matrix.translate(transformation.slideX, transformation.slideY); - } else if (transformation.type == Transformations.ROTATION) { - // nothing, the center position is preserved. - } else if (transformation.type == Transformations.SCALE_PRESERVE || transformation.type == Transformations.STRETCH) { - // nothing, the center position is preserved. - } else if (transformation.type == Transformations.REFLECTION || transformation.type == Transformations.INVERSION) { - matrix.translate(transformation.slideX, transformation.slideY); - matrix.rotate(-transformation.angle * RADIAN); - matrix.scale(transformation.scaleX, transformation.scaleY); - matrix.rotate(transformation.angle * RADIAN); - matrix.translate(-transformation.slideX, -transformation.slideY); - } - }); - - let originalCenter = this._getOriginalCenter(); - transformation.elementTransformedCenter = matrix.transform_point(originalCenter[0], originalCenter[1]); - } - - return transformation.elementTransformedCenter; - }, - - _smooth: function(i) { - if (i < 2) - return; - this.points[i-1] = [(this.points[i-2][0] + this.points[i][0]) / 2, (this.points[i-2][1] + this.points[i][1]) / 2]; - } -}); - -const setDummyStroke = function(cr) { - cr.setLineWidth(2); - cr.setLineCap(0); - cr.setLineJoin(0); - cr.setDash([1, 2], 0); -}; - -/* - Some geometric utils -*/ - -const getNearness = function(pointA, pointB, distance) { - return Math.hypot(pointB[0] - pointA[0], pointB[1] - pointA[1]) < distance; -}; - -// mean of the vertices, ok for regular polygons -const getNaiveCenter = function(points) { - return points.reduce((accumulator, point) => accumulator = [accumulator[0] + point[0], accumulator[1] + point[1]]) - .map(coord => coord / points.length); -}; - -// https://en.wikipedia.org/wiki/Centroid#Of_a_polygon -const getCentroid = function(points) { - let n = points.length; - points.push(points[0]); - - let [sA, sX, sY] = [0, 0, 0]; - for (let i = 0; i <= n-1; i++) { - let a = points[i][0]*points[i+1][1] - points[i+1][0]*points[i][1]; - sA += a; - sX += (points[i][0] + points[i+1][0]) * a; - sY += (points[i][1] + points[i+1][1]) * a; - } - - points.pop(); - if (sA == 0) - return getNaiveCenter(points); - return [sX / (3 * sA), sY / (3 * sA)]; -}; - -/* -Cubic Bézier: -[0, 1] -> ℝ², P(t) = (1-t)³P₀ + 3t(1-t)²P₁ + 3t²(1-t)P₂ + t³P₃ - -general case: - -const cubicBezierCoord = function(x0, x1, x2, x3, t) { - return (1-t)**3*x0 + 3*t*(1-t)**2*x1 + 3*t**2*(1-t)*x2 + t**3*x3; -} - -const cubicBezierPoint = function(p0, p1, p2, p3, t) { - return [cubicBezier(p0[0], p1[0], p2[0], p3[0], t), cubicBezier(p0[1], p1[1], p2[1], p3[1], t)]; -} - -Approximatively: -control point: p0 ---- p1 ---- p2 ---- p3 (p2 is not on the curve) - t: 0 ---- 1/3 ---- 2/3 ---- 1 -*/ - -// If the curve has a symmetry axis, it is truly a center (the intersection of the curve and the axis). -// In other cases, it is not a notable point, just a visual approximation. -const getCurveCenter = function(p0, p1, p2, p3) { - if (p0[0] == p1[0] && p0[1] == p1[1]) - // p0 = p1, t = 2/3 - return [(p1[0] + 6*p1[0] + 12*p2[0] + 8*p3[0]) / 27, (p1[1] + 6*p1[1] + 12*p2[1] + 8*p3[1]) / 27]; - else - // t = 1/2 - return [(p0[0] + 3*p1[0] + 3*p2[0] + p3[0]) / 8, (p0[1] + 3*p1[1] + 3*p2[1] + p3[1]) / 8]; -}; - -const getAngle = function(xO, yO, xA, yA, xB, yB) { - // calculate angle of rotation in absolute value - // cos(AOB) = (OA.OB)/(||OA||*||OB||) where OA.OB = (xA-xO)*(xB-xO) + (yA-yO)*(yB-yO) - let cos = ((xA - xO)*(xB - xO) + (yA - yO)*(yB - yO)) / (Math.hypot(xA - xO, yA - yO) * Math.hypot(xB - xO, yB - yO)); - - // acos is defined on [-1, 1] but - // with A == B and imperfect computer calculations, cos may be equal to 1.00000001. - cos = Math.min(Math.max(-1, cos), 1); - let angle = Math.acos( cos ); - - // determine the sign of the angle - if (xA == xO) { - if (xB > xO) - angle = -angle; - } else { - // equation of OA: y = ax + b - let a = (yA - yO) / (xA - xO); - let b = yA - a*xA; - if (yB < a*xB + b) - angle = - angle; - if (xA < xO) - angle = - angle; - } - - return angle; -}; - From ca96e76d123b935952737b7c5f4d4ec703e67d6b Mon Sep 17 00:00:00 2001 From: abakkk Date: Sat, 4 Jul 2020 08:35:59 +0200 Subject: [PATCH 13/31] draw-to-elements 5 --- draw.js | 21 ++------------------- elements.js | 15 ++++++--------- extension.js | 4 ++-- menu.js | 9 +++++---- 4 files changed, 15 insertions(+), 34 deletions(-) diff --git a/draw.js b/draw.js index 4594f73..e800b95 100644 --- a/draw.js +++ b/draw.js @@ -45,28 +45,11 @@ const CAIRO_DEBUG_EXTENDS = false; const SVG_DEBUG_EXTENDS = false; const TEXT_CURSOR_TIME = 600; // ms -const reverseEnumeration = function(obj) { - let reversed = {}; - Object.keys(obj).forEach(key => { - reversed[obj[key]] = key.slice(0,1) + key.slice(1).toLowerCase().replace('_', '-'); - }); - return reversed; -}; - -const Shapes = { NONE: 0, LINE: 1, ELLIPSE: 2, RECTANGLE: 3, TEXT: 4, POLYGON: 5, POLYLINE: 6 }; -const Manipulations = { MOVE: 100, RESIZE: 101, MIRROR: 102 }; -const Transformations = { TRANSLATION: 0, ROTATION: 1, SCALE_PRESERVE: 2, STRETCH: 3, REFLECTION: 4, INVERSION: 5 }; +const { Shapes, Manipulations, Transformations, LineCapNames, LineJoinNames, FillRuleNames, + FontWeightNames, FontStyleNames, FontStretchNames, FontVariantNames } = Elements; var Tools = Object.assign({}, Shapes, Manipulations); var ToolNames = { 0: "Free drawing", 1: "Line", 2: "Ellipse", 3: "Rectangle", 4: "Text", 5: "Polygon", 6: "Polyline", 100: "Move", 101: "Resize", 102: "Mirror" }; -var LineCapNames = Object.assign(reverseEnumeration(Cairo.LineCap), { 2: 'Square' }); -var LineJoinNames = reverseEnumeration(Cairo.LineJoin); -var FillRuleNames = { 0: 'Nonzero', 1: 'Evenodd' }; var FontGenericNames = { 0: 'Theme', 1: 'Sans-Serif', 2: 'Serif', 3: 'Monospace', 4: 'Cursive', 5: 'Fantasy' }; -var FontWeightNames = Object.assign(reverseEnumeration(Pango.Weight), { 200: "Ultra-light", 350: "Semi-light", 600: "Semi-bold", 800: "Ultra-bold" }); -delete FontWeightNames[Pango.Weight.ULTRAHEAVY]; -var FontStyleNames = reverseEnumeration(Pango.Style); -var FontStretchNames = reverseEnumeration(Pango.Stretch); -var FontVariantNames = reverseEnumeration(Pango.Variant); var getDateString = function() { let date = GLib.DateTime.new_now_local(); diff --git a/elements.js b/elements.js index 9e531d2..eb86e9a 100644 --- a/elements.js +++ b/elements.js @@ -30,8 +30,6 @@ const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; -const SVG_DEBUG_SUPERPOSES_CAIRO = false; - const reverseEnumeration = function(obj) { let reversed = {}; Object.keys(obj).forEach(key => { @@ -40,21 +38,20 @@ const reverseEnumeration = function(obj) { return reversed; }; -const Shapes = { NONE: 0, LINE: 1, ELLIPSE: 2, RECTANGLE: 3, TEXT: 4, POLYGON: 5, POLYLINE: 6 }; -const Manipulations = { MOVE: 100, RESIZE: 101, MIRROR: 102 }; -const Transformations = { TRANSLATION: 0, ROTATION: 1, SCALE_PRESERVE: 2, STRETCH: 3, REFLECTION: 4, INVERSION: 5 }; -var Tools = Object.assign({}, Shapes, Manipulations); -var ToolNames = { 0: "Free drawing", 1: "Line", 2: "Ellipse", 3: "Rectangle", 4: "Text", 5: "Polygon", 6: "Polyline", 100: "Move", 101: "Resize", 102: "Mirror" }; +var Shapes = { NONE: 0, LINE: 1, ELLIPSE: 2, RECTANGLE: 3, TEXT: 4, POLYGON: 5, POLYLINE: 6 }; +var Manipulations = { MOVE: 100, RESIZE: 101, MIRROR: 102 }; +var Transformations = { TRANSLATION: 0, ROTATION: 1, SCALE_PRESERVE: 2, STRETCH: 3, REFLECTION: 4, INVERSION: 5 }; var LineCapNames = Object.assign(reverseEnumeration(Cairo.LineCap), { 2: 'Square' }); var LineJoinNames = reverseEnumeration(Cairo.LineJoin); var FillRuleNames = { 0: 'Nonzero', 1: 'Evenodd' }; -var FontGenericNames = { 0: 'Theme', 1: 'Sans-Serif', 2: 'Serif', 3: 'Monospace', 4: 'Cursive', 5: 'Fantasy' }; var FontWeightNames = Object.assign(reverseEnumeration(Pango.Weight), { 200: "Ultra-light", 350: "Semi-light", 600: "Semi-bold", 800: "Ultra-bold" }); delete FontWeightNames[Pango.Weight.ULTRAHEAVY]; var FontStyleNames = reverseEnumeration(Pango.Style); var FontStretchNames = reverseEnumeration(Pango.Stretch); var FontVariantNames = reverseEnumeration(Pango.Variant); + +const SVG_DEBUG_SUPERPOSES_CAIRO = false; const RADIAN = 180 / Math.PI; // degree const INVERSION_CIRCLE_RADIUS = 12; // px const REFLECTION_TOLERANCE = 5; // px, to select vertical and horizontal directions @@ -67,7 +64,7 @@ const MIN_DRAWING_SIZE = 3; // px // DrawingElement represents a "brushstroke". // 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({ +var DrawingElement = new Lang.Class({ Name: 'DrawOnYourScreenDrawingElement', _init: function(params) { diff --git a/extension.js b/extension.js index 906be7c..bc08dc4 100644 --- a/extension.js +++ b/extension.js @@ -36,7 +36,7 @@ const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Convenience = ExtensionUtils.getSettings && ExtensionUtils.initTranslations ? ExtensionUtils : Me.imports.convenience; const Draw = Me.imports.draw; -const Osd = Me.imports.osd; +const Helper = Me.imports.helper; const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; const GS_VERSION = Config.PACKAGE_VERSION; @@ -155,7 +155,7 @@ var 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 Osd.DrawingHelper({ name: 'drawOnYourSreenHelper' + i }, monitor); + let helper = new Helper.DrawingHelper({ name: 'drawOnYourSreenHelper' + i }, monitor); let loadPersistent = i == Main.layoutManager.primaryIndex && this.settings.get_boolean('persistent-drawing'); let area = new Draw.DrawingArea({ name: 'drawOnYourSreenArea' + i }, monitor, helper, loadPersistent); container.add_child(area); diff --git a/menu.js b/menu.js index ea83845..eb99294 100644 --- a/menu.js +++ b/menu.js @@ -37,6 +37,7 @@ const Slider = imports.ui.slider; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Draw = Me.imports.draw; +const Elements = Me.imports.elements; const Extension = Me.imports.extension; const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; @@ -164,8 +165,8 @@ var DrawingMenu = new Lang.Class({ let lineSection = new PopupMenu.PopupMenuSection(); this._addSliderItem(lineSection, this.area, 'currentLineWidth'); - this._addSubMenuItem(lineSection, this.linejoinIcon, Draw.LineJoinNames, this.area, 'currentLineJoin'); - this._addSubMenuItem(lineSection, this.linecapIcon, Draw.LineCapNames, this.area, 'currentLineCap'); + this._addSubMenuItem(lineSection, this.linejoinIcon, Elements.LineJoinNames, this.area, 'currentLineJoin'); + this._addSubMenuItem(lineSection, this.linecapIcon, Elements.LineCapNames, this.area, 'currentLineCap'); this._addSwitchItem(lineSection, _("Dashed"), this.fullLineIcon, this.dashedLineIcon, this.area, 'dashedLine'); this._addSeparator(lineSection); this.menu.addMenuItem(lineSection); @@ -176,8 +177,8 @@ var DrawingMenu = new Lang.Class({ let FontGenericNamesCopy = Object.create(Draw.FontGenericNames); FontGenericNamesCopy[0] = this.area.currentThemeFontFamily; this._addSubMenuItem(fontSection, 'font-x-generic-symbolic', FontGenericNamesCopy, this.area, 'currentFontGeneric'); - this._addSubMenuItem(fontSection, 'format-text-bold-symbolic', Draw.FontWeightNames, this.area, 'currentFontWeight'); - this._addSubMenuItem(fontSection, 'format-text-italic-symbolic', Draw.FontStyleNames, this.area, 'currentFontStyle'); + this._addSubMenuItem(fontSection, 'format-text-bold-symbolic', Elements.FontWeightNames, this.area, 'currentFontWeight'); + this._addSubMenuItem(fontSection, 'format-text-italic-symbolic', Elements.FontStyleNames, this.area, 'currentFontStyle'); this._addSwitchItem(fontSection, _("Right aligned"), 'format-justify-left-symbolic', 'format-justify-right-symbolic', this.area, 'currentTextRightAligned'); this._addSeparator(fontSection); this.menu.addMenuItem(fontSection); From 96efac1e3fee5ec33dc96aaf0a02b28fea3f4a8d Mon Sep 17 00:00:00 2001 From: abakkk Date: Sat, 4 Jul 2020 08:42:45 +0200 Subject: [PATCH 14/31] draw.js -> area.js --- draw.js => area.js | 0 elements.js | 4 ---- extension.js | 24 ++++++++++++------------ menu.js | 16 ++++++++-------- 4 files changed, 20 insertions(+), 24 deletions(-) rename draw.js => area.js (100%) diff --git a/draw.js b/area.js similarity index 100% rename from draw.js rename to area.js diff --git a/elements.js b/elements.js index eb86e9a..9b4f7bc 100644 --- a/elements.js +++ b/elements.js @@ -26,10 +26,6 @@ const Lang = imports.lang; const Pango = imports.gi.Pango; const PangoCairo = imports.gi.PangoCairo; -const ExtensionUtils = imports.misc.extensionUtils; -const Me = ExtensionUtils.getCurrentExtension(); -const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; - const reverseEnumeration = function(obj) { let reversed = {}; Object.keys(obj).forEach(key => { diff --git a/extension.js b/extension.js index bc08dc4..f4cde62 100644 --- a/extension.js +++ b/extension.js @@ -35,7 +35,7 @@ const PanelMenu = imports.ui.panelMenu; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Convenience = ExtensionUtils.getSettings && ExtensionUtils.initTranslations ? ExtensionUtils : Me.imports.convenience; -const Draw = Me.imports.draw; +const Area = Me.imports.area; const Helper = Me.imports.helper; const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; @@ -157,7 +157,7 @@ var AreaManager = new Lang.Class({ let container = new St.Widget({ name: 'drawOnYourSreenContainer' + i }); let helper = new Helper.DrawingHelper({ name: 'drawOnYourSreenHelper' + i }, monitor); let loadPersistent = i == Main.layoutManager.primaryIndex && this.settings.get_boolean('persistent-drawing'); - let area = new Draw.DrawingArea({ name: 'drawOnYourSreenArea' + i }, monitor, helper, loadPersistent); + let area = new Area.DrawingArea({ name: 'drawOnYourSreenArea' + i }, monitor, helper, loadPersistent); container.add_child(area); container.add_child(helper); @@ -191,16 +191,16 @@ var AreaManager = new Lang.Class({ 'toggle-fill-rule': this.activeArea.toggleFillRule.bind(this.activeArea), 'toggle-dash' : this.activeArea.toggleDash.bind(this.activeArea), 'toggle-fill' : this.activeArea.toggleFill.bind(this.activeArea), - 'select-none-shape': () => this.activeArea.selectTool(Draw.Tools.NONE), - 'select-line-shape': () => this.activeArea.selectTool(Draw.Tools.LINE), - 'select-ellipse-shape': () => this.activeArea.selectTool(Draw.Tools.ELLIPSE), - 'select-rectangle-shape': () => this.activeArea.selectTool(Draw.Tools.RECTANGLE), - 'select-text-shape': () => this.activeArea.selectTool(Draw.Tools.TEXT), - 'select-polygon-shape': () => this.activeArea.selectTool(Draw.Tools.POLYGON), - 'select-polyline-shape': () => this.activeArea.selectTool(Draw.Tools.POLYLINE), - 'select-move-tool': () => this.activeArea.selectTool(Draw.Tools.MOVE), - 'select-resize-tool': () => this.activeArea.selectTool(Draw.Tools.RESIZE), - 'select-mirror-tool': () => this.activeArea.selectTool(Draw.Tools.MIRROR) + 'select-none-shape': () => this.activeArea.selectTool(Area.Tools.NONE), + 'select-line-shape': () => this.activeArea.selectTool(Area.Tools.LINE), + 'select-ellipse-shape': () => this.activeArea.selectTool(Area.Tools.ELLIPSE), + 'select-rectangle-shape': () => this.activeArea.selectTool(Area.Tools.RECTANGLE), + 'select-text-shape': () => this.activeArea.selectTool(Area.Tools.TEXT), + 'select-polygon-shape': () => this.activeArea.selectTool(Area.Tools.POLYGON), + 'select-polyline-shape': () => this.activeArea.selectTool(Area.Tools.POLYLINE), + 'select-move-tool': () => this.activeArea.selectTool(Area.Tools.MOVE), + 'select-resize-tool': () => this.activeArea.selectTool(Area.Tools.RESIZE), + 'select-mirror-tool': () => this.activeArea.selectTool(Area.Tools.MIRROR) }; // available when writing diff --git a/menu.js b/menu.js index eb99294..8adea81 100644 --- a/menu.js +++ b/menu.js @@ -36,7 +36,7 @@ const Slider = imports.ui.slider; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); -const Draw = Me.imports.draw; +const Area = Me.imports.area; const Elements = Me.imports.elements; const Extension = Me.imports.extension; const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; @@ -154,7 +154,7 @@ var DrawingMenu = new Lang.Class({ this.menu.addAction(_("Smooth"), this.area.smoothLastElement.bind(this.area), 'format-text-strikethrough-symbolic'); this._addSeparator(this.menu); - this._addSubMenuItem(this.menu, 'document-edit-symbolic', Draw.ToolNames, this.area, 'currentTool', this._updateSectionVisibility.bind(this)); + this._addSubMenuItem(this.menu, 'document-edit-symbolic', Area.ToolNames, this.area, 'currentTool', this._updateSectionVisibility.bind(this)); this._addColorSubMenuItem(this.menu); this.fillItem = this._addSwitchItem(this.menu, _("Fill"), this.strokeIcon, this.fillIcon, this.area, 'fill', this._updateSectionVisibility.bind(this)); this.fillSection = new PopupMenu.PopupMenuSection(); @@ -174,7 +174,7 @@ var DrawingMenu = new Lang.Class({ this.lineSection = lineSection; let fontSection = new PopupMenu.PopupMenuSection(); - let FontGenericNamesCopy = Object.create(Draw.FontGenericNames); + let FontGenericNamesCopy = Object.create(Area.FontGenericNames); FontGenericNamesCopy[0] = this.area.currentThemeFontFamily; this._addSubMenuItem(fontSection, 'font-x-generic-symbolic', FontGenericNamesCopy, this.area, 'currentFontGeneric'); this._addSubMenuItem(fontSection, 'format-text-bold-symbolic', Elements.FontWeightNames, this.area, 'currentFontWeight'); @@ -204,7 +204,7 @@ var DrawingMenu = new Lang.Class({ }, _updateSectionVisibility: function() { - if (this.area.currentTool != Draw.Tools.TEXT) { + if (this.area.currentTool != Area.Tools.TEXT) { this.lineSection.actor.show(); this.fontSection.actor.hide(); this.fillItem.setSensitive(true); @@ -320,9 +320,9 @@ var DrawingMenu = new Lang.Class({ subItem.label.get_clutter_text().set_use_markup(true); // change the display order of tools - if (obj == Draw.ToolNames && i == Draw.Tools.POLYGON) + if (obj == Area.ToolNames && i == Area.Tools.POLYGON) item.menu.moveMenuItem(subItem, 4); - else if (obj == Draw.ToolNames && i == Draw.Tools.POLYLINE) + else if (obj == Area.ToolNames && i == Area.Tools.POLYLINE) item.menu.moveMenuItem(subItem, 5); } return GLib.SOURCE_REMOVE; @@ -394,7 +394,7 @@ var DrawingMenu = new Lang.Class({ _populateOpenDrawingSubMenu: function() { this.openDrawingSubMenu.removeAll(); - let jsonFiles = Draw.getJsonFiles(); + let jsonFiles = Area.getJsonFiles(); jsonFiles.forEach(file => { let item = this.openDrawingSubMenu.addAction(`${file.displayName}`, () => { this.area.loadJson(file.name); @@ -450,7 +450,7 @@ var DrawingMenu = new Lang.Class({ _populateSaveDrawingSubMenu: function() { this.saveDrawingSubMenu.removeAll(); - let saveEntry = new DrawingMenuEntry({ initialTextGetter: Draw.getDateString, + let saveEntry = new DrawingMenuEntry({ initialTextGetter: Area.getDateString, entryActivateCallback: (text) => { this.area.saveAsJsonWithName(text); this.saveDrawingSubMenu.toggle(); From e8140eae882913c23b9c12149ae1fb8c085febe5 Mon Sep 17 00:00:00 2001 From: abakkk Date: Wed, 8 Jul 2020 11:47:51 +0200 Subject: [PATCH 15/31] Text elements as a separated class --- area.js | 48 ++++---- elements.js | 307 +++++++++++++++++++++++++++++++--------------------- 2 files changed, 211 insertions(+), 144 deletions(-) diff --git a/area.js b/area.js index e800b95..c33de80 100644 --- a/area.js +++ b/area.js @@ -471,7 +471,7 @@ var DrawingArea = new Lang.Class({ if (duplicate) { // deep cloning - let copy = new Elements.DrawingElement(JSON.parse(JSON.stringify(this.grabbedElement))); + let copy = new this.grabbedElement.constructor(JSON.parse(JSON.stringify(this.grabbedElement))); this.elements.push(copy); this.grabbedElement = copy; } @@ -554,28 +554,32 @@ var DrawingArea = new Lang.Class({ this._stopDrawing(); }); - this.currentElement = new Elements.DrawingElement ({ - shape: this.currentTool, - color: this.currentColor.to_string(), - line: { lineWidth: this.currentLineWidth, lineJoin: this.currentLineJoin, lineCap: this.currentLineCap }, - dash: { active: this.dashedLine, array: this.dashedLine ? [this.dashArray[0] || this.currentLineWidth, this.dashArray[1] || this.currentLineWidth * 3] : [0, 0] , offset: this.dashOffset }, - fill: this.fill, - fillRule: this.currentFillRule, - eraser: eraser, - transform: { active: false, center: [0, 0], angle: 0, startAngle: 0, ratio: 1 }, - points: [] - }); - if (this.currentTool == Shapes.TEXT) { - this.currentElement.fill = false; - this.currentElement.font = { - family: (this.currentFontGeneric == 0 ? this.currentThemeFontFamily : FontGenericNames[this.currentFontGeneric]), - weight: this.currentFontWeight, - style: this.currentFontStyle, - stretch: this.currentFontStretch, - variant: this.currentFontVariant }; - this.currentElement.text = _("Text"); - this.currentElement.textRightAligned = this.currentTextRightAligned; + this.currentElement = new Elements.DrawingElement({ + shape: this.currentTool, + color: this.currentColor.to_string(), + eraser: eraser, + font: { + family: (this.currentFontGeneric == 0 ? this.currentThemeFontFamily : FontGenericNames[this.currentFontGeneric]), + weight: this.currentFontWeight, + style: this.currentFontStyle, + stretch: this.currentFontStretch, + variant: this.currentFontVariant }, + text: _("Text"), + textRightAligned: this.currentTextRightAligned, + points: [] + }); + } else { + this.currentElement = new Elements.DrawingElement({ + shape: this.currentTool, + color: this.currentColor.to_string(), + eraser: eraser, + fill: this.fill, + fillRule: this.currentFillRule, + line: { lineWidth: this.currentLineWidth, lineJoin: this.currentLineJoin, lineCap: this.currentLineCap }, + dash: { active: this.dashedLine, array: this.dashedLine ? [this.dashArray[0] || this.currentLineWidth, this.dashArray[1] || this.currentLineWidth * 3] : [0, 0] , offset: this.dashOffset }, + points: [] + }); } this.currentElement.startDrawing(startX, startY); diff --git a/elements.js b/elements.js index 9b4f7bc..6eb40ee 100644 --- a/elements.js +++ b/elements.js @@ -57,10 +57,14 @@ const MIN_TRANSLATION_DISTANCE = 1; // px const MIN_ROTATION_ANGLE = Math.PI / 1000; // rad const MIN_DRAWING_SIZE = 3; // px +var DrawingElement = function(params) { + return params.shape == Shapes.TEXT ? new TextElement(params) : new _DrawingElement(params); +}; + // DrawingElement represents a "brushstroke". // It can be converted into a cairo path as well as a svg element. // See DrawingArea._startDrawing() to know its params. -var DrawingElement = new Lang.Class({ +const _DrawingElement = new Lang.Class({ Name: 'DrawOnYourScreenDrawingElement', _init: function(params) { @@ -69,16 +73,12 @@ var DrawingElement = new Lang.Class({ // compatibility with json generated by old extension versions - if (params.fillRule === undefined) - this.fillRule = Cairo.FillRule.WINDING; if (params.transformations === undefined) this.transformations = []; - if (params.shape == Shapes.TEXT) { - if (params.font && params.font.weight === 0) - this.font.weight = 400; - if (params.font && params.font.weight === 1) - this.font.weight = 700; - } + if (params.font && params.font.weight === 0) + this.font.weight = 400; + if (params.font && params.font.weight === 1) + this.font.weight = 700; if (params.transform && params.transform.center) { let angle = (params.transform.angle || 0) + (params.transform.startAngle || 0); @@ -104,10 +104,6 @@ var DrawingElement = new Lang.Class({ fillRule: this.fillRule, eraser: this.eraser, transformations: this.transformations, - text: this.text, - lineIndex: this.lineIndex !== undefined ? this.lineIndex : undefined, - textRightAligned: this.textRightAligned, - font: this.font, points: this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100]) }; }, @@ -129,9 +125,12 @@ var DrawingElement = new Lang.Class({ cr.stroke(); } - cr.setLineCap(this.line.lineCap); - cr.setLineJoin(this.line.lineJoin); - cr.setLineWidth(this.line.lineWidth); + if (this.line) { + cr.setLineCap(this.line.lineCap); + cr.setLineJoin(this.line.lineJoin); + cr.setLineWidth(this.line.lineWidth); + } + if (this.fillRule) cr.setFillRule(this.fillRule); @@ -175,6 +174,12 @@ var DrawingElement = new Lang.Class({ } }); + this._drawCairo(cr, params); + + cr.identityMatrix(); + }, + + _drawCairo: function(cr, params) { let [points, shape] = [this.points, this.shape]; if (shape == Shapes.LINE && points.length == 3) { @@ -218,51 +223,10 @@ var DrawingElement = new Lang.Class({ if (shape == Shapes.POLYGON) cr.closePath(); - } else if (shape == Shapes.TEXT && points.length == 2) { - let layout = PangoCairo.create_layout(cr); - let fontSize = Math.abs(points[1][1] - points[0][1]) * Pango.SCALE; - let fontDescription = new Pango.FontDescription(); - fontDescription.set_absolute_size(fontSize); - ['family', 'weight', 'style', 'stretch', 'variant'].forEach(attribute => { - if (this.font[attribute] !== undefined) - try { - fontDescription[`set_${attribute}`](this.font[attribute]); - } catch(e) {} - }); - layout.set_font_description(fontDescription); - layout.set_text(this.text, -1); - this.textWidth = layout.get_pixel_size()[0]; - cr.moveTo(points[1][0] - (this.textRightAligned ? this.textWidth : 0), Math.max(points[0][1],points[1][1]) - layout.get_baseline() / Pango.SCALE); - layout.set_text(this.text, -1); - PangoCairo.show_layout(cr, layout); - - if (params.showTextCursor) { - let cursorPosition = this.cursorPosition == -1 ? this.text.length : this.cursorPosition; - layout.set_text(this.text.slice(0, cursorPosition), -1); - let width = layout.get_pixel_size()[0]; - cr.rectangle(points[1][0] - (this.textRightAligned ? this.textWidth : 0) + width, Math.max(points[0][1],points[1][1]), - Math.abs(points[1][1] - points[0][1]) / 25, - Math.abs(points[1][1] - points[0][1])); - cr.fill(); - } - - if (params.showTextRectangle || params.drawTextRectangle) { - cr.rectangle(points[1][0] - (this.textRightAligned ? this.textWidth : 0), Math.max(points[0][1], points[1][1]), - this.textWidth, - Math.abs(points[1][1] - points[0][1])); - if (params.showTextRectangle) - setDummyStroke(cr); - else - // Only draw the rectangle to find the element, not to show it. - cr.setLineWidth(0); - } } - - cr.identityMatrix(); }, getContainsPoint: function(cr, x, y) { - if (this.shape == Shapes.TEXT) - return cr.inFill(x, y); - cr.save(); cr.setLineWidth(Math.max(this.line.lineWidth, 25)); cr.setDash([], 0); @@ -274,33 +238,6 @@ var DrawingElement = new Lang.Class({ }, buildSVG: function(bgColor) { - 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 ? bgColor : this.color; - let fill = this.fill && !this.isStraightLine; - let attributes = ''; - - if (fill) { - attributes = `fill="${color}"`; - if (this.fillRule) - attributes += ` fill-rule="${FillRuleNames[this.fillRule].toLowerCase()}"`; - } else { - attributes = `fill="none"`; - } - - if (this.line && this.line.lineWidth) { - attributes += ` stroke="${color}"` + - ` stroke-width="${this.line.lineWidth}"`; - if (this.line.lineCap) - attributes += ` stroke-linecap="${LineCapNames[this.line.lineCap].toLowerCase()}"`; - if (this.line.lineJoin && !this.isStraightLine) - attributes += ` stroke-linejoin="${LineJoinNames[this.line.lineJoin].toLowerCase()}"`; - if (this.dash && this.dash.active && this.dash.array && this.dash.array[0] && this.dash.array[1]) - attributes += ` stroke-dasharray="${this.dash.array[0]} ${this.dash.array[1]}" stroke-dashoffset="${this.dash.offset}"`; - } else { - attributes += ` stroke="none"`; - } - let transAttribute = ''; this.transformations.slice(0).reverse().forEach(transformation => { transAttribute += transAttribute ? ' ' : ' transform="'; @@ -328,6 +265,37 @@ var DrawingElement = new Lang.Class({ }); transAttribute += transAttribute ? '"' : ''; + return this._drawSvg(transAttribute); + }, + + _drawSvg: function(transAttribute) { + 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 ? bgColor : this.color; + let fill = this.fill && !this.isStraightLine; + let attributes = ''; + + if (fill) { + attributes = `fill="${color}"`; + if (this.fillRule) + attributes += ` fill-rule="${FillRuleNames[this.fillRule].toLowerCase()}"`; + } else { + attributes = `fill="none"`; + } + + if (this.line && this.line.lineWidth) { + attributes += ` stroke="${color}"` + + ` stroke-width="${this.line.lineWidth}"`; + if (this.line.lineCap) + attributes += ` stroke-linecap="${LineCapNames[this.line.lineCap].toLowerCase()}"`; + if (this.line.lineJoin && !this.isStraightLine) + attributes += ` stroke-linejoin="${LineJoinNames[this.line.lineJoin].toLowerCase()}"`; + if (this.dash && this.dash.active && this.dash.array && this.dash.array[0] && this.dash.array[1]) + attributes += ` stroke-dasharray="${this.dash.array[0]} ${this.dash.array[1]}" stroke-dashoffset="${this.dash.offset}"`; + } else { + attributes += ` stroke="none"`; + } + if (this.shape == Shapes.LINE && points.length == 4) { row += ``; - } else if (this.shape == Shapes.TEXT && points.length == 2) { - attributes = `fill="${color}" ` + - `stroke="transparent" ` + - `stroke-opacity="0" ` + - `font-size="${Math.abs(points[1][1] - points[0][1])}"`; - - if (this.font.family) - attributes += ` font-family="${this.font.family}"`; - if (this.font.weight && this.font.weight != Pango.Weight.NORMAL) - attributes += ` font-weight="${this.font.weight}"`; - if (this.font.style && FontStyleNames[this.font.style]) - attributes += ` font-style="${FontStyleNames[this.font.style].toLowerCase()}"`; - if (FontStretchNames[this.font.stretch] && this.font.stretch != Pango.Stretch.NORMAL) - attributes += ` font-stretch="${FontStretchNames[this.font.stretch].toLowerCase()}"`; - if (this.font.variant && FontVariantNames[this.font.variant]) - attributes += ` font-variant="${FontVariantNames[this.font.variant].toLowerCase()}"`; - - // this.textWidth is computed during Cairo building. - row += `${this.text}`; } return row; @@ -469,14 +417,6 @@ var DrawingElement = new Lang.Class({ } else if (this.shape == Shapes.POLYGON || this.shape == Shapes.POLYLINE) { points[points.length - 1] = [x, y]; - } else if (this.shape == Shapes.TEXT && transform) { - if (points.length < 2) - return; - - let [slideX, slideY] = [x - points[1][0], y - points[1][1]]; - points[0] = [points[0][0] + slideX, points[0][1] + slideY]; - points[1] = [x, y]; - } else { points[1] = [x, y]; @@ -590,11 +530,6 @@ var DrawingElement = new Lang.Class({ } }, - // When rotating grouped lines, lineOffset is used to retrieve the rotation center of the first line. - _getLineOffset: function() { - return (this.lineIndex || 0) * Math.abs(this.points[1][1] - this.points[0][1]); - }, - // The figure rotation center before transformations (original). // this.textWidth is computed during Cairo building. _getOriginalCenter: function() { @@ -603,7 +538,6 @@ var DrawingElement = new Lang.Class({ this._originalCenter = this.shape == Shapes.ELLIPSE ? [points[0][0], points[0][1]] : this.shape == Shapes.LINE && points.length == 4 ? getCurveCenter(points[0], points[1], points[2], points[3]) : this.shape == Shapes.LINE && points.length == 3 ? getCurveCenter(points[0], points[0], points[1], points[2]) : - this.shape == Shapes.TEXT && this.textWidth ? [points[1][0], Math.max(points[0][1], points[1][1]) - this._getLineOffset()] : points.length >= 3 ? getCentroid(points) : getNaiveCenter(points); } @@ -648,6 +582,139 @@ var DrawingElement = new Lang.Class({ } }); +const TextElement = new Lang.Class({ + Name: 'DrawOnYourScreenTextElement', + Extends: _DrawingElement, + + toJSON: function() { + return { + shape: this.shape, + color: this.color, + eraser: this.eraser, + transformations: this.transformations, + text: this.text, + lineIndex: this.lineIndex !== undefined ? this.lineIndex : undefined, + textRightAligned: this.textRightAligned, + font: this.font, + points: this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100]) + }; + }, + + _drawCairo: function(cr, params) { + let points = this.points; + + if (points.length == 2) { + let layout = PangoCairo.create_layout(cr); + let fontSize = Math.abs(points[1][1] - points[0][1]) * Pango.SCALE; + let fontDescription = new Pango.FontDescription(); + fontDescription.set_absolute_size(fontSize); + ['family', 'weight', 'style', 'stretch', 'variant'].forEach(attribute => { + if (this.font[attribute] !== undefined) + try { + fontDescription[`set_${attribute}`](this.font[attribute]); + } catch(e) {} + }); + layout.set_font_description(fontDescription); + layout.set_text(this.text, -1); + this.textWidth = layout.get_pixel_size()[0]; + cr.moveTo(points[1][0] - (this.textRightAligned ? this.textWidth : 0), Math.max(points[0][1],points[1][1]) - layout.get_baseline() / Pango.SCALE); + layout.set_text(this.text, -1); + PangoCairo.show_layout(cr, layout); + + if (params.showTextCursor) { + let cursorPosition = this.cursorPosition == -1 ? this.text.length : this.cursorPosition; + layout.set_text(this.text.slice(0, cursorPosition), -1); + let width = layout.get_pixel_size()[0]; + cr.rectangle(points[1][0] - (this.textRightAligned ? this.textWidth : 0) + width, Math.max(points[0][1],points[1][1]), + Math.abs(points[1][1] - points[0][1]) / 25, - Math.abs(points[1][1] - points[0][1])); + cr.fill(); + } + + if (params.showTextRectangle || params.drawTextRectangle) { + cr.rectangle(points[1][0] - (this.textRightAligned ? this.textWidth : 0), Math.max(points[0][1], points[1][1]), + this.textWidth, - Math.abs(points[1][1] - points[0][1])); + if (params.showTextRectangle) + setDummyStroke(cr); + else + // Only draw the rectangle to find the element, not to show it. + cr.setLineWidth(0); + } + } + }, + + getContainsPoint: function(cr, x, y) { + return cr.inFill(x, y); + }, + + _drawSvg: function(transAttribute) { + 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 ? bgColor : this.color; + let attributes = ''; + + if (points.length == 2) { + attributes = `fill="${color}" ` + + `stroke="transparent" ` + + `stroke-opacity="0" ` + + `font-size="${Math.abs(points[1][1] - points[0][1])}"`; + + if (this.font.family) + attributes += ` font-family="${this.font.family}"`; + if (this.font.weight && this.font.weight != Pango.Weight.NORMAL) + attributes += ` font-weight="${this.font.weight}"`; + if (this.font.style && FontStyleNames[this.font.style]) + attributes += ` font-style="${FontStyleNames[this.font.style].toLowerCase()}"`; + if (FontStretchNames[this.font.stretch] && this.font.stretch != Pango.Stretch.NORMAL) + attributes += ` font-stretch="${FontStretchNames[this.font.stretch].toLowerCase()}"`; + if (this.font.variant && FontVariantNames[this.font.variant]) + attributes += ` font-variant="${FontVariantNames[this.font.variant].toLowerCase()}"`; + + // this.textWidth is computed during Cairo building. + row += `${this.text}`; + } + + return row; + }, + + updateDrawing: function(x, y, transform) { + let points = this.points; + if (x == points[points.length - 1][0] && y == points[points.length - 1][1]) + return; + + transform = transform || this.transformations.length >= 1; + + if (transform) { + if (points.length < 2) + return; + + let [slideX, slideY] = [x - points[1][0], y - points[1][1]]; + points[0] = [points[0][0] + slideX, points[0][1] + slideY]; + points[1] = [x, y]; + + } else { + points[1] = [x, y]; + + } + }, + + // When rotating grouped lines, lineOffset is used to retrieve the rotation center of the first line. + _getLineOffset: function() { + return (this.lineIndex || 0) * Math.abs(this.points[1][1] - this.points[0][1]); + }, + + _getOriginalCenter: function() { + if (!this._originalCenter) { + let points = this.points; + this._originalCenter = this.textWidth ? [points[1][0], Math.max(points[0][1], points[1][1]) - this._getLineOffset()] : + points.length >= 3 ? getCentroid(points) : + getNaiveCenter(points); + } + + return this._originalCenter; + }, +}); + const setDummyStroke = function(cr) { cr.setLineWidth(2); cr.setLineCap(0); @@ -655,10 +722,6 @@ const setDummyStroke = function(cr) { cr.setDash([1, 2], 0); }; -/* - Some geometric utils -*/ - const getNearness = function(pointA, pointB, distance) { return Math.hypot(pointB[0] - pointA[0], pointB[1] - pointA[1]) < distance; }; From 813d6b0f1b64323be6f9c07a0d01ffaebf77cf90 Mon Sep 17 00:00:00 2001 From: abakkk Date: Sat, 11 Jul 2020 15:59:08 +0200 Subject: [PATCH 16/31] Show the textEntry when ibusCandidatePopup is visible --- area.js | 19 +++++++++++----- elements.js | 65 +++++++++++++++++++++++++++++++---------------------- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/area.js b/area.js index c33de80..0dea931 100644 --- a/area.js +++ b/area.js @@ -31,9 +31,10 @@ const Lang = imports.lang; const Pango = imports.gi.Pango; const St = imports.gi.St; +const ExtensionUtils = imports.misc.extensionUtils; +const Main = imports.ui.main; const Screenshot = imports.ui.screenshot; -const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Convenience = ExtensionUtils.getSettings ? ExtensionUtils : Me.imports.convenience; const Extension = Me.imports.extension; @@ -643,6 +644,7 @@ var DrawingArea = new Lang.Class({ }, _startWriting: function() { + let [x, y] = [this.currentElement.x, this.currentElement.y]; this.currentElement.text = ''; this.currentElement.cursorPosition = 0; this.emit('show-osd', null, _("Type your text and press %s") @@ -651,12 +653,19 @@ var DrawingArea = new Lang.Class({ this.textHasCursor = true; this._redisplay(); - this.textEntry = new St.Entry({ visible: false }); + this.textEntry = new St.Entry({ visible: false, x, y }); this.get_parent().add_child(this.textEntry); this.textEntry.grab_key_focus(); this.updateActionMode(); this.updatePointerCursor(); + let ibusCandidatePopup = Main.layoutManager.uiGroup.get_children().filter(child => + child.has_style_class_name && child.has_style_class_name('candidate-popup-boxpointer'))[0] || null; + if (ibusCandidatePopup) { + this.ibusHandler = ibusCandidatePopup.connect('notify::visible', popup => popup.visible && (this.textEntry.visible = true)); + this.textEntry.connect('destroy', () => ibusCandidatePopup.disconnect(this.ibusHandler)); + } + this.textEntry.clutterText.connect('activate', (clutterText) => { let startNewLine = true; this._stopWriting(startNewLine); @@ -703,13 +712,13 @@ var DrawingArea = new Lang.Class({ // copy object, the original keep existing in this.elements this.currentElement = Object.create(this.currentElement); this.currentElement.lineIndex ++; - let height = Math.abs(this.currentElement.points[1][1] - this.currentElement.points[0][1]); // define a new 'points' array, the original keep existing in this.elements this.currentElement.points = [ - [this.currentElement.points[0][0], this.currentElement.points[0][1] + height], - [this.currentElement.points[1][0], this.currentElement.points[1][1] + height] + [this.currentElement.points[0][0], this.currentElement.points[0][1] + this.currentElement.height], + [this.currentElement.points[1][0], this.currentElement.points[1][1] + this.currentElement.height] ]; this.currentElement.text = ""; + this.textEntry.set_y(this.currentElement.y); } else { this.currentElement = null; this._stopTextCursorTimeout(); diff --git a/elements.js b/elements.js index 6eb40ee..910e897 100644 --- a/elements.js +++ b/elements.js @@ -600,12 +600,28 @@ const TextElement = new Lang.Class({ }; }, + get x() { + // this.textWidth is computed during Cairo building. + return this.points[1][0] - (this.textRightAligned ? this.textWidth : 0); + }, + + get y() { + return Math.max(this.points[0][1], this.points[1][1]); + }, + + get height() { + return Math.abs(this.points[1][1] - this.points[0][1]); + }, + + // When rotating grouped lines, lineOffset is used to retrieve the rotation center of the first line. + get lineOffset() { + return (this.lineIndex || 0) * this.height; + }, + _drawCairo: function(cr, params) { - let points = this.points; - - if (points.length == 2) { + if (this.points.length == 2) { let layout = PangoCairo.create_layout(cr); - let fontSize = Math.abs(points[1][1] - points[0][1]) * Pango.SCALE; + let fontSize = this.height * Pango.SCALE; let fontDescription = new Pango.FontDescription(); fontDescription.set_absolute_size(fontSize); ['family', 'weight', 'style', 'stretch', 'variant'].forEach(attribute => { @@ -617,7 +633,7 @@ const TextElement = new Lang.Class({ layout.set_font_description(fontDescription); layout.set_text(this.text, -1); this.textWidth = layout.get_pixel_size()[0]; - cr.moveTo(points[1][0] - (this.textRightAligned ? this.textWidth : 0), Math.max(points[0][1],points[1][1]) - layout.get_baseline() / Pango.SCALE); + cr.moveTo(this.x, this.y - layout.get_baseline() / Pango.SCALE); layout.set_text(this.text, -1); PangoCairo.show_layout(cr, layout); @@ -625,19 +641,20 @@ const TextElement = new Lang.Class({ let cursorPosition = this.cursorPosition == -1 ? this.text.length : this.cursorPosition; layout.set_text(this.text.slice(0, cursorPosition), -1); let width = layout.get_pixel_size()[0]; - cr.rectangle(points[1][0] - (this.textRightAligned ? this.textWidth : 0) + width, Math.max(points[0][1],points[1][1]), - Math.abs(points[1][1] - points[0][1]) / 25, - Math.abs(points[1][1] - points[0][1])); + cr.rectangle(this.x + width, this.y, + this.height / 25, - this.height); cr.fill(); } - if (params.showTextRectangle || params.drawTextRectangle) { - cr.rectangle(points[1][0] - (this.textRightAligned ? this.textWidth : 0), Math.max(points[0][1], points[1][1]), - this.textWidth, - Math.abs(points[1][1] - points[0][1])); - if (params.showTextRectangle) - setDummyStroke(cr); - else - // Only draw the rectangle to find the element, not to show it. - cr.setLineWidth(0); + if (params.showTextRectangle) { + cr.rectangle(this.x, this.y - this.lineOffset, + this.textWidth, - this.height); + setDummyStroke(cr); + } else if (params.drawTextRectangle) { + cr.rectangle(this.x, this.y, + this.textWidth, - this.height); + // Only draw the rectangle to find the element, not to show it. + cr.setLineWidth(0); } } }, @@ -648,15 +665,15 @@ const TextElement = new Lang.Class({ _drawSvg: function(transAttribute) { let row = "\n "; - let points = this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100]); + 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 ? bgColor : this.color; let attributes = ''; - if (points.length == 2) { + if (this.points.length == 2) { attributes = `fill="${color}" ` + `stroke="transparent" ` + `stroke-opacity="0" ` + - `font-size="${Math.abs(points[1][1] - points[0][1])}"`; + `font-size="${height}"`; if (this.font.family) attributes += ` font-family="${this.font.family}"`; @@ -669,9 +686,8 @@ const TextElement = new Lang.Class({ if (this.font.variant && FontVariantNames[this.font.variant]) attributes += ` font-variant="${FontVariantNames[this.font.variant].toLowerCase()}"`; - // this.textWidth is computed during Cairo building. - row += `${this.text}`; + row += `${this.text}`; } return row; @@ -698,15 +714,10 @@ const TextElement = new Lang.Class({ } }, - // When rotating grouped lines, lineOffset is used to retrieve the rotation center of the first line. - _getLineOffset: function() { - return (this.lineIndex || 0) * Math.abs(this.points[1][1] - this.points[0][1]); - }, - _getOriginalCenter: function() { if (!this._originalCenter) { let points = this.points; - this._originalCenter = this.textWidth ? [points[1][0], Math.max(points[0][1], points[1][1]) - this._getLineOffset()] : + this._originalCenter = this.textWidth ? [points[1][0], Math.max(points[0][1], points[1][1]) - this.lineOffset] : points.length >= 3 ? getCentroid(points) : getNaiveCenter(points); } From 19355033fffe960790704b0ce9a04f612b7fe941 Mon Sep 17 00:00:00 2001 From: abakkk Date: Sat, 11 Jul 2020 20:41:04 +0200 Subject: [PATCH 17/31] regroup first menu items Like old GS system menu. --- data/icons/smooth-symbolic.svg | 6 ++++++ menu.js | 31 ++++++++++++++++++++++++------ stylesheet.css | 35 ++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 data/icons/smooth-symbolic.svg diff --git a/data/icons/smooth-symbolic.svg b/data/icons/smooth-symbolic.svg new file mode 100644 index 0000000..903a0f5 --- /dev/null +++ b/data/icons/smooth-symbolic.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/menu.js b/menu.js index 8adea81..1e63b86 100644 --- a/menu.js +++ b/menu.js @@ -44,6 +44,7 @@ const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; const GS_VERSION = Config.PACKAGE_VERSION; const ICON_DIR = Me.dir.get_child('data').get_child('icons'); +const SMOOTH_ICON_PATH = ICON_DIR.get_child('smooth-symbolic.svg').get_path(); const COLOR_ICON_PATH = ICON_DIR.get_child('color-symbolic.svg').get_path(); const FILL_ICON_PATH = ICON_DIR.get_child('fill-symbolic.svg').get_path(); const STROKE_ICON_PATH = ICON_DIR.get_child('stroke-symbolic.svg').get_path(); @@ -91,6 +92,7 @@ var DrawingMenu = new Lang.Class({ }; this.colorIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(COLOR_ICON_PATH) }); + this.smoothIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(SMOOTH_ICON_PATH) }); this.strokeIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(STROKE_ICON_PATH) }); this.fillIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(FILL_ICON_PATH) }); this.fillRuleNonzeroIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(FILLRULE_NONZERO_ICON_PATH) }); @@ -148,11 +150,13 @@ var DrawingMenu = new Lang.Class({ _redisplay: function() { this.menu.removeAll(); - this.menu.addAction(_("Undo"), this.area.undo.bind(this.area), 'edit-undo-symbolic'); - this.menu.addAction(_("Redo"), this.area.redo.bind(this.area), 'edit-redo-symbolic'); - this.menu.addAction(_("Erase"), this.area.deleteLastElement.bind(this.area), 'edit-clear-all-symbolic'); - this.menu.addAction(_("Smooth"), this.area.smoothLastElement.bind(this.area), 'format-text-strikethrough-symbolic'); - this._addSeparator(this.menu); + let groupItem = new PopupMenu.PopupBaseMenuItem({ reactive: false, can_focus: false, style_class: "draw-on-your-screen-menu-group-item" }); + groupItem.add_child(this._createActionButton(_("Undo"), this.area.undo.bind(this.area), 'edit-undo-symbolic')); + groupItem.add_child(this._createActionButton(_("Redo"), this.area.redo.bind(this.area), 'edit-redo-symbolic')); + groupItem.add_child(this._createActionButton(_("Erase"), this.area.deleteLastElement.bind(this.area), 'edit-clear-all-symbolic')); + groupItem.add_child(this._createActionButton(_("Smooth"), this.area.smoothLastElement.bind(this.area), this.smoothIcon)/*, { expand: true, x_fill: false }*/); + this.menu.addMenuItem(groupItem); + this._addSeparator(this.menu, true); this._addSubMenuItem(this.menu, 'document-edit-symbolic', Area.ToolNames, this.area, 'currentTool', this._updateSectionVisibility.bind(this)); this._addColorSubMenuItem(this.menu); @@ -203,6 +207,19 @@ var DrawingMenu = new Lang.Class({ this._updateSectionVisibility(); }, + // from system.js (GS 3.34-) + _createActionButton: function(accessibleName, callback, icon) { + let button = new St.Button({ reactive: true, + can_focus: true, + track_hover: true, + x_align: Clutter.ActorAlign.CENTER, + accessible_name: accessibleName, + style_class: 'system-menu-action' }); + button.child = new St.Icon(typeof icon == 'string' ? { icon_name: icon } : { gicon: icon }); + button.connect('clicked', callback); + return new St.Bin({ child: button, x_expand: true }); + }, + _updateSectionVisibility: function() { if (this.area.currentTool != Area.Tools.TEXT) { this.lineSection.actor.show(); @@ -461,10 +478,12 @@ var DrawingMenu = new Lang.Class({ this.saveDrawingSubMenu.addMenuItem(saveEntry.item); }, - _addSeparator: function(menu) { + _addSeparator: function(menu, thin) { if (this.hasSeparators) { let separatorItem = new PopupMenu.PopupSeparatorMenuItem(' '); getActor(separatorItem).add_style_class_name('draw-on-your-screen-menu-separator-item'); + if (thin) + getActor(separatorItem).add_style_class_name('draw-on-your-screen-menu-thin-separator-item'); menu.addMenuItem(separatorItem); } } diff --git a/stylesheet.css b/stylesheet.css index 4d7e94f..0419322 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -58,6 +58,7 @@ } .draw-on-your-screen-menu-separator-item { + margin-top: 0; padding-top: 0.14em; padding-bottom: 0.14em; } @@ -71,6 +72,40 @@ margin-bottom: 0.2em; /* default 6px */ } +.draw-on-your-screen-menu-separator-item .popup-separator-menu-item-separator { + background-color: transparent; +} + +.draw-on-your-screen-menu-thin-separator-item .popup-separator-menu-item-separator { + margin-top: 0; +} + +/* system-menu-action: from GS 3.34- */ +.draw-on-your-screen-menu .system-menu-action { + background-color: #353535; + color: #eeeeec; + border-radius: 32px; + padding: 12px; + border: none; +} + +.draw-on-your-screen-menu .system-menu-action:hover, +.draw-on-your-screen-menu .system-menu-action:focus { + background-color: #484848; + color: #eeeeec; + border: none; + padding: 12px; +} + +.draw-on-your-screen-menu .system-menu-action:active { + background-color: #1b6acb; + color: #ffffff; +} + +.draw-on-your-screen-menu .system-menu-action > StIcon { + icon-size: 16px; +} + .draw-on-your-screen-menu-slider-label { min-width: 3em; text-align: right; From 352f86018ba739d2d221b39c1171f652f045dd06 Mon Sep 17 00:00:00 2001 From: abakkk Date: Sat, 11 Jul 2020 22:42:48 +0200 Subject: [PATCH 18/31] center grid overlay --- area.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/area.js b/area.js index 0dea931..a6a2172 100644 --- a/area.js +++ b/area.js @@ -277,18 +277,22 @@ var DrawingArea = new Lang.Class({ cr.save(); Clutter.cairo_set_source_color(cr, this.gridColor); - let [gridX, gridY] = [this.gridGap, this.gridGap]; - while (gridX < this.monitor.width) { + let [gridX, gridY] = [0, 0]; + while (gridX < this.monitor.width / 2) { cr.setLineWidth((gridX / this.gridGap) % 5 ? this.gridInterlineWidth : this.gridLineWidth); - cr.moveTo(gridX, 0); - cr.lineTo(gridX, this.monitor.height); + 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.gridGap; cr.stroke(); } - while (gridY < this.monitor.height) { + while (gridY < this.monitor.height / 2) { cr.setLineWidth((gridY / this.gridGap) % 5 ? this.gridInterlineWidth : this.gridLineWidth); - cr.moveTo(0, gridY); - cr.lineTo(this.monitor.width, gridY); + 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.gridGap; cr.stroke(); } From 7a41c6157c1eb3ef6e988ab6d530c31c136082bc Mon Sep 17 00:00:00 2001 From: abakkk Date: Mon, 13 Jul 2020 12:53:01 +0200 Subject: [PATCH 19/31] improve menu grouped action items * Fix style: use 'popup-menu' and 'popup-menu-item' style classes to provide theme colors * Synchonize button sensitivity --- menu.js | 43 ++++++++++++++++++++++++++++++------------- stylesheet.css | 13 +++---------- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/menu.js b/menu.js index 1e63b86..0ecaa16 100644 --- a/menu.js +++ b/menu.js @@ -150,16 +150,17 @@ var DrawingMenu = new Lang.Class({ _redisplay: function() { this.menu.removeAll(); + this.actionButtons = []; let groupItem = new PopupMenu.PopupBaseMenuItem({ reactive: false, can_focus: false, style_class: "draw-on-your-screen-menu-group-item" }); groupItem.add_child(this._createActionButton(_("Undo"), this.area.undo.bind(this.area), 'edit-undo-symbolic')); groupItem.add_child(this._createActionButton(_("Redo"), this.area.redo.bind(this.area), 'edit-redo-symbolic')); groupItem.add_child(this._createActionButton(_("Erase"), this.area.deleteLastElement.bind(this.area), 'edit-clear-all-symbolic')); - groupItem.add_child(this._createActionButton(_("Smooth"), this.area.smoothLastElement.bind(this.area), this.smoothIcon)/*, { expand: true, x_fill: false }*/); + groupItem.add_child(this._createActionButton(_("Smooth"), this.area.smoothLastElement.bind(this.area), this.smoothIcon)); this.menu.addMenuItem(groupItem); this._addSeparator(this.menu, true); this._addSubMenuItem(this.menu, 'document-edit-symbolic', Area.ToolNames, this.area, 'currentTool', this._updateSectionVisibility.bind(this)); - this._addColorSubMenuItem(this.menu); + this.colorItem = this._addColorSubMenuItem(this.menu); this.fillItem = this._addSwitchItem(this.menu, _("Fill"), this.strokeIcon, this.fillIcon, this.area, 'fill', this._updateSectionVisibility.bind(this)); this.fillSection = new PopupMenu.PopupMenuSection(); this.fillSection.itemActivated = () => {}; @@ -204,33 +205,48 @@ var DrawingMenu = new Lang.Class({ this.menu.addAction(_("Edit style"), manager.openUserStyleFile.bind(manager), 'document-page-setup-symbolic'); this.menu.addAction(_("Show help"), () => { this.close(); this.area.toggleHelp(); }, 'preferences-desktop-keyboard-shortcuts-symbolic'); + this._updateActionSensitivity(); this._updateSectionVisibility(); }, // from system.js (GS 3.34-) _createActionButton: function(accessibleName, callback, icon) { - let button = new St.Button({ reactive: true, - can_focus: true, - track_hover: true, + let button = new St.Button({ track_hover: true, x_align: Clutter.ActorAlign.CENTER, accessible_name: accessibleName, - style_class: 'system-menu-action' }); + // use 'popup-menu' and 'popup-menu-item' style classes to provide theme colors + style_class: 'system-menu-action popup-menu-item popup-menu' }); button.child = new St.Icon(typeof icon == 'string' ? { icon_name: icon } : { gicon: icon }); - button.connect('clicked', callback); + button.connect('clicked', () => { + callback(); + this._updateActionSensitivity(); + }); + button.bind_property('reactive', button, 'can_focus', GObject.BindingFlags.DEFAULT); + this.actionButtons.push(button); return new St.Bin({ child: button, x_expand: true }); }, + _updateActionSensitivity: function() { + let [undoButton, redoButton, eraseButton, smoothButton] = this.actionButtons; + undoButton.reactive = this.area.elements.length > 0; + redoButton.reactive = this.area.undoneElements.length > 0; + eraseButton.reactive = this.area.elements.length > 0; + smoothButton.reactive = this.area.elements.length > 0 && this.area.elements[this.area.elements.length - 1].shape == Area.Tools.NONE; + }, + _updateSectionVisibility: function() { - if (this.area.currentTool != Area.Tools.TEXT) { - this.lineSection.actor.show(); - this.fontSection.actor.hide(); - this.fillItem.setSensitive(true); - this.fillSection.setSensitive(true); - } else { + if (this.area.currentTool == Area.Tools.TEXT) { this.lineSection.actor.hide(); this.fontSection.actor.show(); + this.colorItem.setSensitive(true); this.fillItem.setSensitive(false); this.fillSection.setSensitive(false); + } else { + this.lineSection.actor.show(); + this.fontSection.actor.hide(); + this.colorItem.setSensitive(true); + this.fillItem.setSensitive(true); + this.fillSection.setSensitive(true); } if (this.area.fill) @@ -371,6 +387,7 @@ var DrawingMenu = new Lang.Class({ return GLib.SOURCE_REMOVE; }); menu.addMenuItem(item); + return item; }, _addDrawingNameItem: function(menu) { diff --git a/stylesheet.css b/stylesheet.css index 0419322..8da1267 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -82,26 +82,19 @@ /* system-menu-action: from GS 3.34- */ .draw-on-your-screen-menu .system-menu-action { - background-color: #353535; - color: #eeeeec; + min-width: 0; + border: none; border-radius: 32px; padding: 12px; - border: none; + margin: 0; } .draw-on-your-screen-menu .system-menu-action:hover, .draw-on-your-screen-menu .system-menu-action:focus { - background-color: #484848; - color: #eeeeec; border: none; padding: 12px; } -.draw-on-your-screen-menu .system-menu-action:active { - background-color: #1b6acb; - color: #ffffff; -} - .draw-on-your-screen-menu .system-menu-action > StIcon { icon-size: 16px; } From 1172e9da6fa70c9d8aaa4f9a3614c0cf4ea31769 Mon Sep 17 00:00:00 2001 From: abakkk Date: Thu, 30 Jul 2020 11:13:23 +0200 Subject: [PATCH 20/31] new image shape Images files are picked from `~/DrawOnYourScreen/images/` (and `extensiondir/images/`) svg, png, jpeg, ...(pixbuf) --- README.md | 6 +- area.js | 56 +++++++- data/images/Example.svg | 43 ++++++ elements.js | 96 ++++++++++++- extension.js | 4 + files.js | 132 ++++++++++++++++++ locale/draw-on-your-screen.pot | 9 ++ menu.js | 42 +++--- prefs.js | 2 + schemas/gschemas.compiled | Bin 4280 -> 4428 bytes ...extensions.draw-on-your-screen.gschema.xml | 12 +- 11 files changed, 372 insertions(+), 30 deletions(-) create mode 100644 data/images/Example.svg create mode 100644 files.js diff --git a/README.md b/README.md index f086e4c..83a00b0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Then save your beautiful work by taking a screenshot. ## Features -* Basic shapes (rectangle, circle, ellipse, line, curve, text, free) +* Basic shapes (rectangle, circle, ellipse, line, curve, text, image, free) * Basic transformations (move, rotate, resize, stretch, mirror, inverse) * Smooth stroke * Draw over applications @@ -40,6 +40,10 @@ Then save your beautiful work by taking a screenshot. ![How to duplicate an element](https://framagit.org/abakkk/DrawOnYourScreen/-/raw/ressources/duplicate.webm) +* Insertable images: + + Add your images (jpeg, png, svg) to `~/.local/share/drawOnYourScreen/images/`. + * Screenshot Tool extension: [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`). diff --git a/area.js b/area.js index a6a2172..0a65586 100644 --- a/area.js +++ b/area.js @@ -30,6 +30,7 @@ const Gtk = imports.gi.Gtk; const Lang = imports.lang; const Pango = imports.gi.Pango; const St = imports.gi.St; +const System = imports.system; const ExtensionUtils = imports.misc.extensionUtils; const Main = imports.ui.main; @@ -39,6 +40,7 @@ const Me = ExtensionUtils.getCurrentExtension(); const Convenience = ExtensionUtils.getSettings ? ExtensionUtils : Me.imports.convenience; const Extension = Me.imports.extension; const Elements = Me.imports.elements; +const Files = Me.imports.files; const Menu = Me.imports.menu; const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; @@ -46,10 +48,13 @@ const CAIRO_DEBUG_EXTENDS = false; const SVG_DEBUG_EXTENDS = false; const TEXT_CURSOR_TIME = 600; // ms -const { Shapes, Manipulations, Transformations, LineCapNames, LineJoinNames, FillRuleNames, +const { Shapes, ShapeNames, Transformations, LineCapNames, LineJoinNames, FillRuleNames, FontWeightNames, FontStyleNames, FontStretchNames, FontVariantNames } = Elements; +const Manipulations = { MOVE: 100, RESIZE: 101, MIRROR: 102 }; +const ManipulationNames = { 100: "Move", 101: "Resize", 102: "Mirror" }; var Tools = Object.assign({}, Shapes, Manipulations); -var ToolNames = { 0: "Free drawing", 1: "Line", 2: "Ellipse", 3: "Rectangle", 4: "Text", 5: "Polygon", 6: "Polyline", 100: "Move", 101: "Resize", 102: "Mirror" }; +var ToolNames = Object.assign({}, ShapeNames, ManipulationNames); + var FontGenericNames = { 0: 'Theme', 1: 'Sans-Serif', 2: 'Serif', 3: 'Monospace', 4: 'Cursive', 5: 'Fantasy' }; var getDateString = function() { @@ -96,6 +101,7 @@ var DrawingArea = new Lang.Class({ Name: 'DrawOnYourScreenDrawingArea', Extends: St.DrawingArea, Signals: { 'show-osd': { param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_DOUBLE, GObject.TYPE_BOOLEAN] }, + 'show-osd-gicon': { param_types: [Gio.Icon.$gtype, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_DOUBLE, GObject.TYPE_BOOLEAN] }, 'update-action-mode': {}, 'leave-drawing-mode': {} }, @@ -113,6 +119,7 @@ var DrawingArea = new Lang.Class({ this.undoneElements = []; this.currentElement = null; this.currentTool = Shapes.NONE; + this.currentImage = 0; this.currentFontGeneric = 0; this.isSquareArea = false; this.hasGrid = false; @@ -169,6 +176,13 @@ var DrawingArea = new Lang.Class({ this.currentFillRule = evenodd ? Cairo.FillRule.EVEN_ODD : Cairo.FillRule.WINDING; }, + getImages() { + let images = Files.getImages(); + if (!images[this.currentImage]) + this.currentImage = Math.max(images.length - 1, 0); + return images; + }, + vfunc_repaint: function() { let cr = this.get_context(); @@ -179,6 +193,8 @@ var DrawingArea = new Lang.Class({ } cr.$dispose(); + if (this.elements.some(element => element.shape == Shapes.IMAGE) || this.currentElement && this.currentElement.shape == Shapes.IMAGE) + System.gc(); }, _redisplay: function() { @@ -266,7 +282,7 @@ var DrawingArea = new Lang.Class({ if (this.currentElement) { cr.save(); this.currentElement.buildCairo(cr, { showTextCursor: this.textHasCursor, - showTextRectangle: this.currentElement.shape == Shapes.TEXT && !this.isWriting, + showTextRectangle: this.currentElement.shape != Shapes.TEXT || !this.isWriting, dummyStroke: this.currentElement.fill && this.currentElement.line.lineWidth == 0 }); cr.stroke(); @@ -477,6 +493,8 @@ var DrawingArea = new Lang.Class({ if (duplicate) { // deep cloning let copy = new this.grabbedElement.constructor(JSON.parse(JSON.stringify(this.grabbedElement))); + if (this.grabbedElement.image) + copy.image = this.grabbedElement.image; this.elements.push(copy); this.grabbedElement = copy; } @@ -574,6 +592,18 @@ var DrawingArea = new Lang.Class({ textRightAligned: this.currentTextRightAligned, points: [] }); + } else if (this.currentTool == Shapes.IMAGE) { + let images = this.getImages(); + if (!images.length) + return; + this.currentElement = new Elements.DrawingElement({ + shape: this.currentTool, + color: this.currentColor.to_string(), + eraser: eraser, + image: images[this.currentImage], + operator: this.currentOperator, + points: [] + }); } else { this.currentElement = new Elements.DrawingElement({ shape: this.currentTool, @@ -939,6 +969,15 @@ var DrawingArea = new Lang.Class({ this.emit('show-osd', null, this.currentTextRightAligned ? _("Right aligned") : _("Left aligned"), "", -1, false); }, + toggleImageFile: function() { + let images = this.getImages(); + if (!images.length) + return; + if (images.length > 1) + this.currentImage = this.currentImage == images.length - 1 ? 0 : this.currentImage + 1; + this.emit('show-osd-gicon', images[this.currentImage].gicon, images[this.currentImage].toString(), "", -1, false); + }, + toggleHelp: function() { if (this.helper.visible) { this.helper.hideHelp(); @@ -1034,7 +1073,10 @@ var DrawingArea = new Lang.Class({ this._stopDrawing(); } - let content = ``; + 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 backgroundColorString = this.hasBackground ? this.activeBackgroundColor.to_string() : 'transparent'; @@ -1156,7 +1198,11 @@ var DrawingArea = new Lang.Class({ return; if (contents instanceof Uint8Array) contents = ByteArray.toString(contents); - this.elements.push(...JSON.parse(contents).map(object => new Elements.DrawingElement(object))); + this.elements.push(...JSON.parse(contents).map(object => { + if (object.image) + object.image = new Files.Image(object.image); + return new Elements.DrawingElement(object); + })); if (notify) this.emit('show-osd', 'document-open-symbolic', name, "", -1, false); diff --git a/data/images/Example.svg b/data/images/Example.svg new file mode 100644 index 0000000..c2fb36c --- /dev/null +++ b/data/images/Example.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/elements.js b/elements.js index 910e897..4615e93 100644 --- a/elements.js +++ b/elements.js @@ -34,8 +34,8 @@ const reverseEnumeration = function(obj) { return reversed; }; -var Shapes = { NONE: 0, LINE: 1, ELLIPSE: 2, RECTANGLE: 3, TEXT: 4, POLYGON: 5, POLYLINE: 6 }; -var Manipulations = { MOVE: 100, RESIZE: 101, MIRROR: 102 }; +var Shapes = { NONE: 0, LINE: 1, ELLIPSE: 2, RECTANGLE: 3, TEXT: 4, POLYGON: 5, POLYLINE: 6, IMAGE: 7 }; +var ShapeNames = { 0: "Free drawing", 1: "Line", 2: "Ellipse", 3: "Rectangle", 4: "Text", 5: "Polygon", 6: "Polyline", 7: "Image" }; var Transformations = { TRANSLATION: 0, ROTATION: 1, SCALE_PRESERVE: 2, STRETCH: 3, REFLECTION: 4, INVERSION: 5 }; var LineCapNames = Object.assign(reverseEnumeration(Cairo.LineCap), { 2: 'Square' }); var LineJoinNames = reverseEnumeration(Cairo.LineJoin); @@ -46,7 +46,6 @@ var FontStyleNames = reverseEnumeration(Pango.Style); var FontStretchNames = reverseEnumeration(Pango.Stretch); var FontVariantNames = reverseEnumeration(Pango.Variant); - const SVG_DEBUG_SUPERPOSES_CAIRO = false; const RADIAN = 180 / Math.PI; // degree const INVERSION_CIRCLE_RADIUS = 12; // px @@ -58,7 +57,9 @@ const MIN_ROTATION_ANGLE = Math.PI / 1000; // rad const MIN_DRAWING_SIZE = 3; // px var DrawingElement = function(params) { - return params.shape == Shapes.TEXT ? new TextElement(params) : new _DrawingElement(params); + return params.shape == Shapes.TEXT ? new TextElement(params) : + params.shape == Shapes.IMAGE ? new ImageElement(params) : + new _DrawingElement(params); }; // DrawingElement represents a "brushstroke". @@ -109,9 +110,11 @@ const _DrawingElement = new Lang.Class({ }, buildCairo: function(cr, params) { - let [success, color] = Clutter.Color.from_string(this.color); - if (success) - Clutter.cairo_set_source_color(cr, color); + if (this.color) { + let [success, color] = Clutter.Color.from_string(this.color); + if (success) + Clutter.cairo_set_source_color(cr, color); + } if (this.showSymmetryElement) { let transformation = this.lastTransformation; @@ -726,6 +729,85 @@ const TextElement = new Lang.Class({ }, }); +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, + fill: this.fill, + eraser: this.eraser, + transformations: this.transformations, + image: this.image.toJson(), + preserveAspectRatio: this.preserveAspectRatio, + points: this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100]) + }; + }, + + _drawCairo: function(cr, params) { + if (this.points.length < 2) + return; + + let points = this.points; + let [x, y] = [Math.min(points[0][0], points[1][0]), Math.min(points[0][1], points[1][1])]; + let [width, height] = [Math.abs(points[1][0] - points[0][0]), Math.abs(points[1][1] - points[0][1])]; + + if (width < 1 || height < 1) + return; + + cr.save(); + this.image.setCairoSource(cr, x, y, width, height, this.preserveAspectRatio); + cr.rectangle(x, y, width, height); + cr.fill(); + cr.restore(); + + if (params.showTextRectangle) { + cr.rectangle(x, y, width, height); + setDummyStroke(cr); + } else if (params.drawTextRectangle) { + cr.rectangle(x, y, width, height); + // Only draw the rectangle to find the element, not to show it. + cr.setLineWidth(0); + } + }, + + getContainsPoint: function(cr, x, y) { + return cr.inFill(x, y); + }, + + _drawSvg: function(transAttribute) { + let points = this.points; + let row = "\n "; + let attributes = ''; + + if (points.length == 2) { + attributes = `fill="none"`; + row += ``; + } + + return row; + }, + + updateDrawing: function(x, y, transform) { + let points = this.points; + if (x == points[0][0] || y == points[0][1]) + return; + + points[1] = [x, y]; + this.preserveAspectRatio = !transform; + } +}); + const setDummyStroke = function(cr) { cr.setLineWidth(2); cr.setLineCap(0); diff --git a/extension.js b/extension.js index f4cde62..42011a6 100644 --- a/extension.js +++ b/extension.js @@ -171,6 +171,7 @@ var AreaManager = new Lang.Class({ area.leaveDrawingHandler = area.connect('leave-drawing-mode', this.toggleDrawing.bind(this)); area.updateActionModeHandler = area.connect('update-action-mode', this.updateActionMode.bind(this)); area.showOsdHandler = area.connect('show-osd', this.showOsd.bind(this)); + area.showOsdGiconHandler = area.connect('show-osd-gicon', this.showOsd.bind(this)); this.areas.push(area); } }, @@ -191,11 +192,13 @@ var AreaManager = new Lang.Class({ 'toggle-fill-rule': this.activeArea.toggleFillRule.bind(this.activeArea), 'toggle-dash' : this.activeArea.toggleDash.bind(this.activeArea), 'toggle-fill' : this.activeArea.toggleFill.bind(this.activeArea), + 'toggle-image-file' : this.activeArea.toggleImageFile.bind(this.activeArea), 'select-none-shape': () => this.activeArea.selectTool(Area.Tools.NONE), 'select-line-shape': () => this.activeArea.selectTool(Area.Tools.LINE), 'select-ellipse-shape': () => this.activeArea.selectTool(Area.Tools.ELLIPSE), 'select-rectangle-shape': () => this.activeArea.selectTool(Area.Tools.RECTANGLE), 'select-text-shape': () => this.activeArea.selectTool(Area.Tools.TEXT), + 'select-image-shape': () => this.activeArea.selectTool(Area.Tools.IMAGE), 'select-polygon-shape': () => this.activeArea.selectTool(Area.Tools.POLYGON), 'select-polyline-shape': () => this.activeArea.selectTool(Area.Tools.POLYLINE), 'select-move-tool': () => this.activeArea.selectTool(Area.Tools.MOVE), @@ -505,6 +508,7 @@ var AreaManager = new Lang.Class({ area.disconnect(area.leaveDrawingHandler); area.disconnect(area.updateActionModeHandler); area.disconnect(area.showOsdHandler); + area.disconnect(area.showOsdGiconHandler); let container = area.get_parent(); container.get_parent().remove_actor(container); container.destroy(); diff --git a/files.js b/files.js new file mode 100644 index 0000000..ec55aa7 --- /dev/null +++ b/files.js @@ -0,0 +1,132 @@ +/* jslint esversion: 6 */ + +/* + * 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 Gdk = imports.gi.Gdk; +const GdkPixbuf = imports.gi.GdkPixbuf; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Lang = imports.lang; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const EXAMPLE_IMAGES = Me.dir.get_child('data').get_child('images'); +const USER_IMAGES = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir'], 'images'])); + +var Image = new Lang.Class({ + Name: 'DrawOnYourScreenImage', + + _init: function(params) { + for (let key in params) + this[key] = params[key]; + }, + + toString: function() { + return this.displayName; + }, + + toJson: function() { + return { + displayName: this.displayName, + contentType: this.contentType, + _base64: this.base64, + _hash: this.hash + }; + }, + + // only called from menu so file exists + get gicon() { + if (!this._gicon) + this._gicon = new Gio.FileIcon({ file: this.file }); + return this._gicon; + }, + + get bytes() { + if (!this._bytes) { + if (this.file) + this._bytes = this.file.load_bytes(null)[0]; + else + this._bytes = new GLib.Bytes(GLib.base64_decode(this._base64)); + } + return this._bytes; + }, + + get base64() { + if (!this._base64) + this._base64 = GLib.base64_encode(this.bytes.get_data()); + return this._base64; + }, + + get hash() { + if (!this._hash) + this._hash = this.bytes.hash(); + return this._hash; + }, + + get pixbuf() { + if (!this._pixbuf) { + let stream = Gio.MemoryInputStream.new_from_bytes(this.bytes); + this._pixbuf = GdkPixbuf.Pixbuf.new_from_stream(stream, null); + stream.close(null); + } + return this._pixbuf; + }, + + getPixbufAtScale: function(width, height) { + let stream = Gio.MemoryInputStream.new_from_bytes(this.bytes); + 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); + Gdk.cairo_set_source_pixbuf(cr, pixbuf, x, y); + } +}); + +var getImages = function() { + let images = []; + + [EXAMPLE_IMAGES, USER_IMAGES].forEach(directory => { + let enumerator; + try { + enumerator = directory.enumerate_children('standard::display-name,standard::content-type', Gio.FileQueryInfoFlags.NONE, null); + } catch(e) { + return; + } + + let fileInfo = enumerator.next_file(null); + while (fileInfo) { + if (fileInfo.get_content_type().indexOf('image') == 0) + images.push(new Image({ file: enumerator.get_child(fileInfo), contentType: fileInfo.get_content_type(), displayName: fileInfo.get_display_name() })); + fileInfo = enumerator.next_file(null); + } + enumerator.close(null); + }); + + images.sort((a, b) => { + return b.displayName < a.displayName; + }); + + return images; +}; diff --git a/locale/draw-on-your-screen.pot b/locale/draw-on-your-screen.pot index bb0a1dc..7ff6129 100644 --- a/locale/draw-on-your-screen.pot +++ b/locale/draw-on-your-screen.pot @@ -179,6 +179,9 @@ msgstr "" msgid "Select polyline" msgstr "" +msgid "Select image" +msgstr "" + msgid "Select text" msgstr "" @@ -227,6 +230,9 @@ msgstr "" msgid "Toggle text alignment" msgstr "" +msgid "Change image file" +msgstr "" + msgid "Hide panel and dock" msgstr "" @@ -316,6 +322,9 @@ msgstr "" msgid "Polyline" msgstr "" +msgid "Image" +msgstr "" + msgid "Move" msgstr "" diff --git a/menu.js b/menu.js index 0ecaa16..a8ccba0 100644 --- a/menu.js +++ b/menu.js @@ -190,6 +190,18 @@ var DrawingMenu = new Lang.Class({ fontSection.itemActivated = () => {}; this.fontSection = fontSection; + let imageSection = new PopupMenu.PopupMenuSection(); + let images = this.area.getImages(); + if (images.length) { + if (this.area.currentImage > images.length - 1) + this.area.currentImage = images.length - 1; + this._addSubMenuItem(imageSection, null, images, this.area, 'currentImage'); + } + this._addSeparator(imageSection); + this.menu.addMenuItem(imageSection); + imageSection.itemActivated = () => {}; + this.imageSection = imageSection; + let manager = Extension.manager; this._addSimpleSwitchItem(this.menu, _("Hide panel and dock"), manager.hiddenList ? true : false, manager.togglePanelAndDockOpacity.bind(manager)); this._addSimpleSwitchItem(this.menu, _("Add a drawing background"), this.area.hasBackground, this.area.toggleBackground.bind(this.area)); @@ -235,19 +247,13 @@ var DrawingMenu = new Lang.Class({ }, _updateSectionVisibility: function() { - if (this.area.currentTool == Area.Tools.TEXT) { - this.lineSection.actor.hide(); - this.fontSection.actor.show(); - this.colorItem.setSensitive(true); - this.fillItem.setSensitive(false); - this.fillSection.setSensitive(false); - } else { - this.lineSection.actor.show(); - this.fontSection.actor.hide(); - this.colorItem.setSensitive(true); - this.fillItem.setSensitive(true); - this.fillSection.setSensitive(true); - } + let [isText, isImage] = [this.area.currentTool == Area.Tools.TEXT, this.area.currentTool == Area.Tools.IMAGE]; + this.lineSection.actor.visible = !isText && !isImage; + this.fontSection.actor.visible = isText; + this.imageSection.actor.visible = isImage; + this.colorItem.setSensitive(!isImage); + this.fillItem.setSensitive(!isText && !isImage); + this.fillSection.setSensitive(!isText && !isImage); if (this.area.fill) this.fillSection.actor.show(); @@ -320,7 +326,9 @@ var DrawingMenu = new Lang.Class({ }, _addSubMenuItem: function(menu, icon, obj, target, targetProperty, callback) { - let item = new PopupMenu.PopupSubMenuMenuItem(_(obj[target[targetProperty]]), icon ? true : false); + if (targetProperty == 'currentImage') + icon = obj[target[targetProperty]].gicon; + let item = new PopupMenu.PopupSubMenuMenuItem(_(String(obj[target[targetProperty]])), icon ? true : false); if (icon && icon instanceof GObject.Object && GObject.type_is_a(icon, Gio.Icon)) item.icon.set_gicon(icon); else if (icon) @@ -340,12 +348,14 @@ var DrawingMenu = new Lang.Class({ else if (targetProperty == 'currentFontStyle') text = `${_(obj[i])}`; else - text = _(obj[i]); + text = _(String(obj[i])); let iCaptured = Number(i); let subItem = item.menu.addAction(text, () => { - item.label.set_text(_(obj[iCaptured])); + item.label.set_text(_(String(obj[iCaptured]))); target[targetProperty] = iCaptured; + if (targetProperty == 'currentImage') + item.icon.set_gicon(obj[iCaptured].gicon); if (callback) callback(); }); diff --git a/prefs.js b/prefs.js index 9e7df59..dc1f8c0 100644 --- a/prefs.js +++ b/prefs.js @@ -54,6 +54,7 @@ var INTERNAL_KEYBINDINGS = { 'select-polygon-shape': "Select polygon", 'select-polyline-shape': "Select polyline", 'select-text-shape': "Select text", + 'select-image-shape': "Select image", 'select-move-tool': "Select move", 'select-resize-tool': "Select resize", 'select-mirror-tool': "Select mirror", @@ -73,6 +74,7 @@ var INTERNAL_KEYBINDINGS = { 'toggle-font-weight': "Change font weight", 'toggle-font-style': "Change font style", 'toggle-text-alignment': "Toggle text alignment", + 'toggle-image-file': "Change image file", '-separator-5': '', 'toggle-panel-and-dock-visibility': "Hide panel and dock", 'toggle-background': "Add a drawing background", diff --git a/schemas/gschemas.compiled b/schemas/gschemas.compiled index 4ce499444b20d3b7b8ae1ad8e742897db4aa73c6..61bc640a50075f4b049de2caf6c187d19fc0ae8a 100644 GIT binary patch literal 4428 zcmZu!Yitx%7#$P@L1B@H6si_1vQ=l>QVNA&MMWqG3D{_=QDM4!yPbA+?l?2s7mxT# z#8=W76A>kXMj(cSCO!g0BmU5+iAjGL#rUTtn8*(a8Vn}J$2oWJ&dioha`v44=9}-n zzkBb-hb`Z>+|ba^0&qu3r?1ksN#Mz2L(OEq%NT9OKJc&C7{-)&hVdVsw%ITkTMY6R zK>GPi-jxNB3A%;jWW+!y+`yJ@kjeR0zbRdFP!@eN$ofLK{MJaXQ565M{Hp;Jqc9Cj zT&pn?JPTmaHvn@1RL8gxU^}W3SO|R+a5Hd=#$qtB1h`eB7Q7U=4M+oZz#V{@KnA*u zdgumVxkeM1Xa-gSt2A1`Y+o4*TL+u@9s6*u(PtP_(GK9^_b+Xyoq8thHQ+|z;E5%Y zcIp|huLC~_^fYW#aj5CP6WkAU?Em;(`cuz_eINJ@p!#yPPdoK>upa}T0uJr`xQljb z`j3Kt29AI8$^zP{SUtVIKjH0>;80{-K?k?HU7L0v0sfbsz22j57_Bp$fQ9$DwANG`I~| zukF;dcYp_gaM!o%8Hbwf9R?o;?l0E|TGaPrDxL8F>Ei?PqDHu7Q0Wcst;1-*GqX)GXHnzXD9^JKIh> zHRt~!@Tb66;Y%OTPR(|Wg3kj_eD=9RJGEM;NIJ8DMOAO_rk#2+{1<@hfQ65i)+sgn ztp&UZNV&TQ=ugdZJHU?vb?57+(N4{N@WA_kSwm~oI-+KM4uRhTQe*zV^rvP&oCJRX z6z_WI1nty}a~6Ca`0khXA=;@qE=-uSfpXraX5O9)z63PAF!NLTQ>*orLjM6LwjNaN zrDl6;z%4-U-%ZsG@TaE#Hn0Uu`Tf#6v{Q3l4SF8oL->BJd zUx9xFj-LJ})c}8L`dNq3aS?=pDiQ08p1=|^n>(N!&&a8B# z-fIQ7HySgmlEbIVg#SHG*t)j8=n20yhn*zR`32kerEi8(I*GX13ChLh9*qym zT^|2iUM^P;sH!IS|D=CY{I99(ucuk3mF>;@vgqd6xp<=QIwz>dio|==V@2$}q8v9_ z51v7yc<^tI|23EWg>MDIjJm6$zeHt!skZSE$22n73@5szFX%nMTZl^sgvK2S9P{ z#m>;TvY}C}BIpi7w#^F@)hvA()lYwCBg?bJ6O}i%~ELx79pSsGPH@^cVVpwj$%!FbuS~|&WZOb+y5rYtsOzT_M?qp+Ev0-FJ!(fm`@zaPA@7JL zz+?U#0M{OacSEKr-bs1SVC>xUkC8cTmwu3c;{UT z@NUaJ3EPLd)9S7$t;F&DW$S+MMdduu<29tmOD~N8tHsAT#TOQW8 z^KNOuMSeQ2RT)I&3T+k a%4?w_&Y+G{K8FQ;`HJ_t-hr++-~A7^(jc$^ literal 4280 zcmai1ZHN_B7@jRHzxQgXt8S&Hc{iK8yXyLdZflvE73{JYOH?|0@7a5gJ9lQ9nY&+_ zNCkbEAW^hbk`<&z;6^4YhW4Y7An-?&5s`vH1<3}1LXteOw8RBtw%ct{t1}d+}}hy_3f}<2T#Nx!~9Y+ zzf-_hfIVAluh5@5@CUD*plRp*eV@`!Jp=8uf+di;!p`Hf&dGOD`1ATv7pg;9g*oVO>OvHoAPEGrKa0{?e*{Nx71v|jUU+=GB zJJjs=CGc^ew0_$$+Nr0*ejfY-@Q`YUn(h1wo`A{U^TNE8kF z173XZ-i!37o)7yV_$aVh`BT&XB={n5Xvc{z`cpHWVQ|%CP1~aUsp+2rHvkVSJ2mYs z;2pqLWv8ay0UrR&w(a+`eQL%(1pWp%cx<6fJN0DPFM@vuo<949Njo+3#>GD!c=Jr# z5be~Q-!ynVaOka9-lm;81$!%a5YXm+{}1idoZlnhGr+lL_e|z~rXCObdGOyr-EZ|1 z=ugc&q^4s30CS%x$4SjN8^9ZY&a`NxKQ-532ly%AwHuc|qn(=m1@K~{-z2XO4MgS%;`9tV2?ycejxwrf!kCp6<31Rn)b*6u#qskyIDf(mp^tHRr1aTnDVY|D(0EQ`6o8-UeJf zo_UUTYObpScsFqP^gk)usTt=1@EgFnE;);KYTA#0PXWW4CH+y=~>^KOt&YUZ;8EP>i>Hs`e2TH5gH zY)4pn$rC6ue!nSDi9}fFqUUTZ2l}Ed))-6!*1e|P1!Mqhv!IUDgK@-aI>0)_`ow1z z0!sj%^YZ~5hf!U^(!<k{i1>kDgG1Hd}Q`ql_61^7dnG}fMbfF>X(P-Kh3@^w>MLhqG1zYvZ8k=E_)rB2^9 zvVNS*qO?k0e7UZ@AiMnLvUTR7wQ+JCSM*4`n-iw+ zg>D+2uR}5%`fm)(NG*LlTx5OSvCaNGUJ$&3;Rrh67scDJ<|~+63$GRBuA}=wzuO!n zJ<(#;Hf@(Firc}s8{?}+ZQ7hs4{&~>cHzG)zFIbxKj$UszdXKL9{Njb^jwULYeOy< zKdEx>n3#~%;$)0I2Os7r>epBKQMn98j>Qn&$2eBRS1ZD}##$rTW$vi_t9X*jrZyQ> z&|y>D0~`}qO<{j(A3KI6Ox>_@dd|*v>pjwwozj$ke-v+FjPYQzMfcJE%J^z!*uEOR zWn;mIIjYFBGe*1Ee93lK#aF9Fv>S|)?{i82)$!HpvHaJJ@Lv=AuL=FfS|9&24_q$g z^G+odFWgQP*QQ3CC5f$omvQNMQBop55$2<4+kRoxK8^jJDdFU;4w_W`bxG6I-IB?* z68T|X9xIu?bj<$Ph|Mot>2=2zwVa|GGncd^o94HIwDM@T;&`7H_+%WzFK|-vtH%dN zeel6iAAE4s#~&Q^V|;MnIQPQnZ5)h2ePBCL{8HsZdF5T1tGJ(6_f>@apveiZ+qWIM z;g{~L^Nb#$8=mg<BAK|TAG2~?^x0`kE4sf^ z-B)cCZA?|%eH?Ew?4R!vD0f}L6;@VwLGBZAX6<6pQ2Ro-26kA*G$q%sz*`2hDXpB$ z8oupT3=HE6=aVb5SIB%JQJQ;4k1K09alG9l#wA@B9YiCG#-FWhr;B}c8AXZhS}|W; zm3|(!zl%lf3Zv4`Qhr!robafhfiNDm7u1VFbl(f_m*>*O3xHwq6|cM#B0pF81-)w8 Ef6s{ZTL1t6 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 767f859..683fbcb 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 @@ -106,6 +106,11 @@ select text select text + + ["<Primary>i"] + select image + select image + ["<Primary>p"] unselect shape (free drawing) @@ -227,7 +232,7 @@ toggle font weight - ["<Primary>i"] + ["<Primary><Shift>w"] toggle font style toggle font style @@ -236,6 +241,11 @@ toggle text alignment toggle text alignment + + ["<Primary><Shift>i"] + toggle image file + toggle image file + ["<Primary>o"] open user stylesheet to edit style From e686f03082e8969f79bedcc86eb7ba5474514d87 Mon Sep 17 00:00:00 2001 From: abakkk Date: Sat, 1 Aug 2020 15:00:49 +0200 Subject: [PATCH 21/31] minor, remove Lang.bind --- prefs.js | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/prefs.js b/prefs.js index dc1f8c0..6939652 100644 --- a/prefs.js +++ b/prefs.js @@ -394,26 +394,24 @@ const KeybindingsWidget = new GObject.Class({ accel_mode: Gtk.CellRendererAccelMode.GTK, xalign: 1 }); - keybinding_renderer.connect('accel-edited', - Lang.bind(this, function(renderer, iter, key, mods) { - let value = Gtk.accelerator_name(key, mods); - let [success, iterator ] = - this._store.get_iter_from_string(iter); + keybinding_renderer.connect('accel-edited', (renderer, iter, key, mods) => { + let value = Gtk.accelerator_name(key, mods); + let [success, iterator ] = + this._store.get_iter_from_string(iter); - if(!success) { - printerr("Can't change keybinding"); - } + if(!success) { + printerr("Can't change keybinding"); + } - let name = this._store.get_value(iterator, 0); + let name = this._store.get_value(iterator, 0); - this._store.set( - iterator, - [this._columns.MODS, this._columns.KEY], - [mods, key] - ); - this._settings.set_strv(name, [value]); - }) - ); + this._store.set( + iterator, + [this._columns.MODS, this._columns.KEY], + [mods, key] + ); + this._settings.set_strv(name, [value]); + }); let keybinding_column = new Gtk.TreeViewColumn({ title: "", From 11b90ecc0aefbec3a6d2c262d3bb80578d688642 Mon Sep 17 00:00:00 2001 From: abakkk Date: Wed, 5 Aug 2020 02:00:20 +0200 Subject: [PATCH 22/31] put json file stuff in files.js In the same way than image files. --- area.js | 75 +++++++--------------------------------------- files.js | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ menu.js | 13 ++++---- 3 files changed, 109 insertions(+), 70 deletions(-) diff --git a/area.js b/area.js index 0a65586..0b3ad26 100644 --- a/area.js +++ b/area.js @@ -20,7 +20,6 @@ * along with this program. If not, see . */ -const ByteArray = imports.byteArray; const Cairo = imports.cairo; const Clutter = imports.gi.Clutter; const Gio = imports.gi.Gio; @@ -57,43 +56,6 @@ var ToolNames = Object.assign({}, ShapeNames, ManipulationNames); var FontGenericNames = { 0: 'Theme', 1: 'Sans-Serif', 2: 'Serif', 3: 'Monospace', 4: 'Cursive', 5: 'Fantasy' }; -var getDateString = function() { - let date = GLib.DateTime.new_now_local(); - return `${date.format("%F")} ${date.format("%X")}`; -}; - -var getJsonFiles = function() { - let directory = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir']])); - - let enumerator; - try { - enumerator = directory.enumerate_children('standard::name,standard::display-name,standard::content-type,time::modified', Gio.FileQueryInfoFlags.NONE, null); - } catch(e) { - return []; - } - - let jsonFiles = []; - let fileInfo = enumerator.next_file(null); - while (fileInfo) { - if (fileInfo.get_content_type().indexOf('json') != -1 && fileInfo.get_name() != `${Me.metadata['persistent-file-name']}.json`) { - let file = enumerator.get_child(fileInfo); - jsonFiles.push({ name: fileInfo.get_name().slice(0, -5), - displayName: fileInfo.get_display_name().slice(0, -5), - // fileInfo.get_modification_date_time: Gio 2.62+ - modificationUnixTime: fileInfo.get_attribute_uint64('time::modified'), - delete: () => file.delete(null) }); - } - fileInfo = enumerator.next_file(null); - } - enumerator.close(null); - - jsonFiles.sort((a, b) => { - return b.modificationUnixTime - a.modificationUnixTime; - }); - - return jsonFiles; -}; - // DrawingArea is the widget in which we draw, thanks to Cairo. // It creates and manages a DrawingElement for each "brushstroke". // It handles pointer/mouse/(touch?) events and some keyboard events. @@ -1092,7 +1054,7 @@ var DrawingArea = new Lang.Class({ } content += "\n"; - let filename = `${Me.metadata['svg-file-name']} ${getDateString()}.svg`; + let filename = `${Me.metadata['svg-file-name']} ${Files.getDateString()}.svg`; let dir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES); let path = GLib.build_filenamev([dir, filename]); if (GLib.file_test(path, GLib.FileTest.EXISTS)) @@ -1120,34 +1082,25 @@ var DrawingArea = new Lang.Class({ this._stopDrawing(); } - let dir = GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir']]); - if (!GLib.file_test(dir, GLib.FileTest.EXISTS)) - GLib.mkdir_with_parents(dir, 0o700); - let path = GLib.build_filenamev([dir, `${name}.json`]); - + let json = new Files.Json({ name }); let oldContents; if (name == Me.metadata['persistent-file-name']) { - if (GLib.file_test(path, GLib.FileTest.EXISTS)) { - oldContents = GLib.file_get_contents(path)[1]; - if (oldContents instanceof Uint8Array) - oldContents = ByteArray.toString(oldContents); - } - + let oldContents = json.contents; // do not create a file to write just an empty array if (!oldContents && this.elements.length == 0) return; } // do not use "content = JSON.stringify(this.elements, null, 2);", neither "content = JSON.stringify(this.elements);" - // because of compromise between disk usage and human readability + // do compromise between disk usage and human readability let contents = `[\n ` + new Array(...this.elements.map(element => JSON.stringify(element))).join(`,\n\n `) + `\n]`; if (name == Me.metadata['persistent-file-name'] && contents == oldContents) return; GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { - GLib.file_set_contents(path, contents); + json.contents = contents; if (notify) this.emit('show-osd', 'document-save-symbolic', name, "", -1, false); if (name != Me.metadata['persistent-file-name']) { @@ -1162,7 +1115,7 @@ var DrawingArea = new Lang.Class({ }, saveAsJson: function() { - this._saveAsJson(getDateString(), true); + this._saveAsJson(Files.getDateString(), true); }, savePersistent: function() { @@ -1188,16 +1141,10 @@ var DrawingArea = new Lang.Class({ this.elements = []; this.currentElement = null; - let dir = GLib.get_user_data_dir(); - let path = GLib.build_filenamev([dir, Me.metadata['data-dir'], `${name}.json`]); + let contents = (new Files.Json({ name })).contents; + if (!contents) + return; - if (!GLib.file_test(path, GLib.FileTest.EXISTS)) - return; - let [success, contents] = GLib.file_get_contents(path); - if (!success) - return; - if (contents instanceof Uint8Array) - contents = ByteArray.toString(contents); this.elements.push(...JSON.parse(contents).map(object => { if (object.image) object.image = new Files.Image(object.image); @@ -1222,7 +1169,7 @@ var DrawingArea = new Lang.Class({ }, loadNextJson: function() { - let names = getJsonFiles().map(file => file.name); + let names = Files.getJsons().map(json => json.name); if (!names.length) return; @@ -1232,7 +1179,7 @@ var DrawingArea = new Lang.Class({ }, loadPreviousJson: function() { - let names = getJsonFiles().map(file => file.name); + let names = Files.getJsons().map(json => json.name); if (!names.length) return; diff --git a/files.js b/files.js index ec55aa7..c7bef27 100644 --- a/files.js +++ b/files.js @@ -20,6 +20,7 @@ * along with this program. If not, see . */ +const ByteArray = imports.byteArray; const Gdk = imports.gi.Gdk; const GdkPixbuf = imports.gi.GdkPixbuf; const Gio = imports.gi.Gio; @@ -31,6 +32,7 @@ const Me = ExtensionUtils.getCurrentExtension(); const EXAMPLE_IMAGES = Me.dir.get_child('data').get_child('images'); const USER_IMAGES = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir'], 'images'])); +// wrapper around an image file var Image = new Lang.Class({ Name: 'DrawOnYourScreenImage', @@ -130,3 +132,92 @@ var getImages = function() { return images; }; + +// wrapper around a json file +var Json = new Lang.Class({ + Name: 'DrawOnYourScreenJson', + + _init: function(params) { + for (let key in params) + this[key] = params[key]; + }, + + toString: function() { + return this.displayName || this.name; + }, + + delete: function() { + this.file.delete(null); + }, + + get file() { + if (!this._file && this.name) + this._file = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir'], `${this.name}.json`])); + + return this._file || null; + }, + + set file(file) { + this._file = file; + }, + + get contents() { + let success_, contents; + try { + [success_, contents] = this.file.load_contents(null); + if (contents instanceof Uint8Array) + contents = ByteArray.toString(contents); + } catch(e) { + return null; + } + return contents; + }, + + set contents(contents) { + try { + this.file.replace_contents(contents, null, false, Gio.FileCreateFlags.NONE, null); + } catch(e) { + this.file.get_parent().make_directory_with_parents(null); + this.file.replace_contents(contents, null, false, Gio.FileCreateFlags.NONE, null); + } + } +}); + +var getJsons = function() { + let directory = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir']])); + + let enumerator; + try { + enumerator = directory.enumerate_children('standard::name,standard::display-name,standard::content-type,time::modified', Gio.FileQueryInfoFlags.NONE, null); + } catch(e) { + return []; + } + + let jsons = []; + let fileInfo = enumerator.next_file(null); + while (fileInfo) { + if (fileInfo.get_content_type().indexOf('json') != -1 && fileInfo.get_name() != `${Me.metadata['persistent-file-name']}.json`) { + let file = enumerator.get_child(fileInfo); + jsons.push(new Json({ + file, + name: fileInfo.get_name().slice(0, -5), + displayName: fileInfo.get_display_name().slice(0, -5), + // fileInfo.get_modification_date_time: Gio 2.62+ + modificationUnixTime: fileInfo.get_attribute_uint64('time::modified') + })); + } + fileInfo = enumerator.next_file(null); + } + enumerator.close(null); + + jsons.sort((a, b) => { + return b.modificationUnixTime - a.modificationUnixTime; + }); + + return jsons; +}; + +var getDateString = function() { + let date = GLib.DateTime.new_now_local(); + return `${date.format("%F")} ${date.format("%X")}`; +}; diff --git a/menu.js b/menu.js index a8ccba0..0a09f03 100644 --- a/menu.js +++ b/menu.js @@ -39,6 +39,7 @@ const Me = ExtensionUtils.getCurrentExtension(); const Area = Me.imports.area; const Elements = Me.imports.elements; const Extension = Me.imports.extension; +const Files = Me.imports.files; const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; const GS_VERSION = Config.PACKAGE_VERSION; @@ -438,10 +439,10 @@ var DrawingMenu = new Lang.Class({ _populateOpenDrawingSubMenu: function() { this.openDrawingSubMenu.removeAll(); - let jsonFiles = Area.getJsonFiles(); - jsonFiles.forEach(file => { - let item = this.openDrawingSubMenu.addAction(`${file.displayName}`, () => { - this.area.loadJson(file.name); + let jsons = Files.getJsons(); + jsons.forEach(json => { + let item = this.openDrawingSubMenu.addAction(`${String(json)}`, () => { + this.area.loadJson(json.name); this._updateDrawingNameMenuItem(); this._updateSaveDrawingSubMenuItemSensitivity(); }); @@ -460,7 +461,7 @@ var DrawingMenu = new Lang.Class({ getActor(item).add_child(deleteButton); deleteButton.connect('clicked', () => { - file.delete(); + json.delete(); item.destroy(); }); }); @@ -494,7 +495,7 @@ var DrawingMenu = new Lang.Class({ _populateSaveDrawingSubMenu: function() { this.saveDrawingSubMenu.removeAll(); - let saveEntry = new DrawingMenuEntry({ initialTextGetter: Area.getDateString, + let saveEntry = new DrawingMenuEntry({ initialTextGetter: Files.getDateString, entryActivateCallback: (text) => { this.area.saveAsJsonWithName(text); this.saveDrawingSubMenu.toggle(); From 408929848de3adfc0910c764756abb100a0de5d0 Mon Sep 17 00:00:00 2001 From: abakkk Date: Wed, 5 Aug 2020 02:13:56 +0200 Subject: [PATCH 23/31] minor, property name --- files.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/files.js b/files.js index c7bef27..9e301d3 100644 --- a/files.js +++ b/files.js @@ -49,8 +49,8 @@ var Image = new Lang.Class({ return { displayName: this.displayName, contentType: this.contentType, - _base64: this.base64, - _hash: this.hash + base64: this.base64, + hash: this.hash }; }, @@ -66,7 +66,7 @@ var Image = new Lang.Class({ if (this.file) this._bytes = this.file.load_bytes(null)[0]; else - this._bytes = new GLib.Bytes(GLib.base64_decode(this._base64)); + this._bytes = new GLib.Bytes(GLib.base64_decode(this.base64)); } return this._bytes; }, @@ -77,12 +77,21 @@ var Image = new Lang.Class({ return this._base64; }, + set base64(base64) { + this._base64 = base64; + }, + + // hash is not used get hash() { if (!this._hash) this._hash = this.bytes.hash(); return this._hash; }, + set hash(hash) { + this._hash = hash; + }, + get pixbuf() { if (!this._pixbuf) { let stream = Gio.MemoryInputStream.new_from_bytes(this.bytes); From 80e2955f8ce7922b18ca1f93b65ef392fedd04fb Mon Sep 17 00:00:00 2001 From: abakkk Date: Wed, 5 Aug 2020 02:49:52 +0200 Subject: [PATCH 24/31] fix menu item name and sensitivity stynchronization --- area.js | 8 +++++--- menu.js | 10 ++++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/area.js b/area.js index 0b3ad26..e8e220b 100644 --- a/area.js +++ b/area.js @@ -1074,7 +1074,7 @@ var DrawingArea = new Lang.Class({ } }, - _saveAsJson: function(name, notify) { + _saveAsJson: function(name, notify, callback) { // stop drawing or writing if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) { this._stopWriting(); @@ -1107,11 +1107,13 @@ var DrawingArea = new Lang.Class({ this.jsonName = name; this.lastJsonContents = contents; } + if (callback) + callback(); }); }, - saveAsJsonWithName: function(name) { - this._saveAsJson(name); + saveAsJsonWithName: function(name, callback) { + this._saveAsJson(name, false, callback); }, saveAsJson: function() { diff --git a/menu.js b/menu.js index 0a09f03..1c64232 100644 --- a/menu.js +++ b/menu.js @@ -420,6 +420,7 @@ var DrawingMenu = new Lang.Class({ _addOpenDrawingSubMenuItem: function(menu) { let item = new PopupMenu.PopupSubMenuMenuItem(_("Open drawing"), true); this.openDrawingSubMenuItem = item; + this.openDrawingSubMenuItem.setSensitive(Boolean(Files.getJsons().length)); this.openDrawingSubMenu = item.menu; item.icon.set_icon_name('document-open-symbolic'); @@ -463,6 +464,7 @@ var DrawingMenu = new Lang.Class({ deleteButton.connect('clicked', () => { json.delete(); item.destroy(); + this.openDrawingSubMenuItem.setSensitive(!this.openDrawingSubMenu.isEmpty()); }); }); @@ -493,13 +495,17 @@ var DrawingMenu = new Lang.Class({ this.saveDrawingSubMenuItem.setSensitive(this.area.elements.length > 0); }, + _onDrawingSaved() { + this._updateDrawingNameMenuItem(); + this.openDrawingSubMenuItem.setSensitive(true); + }, + _populateSaveDrawingSubMenu: function() { this.saveDrawingSubMenu.removeAll(); let saveEntry = new DrawingMenuEntry({ initialTextGetter: Files.getDateString, entryActivateCallback: (text) => { - this.area.saveAsJsonWithName(text); + this.area.saveAsJsonWithName(text, this._onDrawingSaved.bind(this)); this.saveDrawingSubMenu.toggle(); - this._updateDrawingNameMenuItem(); }, invalidStrings: [Me.metadata['persistent-file-name'], '/'], primaryIconName: 'insert-text' }); From c78f5e82da5d8fdc096f561bec46cd9dd21d9398 Mon Sep 17 00:00:00 2001 From: abakkk Date: Wed, 5 Aug 2020 23:30:25 +0200 Subject: [PATCH 25/31] Extend font family possibilities To fix in the future: memory usage of font-styled item labels --- area.js | 40 +++++++++++++++++++++++++++++----------- elements.js | 4 ++++ menu.js | 40 ++++++++++++++++++++++++++++++++-------- 3 files changed, 65 insertions(+), 19 deletions(-) diff --git a/area.js b/area.js index e8e220b..775bdeb 100644 --- a/area.js +++ b/area.js @@ -54,7 +54,7 @@ const ManipulationNames = { 100: "Move", 101: "Resize", 102: "Mirror" }; var Tools = Object.assign({}, Shapes, Manipulations); var ToolNames = Object.assign({}, ShapeNames, ManipulationNames); -var FontGenericNames = { 0: 'Theme', 1: 'Sans-Serif', 2: 'Serif', 3: 'Monospace', 4: 'Cursive', 5: 'Fantasy' }; +var FontGenericFamilies = ['Sans-Serif', 'Serif', 'Monospace', 'Cursive', 'Fantasy']; // DrawingArea is the widget in which we draw, thanks to Cairo. // It creates and manages a DrawingElement for each "brushstroke". @@ -82,7 +82,6 @@ var DrawingArea = new Lang.Class({ this.currentElement = null; this.currentTool = Shapes.NONE; this.currentImage = 0; - this.currentFontGeneric = 0; this.isSquareArea = false; this.hasGrid = false; this.hasBackground = false; @@ -145,6 +144,24 @@ var DrawingArea = new Lang.Class({ return images; }, + get currentFontFamily() { + return this._currentFontFamily || this.currentThemeFontFamily; + }, + + set currentFontFamily(fontFamily) { + this._currentFontFamily = fontFamily; + }, + + get fontFamilies() { + if (!this._fontFamilies) { + let pangoFontFamilies = Elements.getPangoFontFamilies().filter(family => { + return family != this.currentThemeFontFamily && FontGenericFamilies.indexOf(family) == -1; + }); + this._fontFamilies = [this.currentThemeFontFamily].concat(FontGenericFamilies, pangoFontFamilies); + } + return this._fontFamilies; + }, + vfunc_repaint: function() { let cr = this.get_context(); @@ -198,6 +215,7 @@ var DrawingArea = new Lang.Class({ this.colors[i] = this.colors[i].alpha ? this.colors[i] : this.colors[0]; } this.currentColor = this.currentColor || this.colors[1]; + this._fontFamilies = null; // SVG does not support 'Ultra-heavy' weight (1000) this.newThemeAttributes.FontWeight = Math.min(this.newThemeAttributes.FontWeight, 900); this.newThemeAttributes.LineWidth = (this.newThemeAttributes.LineWidth > 0) ? this.newThemeAttributes.LineWidth : 3; @@ -545,7 +563,7 @@ var DrawingArea = new Lang.Class({ color: this.currentColor.to_string(), eraser: eraser, font: { - family: (this.currentFontGeneric == 0 ? this.currentThemeFontFamily : FontGenericNames[this.currentFontGeneric]), + family: this.currentFontFamily, weight: this.currentFontWeight, style: this.currentFontStyle, stretch: this.currentFontStretch, @@ -913,13 +931,13 @@ var DrawingArea = new Lang.Class({ }, toggleFontFamily: function() { - this.currentFontGeneric = this.currentFontGeneric == 5 ? 0 : this.currentFontGeneric + 1; - let currentFontFamily = this.currentFontGeneric == 0 ? this.currentThemeFontFamily : FontGenericNames[this.currentFontGeneric]; + let index = Math.max(0, this.fontFamilies.indexOf(this.currentFontFamily)); + this.currentFontFamily = (index == this.fontFamilies.length - 1) ? 0 : this.fontFamilies[index + 1]; if (this.currentElement && this.currentElement.font) { - this.currentElement.font.family = currentFontFamily; + this.currentElement.font.family = this.currentFontFamily; this._redisplay(); } - this.emit('show-osd', null, `${_(currentFontFamily)}`, "", -1, false); + this.emit('show-osd', null, `${_(this.currentFontFamily)}`, "", -1, false); }, toggleTextAlignment: function() { @@ -978,7 +996,7 @@ var DrawingArea = new Lang.Class({ 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._onKeyboardPopupMenuHandler = this.connect('popup-menu', this._onKeyboardPopupMenu.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.activeBackgroundColor : null); this._updateStyle(); @@ -1001,9 +1019,9 @@ var DrawingArea = new Lang.Class({ this.disconnect(this.buttonPressedHandler); this.buttonPressedHandler = null; } - if (this._onKeyboardPopupMenuHandler) { - this.disconnect(this._onKeyboardPopupMenuHandler); - this._onKeyboardPopupMenuHandler = null; + if (this.keyboardPopupMenuHandler) { + this.disconnect(this.keyboardPopupMenuHandler); + this.keyboardPopupMenuHandler = null; } if (this.motionHandler) { this.disconnect(this.motionHandler); diff --git a/elements.js b/elements.js index 4615e93..c2214c7 100644 --- a/elements.js +++ b/elements.js @@ -46,6 +46,10 @@ var FontStyleNames = reverseEnumeration(Pango.Style); var FontStretchNames = reverseEnumeration(Pango.Stretch); var FontVariantNames = reverseEnumeration(Pango.Variant); +var getPangoFontFamilies = function() { + return PangoCairo.font_map_get_default().list_families().map(fontFamily => fontFamily.get_name()).sort((a,b) => a.localeCompare(b)); +}; + const SVG_DEBUG_SUPERPOSES_CAIRO = false; const RADIAN = 180 / Math.PI; // degree const INVERSION_CIRCLE_RADIUS = 12; // px diff --git a/menu.js b/menu.js index 1c64232..b70eca4 100644 --- a/menu.js +++ b/menu.js @@ -56,6 +56,9 @@ const FILLRULE_EVENODD_ICON_PATH = ICON_DIR.get_child('fillrule-evenodd-symbolic const DASHED_LINE_ICON_PATH = ICON_DIR.get_child('dashed-line-symbolic.svg').get_path(); const FULL_LINE_ICON_PATH = ICON_DIR.get_child('full-line-symbolic.svg').get_path(); +// 150 labels with font-family style take ~15Mo +const FONT_FAMILY_STYLE = true; + const getActor = function(object) { return GS_VERSION < '3.33.0' ? object.actor : object; }; @@ -180,9 +183,7 @@ var DrawingMenu = new Lang.Class({ this.lineSection = lineSection; let fontSection = new PopupMenu.PopupMenuSection(); - let FontGenericNamesCopy = Object.create(Area.FontGenericNames); - FontGenericNamesCopy[0] = this.area.currentThemeFontFamily; - this._addSubMenuItem(fontSection, 'font-x-generic-symbolic', FontGenericNamesCopy, this.area, 'currentFontGeneric'); + this._addFontFamilySubMenuItem(fontSection, 'font-x-generic-symbolic'); this._addSubMenuItem(fontSection, 'format-text-bold-symbolic', Elements.FontWeightNames, this.area, 'currentFontWeight'); this._addSubMenuItem(fontSection, 'format-text-italic-symbolic', Elements.FontStyleNames, this.area, 'currentFontStyle'); this._addSwitchItem(fontSection, _("Right aligned"), 'format-justify-left-symbolic', 'format-justify-right-symbolic', this.area, 'currentTextRightAligned'); @@ -342,9 +343,7 @@ var DrawingMenu = new Lang.Class({ GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { for (let i in obj) { let text; - if (targetProperty == 'currentFontGeneric') - text = `${_(obj[i])}`; - else if (targetProperty == 'currentFontWeight') + if (targetProperty == 'currentFontWeight') text = `${_(obj[i])}`; else if (targetProperty == 'currentFontStyle') text = `${_(obj[i])}`; @@ -391,7 +390,6 @@ var DrawingMenu = new Lang.Class({ this.area.currentColor = this.area.colors[iCaptured]; item.icon.set_style(`color:${this.area.currentColor.to_string().slice(0, 7)};`); }); - colorItem.label.get_clutter_text().set_use_markup(true); // Foreground color markup is not displayed since 3.36, use style instead but the transparency is lost. colorItem.label.set_style(`color:${this.area.colors[i].to_string().slice(0, 7)};`); } @@ -401,6 +399,32 @@ var DrawingMenu = new Lang.Class({ return item; }, + _addFontFamilySubMenuItem: function(menu, icon) { + let item = new PopupMenu.PopupSubMenuMenuItem(this.area.currentFontFamily, true); + item.icon.set_icon_name(icon); + + item.menu.itemActivated = () => { + item.menu.close(); + }; + + item.menu.openOld = item.menu.open; + item.menu.open = (animate) => { + if (!item.menu.isOpen && item.menu.isEmpty()) { + this.area.fontFamilies.forEach(family => { + let subItem = item.menu.addAction(_(family), () => { + item.label.set_text(_(family)); + this.area.currentFontFamily = family; + }); + if (FONT_FAMILY_STYLE) + subItem.label.set_style(`font-family:${family}`); + }); + } + item.menu.openOld(); + }; + + menu.addMenuItem(item); + }, + _addDrawingNameItem: function(menu) { this.drawingNameMenuItem = new PopupMenu.PopupMenuItem('', { reactive: false, activate: false }); this.drawingNameMenuItem.setSensitive(false); @@ -420,8 +444,8 @@ var DrawingMenu = new Lang.Class({ _addOpenDrawingSubMenuItem: function(menu) { let item = new PopupMenu.PopupSubMenuMenuItem(_("Open drawing"), true); this.openDrawingSubMenuItem = item; - this.openDrawingSubMenuItem.setSensitive(Boolean(Files.getJsons().length)); this.openDrawingSubMenu = item.menu; + item.setSensitive(Boolean(Files.getJsons().length)); item.icon.set_icon_name('document-open-symbolic'); item.menu.itemActivated = () => { From 1abe0b735f97b4639d1504bf21c6cb5c8b4a303c Mon Sep 17 00:00:00 2001 From: abakkk Date: Wed, 5 Aug 2020 23:34:40 +0200 Subject: [PATCH 26/31] Fix image submenu items sorting Uppercases, non-ascii characters, etc --- files.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files.js b/files.js index 9e301d3..16bb534 100644 --- a/files.js +++ b/files.js @@ -136,7 +136,7 @@ var getImages = function() { }); images.sort((a, b) => { - return b.displayName < a.displayName; + return a.displayName.localeCompare(b.displayName); }); return images; From 39ec1ba88811d4d6026b42554d94b485d0f2dcbc Mon Sep 17 00:00:00 2001 From: abakkk Date: Thu, 6 Aug 2020 00:04:12 +0200 Subject: [PATCH 27/31] toggle -> switch --- area.js | 24 +++---- extension.js | 20 +++--- prefs.js | 20 +++--- schemas/gschemas.compiled | Bin 4428 -> 4440 bytes ...extensions.draw-on-your-screen.gschema.xml | 60 +++++++++--------- 5 files changed, 62 insertions(+), 62 deletions(-) diff --git a/area.js b/area.js index 775bdeb..aad49e2 100644 --- a/area.js +++ b/area.js @@ -324,7 +324,7 @@ var DrawingArea = new Lang.Class({ } return Clutter.EVENT_STOP; } else if (button == 2) { - this.toggleFill(); + this.switchFill(); } else if (button == 3) { this._stopDrawing(); this.menu.open(x, y); @@ -858,7 +858,7 @@ var DrawingArea = new Lang.Class({ } }, - toggleColor: function() { + switchColor: function() { this.selectColor((this.currentColor == this.colors[1]) ? 2 : 1); }, @@ -878,12 +878,12 @@ var DrawingArea = new Lang.Class({ this.updatePointerCursor(); }, - toggleFill: function() { + switchFill: function() { this.fill = !this.fill; this.emit('show-osd', null, this.fill ? _("Fill") : _("Outline"), "", -1, false); }, - toggleDash: function() { + switchDash: function() { this.dashedLine = !this.dashedLine; this.emit('show-osd', null, this.dashedLine ? _("Dashed line") : _("Full line"), "", -1, false); }, @@ -893,22 +893,22 @@ var DrawingArea = new Lang.Class({ this.emit('show-osd', null, _("%d px").format(this.currentLineWidth), "", 2 * this.currentLineWidth, false); }, - toggleLineJoin: function() { + switchLineJoin: function() { this.currentLineJoin = this.currentLineJoin == 2 ? 0 : this.currentLineJoin + 1; this.emit('show-osd', null, _(LineJoinNames[this.currentLineJoin]), "", -1, false); }, - toggleLineCap: function() { + switchLineCap: function() { this.currentLineCap = this.currentLineCap == 2 ? 0 : this.currentLineCap + 1; this.emit('show-osd', null, _(LineCapNames[this.currentLineCap]), "", -1, false); }, - toggleFillRule: function() { + switchFillRule: function() { this.currentFillRule = this.currentFillRule == 1 ? 0 : this.currentFillRule + 1; this.emit('show-osd', null, _(FillRuleNames[this.currentFillRule]), "", -1, false); }, - toggleFontWeight: function() { + switchFontWeight: function() { let fontWeights = Object.keys(FontWeightNames).map(key => Number(key)); let index = fontWeights.indexOf(this.currentFontWeight); this.currentFontWeight = index == fontWeights.length - 1 ? fontWeights[0] : fontWeights[index + 1]; @@ -920,7 +920,7 @@ var DrawingArea = new Lang.Class({ `${_(FontWeightNames[this.currentFontWeight])}`, "", -1, false); }, - toggleFontStyle: function() { + switchFontStyle: function() { this.currentFontStyle = this.currentFontStyle == 2 ? 0 : this.currentFontStyle + 1; if (this.currentElement && this.currentElement.font) { this.currentElement.font.style = this.currentFontStyle; @@ -930,7 +930,7 @@ var DrawingArea = new Lang.Class({ `${_(FontStyleNames[this.currentFontStyle])}`, "", -1, false); }, - toggleFontFamily: function() { + switchFontFamily: function() { let index = Math.max(0, this.fontFamilies.indexOf(this.currentFontFamily)); this.currentFontFamily = (index == this.fontFamilies.length - 1) ? 0 : this.fontFamilies[index + 1]; if (this.currentElement && this.currentElement.font) { @@ -940,7 +940,7 @@ var DrawingArea = new Lang.Class({ this.emit('show-osd', null, `${_(this.currentFontFamily)}`, "", -1, false); }, - toggleTextAlignment: function() { + switchTextAlignment: function() { this.currentTextRightAligned = !this.currentTextRightAligned; if (this.currentElement && this.currentElement.textRightAligned !== undefined) { this.currentElement.textRightAligned = this.currentTextRightAligned; @@ -949,7 +949,7 @@ var DrawingArea = new Lang.Class({ this.emit('show-osd', null, this.currentTextRightAligned ? _("Right aligned") : _("Left aligned"), "", -1, false); }, - toggleImageFile: function() { + switchImageFile: function() { let images = this.getImages(); if (!images.length) return; diff --git a/extension.js b/extension.js index 42011a6..b0f964d 100644 --- a/extension.js +++ b/extension.js @@ -187,12 +187,12 @@ var AreaManager = new Lang.Class({ 'decrement-line-width': () => this.activeArea.incrementLineWidth(-1), 'increment-line-width-more': () => this.activeArea.incrementLineWidth(5), 'decrement-line-width-more': () => this.activeArea.incrementLineWidth(-5), - 'toggle-linejoin': this.activeArea.toggleLineJoin.bind(this.activeArea), - 'toggle-linecap': this.activeArea.toggleLineCap.bind(this.activeArea), - 'toggle-fill-rule': this.activeArea.toggleFillRule.bind(this.activeArea), - 'toggle-dash' : this.activeArea.toggleDash.bind(this.activeArea), - 'toggle-fill' : this.activeArea.toggleFill.bind(this.activeArea), - 'toggle-image-file' : this.activeArea.toggleImageFile.bind(this.activeArea), + 'switch-linejoin': this.activeArea.switchLineJoin.bind(this.activeArea), + 'switch-linecap': this.activeArea.switchLineCap.bind(this.activeArea), + 'switch-fill-rule': this.activeArea.switchFillRule.bind(this.activeArea), + 'switch-dash' : this.activeArea.switchDash.bind(this.activeArea), + 'switch-fill' : this.activeArea.switchFill.bind(this.activeArea), + 'switch-image-file' : this.activeArea.switchImageFile.bind(this.activeArea), 'select-none-shape': () => this.activeArea.selectTool(Area.Tools.NONE), 'select-line-shape': () => this.activeArea.selectTool(Area.Tools.LINE), 'select-ellipse-shape': () => this.activeArea.selectTool(Area.Tools.ELLIPSE), @@ -215,10 +215,10 @@ var AreaManager = new Lang.Class({ '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), - 'toggle-font-family': this.activeArea.toggleFontFamily.bind(this.activeArea), - 'toggle-font-weight': this.activeArea.toggleFontWeight.bind(this.activeArea), - 'toggle-font-style': this.activeArea.toggleFontStyle.bind(this.activeArea), - 'toggle-text-alignment': this.activeArea.toggleTextAlignment.bind(this.activeArea), + 'switch-font-family': this.activeArea.switchFontFamily.bind(this.activeArea), + 'switch-font-weight': this.activeArea.switchFontWeight.bind(this.activeArea), + 'switch-font-style': this.activeArea.switchFontStyle.bind(this.activeArea), + 'switch-text-alignment': this.activeArea.switchTextAlignment.bind(this.activeArea), 'toggle-panel-and-dock-visibility': this.togglePanelAndDockOpacity.bind(this), 'toggle-help': this.activeArea.toggleHelp.bind(this.activeArea), 'open-user-stylesheet': this.openUserStyleFile.bind(this), diff --git a/prefs.js b/prefs.js index 6939652..91abf81 100644 --- a/prefs.js +++ b/prefs.js @@ -59,22 +59,22 @@ var INTERNAL_KEYBINDINGS = { 'select-resize-tool': "Select resize", 'select-mirror-tool': "Select mirror", '-separator-2': '', - 'toggle-fill': "Toggle fill/outline", - 'toggle-fill-rule': "Toggle fill rule", + 'switch-fill': "Toggle fill/outline", + 'switch-fill-rule': "Toggle fill rule", '-separator-3': '', 'increment-line-width': "Increment line width", 'decrement-line-width': "Decrement line width", 'increment-line-width-more': "Increment line width even more", 'decrement-line-width-more': "Decrement line width even more", - 'toggle-linejoin': "Change linejoin", - 'toggle-linecap': "Change linecap", - 'toggle-dash': "Dashed line", + 'switch-linejoin': "Change linejoin", + 'switch-linecap': "Change linecap", + 'switch-dash': "Dashed line", '-separator-4': '', - 'toggle-font-family': "Change font family (generic name)", - 'toggle-font-weight': "Change font weight", - 'toggle-font-style': "Change font style", - 'toggle-text-alignment': "Toggle text alignment", - 'toggle-image-file': "Change image file", + 'switch-font-family': "Change font family (generic name)", + 'switch-font-weight': "Change font weight", + 'switch-font-style': "Change font style", + 'switch-text-alignment': "Toggle text alignment", + 'switch-image-file': "Change image file", '-separator-5': '', 'toggle-panel-and-dock-visibility': "Hide panel and dock", 'toggle-background': "Add a drawing background", diff --git a/schemas/gschemas.compiled b/schemas/gschemas.compiled index 61bc640a50075f4b049de2caf6c187d19fc0ae8a..e05b0aa63b886a48c11c13eb10504ac5d338bd82 100644 GIT binary patch literal 4440 zcmaJ_e~4UH8GX^jZkw7+8k26et8Q!3Oj6(MY<4%|&JpJf?8?U+ zXWbUZxrM*UO%R+fI#0q*11hYQYJqBqQdAedUlL2P2qLM1s8kKTMOOuGTeU(rs)RxW z{H;?wpiTA<+xsYhW;D8BiMtFoz-|QC^kx95I9q{jKo4-Agj5J;4{E> zUgf=5_?x+A3pE5&@JxHB=3-lwfCxE%~VI7Bj1N0{Bv%vJ(*M3BQ@=egs!u|y4{h&9b zPJTD^OR&ENUO4gEEOm1FUxU2?{OmX1>ZVT4Ji9OfL%>^KJGGwkPQDKM7;FSwKkwh7 zKY0QA8Q8ahnW7k=PJR#c71;NI&n>ocgz6e+(Qp{^Zmz!CnRSKK#=I^e5-It-#&{ju?M( z`WMzaP8k^6^!FR|C+GN#!A=5?8-H@05A12+sL{!FKCl;obptQm#W>_!5$6i*pMbCI zb6=-U&h>U3cFo<`Zy!9hojN(kzZ-TJaO#Jg2K@-^958){=Ox%b0RKArMwvSKM(8WB{{+@7T%Dp$&i&GbqO$|I5`Xg* z>f~&91a=bm>YJB+>g4QK9kvbJx9x{dQzu^w{WR=%fSxaR_B%Pp=LOiG1BKw}CHj-I z-M3(W1#DbCp!X3u+r0|=4p6uj{+s^f9G~m3H-XlnV;88Cb3GSOFz*M3-rM{lb#m$> zuoJ-V|2nlyot*3N1nfy5sq^Hl^D*p`1;-iu#)eH3$FVMdls1g zaIlyDSM4xn^aQ-ZYvTvapJZwa3~3V<{g{}Wy1^GqYCatFWMEWTFnsVXs+kWTF?+UK2yJgQ*}8Tk9z0@y>+_*edi(-Sn(#jnmB%}iNV)F zg)b_x>&rm6QQd2bG@ftrio6BkdXWi}wHGDdcOnETiNNfw_L{~|1~_qcMO~0LqUKEQ^M)R4H40H0umYridH?}E*8t)fX5zWU+&mvnW&g_ah zV^mST3J0j=+a1Wl4>%jP>3C*2n6U7F#E(Z0PPLjM9Ic}AT9en1VW>hkR?4^HR`Tn2xGK-V%zrTPH+jx@mHAqzT0vF!&T3~S$)mG;XBAd}-U=PZ zG#@jti)Q`WdGL;t`XTS3?9)(^hwaaMknKO5eHu>uMd(GsO@}u(z9K)qdgyqs;-4LN zZ}w?#5?AlLW+-MwD1wTJQvZr-G(41eYvOcZUeH#>-IvAPm&7%Tmm9U>VQr;x=accZ z*4T*kJ;rM!^Bzg#S?;_art#pvKl`+Qm4Aq8;RP&Ix>{Jj3-ft2mEX>)isjeJ`-LKs z%fc*PJC7yfR~7$VH{6B_JKHzQ)T|$*Kh>IvL$4A$y`91CEWD~0oufZvSv*@voV`qAbi&gs%}+P=G_Gu$rZ5%1K8j6yX2au>P!lfBtpBmzWxifE6i#cN1Zn;a6Ayjv1NaUt0l3!V zUZbCX`t8a3#y9U)pa}5qvw#b*{;mU1RuGK${?7aN9@ymldoOtM{@n%6vtuoQ>#M^f zk8f#~I=+YbKIWSkhq8VH^DWF8gr|>w598Y8+z*t2Zs0+H|0hrYa6Q*=W4_UM0^0%B z``v&!13E*tdR(O$hfw6_-|X3CGmd84MA&U1?meOK5Ey^ycm^hp463r?#W;9#tHM6- rSp#dh5F~3c7pHCFB=@Ump_cpJC+^E literal 4428 zcmZu!Yitx%7#$P@L1B@H6si_1vQ=l>QVNA&MMWqG3D{_=QDM4!yPbA+?l?2s7mxT# z#8=W76A>kXMj(cSCO!g0BmU5+iAjGL#rUTtn8*(a8Vn}J$2oWJ&dioha`v44=9}-n zzkBb-hb`Z>+|ba^0&qu3r?1ksN#Mz2L(OEq%NT9OKJc&C7{-)&hVdVsw%ITkTMY6R zK>GPi-jxNB3A%;jWW+!y+`yJ@kjeR0zbRdFP!@eN$ofLK{MJaXQ565M{Hp;Jqc9Cj zT&pn?JPTmaHvn@1RL8gxU^}W3SO|R+a5Hd=#$qtB1h`eB7Q7U=4M+oZz#V{@KnA*u zdgumVxkeM1Xa-gSt2A1`Y+o4*TL+u@9s6*u(PtP_(GK9^_b+Xyoq8thHQ+|z;E5%Y zcIp|huLC~_^fYW#aj5CP6WkAU?Em;(`cuz_eINJ@p!#yPPdoK>upa}T0uJr`xQljb z`j3Kt29AI8$^zP{SUtVIKjH0>;80{-K?k?HU7L0v0sfbsz22j57_Bp$fQ9$DwANG`I~| zukF;dcYp_gaM!o%8Hbwf9R?o;?l0E|TGaPrDxL8F>Ei?PqDHu7Q0Wcst;1-*GqX)GXHnzXD9^JKIh> zHRt~!@Tb66;Y%OTPR(|Wg3kj_eD=9RJGEM;NIJ8DMOAO_rk#2+{1<@hfQ65i)+sgn ztp&UZNV&TQ=ugdZJHU?vb?57+(N4{N@WA_kSwm~oI-+KM4uRhTQe*zV^rvP&oCJRX z6z_WI1nty}a~6Ca`0khXA=;@qE=-uSfpXraX5O9)z63PAF!NLTQ>*orLjM6LwjNaN zrDl6;z%4-U-%ZsG@TaE#Hn0Uu`Tf#6v{Q3l4SF8oL->BJd zUx9xFj-LJ})c}8L`dNq3aS?=pDiQ08p1=|^n>(N!&&a8B# z-fIQ7HySgmlEbIVg#SHG*t)j8=n20yhn*zR`32kerEi8(I*GX13ChLh9*qym zT^|2iUM^P;sH!IS|D=CY{I99(ucuk3mF>;@vgqd6xp<=QIwz>dio|==V@2$}q8v9_ z51v7yc<^tI|23EWg>MDIjJm6$zeHt!skZSE$22n73@5szFX%nMTZl^sgvK2S9P{ z#m>;TvY}C}BIpi7w#^F@)hvA()lYwCBg?bJ6O}i%~ELx79pSsGPH@^cVVpwj$%!FbuS~|&WZOb+y5rYtsOzT_M?qp+Ev0-FJ!(fm`@zaPA@7JL zz+?U#0M{OacSEKr-bs1SVC>xUkC8cTmwu3c;{UT z@NUaJ3EPLd)9S7$t;F&DW$S+MMdduu<29tmOD~N8tHsAT#TOQW8 z^KNOuMSeQ2RT)I&3T+k a%4?w_&Y+G{K8FQ;`HJ_t-hr++-~A7^(jc$^ 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 683fbcb..36065fa 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 @@ -151,30 +151,30 @@ decrement the line width even more decrement the line width even more - + ["<Primary>j"] - toggle linejoin - toggle linejoin + switch linejoin + switch linejoin - + ["<Primary>k"] - toggle linecap - toggle linecap + switch linecap + switch linecap - + KP_Multiply','asterisk','asterisk']]]> - toggle fill rule - toggle fill rule + switch fill rule + switch fill rule - + ["<Primary>period"] - toggle dash - toggle dash + switch dash + switch dash - + ["<Primary>a"] - toggle fill - toggle fill + switch fill + switch fill KP_1','1']]]> @@ -221,30 +221,30 @@ select color9 select color9 - + ["<Primary>f"] - toggle font family - toggle font family + switch font family + switch font family - + ["<Primary>w"] - toggle font weight - toggle font weight + switch font weight + switch font weight - + ["<Primary><Shift>w"] - toggle font style - toggle font style + switch font style + switch font style - + ["<Primary><Shift>a"] - toggle text alignment - toggle text alignment + switch text alignment + switch text alignment - + ["<Primary><Shift>i"] - toggle image file - toggle image file + switch image file + switch image file ["<Primary>o"] From a1ce4aaf79007d65c4b4fb121cbceab916ec133b Mon Sep 17 00:00:00 2001 From: abakkk Date: Thu, 6 Aug 2020 00:26:10 +0200 Subject: [PATCH 28/31] Add font family reversed switching --- area.js | 7 +++++-- extension.js | 3 ++- locale/draw-on-your-screen.pot | 5 ++++- prefs.js | 3 ++- schemas/gschemas.compiled | Bin 4440 -> 4544 bytes ...extensions.draw-on-your-screen.gschema.xml | 5 +++++ 6 files changed, 18 insertions(+), 5 deletions(-) diff --git a/area.js b/area.js index aad49e2..2684308 100644 --- a/area.js +++ b/area.js @@ -930,9 +930,12 @@ var DrawingArea = new Lang.Class({ `${_(FontStyleNames[this.currentFontStyle])}`, "", -1, false); }, - switchFontFamily: function() { + switchFontFamily: function(reverse) { let index = Math.max(0, this.fontFamilies.indexOf(this.currentFontFamily)); - this.currentFontFamily = (index == this.fontFamilies.length - 1) ? 0 : this.fontFamilies[index + 1]; + if (reverse) + this.currentFontFamily = (index == 0) ? this.fontFamilies[this.fontFamilies.length - 1] : this.fontFamilies[index - 1]; + else + this.currentFontFamily = (index == this.fontFamilies.length - 1) ? this.fontFamilies[0] : this.fontFamilies[index + 1]; if (this.currentElement && this.currentElement.font) { this.currentElement.font.family = this.currentFontFamily; this._redisplay(); diff --git a/extension.js b/extension.js index b0f964d..a253683 100644 --- a/extension.js +++ b/extension.js @@ -215,7 +215,8 @@ var AreaManager = new Lang.Class({ '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), - 'switch-font-family': this.activeArea.switchFontFamily.bind(this.activeArea), + 'reverse-switch-font-family': this.activeArea.switchFontFamily.bind(this.activeArea, true), + 'switch-font-family': this.activeArea.switchFontFamily.bind(this.activeArea, false), 'switch-font-weight': this.activeArea.switchFontWeight.bind(this.activeArea), 'switch-font-style': this.activeArea.switchFontStyle.bind(this.activeArea), 'switch-text-alignment': this.activeArea.switchTextAlignment.bind(this.activeArea), diff --git a/locale/draw-on-your-screen.pot b/locale/draw-on-your-screen.pot index 7ff6129..15ef699 100644 --- a/locale/draw-on-your-screen.pot +++ b/locale/draw-on-your-screen.pot @@ -218,7 +218,10 @@ msgstr "" msgid "Toggle fill rule" msgstr "" -msgid "Change font family (generic name)" +msgid "Change font family" +msgstr "" + +msgid "Change font family (reverse)" msgstr "" msgid "Change font weight" diff --git a/prefs.js b/prefs.js index 91abf81..8ff013d 100644 --- a/prefs.js +++ b/prefs.js @@ -70,7 +70,8 @@ var INTERNAL_KEYBINDINGS = { 'switch-linecap': "Change linecap", 'switch-dash': "Dashed line", '-separator-4': '', - 'switch-font-family': "Change font family (generic name)", + 'switch-font-family': "Change font family", + 'reverse-switch-font-family': "Change font family (reverse)", 'switch-font-weight': "Change font weight", 'switch-font-style': "Change font style", 'switch-text-alignment': "Toggle text alignment", diff --git a/schemas/gschemas.compiled b/schemas/gschemas.compiled index e05b0aa63b886a48c11c13eb10504ac5d338bd82..92584ee8996575d7c52df4ad18a835ea688069e0 100644 GIT binary patch literal 4544 zcmZ`-TWl3o6rCz4Z(0y6rD{cx@|d=l(ozsh)d~eQDQPf`QR(!~>Alm=of&54_5~83 z5n@P;F_9055EDgX0>&2*2=GTtOfZ^=CPqj!Ax2I8Q4AukHFM^1OTEcr?OC&DpL6zk z?Xu}f%{4U3SLCk}+#TArw@9rFJn7PC6PfSoln!MNwc8bC@;pVkf!~P=2+HHiDezgq zc5`((%PxpIFE329P7M3P@(kPZ>U38dQf*5ev5T(iWnCdGeyh|z5Rjs<9kzWNFa?+j zOarE;Favgk*|6FCTnThZnGY`ACpY|N+ z&x2n9jy`d47j@c`puY}20&J9VXfw`n@I~PFPwHNzKW*w)!2bY~Gw)8LPJ2G|N+f?7 zut~q&!b=uU=gRcPPo2Oi3yRMy=fU3r1J@d>sne!D1}?*5J@3EvF?HJ1E5R9H z+m2uFr%rnw^bT+jQ1#Rzn>uaw#{s_p?A}uS6Ls3`&wlXtz&Ag4jZ&x0agBk?uqh56 z4dc*eoO19YpmFc4)AXm!{(xtIy?{l4j4OP-k?A2pr7EDnGmYJy-uC>EckbTp9VhsVqYb7 z+8nO~J^*y@|Lh&=v^np`z~_Lria-CLPMdKqfG-12uTf7@r_DTH1y6{?(mehsu1z~A|0 zuwQ6X?*wbWTc@@jqd#rN9|rFO`WrR{`-nE@nM6UzCGS4u^;-^lsq*>?Fm=&geqI5|3jwV zy1uLE2)9*WHc~NN3|_Nxrck}S=7_K(aeNm`GUXg32^U)Y9_=?LPt8&L>HP5SOZ%@$ zp4Lp{-!jg>CGl^W$e(vII3Wcb;<+Gkt7IwXu-URFHucSF%QLq}cn^2|j1 z9*Hw-9p_h&jL-=yBq!89!xV|1oHs+x+ntEP+Mym`O*0u_eX$Z)0&wkM+hHGYPldG! z>xEkZ=4~0U0$@%!PX5Y(NdT_tlKP12q6%Q`vJR*OYJmoTH4N*iIe-dqo#PnBc*6Q; zIXDC03c;RHl=}eIO01<=&(r|(0oGJYfjWTo7V9Y65tiz>qEEQO$_fusl8>xiC}x*E;>JG*hNq6_iU@wuM<EN7-hm&Re`LyV~KFnj*|3&ds8A^SL=H{bf|HHPtO*hlann-^l9| zMbkGNb0ld@^L%V`Zy-^k>5N{?TiO<6Mb2w{z^2bvU7oLob@SAw+kZ&cZ)(UfF|p-K z(wPs|MU?pXd$V+$bjoT@5^o@SAMO{&Hb>Hd;y6X=XIQ$C)qLA6&2n%*O|(YUg6$%y zaU7Yb^m46EXR2W98n$ZUpSqBA<<9hgEgKs_4mkFAqVZ^0&sw}wz}MZ+ixIFAmsvvj%+)>aIb&#pkK9%QX|nk>l=@<4)H#yf0%v qEc{IJl!@z(aQg*i6YtTq|ElC^)j0ox;kww0@%5H?X63kKzW)OPC^M@7 literal 4440 zcmaJ_e~4UH8GX^jZkw7+8k26et8Q!3Oj6(MY<4%|&JpJf?8?U+ zXWbUZxrM*UO%R+fI#0q*11hYQYJqBqQdAedUlL2P2qLM1s8kKTMOOuGTeU(rs)RxW z{H;?wpiTA<+xsYhW;D8BiMtFoz-|QC^kx95I9q{jKo4-Agj5J;4{E> zUgf=5_?x+A3pE5&@JxHB=3-lwfCxE%~VI7Bj1N0{Bv%vJ(*M3BQ@=egs!u|y4{h&9b zPJTD^OR&ENUO4gEEOm1FUxU2?{OmX1>ZVT4Ji9OfL%>^KJGGwkPQDKM7;FSwKkwh7 zKY0QA8Q8ahnW7k=PJR#c71;NI&n>ocgz6e+(Qp{^Zmz!CnRSKK#=I^e5-It-#&{ju?M( z`WMzaP8k^6^!FR|C+GN#!A=5?8-H@05A12+sL{!FKCl;obptQm#W>_!5$6i*pMbCI zb6=-U&h>U3cFo<`Zy!9hojN(kzZ-TJaO#Jg2K@-^958){=Ox%b0RKArMwvSKM(8WB{{+@7T%Dp$&i&GbqO$|I5`Xg* z>f~&91a=bm>YJB+>g4QK9kvbJx9x{dQzu^w{WR=%fSxaR_B%Pp=LOiG1BKw}CHj-I z-M3(W1#DbCp!X3u+r0|=4p6uj{+s^f9G~m3H-XlnV;88Cb3GSOFz*M3-rM{lb#m$> zuoJ-V|2nlyot*3N1nfy5sq^Hl^D*p`1;-iu#)eH3$FVMdls1g zaIlyDSM4xn^aQ-ZYvTvapJZwa3~3V<{g{}Wy1^GqYCatFWMEWTFnsVXs+kWTF?+UK2yJgQ*}8Tk9z0@y>+_*edi(-Sn(#jnmB%}iNV)F zg)b_x>&rm6QQd2bG@ftrio6BkdXWi}wHGDdcOnETiNNfw_L{~|1~_qcMO~0LqUKEQ^M)R4H40H0umYridH?}E*8t)fX5zWU+&mvnW&g_ah zV^mST3J0j=+a1Wl4>%jP>3C*2n6U7F#E(Z0PPLjM9Ic}AT9en1VW>hkR?4^HR`Tn2xGK-V%zrTPH+jx@mHAqzT0vF!&T3~S$)mG;XBAd}-U=PZ zG#@jti)Q`WdGL;t`XTS3?9)(^hwaaMknKO5eHu>uMd(GsO@}u(z9K)qdgyqs;-4LN zZ}w?#5?AlLW+-MwD1wTJQvZr-G(41eYvOcZUeH#>-IvAPm&7%Tmm9U>VQr;x=accZ z*4T*kJ;rM!^Bzg#S?;_art#pvKl`+Qm4Aq8;RP&Ix>{Jj3-ft2mEX>)isjeJ`-LKs z%fc*PJC7yfR~7$VH{6B_JKHzQ)T|$*Kh>IvL$4A$y`91CEWD~0oufZvSv*@voV`qAbi&gs%}+P=G_Gu$rZ5%1K8j6yX2au>P!lfBtpBmzWxifE6i#cN1Zn;a6Ayjv1NaUt0l3!V zUZbCX`t8a3#y9U)pa}5qvw#b*{;mU1RuGK${?7aN9@ymldoOtM{@n%6vtuoQ>#M^f zk8f#~I=+YbKIWSkhq8VH^DWF8gr|>w598Y8+z*t2Zs0+H|0hrYa6Q*=W4_UM0^0%B z``v&!13E*tdR(O$hfw6_-|X3CGmd84MA&U1?meOK5Ey^ycm^hp463r?#W;9#tHM6- rSp#dh5F~3c7pHCFB=@Ump_cpJC+^E 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 36065fa..4fe2746 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 @@ -226,6 +226,11 @@ switch font family switch font family + + ["<Primary><Shift>f"] + switch font family (reverse) + switch font family (reverse) + ["<Primary>w"] switch font weight From 075b080aaf310f0cc8fe8e0f19b897cafa9230b0 Mon Sep 17 00:00:00 2001 From: abakkk Date: Thu, 6 Aug 2020 22:10:12 +0200 Subject: [PATCH 29/31] fix submenu scrollview adjustment * auto-scroll when pointer is near the top or the bottom * scroll with keyboard arrows copy files-view extension --- menu.js | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/menu.js b/menu.js index b70eca4..2c97c36 100644 --- a/menu.js +++ b/menu.js @@ -361,6 +361,7 @@ var DrawingMenu = new Lang.Class({ }); subItem.label.get_clutter_text().set_use_markup(true); + getActor(subItem).connect('key-focus-in', updateSubMenuAdjustment); // change the display order of tools if (obj == Area.ToolNames && i == Area.Tools.POLYGON) @@ -392,6 +393,7 @@ var DrawingMenu = new Lang.Class({ }); // Foreground color markup is not displayed since 3.36, use style instead but the transparency is lost. colorItem.label.set_style(`color:${this.area.colors[i].to_string().slice(0, 7)};`); + getActor(colorItem).connect('key-focus-in', updateSubMenuAdjustment); } return GLib.SOURCE_REMOVE; }); @@ -417,6 +419,7 @@ var DrawingMenu = new Lang.Class({ }); if (FONT_FAMILY_STYLE) subItem.label.set_style(`font-family:${family}`); + getActor(subItem).connect('key-focus-in', updateSubMenuAdjustment); }); } item.menu.openOld(); @@ -466,28 +469,29 @@ var DrawingMenu = new Lang.Class({ this.openDrawingSubMenu.removeAll(); let jsons = Files.getJsons(); jsons.forEach(json => { - let item = this.openDrawingSubMenu.addAction(`${String(json)}`, () => { + let subItem = this.openDrawingSubMenu.addAction(`${String(json)}`, () => { this.area.loadJson(json.name); this._updateDrawingNameMenuItem(); this._updateSaveDrawingSubMenuItemSensitivity(); }); - item.label.get_clutter_text().set_use_markup(true); + subItem.label.get_clutter_text().set_use_markup(true); + getActor(subItem).connect('key-focus-in', updateSubMenuAdjustment); let expander = new St.Bin({ style_class: 'popup-menu-item-expander', x_expand: true, }); - getActor(item).add_child(expander); + getActor(subItem).add_child(expander); let deleteButton = new St.Button({ style_class: 'draw-on-your-screen-menu-delete-button', child: new St.Icon({ icon_name: 'edit-delete-symbolic', style_class: 'popup-menu-icon', x_align: Clutter.ActorAlign.END }) }); - getActor(item).add_child(deleteButton); + getActor(subItem).add_child(deleteButton); deleteButton.connect('clicked', () => { json.delete(); - item.destroy(); + subItem.destroy(); this.openDrawingSubMenuItem.setSensitive(!this.openDrawingSubMenu.isEmpty()); }); }); @@ -547,6 +551,23 @@ var DrawingMenu = new Lang.Class({ } }); +// based on ApplicationsButton.scrollToButton , https://gitlab.gnome.org/GNOME/gnome-shell-extensions/blob/master/extensions/apps-menu/extension.js +const updateSubMenuAdjustment = function(itemActor) { + let scrollView = itemActor.get_parent().get_parent(); + let adjustment = scrollView.get_vscroll_bar().get_adjustment(); + let scrollViewAlloc = scrollView.get_allocation_box(); + let currentScrollValue = adjustment.get_value(); + let height = scrollViewAlloc.y2 - scrollViewAlloc.y1; + let itemActorAlloc = itemActor.get_allocation_box(); + let newScrollValue = currentScrollValue; + if (currentScrollValue > itemActorAlloc.y1 - 10) + newScrollValue = itemActorAlloc.y1 - 10; + if (height + currentScrollValue < itemActorAlloc.y2 + 10) + newScrollValue = itemActorAlloc.y2 - height + 10; + if (newScrollValue != currentScrollValue) + adjustment.set_value(newScrollValue); +}; + // based on searchItem.js, https://github.com/leonardo-bartoli/gnome-shell-extension-Recents const DrawingMenuEntry = new Lang.Class({ Name: 'DrawOnYourScreenDrawingMenuEntry', From 4e0455af34fd6e8c8f8c410e981bb66426b58a95 Mon Sep 17 00:00:00 2001 From: abakkk Date: Sat, 8 Aug 2020 00:27:41 +0200 Subject: [PATCH 30/31] GS 3.24 compatibility * item actor * Gio.File.load_bytes not available --- files.js | 11 ++++++++++- menu.js | 8 ++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/files.js b/files.js index 16bb534..0001cc4 100644 --- a/files.js +++ b/files.js @@ -64,7 +64,16 @@ var Image = new Lang.Class({ get bytes() { if (!this._bytes) { if (this.file) - this._bytes = this.file.load_bytes(null)[0]; + try { + // load_bytes available in GLib 2.56+ + this._bytes = this.file.load_bytes(null)[0]; + } catch(e) { + let [success_, contents] = this.file.load_contents(null); + if (contents instanceof Uint8Array) + this._bytes = ByteArray.toGBytes(contents); + else + this._bytes = contents.toGBytes(); + } else this._bytes = new GLib.Bytes(GLib.base64_decode(this.base64)); } diff --git a/menu.js b/menu.js index 2c97c36..911f082 100644 --- a/menu.js +++ b/menu.js @@ -156,10 +156,10 @@ var DrawingMenu = new Lang.Class({ this.actionButtons = []; let groupItem = new PopupMenu.PopupBaseMenuItem({ reactive: false, can_focus: false, style_class: "draw-on-your-screen-menu-group-item" }); - groupItem.add_child(this._createActionButton(_("Undo"), this.area.undo.bind(this.area), 'edit-undo-symbolic')); - groupItem.add_child(this._createActionButton(_("Redo"), this.area.redo.bind(this.area), 'edit-redo-symbolic')); - groupItem.add_child(this._createActionButton(_("Erase"), this.area.deleteLastElement.bind(this.area), 'edit-clear-all-symbolic')); - groupItem.add_child(this._createActionButton(_("Smooth"), this.area.smoothLastElement.bind(this.area), this.smoothIcon)); + getActor(groupItem).add_child(this._createActionButton(_("Undo"), this.area.undo.bind(this.area), 'edit-undo-symbolic')); + getActor(groupItem).add_child(this._createActionButton(_("Redo"), this.area.redo.bind(this.area), 'edit-redo-symbolic')); + getActor(groupItem).add_child(this._createActionButton(_("Erase"), this.area.deleteLastElement.bind(this.area), 'edit-clear-all-symbolic')); + getActor(groupItem).add_child(this._createActionButton(_("Smooth"), this.area.smoothLastElement.bind(this.area), this.smoothIcon)); this.menu.addMenuItem(groupItem); this._addSeparator(this.menu, true); From 9585d3c80bbf34da4eca222bb9bd6dd3a66f05c3 Mon Sep 17 00:00:00 2001 From: abakkk Date: Sat, 8 Aug 2020 01:13:52 +0200 Subject: [PATCH 31/31] version -> 6.2 --- NEWS | 14 ++++++++++++-- metadata.json | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/NEWS b/NEWS index fd428fd..9b28481 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,15 @@ +v6.2 - August 2020 +================== + +* Show the text entry when ibusCandidatePopup is used +* Regroup first menu items +* Center grid overlay +* Image shape +* Extend font family choices +* Fix sub-menu scroll view adjustment + v6.1 - June 2020 -================= +================ * Fix empty media-keys settings case #28 * Fix label color in OSD and menu #31 @@ -20,7 +30,7 @@ v6.1 - June 2020 * Attributes are now persistent through drawing mode toggling #27 v6 - March 2020 -================= +=============== * GS 3.36 compatibility diff --git a/metadata.json b/metadata.json index 49297dc..4690c5b 100644 --- a/metadata.json +++ b/metadata.json @@ -17,5 +17,5 @@ "3.34", "3.36" ], - "version": 6.1 + "version": 6.2 }