diff --git a/NEWS b/NEWS index bab0ca3..fd428fd 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,24 @@ +v6.1 - June 2020 +================= + +* Fix empty media-keys settings case #28 +* Fix label color in OSD and menu #31 +* Fix OSD width +* Fix "Hide panel and dock" +* Small menu on small monitor +* Start a new text line with the `Enter` key #30 +* IBus inputs #29 #34 +* Non-latin and emoji characters rendered +* GS keyboard is now available to type text +* Grid overlay +* Polygon and polyline shapes +* Move, resize and mirror tools +* Fill rule and text alignment style attributes +* An optional fourth control point for Bézier curves +* Keybinding to toggle modeless/modal #6 #9 #20 #24 #33 + (ungrabs keyboard and mouse while drawing remains at the top) +* Attributes are now persistent through drawing mode toggling #27 + v6 - March 2020 ================= diff --git a/README.md b/README.md index c88b33f..f086e4c 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Then save your beautiful work by taking a screenshot. ## Features * Basic shapes (rectangle, circle, ellipse, line, curve, text, free) +* Basic transformations (move, rotate, resize, stretch, mirror, inverse) * Smooth stroke * Draw over applications * Keep drawings on desktop background with persistence (notes, children's art ...) @@ -33,7 +34,13 @@ Then save your beautiful work by taking a screenshot. ![How to draw an arrow](https://framagit.org/abakkk/DrawOnYourScreen/uploads/af8f96d33cfeff49bb922a1ef9f4a4ce/arrow-screencast.webm) +* Duplicate an element: + + Hold the `Shift` key while starting moving. + + ![How to duplicate an element](https://framagit.org/abakkk/DrawOnYourScreen/-/raw/ressources/duplicate.webm) + * Screenshot Tool extension: - [Screenshot Tool](https://extensions.gnome.org/extension/1112/screenshot-tool/) is a convenient extension to “create, copy, store and upload screenshots”. To use it while drawing mode is active, toggle the area selection mode thanks to the Screenshot Tool shortcut (`Super + F11` by default, see its preferences) and **hold** the `space` key when selecting the area with pointer to avoid drawing. + [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/data/default.css b/data/default.css index b5ab3ef..4a03e08 100644 --- a/data/default.css +++ b/data/default.css @@ -1,18 +1,24 @@ /* - * Except for the font, you don't need to restart the extension. - * Just save this file as ~/.local/share/drawOnYourScreen/user.css and the changes will be applied for your next brushstroke. + * WARNING : user.css may be obsolete after an extension update. * * ~/.local/share/drawOnYourScreen/user.css file is automatically generated by activating "Edit style". - * Delete ~/.local/share/drawOnYourScreen/user.css file to retrieve default drawing style. + * Delete ~/.local/share/drawOnYourScreen/user.css file to retrieve the default drawing style. + * + * Except for the font, you don't need to restart the extension. + * Just save this file as ~/.local/share/drawOnYourScreen/user.css and the changes will be applied for your next brushstroke. + * Some attributes are modifiable in the user interface. * * line-join (no string): * 0 : miter, 1 : round, 2 : bevel * line-cap (no string): * 0 : butt, 1 : round, 2 : square + * fill-rule (no string): + * 0 : nonzero (winding in Cairo), 1 : evenodd * * dash: - * dash-array-on is the length of dashes (no dashes if 0, you can put 0.1 to get dots or square according to line-cap). - * dash-array-off is the length of gaps (no dashes if 0). + * By default, it is computed from the line width. + * dash-array-on is the length of dashes (put 0.1 to get dots or squares according to line-cap). + * dash-array-off is the length of gaps. * * square area: * Drawing in a square area is convenient when using the extension as a vector graphics editor. By default, @@ -23,9 +29,8 @@ * Only one family : no comma separated list of families like "font1, font2, ..., Sans-Serif". * Font family can be any font installed, or a generic family name (Serif, Sans-Serif, Monospace, Cursive, Fantasy). * Font weight and font style : no upper case when string. - * Weight <= 500 (or lighter, normal, medium) is rendered as normal. - * Weight > 500 (or bolder, bold) is rendered as bold. - * Oblique and italic style supports depend on the font family and seem to be rendered identically. + * + * text-align: left or right. * */ @@ -33,22 +38,114 @@ -drawing-line-width: 5px; -drawing-line-join: 1; -drawing-line-cap: 1; - -drawing-dash-array-on: 5px; - -drawing-dash-array-off: 15px; - -drawing-dash-offset: 0px; + -drawing-fill-rule: 0; + /*-drawing-dash-array-on: 5px;*/ + /*-drawing-dash-array-off: 15px;*/ + /*-drawing-dash-offset: 0px;*/ + -drawing-background-color: #2e2e2e; + -grid-overlay-gap: 10px; + -grid-overlay-line-width: 0.4px; + -grid-overlay-interline-width: 0.2px; + -grid-overlay-color: Gray; + /*-drawing-square-area-width: 512px;*/ + /*-drawing-square-area-height: 512px;*/ + font-family: Cantarell; + font-weight: normal; + font-style: normal; + text-align: left; +} + +/* Palette */ +.draw-on-your-screen { -drawing-color1: HotPink; -drawing-color2: Cyan; -drawing-color3: yellow; -drawing-color4: Orangered; -drawing-color5: Chartreuse; -drawing-color6: DarkViolet; - -drawing-color7: #ffffff; - -drawing-color8: rgba(130, 130, 130, 0.3); - -drawing-color9: rgb(0, 0, 0); - -drawing-background-color: #2e3436; - /*-drawing-square-area-width: 512px;*/ - /*-drawing-square-area-height: 512px;*/ - font-family: Cantarell; - font-weight: normal; - font-style: normal; + -drawing-color7: White; + -drawing-color8: Gray; + -drawing-color9: Black; } + +/* +Example of alternative palettes from GNOME HIG Colors. +https://developer.gnome.org/hig/stable/icon-design.html + +The last uncommented palette wins. +*/ + +/* lighter */ +/* +.draw-on-your-screen { + -drawing-color1: rgb(153, 193, 241); + -drawing-color2: rgb(143, 240, 164); + -drawing-color3: rgb(249, 240, 107); + -drawing-color4: rgb(255, 190, 111); + -drawing-color5: rgb(246, 97, 81); + -drawing-color6: rgb(220, 138, 221); + -drawing-color7: rgb(205, 171, 143); + -drawing-color8: rgb(255, 255, 255); + -drawing-color9: rgb(119, 118, 123); +} +*/ + +/* light */ +/* +.draw-on-your-screen { + -drawing-color1: rgb( 98, 160, 241); + -drawing-color2: rgb( 87, 227, 137); + -drawing-color3: rgb(248, 228, 92); + -drawing-color4: rgb(255, 163, 72); + -drawing-color5: rgb(237, 51, 59); + -drawing-color6: rgb(192, 97, 203); + -drawing-color7: rgb(181, 131, 90); + -drawing-color8: rgb(246, 245, 244); + -drawing-color9: rgb( 94, 92, 100); +} +*/ + +/* normal */ +/* +.draw-on-your-screen { + -drawing-color1: rgb( 53, 132, 228); + -drawing-color2: rgb( 51, 209, 122); + -drawing-color3: rgb(246, 211, 45); + -drawing-color4: rgb(255, 120, 0); + -drawing-color5: rgb(224, 27, 36); + -drawing-color6: rgb(145, 65, 172); + -drawing-color7: rgb(152, 106, 68); + -drawing-color8: rgb(222, 221, 218); + -drawing-color9: rgb( 61, 56, 70); +} +*/ + +/* dark */ +/* +.draw-on-your-screen { + -drawing-color1: rgb( 28, 113, 216); + -drawing-color2: rgb( 46, 194, 126); + -drawing-color3: rgb(245, 194, 17); + -drawing-color4: rgb(230, 97, 0); + -drawing-color5: rgb(192, 28, 40); + -drawing-color6: rgb(129, 61, 156); + -drawing-color7: rgb(134, 94, 60); + -drawing-color8: rgb(192, 191, 188); + -drawing-color9: rgb( 36, 31, 49); +} +*/ + +/* darker */ +/* +.draw-on-your-screen { + -drawing-color1: rgb( 26, 095, 180); + -drawing-color2: rgb( 38, 162, 105); + -drawing-color3: rgb(229, 165, 10); + -drawing-color4: rgb(198, 70, 0); + -drawing-color5: rgb(165, 29, 45); + -drawing-color6: rgb( 97, 53, 131); + -drawing-color7: rgb( 99, 69, 44); + -drawing-color8: rgb(154, 153, 150); + -drawing-color9: rgb( 0, 0, 0); +} +*/ diff --git a/data/icons/color-symbolic.svg b/data/icons/color-symbolic.svg new file mode 100644 index 0000000..39ae95d --- /dev/null +++ b/data/icons/color-symbolic.svg @@ -0,0 +1,18 @@ + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 +https://svgsilh.com/image/1745699.html +https://creativecommons.org/publicdomain/zero/1.0 + + + + + diff --git a/data/icons/fillrule-evenodd-symbolic.svg b/data/icons/fillrule-evenodd-symbolic.svg new file mode 100644 index 0000000..a74de4c --- /dev/null +++ b/data/icons/fillrule-evenodd-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/data/icons/fillrule-nonzero-symbolic.svg b/data/icons/fillrule-nonzero-symbolic.svg new file mode 100644 index 0000000..a3b9b2b --- /dev/null +++ b/data/icons/fillrule-nonzero-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/draw.js b/draw.js index cb542ac..087f504 100644 --- a/draw.js +++ b/draw.js @@ -28,8 +28,8 @@ const GLib = imports.gi.GLib; const GObject = imports.gi.GObject; const Gtk = imports.gi.Gtk; const Lang = imports.lang; -const Mainloop = imports.mainloop; -const Shell = imports.gi.Shell; +const Pango = imports.gi.Pango; +const PangoCairo = imports.gi.PangoCairo; const St = imports.gi.St; const BoxPointer = imports.ui.boxpointer; @@ -48,22 +48,44 @@ 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 FILL_ICON_PATH = Me.dir.get_child('data').get_child('icons').get_child('fill-symbolic.svg').get_path(); -const STROKE_ICON_PATH = Me.dir.get_child('data').get_child('icons').get_child('stroke-symbolic.svg').get_path(); -const LINEJOIN_ICON_PATH = Me.dir.get_child('data').get_child('icons').get_child('linejoin-symbolic.svg').get_path(); -const LINECAP_ICON_PATH = Me.dir.get_child('data').get_child('icons').get_child('linecap-symbolic.svg').get_path(); -const DASHED_LINE_ICON_PATH = Me.dir.get_child('data').get_child('icons').get_child('dashed-line-symbolic.svg').get_path(); -const FULL_LINE_ICON_PATH = Me.dir.get_child('data').get_child('icons').get_child('full-line-symbolic.svg').get_path(); +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(); -var Shapes = { NONE: 0, LINE: 1, ELLIPSE: 2, RECTANGLE: 3, TEXT: 4 }; -const TextState = { DRAWING: 0, WRITING: 1 }; -const ShapeNames = { 0: "Free drawing", 1: "Line", 2: "Ellipse", 3: "Rectangle", 4: "Text" }; -const LineCapNames = { 0: 'Butt', 1: 'Round', 2: 'Square' }; -const LineJoinNames = { 0: 'Miter', 1: 'Round', 2: 'Bevel' }; -const FontWeightNames = { 0: 'Normal', 1: 'Bold' }; -const FontStyleNames = { 0: 'Normal', 1: 'Italic', 2: 'Oblique' }; -const FontFamilyNames = { 0: 'Default', 1: 'Sans-Serif', 2: 'Serif', 3: 'Monospace', 4: 'Cursive', 5: 'Fantasy' }; +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(); @@ -108,12 +130,16 @@ const getJsonFiles = function() { var DrawingArea = new Lang.Class({ Name: 'DrawOnYourScreenDrawingArea', Extends: St.DrawingArea, - Signals: { 'show-osd': { param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_DOUBLE] }, - 'stop-drawing': {} }, + 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; @@ -121,13 +147,17 @@ var DrawingArea = new Lang.Class({ this.elements = []; this.undoneElements = []; this.currentElement = null; - this.currentShape = Shapes.NONE; + 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(); @@ -139,6 +169,53 @@ var DrawingArea = new Lang.Class({ 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(); @@ -150,18 +227,26 @@ var DrawingArea = new Lang.Class({ for (let i = 1; i < 10; i++) { this.colors[i] = themeNode.get_color('-drawing-color' + i); } - this.activeBackgroundColor = themeNode.get_color('-drawing-background-color'); - this.currentLineWidth = themeNode.get_length('-drawing-line-width'); - this.currentLineJoin = themeNode.get_double('-drawing-line-join'); - this.currentLineCap = themeNode.get_double('-drawing-line-cap'); - this.dashArray = [themeNode.get_length('-drawing-dash-array-on'), themeNode.get_length('-drawing-dash-array-off')]; - this.dashOffset = themeNode.get_length('-drawing-dash-offset'); let font = themeNode.get_font(); - this.fontFamily = font.get_family(); - this.currentFontWeight = font.get_weight(); - this.currentFontStyle = font.get_style(); + 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); } @@ -169,45 +254,81 @@ var DrawingArea = new Lang.Class({ for (let i = 1; i < 10; i++) { this.colors[i] = this.colors[i].alpha ? this.colors[i] : this.colors[0]; } - this.currentColor = this.colors[1]; - - this.currentLineWidth = (this.currentLineWidth > 0) ? this.currentLineWidth : 3; - this.currentLineJoin = ([0, 1, 2].indexOf(this.currentLineJoin) != -1) ? this.currentLineJoin : Cairo.LineJoin.ROUND; - this.currentLineCap = ([0, 1, 2].indexOf(this.currentLineCap) != -1) ? this.currentLineCap : Cairo.LineCap.ROUND; - this.currentFontFamilyId = 0; - this.currentFontWeight = this.currentFontWeight > 500 ? 1 : 0 ; - // font style enum order of Cairo and Pango are different - this.currentFontStyle = this.currentFontStyle == 2 ? 1 : ( this.currentFontStyle == 1 ? 2 : 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); }, - vfunc_repaint: function() { - let cr = this.get_context(); + _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++) { - let isStraightLine = this.elements[i].shape == Shapes.LINE && - (this.elements[i].points.length < 3 || this.elements[i].points[2] == this.elements[i].points[1] || this.elements[i].points[2] == this.elements[i].points[0]); + cr.save(); - if (this.elements[i].fill && !isStraightLine) { - // first paint stroke - this.elements[i].buildCairo(cr, false); + 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(); - // secondly paint fill - this.elements[i].buildCairo(cr, false); - cr.fill(); - } else { - this.elements[i].buildCairo(cr, false); - cr.stroke(); - } + } + + cr.stroke(); + cr.restore(); } if (this.currentElement) { - this.currentElement.buildCairo(cr, this.textHasCursor); + 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(); } - cr.$dispose(); + 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) { @@ -216,21 +337,26 @@ var DrawingArea = new Lang.Class({ let button = event.get_button(); let [x, y] = event.get_coords(); + let controlPressed = event.has_control_modifier(); let shiftPressed = event.has_shift_modifier(); - // stop writing - if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.currentElement.state == TextState.WRITING ) { + if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) + // finish writing this._stopWriting(); - } - // hide helper if (this.helper.visible) { - this.helper.hideHelp(); + // hide helper + this.toggleHelp(); return Clutter.EVENT_STOP; } if (button == 1) { - this._startDrawing(x, y, shiftPressed); + 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(); @@ -246,7 +372,7 @@ var DrawingArea = new Lang.Class({ _onKeyboardPopupMenu: function() { this._stopDrawing(); if (this.helper.visible) - this.helper.hideHelp(); + this.toggleHelp(); this.menu.popup(); return Clutter.EVENT_STOP; }, @@ -266,35 +392,32 @@ var DrawingArea = new Lang.Class({ }, _onKeyPressed: function(actor, event) { - if (event.get_key_symbol() == Clutter.KEY_Escape) { - this.emit('stop-drawing'); + 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 (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.currentElement.state == TextState.WRITING) { - if (event.get_key_symbol() == Clutter.KEY_BackSpace) { - this.currentElement.text = this.currentElement.text.slice(0, -1); - this._updateTextCursorTimeout(); - } else if (event.has_control_modifier() && event.get_key_symbol() == 118) { - // Ctrl + V - St.Clipboard.get_default().get_text(St.ClipboardType.CLIPBOARD, (clipBoard, clipText) => { - this.currentElement.text += clipText; - this._updateTextCursorTimeout(); - this._redisplay(); - }); - return Clutter.EVENT_STOP; - } else if (event.get_key_symbol() == Clutter.KEY_Return || event.get_key_symbol() == 65421) { - // stop writing - // Clutter.KEY_Return is "Enter" and 65421 is KP_Enter - this._stopWriting(); - } else if (event.has_control_modifier()){ - // it's a shortcut, do not write text - return Clutter.EVENT_PROPAGATE; - } else { - let unicode = event.get_key_unicode(); - this.currentElement.text += unicode; - this._updateTextCursorTimeout(); - } - this._redisplay(); + } 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 { @@ -315,6 +438,148 @@ var DrawingArea = new Lang.Class({ 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); @@ -326,26 +591,35 @@ var DrawingArea = new Lang.Class({ }); this.currentElement = new DrawingElement ({ - shape: this.currentShape, + shape: this.currentTool, color: this.currentColor.to_string(), line: { lineWidth: this.currentLineWidth, lineJoin: this.currentLineJoin, lineCap: this.currentLineCap }, - dash: { array: this.dashedLine ? this.dashArray : [0, 0] , offset: this.dashedLine ? this.dashOffset : 0 }, + 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 }, - text: '', - font: { family: (this.currentFontFamilyId == 0 ? this.fontFamily : FontFamilyNames[this.currentFontFamilyId]), weight: this.currentFontWeight, style: this.currentFontStyle }, - points: [[startX, startY]] + points: [] }); - if (this.currentShape == Shapes.TEXT) { - this.currentElement.line = { lineWidth: 1, lineJoin: 0, lineCap: 0 }; - this.currentElement.dash = { array: [1, 1] , offset: 0 }; + 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.state = TextState.DRAWING; + 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; @@ -359,6 +633,16 @@ var DrawingArea = new Lang.Class({ }); }, + _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); @@ -369,20 +653,16 @@ var DrawingArea = new Lang.Class({ this.buttonReleasedHandler = null; } - // skip when the size is too small to be visible (3px) (except for free drawing) - if (this.currentElement && this.currentElement.points.length >= 2 && - (this.currentShape == Shapes.NONE || - Math.hypot(this.currentElement.points[1][0] - this.currentElement.points[0][0], this.currentElement.points[1][1] - this.currentElement.points[0][1]) > 3)) { - - // start writing - if (this.currentElement.shape == Shapes.TEXT && this.currentElement.state == TextState.DRAWING) { - this.currentElement.state = TextState.WRITING; - this.currentElement.text = ''; - this.emit('show-osd', null, _("Type your text\nand press Enter"), -1); - this._updateTextCursorTimeout(); - this.textHasCursor = true; - this._redisplay(); - this.updatePointerCursor(); + // 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; } @@ -394,29 +674,84 @@ var DrawingArea = new Lang.Class({ this.updatePointerCursor(); }, - _updateDrawing: function(x, y, controlPressed) { - if (!this.currentElement) - return; - if (this.currentElement.shape == Shapes.NONE) - this.currentElement.addPoint(x, y, controlPressed); - else if ((this.currentElement.shape == Shapes.RECTANGLE || this.currentElement.shape == Shapes.TEXT) && (controlPressed || this.currentElement.transform.active)) - this.currentElement.transformRectangle(x, y); - else if (this.currentElement.shape == Shapes.ELLIPSE && (controlPressed || this.currentElement.transform.active)) - this.currentElement.transformEllipse(x, y); - else if (this.currentElement.shape == Shapes.LINE && (controlPressed || this.currentElement.transform.active)) - this.currentElement.transformLine(x, y); - else - this.currentElement.points[1] = [x, y]; - + _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.updatePointerCursor(controlPressed); + + 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() { + _stopWriting: function(startNewLine) { if (this.currentElement.text.length > 0) this.elements.push(this.currentElement); - this.currentElement = null; - this._stopTextCursorTimeout(); + + 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(); }, @@ -428,15 +763,26 @@ var DrawingArea = new Lang.Class({ }, updatePointerCursor: function(controlPressed) { - if (!this.currentElement || (this.currentElement.shape == Shapes.TEXT && this.currentElement.state == TextState.WRITING)) - this.setPointerCursor(this.currentShape == Shapes.NONE ? 'POINTING_HAND' : 'CROSSHAIR'); + 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) { - Mainloop.source_remove(this.textCursorTimeoutId); + GLib.source_remove(this.textCursorTimeoutId); this.textCursorTimeoutId = null; } this.textHasCursor = false; @@ -444,7 +790,7 @@ var DrawingArea = new Lang.Class({ _updateTextCursorTimeout: function() { this._stopTextCursorTimeout(); - this.textCursorTimeoutId = Mainloop.timeout_add(600, () => { + this.textCursorTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, TEXT_CURSOR_TIME, () => { this.textHasCursor = !this.textHasCursor; this._redisplay(); return GLib.SOURCE_CONTINUE; @@ -452,9 +798,9 @@ var DrawingArea = new Lang.Class({ }, erase: function() { + this.deleteLastElement(); this.elements = []; this.undoneElements = []; - this.currentElement = null; this._redisplay(); }, @@ -468,8 +814,9 @@ var DrawingArea = new Lang.Class({ this.disconnect(this.buttonReleasedHandler); this.buttonReleasedHandler = null; } + if (this.isWriting) + this._stopWriting(); this.currentElement = null; - this._stopTextCursorTimeout(); } else { this.elements.pop(); } @@ -500,6 +847,11 @@ var DrawingArea = new Lang.Class({ 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) { @@ -525,73 +877,118 @@ var DrawingArea = new Lang.Class({ this.currentElement.color = this.currentColor.to_string(); this._redisplay(); } - this.emit('show-osd', null, `${this.currentColor.to_string()}`, -1); + // 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); }, - selectShape: function(shape) { - this.currentShape = shape; - this.emit('show-osd', null, _(ShapeNames[shape]), -1); + 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") : _("Stroke"), -1); + 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); + 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, this.currentLineWidth + " " + _("px"), 2 * this.currentLineWidth); + 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); + 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); + 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() { - this.currentFontWeight = this.currentFontWeight == 1 ? 0 : this.currentFontWeight + 1; - if (this.currentElement) { + 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); + this.emit('show-osd', null, `` + + `${_(FontWeightNames[this.currentFontWeight])}`, "", -1, false); }, toggleFontStyle: function() { this.currentFontStyle = this.currentFontStyle == 2 ? 0 : this.currentFontStyle + 1; - if (this.currentElement) { + if (this.currentElement && this.currentElement.font) { this.currentElement.font.style = this.currentFontStyle; this._redisplay(); } - this.emit('show-osd', null, `${_(FontStyleNames[this.currentFontStyle])}`, -1); + this.emit('show-osd', null, `` + + `${_(FontStyleNames[this.currentFontStyle])}`, "", -1, false); }, toggleFontFamily: function() { - this.currentFontFamilyId = this.currentFontFamilyId == 5 ? 0 : this.currentFontFamilyId + 1; - let currentFontFamily = this.currentFontFamilyId == 0 ? this.fontFamily : FontFamilyNames[this.currentFontFamilyId]; - if (this.currentElement) { + 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); + 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) + if (this.helper.visible) { this.helper.hideHelp(); - else + 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() { @@ -601,7 +998,7 @@ var DrawingArea = new Lang.Class({ 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.hasBackground ? this.activeBackgroundColor : null); + this.get_parent().set_background_color(this.reactive && this.hasBackground ? this.activeBackgroundColor : null); this._updateStyle(); }, @@ -639,17 +1036,10 @@ var DrawingArea = new Lang.Class({ this.scrollHandler = null; } - if (this.helper.visible) - this.helper.hideHelp(); - this.currentElement = null; this._stopTextCursorTimeout(); - this.currentShape = Shapes.NONE; - this.dashedLine = false; - this.fill = false; this._redisplay(); - if (this._menu) - this._menu.close(); + this.closeMenu(); this.get_parent().set_background_color(null); if (save) this.savePersistent(); @@ -657,17 +1047,23 @@ var DrawingArea = new Lang.Class({ saveAsSvg: function() { // stop drawing or writing - if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.currentElement.state == TextState.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); } @@ -695,7 +1091,7 @@ var DrawingArea = new Lang.Class({ _saveAsJson: function(name, notify) { // stop drawing or writing - if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.currentElement.state == TextState.WRITING) { + if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) { this._stopWriting(); } else if (this.currentElement && this.currentElement.shape != Shapes.TEXT) { this._stopDrawing(); @@ -727,13 +1123,15 @@ var DrawingArea = new Lang.Class({ if (name == Me.metadata['persistent-file-name'] && contents == oldContents) return; - GLib.file_set_contents(path, contents); - if (notify) - this.emit('show-osd', 'document-save-symbolic', name, -1); - if (name != Me.metadata['persistent-file-name']) { - this.jsonName = name; - this.lastJsonContents = contents; - } + 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) { @@ -758,6 +1156,15 @@ var DrawingArea = new Lang.Class({ }, _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`]); @@ -771,7 +1178,7 @@ var DrawingArea = new Lang.Class({ this.elements.push(...JSON.parse(contents).map(object => new DrawingElement(object))); if (notify) - this.emit('show-osd', 'document-open-symbolic', name, -1); + this.emit('show-osd', 'document-open-symbolic', name, "", -1, false); if (name != Me.metadata['persistent-file-name']) { this.jsonName = name; this.lastJsonContents = contents; @@ -783,9 +1190,6 @@ var DrawingArea = new Lang.Class({ }, loadJson: function(name, notify) { - this.elements = []; - this.currentElement = null; - this._stopTextCursorTimeout(); this._loadJson(name, notify); this._redisplay(); }, @@ -813,14 +1217,18 @@ var DrawingArea = new Lang.Class({ get drawingContentsHasChanged() { let contents = `[\n ` + new Array(...this.elements.map(element => JSON.stringify(element))).join(`,\n\n `) + `\n]`; return contents != this.lastJsonContents; - }, - - disable: function() { - this.erase(); - this.menu.disable(); } }); +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. @@ -830,6 +1238,31 @@ const DrawingElement = new Lang.Class({ _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 @@ -840,222 +1273,647 @@ const DrawingElement = new Lang.Class({ line: this.line, dash: this.dash, fill: this.fill, + fillRule: this.fillRule, eraser: this.eraser, - transform: this.transform, + 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, showTextCursor) { + 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.array[0] > 0 && this.dash.array[1] > 0) + 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); - else - cr.setDash([1000000], 0); if (this.eraser) cr.setOperator(Cairo.Operator.CLEAR); else cr.setOperator(Cairo.Operator.OVER); - let [success, color] = Clutter.Color.from_string(this.color); - if (success) - Clutter.cairo_set_source_color(cr, color); + if (params.dummyStroke) + setDummyStroke(cr); - let [points, shape, trans] = [this.points, this.shape, this.transform]; + 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) { - this.rotate(cr, trans.angle + trans.startAngle, trans.center[0], trans.center[1]); - this.scale(cr, trans.ratio, trans.center[0], trans.center[1]); - let r = Math.hypot(points[1][0] - points[0][0], points[1][1] - points[0][1]); - cr.arc(points[0][0], points[0][1], r, 0, 2 * Math.PI); - this.scale(cr, 1 / trans.ratio, trans.center[0], trans.center[1]); - this.rotate(cr, - (trans.angle + trans.startAngle), trans.center[0], trans.center[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) { - this.rotate(cr, trans.angle, trans.center[0], trans.center[1]); cr.rectangle(points[0][0], points[0][1], points[1][0] - points[0][0], points[1][1] - points[0][1]); - this.rotate(cr, - trans.angle, trans.center[0], trans.center[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) { - this.rotate(cr, trans.angle, trans.center[0], trans.center[1]); - if (this.state == TextState.DRAWING) - cr.rectangle(points[0][0], points[0][1], points[1][0] - points[0][0], points[1][1] - points[0][1]); - cr.selectFontFace(this.font.family, this.font.style, this.font.weight); - cr.setFontSize(Math.abs(points[1][1] - points[0][1])); - cr.moveTo(Math.min(points[0][0], points[1][0]), Math.max(points[0][1], points[1][1])); - cr.showText((showTextCursor) ? (this.text + "_") : this.text); - this.rotate(cr, - trans.angle, trans.center[0], trans.center[1]); + 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 isStraightLine = this.shape == Shapes.LINE && (points.length < 3 || points[2] == points[1] || points[2] == points[0]); - let fill = this.fill && !isStraightLine; - let attributes = `fill="${fill ? color : 'transparent'}" ` + - `stroke="${color}" ` + - `${fill ? '' : 'fill-opacity="0"'} ` + - `stroke-width="${this.line.lineWidth}" ` + - `stroke-linecap="${LineCapNames[this.line.lineCap].toLowerCase()}" ` + - `stroke-linejoin="${LineJoinNames[this.line.lineJoin].toLowerCase()}"`; - - if (this.dash.array[0] > 0 && this.dash.array[1] > 0) - attributes += ` stroke-dasharray="${this.dash.array[0]} ${this.dash.array[1]}" stroke-dashoffset="${this.dash.offset}"`; + let fill = this.fill && !this.isStraightLine; + let attributes = ''; - if (this.shape == Shapes.LINE && points.length == 3) { + 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 += ``; + row += `${fill ? 'z' : ''}"${transAttribute}/>`; - } else if (this.shape == Shapes.NONE || this.shape == Shapes.LINE) { + } else if (this.shape == Shapes.LINE) { + row += ``; + + } else if (this.shape == Shapes.NONE) { row += ``; + row += `${fill ? 'z' : ''}"${transAttribute}/>`; - } else if (this.shape == Shapes.ELLIPSE && points.length == 2 && this.transform.ratio != 1) { + } 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 = ry * this.transform.ratio; - let angle = (this.transform.angle + this.transform.startAngle) * 180 / Math.PI; - row += ``; + 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 += ``; + row += ``; } else if (this.shape == Shapes.RECTANGLE && points.length == 2) { - let transAttribute = ""; - if (this.transform.angle != 0) { - let angle = this.transform.angle * 180 / Math.PI; - transAttribute = ` transform="rotate(${angle}, ${this.transform.center[0]}, ${this.transform.center[1]})"`; - } 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) { - let transAttribute = ""; - if (this.transform.angle != 0) { - let angle = this.transform.angle * 180 / Math.PI; - transAttribute = ` transform="rotate(${angle}, ${this.transform.center[0]}, ${this.transform.center[1]})"`; - } attributes = `fill="${color}" ` + `stroke="transparent" ` + `stroke-opacity="0" ` + - `font-family="${this.font.family}" ` + - `font-size="${Math.abs(points[1][1] - points[0][1])}" ` + - `font-weight="${FontWeightNames[this.font.weight].toLowerCase()}" ` + - `font-style="${FontStyleNames[this.font.style].toLowerCase()}"`; + `font-size="${Math.abs(points[1][1] - points[0][1])}"`; - row += `${this.text}`; + 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; }, - addPoint: function(x, y, smoothedStroke) { - this.points.push([x, y]); - if (smoothedStroke) - this.smooth(this.points.length - 1); + get lastTransformation() { + if (!this.transformations.length) + return null; + + return this.transformations[this.transformations.length - 1]; }, - 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]; + 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); + this._smooth(i); } }, - rotate: function(cr, angle, x, y) { - if (angle == 0) - return; - cr.translate(x, y); - cr.rotate(angle); - cr.translate(-x, -y); + 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]; + } + } }, - scale: function(cr, ratio, x, y) { - if (ratio == 1) - return; - cr.translate(x, y); - cr.scale(ratio, 1); - cr.translate(-x, -y); + startDrawing: function(startX, startY) { + this.points.push([startX, startY]); + + if (this.shape == Shapes.POLYGON || this.shape == Shapes.POLYLINE) + this.points.push([startX, startY]); }, - transformRectangle: function(x, y) { + updateDrawing: function(x, y, transform) { let points = this.points; - if (points.length < 2 || points[0][0] == points[1][0] || points[0][1] == points[1][1]) + 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); - this.transform.center = [points[0][0] + (points[1][0] - points[0][0]) / 2, points[0][1] + (points[1][1] - points[0][1]) / 2]; + } 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; - this.transform.angle = getAngle(this.transform.center[0], this.transform.center[1], points[1][0], points[1][1], x, y); - this.transform.active = true; + 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]; + + } }, - transformEllipse: function(x, y) { - let points = this.points; - if (points.length < 2 || points[0][0] == points[1][0] || points[0][1] == points[1][1]) - return; + 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(); + } - this.transform.center = [points[0][0], points[0][1]]; - - let r1 = Math.hypot(points[1][0] - points[0][0], points[1][1] - points[0][1]); - let r2 = Math.hypot(x - points[0][0], y - points[0][1]); - this.transform.ratio = r2 / r1; - - this.transform.angle = getAngle(this.transform.center[0], this.transform.center[1], points[1][0], points[1][1], x, y); - if (!this.transform.startAngle) - // that is the angle between the direction when starting ellipticalizing, and the x-axis - this.transform.startAngle = getAngle(points[0][0], points[0][1], points[0][0] + 1, points[0][1], points[1][0], points[1][1]); - this.transform.active = true; + if (this.transformations[0] && this.transformations[0].type == Transformations.ROTATION && + Math.abs(this.transformations[0].angle) < MIN_ROTATION_ANGLE) + this.transformations.shift(); }, - transformLine: function(x, y) { - if (this.points.length < 2) - return; - if (this.points.length == 2) - this.points[2] = this.points[1]; - this.points[1] = [x, y]; - this.transform.active = true; + 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 angle = Math.acos( ((xA - xO)*(xB - xO) + (yA - yO)*(yB - yO)) / (Math.hypot(xA - xO, yA - yO) * Math.hypot(xB - xO, 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 - // 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; + 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; }; @@ -1079,21 +1937,48 @@ var DrawingHelper = new Lang.Class({ 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") })); - let settings = Convenience.getSettings(); - 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 (!settings.get_strv(settingKey)[0]) + if (!this.settings.get_strv(settingKey)[0]) continue; - let [keyval, mods] = Gtk.accelerator_parse(settings.get_strv(settingKey)[0]); + 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); @@ -1108,7 +1993,8 @@ var DrawingHelper = new Lang.Class({ } 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.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); } @@ -1120,9 +2006,9 @@ var DrawingHelper = new Lang.Class({ continue; } let hbox = new St.BoxLayout({ vertical: false }); - if (!settings.get_strv(settingKey)[0]) + if (!this.settings.get_strv(settingKey)[0]) continue; - let [keyval, mods] = Gtk.accelerator_parse(settings.get_strv(settingKey)[0]); + 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); @@ -1136,6 +2022,8 @@ var DrawingHelper = new Lang.Class({ 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]) })); @@ -1145,6 +2033,9 @@ var DrawingHelper = new Lang.Class({ }, showHelp: function() { + if (!this.vbox) + this._populate(); + this.opacity = 0; this.show(); @@ -1172,7 +2063,7 @@ var DrawingHelper = new Lang.Class({ transition: 'easeOutQuad', onComplete: this.hide.bind(this) }); - }, + } }); const getActor = function(object) { @@ -1193,6 +2084,7 @@ const DrawingMenu = new Lang.Class({ 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 = () => {}; @@ -1210,8 +2102,11 @@ const DrawingMenu = new Lang.Class({ 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) }); @@ -1221,7 +2116,7 @@ const DrawingMenu = new Lang.Class({ disable: function() { this.menuManager.removeMenu(this.menu); Main.layoutManager.uiGroup.remove_actor(this.menu.actor); - this.menu.actor.destroy(); + this.menu.destroy(); }, _onMenuOpenStateChanged: function(menu, open) { @@ -1230,7 +2125,7 @@ const DrawingMenu = new Lang.Class({ } else { this.area.updatePointerCursor(); // actionMode has changed, set previous actionMode in order to keep internal shortcuts working - Main.actionMode = Extension.DRAWING_ACTION_MODE | Shell.ActionMode.NORMAL; + this.area.updateActionMode(); this.area.grab_key_focus(); } }, @@ -1271,9 +2166,13 @@ 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, null, ShapeNames, this.area, 'currentShape', this.updateSectionVisibility.bind(this)); + 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.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(); @@ -1287,19 +2186,22 @@ const DrawingMenu = new Lang.Class({ this.lineSection = lineSection; let fontSection = new PopupMenu.PopupMenuSection(); - let FontFamilyNamesCopy = Object.create(FontFamilyNames); - FontFamilyNamesCopy[0] = this.area.fontFamily; - this._addSubMenuItem(fontSection, 'font-x-generic-symbolic', FontFamilyNamesCopy, this.area, 'currentFontFamilyId'); + 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._addSwitchItemWithCallback(this.menu, _("Hide panel and dock"), manager.hiddenList ? true : false, manager.togglePanelAndDockOpacity.bind(manager)); - this._addSwitchItemWithCallback(this.menu, _("Add a drawing background"), this.area.hasBackground, this.area.toggleBackground.bind(this.area)); - this._addSwitchItemWithCallback(this.menu, _("Square drawing area"), this.area.isSquareArea, this.area.toggleSquareArea.bind(this.area)); + 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); @@ -1310,37 +2212,54 @@ const 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.updateSectionVisibility(); + this._updateSectionVisibility(); }, - updateSectionVisibility: function() { - if (this.area.currentShape != Shapes.TEXT) { + _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) { + _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); - item.icon.set_gicon(target[targetProperty] ? iconTrue : iconFalse); + 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; - item.icon.set_gicon(target[targetProperty] ? iconTrue : iconFalse); + 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; }, - _addSwitchItemWithCallback: function(menu, label, active, onToggled) { + _addSimpleSwitchItem: function(menu, label, active, onToggled) { let item = new PopupMenu.PopupSwitchMenuItem(label, active); item.connect('toggled', onToggled); menu.addMenuItem(item); @@ -1348,7 +2267,7 @@ const DrawingMenu = new Lang.Class({ _addSliderItem: function(menu, target, targetProperty) { let item = new PopupMenu.PopupBaseMenuItem({ activate: false }); - let label = new St.Label({ text: target[targetProperty] + " " + _("px"), style_class: 'draw-on-your-screen-menu-slider-label' }); + 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') { @@ -1390,19 +2309,19 @@ const DrawingMenu = new Lang.Class({ item.menu.close(); }; - Mainloop.timeout_add(0, () => { + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { for (let i in obj) { let text; - if (targetProperty == 'currentFontFamilyId') + if (targetProperty == 'currentFontGeneric') text = `${_(obj[i])}`; else if (targetProperty == 'currentFontWeight') - text = `${_(obj[i])}`; + text = `${_(obj[i])}`; else if (targetProperty == 'currentFontStyle') text = `${_(obj[i])}`; else text = _(obj[i]); - let iCaptured = i; + let iCaptured = Number(i); let subItem = item.menu.addAction(text, () => { item.label.set_text(_(obj[iCaptured])); target[targetProperty] = iCaptured; @@ -1411,6 +2330,12 @@ 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) + item.menu.moveMenuItem(subItem, 4); + else if (obj == ToolNames && i == Shapes.POLYLINE) + item.menu.moveMenuItem(subItem, 5); } return GLib.SOURCE_REMOVE; }); @@ -1419,22 +2344,24 @@ const DrawingMenu = new Lang.Class({ _addColorSubMenuItem: function(menu) { let item = new PopupMenu.PopupSubMenuMenuItem(_("Color"), true); - item.icon.set_icon_name('document-edit-symbolic'); + 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(); }; - Mainloop.timeout_add(0, () => { + 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 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; }); @@ -1467,13 +2394,13 @@ const DrawingMenu = new Lang.Class({ item.menu.close(); }; - Mainloop.timeout_add(0, () => { - this._populateOpenDrawingSubMenu(); - // small trick to prevent the menu from "jumping" on first opening - item.menu.open(); - item.menu.close(); - return GLib.SOURCE_REMOVE; - }); + item.menu.openOld = item.menu.open; + item.menu.open = (animate) => { + if (!item.menu.isOpen) + this._populateOpenDrawingSubMenu(); + item.menu.openOld(); + }; + menu.addMenuItem(item); }, @@ -1502,7 +2429,7 @@ const DrawingMenu = new Lang.Class({ deleteButton.connect('clicked', () => { file.delete(); - this._populateOpenDrawingSubMenu(); + item.destroy(); }); }); @@ -1520,13 +2447,12 @@ const DrawingMenu = new Lang.Class({ item.menu.close(); }; - Mainloop.timeout_add(0, () => { - this._populateSaveDrawingSubMenu(); - // small trick to prevent the menu from "jumping" on first opening - item.menu.open(); - item.menu.close(); - return GLib.SOURCE_REMOVE; - }); + item.menu.openOld = item.menu.open; + item.menu.open = (animate) => { + if (!item.menu.isOpen) + this._populateSaveDrawingSubMenu(); + item.menu.openOld(); + }; menu.addMenuItem(item); }, @@ -1535,22 +2461,24 @@ const DrawingMenu = new Lang.Class({ }, _populateSaveDrawingSubMenu: function() { - this.saveEntry = new DrawingMenuEntry({ initialTextGetter: getDateString, + this.saveDrawingSubMenu.removeAll(); + let saveEntry = new DrawingMenuEntry({ initialTextGetter: getDateString, entryActivateCallback: (text) => { this.area.saveAsJsonWithName(text); this.saveDrawingSubMenu.toggle(); this._updateDrawingNameMenuItem(); - this._populateOpenDrawingSubMenu(); }, invalidStrings: [Me.metadata['persistent-file-name'], '/'], primaryIconName: 'insert-text' }); - this.saveDrawingSubMenu.addMenuItem(this.saveEntry.item); + this.saveDrawingSubMenu.addMenuItem(saveEntry.item); }, _addSeparator: function(menu) { - let separatorItem = new PopupMenu.PopupSeparatorMenuItem(' '); - getActor(separatorItem).add_style_class_name('draw-on-your-screen-menu-separator-item'); - menu.addMenuItem(separatorItem); + if (this.hasSeparators) { + let separatorItem = new PopupMenu.PopupSeparatorMenuItem(' '); + getActor(separatorItem).add_style_class_name('draw-on-your-screen-menu-separator-item'); + menu.addMenuItem(separatorItem); + } } }); diff --git a/extension.js b/extension.js index ef5d9ed..2f42a55 100644 --- a/extension.js +++ b/extension.js @@ -39,9 +39,11 @@ const Draw = Me.imports.draw; const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; const GS_VERSION = Config.PACKAGE_VERSION; +const HIDE_TIMEOUT_LONG = 2500; // ms, default is 1500 ms -// DRAWING_ACTION_MODE is a custom Shell.ActionMode -var DRAWING_ACTION_MODE = Math.pow(2,14); +// custom Shell.ActionMode, assuming that they are unused +const DRAWING_ACTION_MODE = Math.pow(2,14); +const WRITING_ACTION_MODE = Math.pow(2,15); // use 'login-dialog-message-warning' class in order to get GS theme warning color (default: #f57900) var WARNING_COLOR_STYLE_CLASS_NAME = 'login-dialog-message-warning'; @@ -79,6 +81,12 @@ var AreaManager = new Lang.Class({ Shell.ActionMode.ALL, this.toggleDrawing.bind(this)); + Main.wm.addKeybinding('toggle-modal', + this.settings, + Meta.KeyBindingFlags.NONE, + Shell.ActionMode.ALL, + this.toggleModal.bind(this)); + Main.wm.addKeybinding('erase-drawing', this.settings, Meta.KeyBindingFlags.NONE, @@ -159,51 +167,74 @@ var AreaManager = new Lang.Class({ container.set_position(monitor.x, monitor.y); container.set_size(monitor.width, monitor.height); area.set_size(monitor.width, monitor.height); - area.stopDrawingHandler = area.connect('stop-drawing', this.toggleDrawing.bind(this)); + 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)); this.areas.push(area); } }, addInternalKeybindings: function() { - this.internalKeybindings = { + // unavailable when writing + this.internalKeybindings1 = { 'undo': this.activeArea.undo.bind(this.activeArea), 'redo': this.activeArea.redo.bind(this.activeArea), 'delete-last-element': this.activeArea.deleteLastElement.bind(this.activeArea), 'smooth-last-element': this.activeArea.smoothLastElement.bind(this.activeArea), - 'save-as-svg': this.activeArea.saveAsSvg.bind(this.activeArea), - 'save-as-json': this.activeArea.saveAsJson.bind(this.activeArea), - 'open-previous-json': this.activeArea.loadPreviousJson.bind(this.activeArea), - 'open-next-json': this.activeArea.loadNextJson.bind(this.activeArea), - 'toggle-background': this.activeArea.toggleBackground.bind(this.activeArea), - 'toggle-square-area': this.activeArea.toggleSquareArea.bind(this.activeArea), 'increment-line-width': () => this.activeArea.incrementLineWidth(1), '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), - 'select-none-shape': () => this.activeArea.selectShape(Draw.Shapes.NONE), - 'select-line-shape': () => this.activeArea.selectShape(Draw.Shapes.LINE), - 'select-ellipse-shape': () => this.activeArea.selectShape(Draw.Shapes.ELLIPSE), - 'select-rectangle-shape': () => this.activeArea.selectShape(Draw.Shapes.RECTANGLE), - 'select-text-shape': () => this.activeArea.selectShape(Draw.Shapes.TEXT), + '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) + }; + + // available when writing + this.internalKeybindings2 = { + 'save-as-svg': this.activeArea.saveAsSvg.bind(this.activeArea), + 'save-as-json': this.activeArea.saveAsJson.bind(this.activeArea), + 'open-previous-json': this.activeArea.loadPreviousJson.bind(this.activeArea), + 'open-next-json': this.activeArea.loadNextJson.bind(this.activeArea), + 'toggle-background': this.activeArea.toggleBackground.bind(this.activeArea), + 'toggle-grid': this.activeArea.toggleGrid.bind(this.activeArea), + 'toggle-square-area': this.activeArea.toggleSquareArea.bind(this.activeArea), '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), 'toggle-panel-and-dock-visibility': this.togglePanelAndDockOpacity.bind(this), 'toggle-help': this.activeArea.toggleHelp.bind(this.activeArea), - 'open-user-stylesheet': this.openUserStyleFile.bind(this) + 'open-user-stylesheet': this.openUserStyleFile.bind(this), + 'open-preferences': this.openPreferences.bind(this) }; - for (let key in this.internalKeybindings) { + for (let key in this.internalKeybindings1) { Main.wm.addKeybinding(key, this.settings, Meta.KeyBindingFlags.NONE, DRAWING_ACTION_MODE, - this.internalKeybindings[key]); + this.internalKeybindings1[key]); + } + + for (let key in this.internalKeybindings2) { + Main.wm.addKeybinding(key, + this.settings, + Meta.KeyBindingFlags.NONE, + DRAWING_ACTION_MODE | WRITING_ACTION_MODE, + this.internalKeybindings2[key]); } for (let i = 1; i < 10; i++) { @@ -211,18 +242,28 @@ var AreaManager = new Lang.Class({ Main.wm.addKeybinding('select-color' + i, this.settings, Meta.KeyBindingFlags.NONE, - DRAWING_ACTION_MODE, + DRAWING_ACTION_MODE | WRITING_ACTION_MODE, () => this.activeArea.selectColor(iCaptured)); } }, removeInternalKeybindings: function() { - for (let key in this.internalKeybindings) { + for (let key in this.internalKeybindings1) Main.wm.removeKeybinding(key); - } - for (let i = 1; i < 10; i++) { + for (let key in this.internalKeybindings2) + Main.wm.removeKeybinding(key); + + for (let i = 1; i < 10; i++) Main.wm.removeKeybinding('select-color' + i); + }, + + openPreferences: function() { + // since GS 3.36 + if (ExtensionUtils.openPrefs) { + if (this.activeArea) + this.toggleDrawing(); + ExtensionUtils.openPrefs(); } }, @@ -262,19 +303,23 @@ var AreaManager = new Lang.Class({ // dash-to-dock let dtdContainers = Main.uiGroup.get_children().filter((actor) => { return actor.name && actor.name == 'dashtodockContainer' && - actor._delegate && + ((actor._delegate && actor._delegate._monitorIndex !== undefined && - actor._delegate._monitorIndex == activeIndex; + actor._delegate._monitorIndex == activeIndex) || + // dtd v68+ + (actor._monitorIndex !== undefined && + actor._monitorIndex == activeIndex)); }); // for simplicity, we assume that main dash-to-panel panel is displayed on primary monitor // and we hide all secondary panels together if the active area is not on the primary let name = activeIndex == Main.layoutManager.primaryIndex ? 'panelBox' : 'dashtopanelSecondaryPanelBox'; let panelBoxes = Main.uiGroup.get_children().filter((actor) => { - return actor.name && actor.name == name; + return actor.name && actor.name == name || + // dtp v37+ + actor.get_children().length && actor.get_children()[0].name && actor.get_children()[0].name == name; }); - let actorToHide = dtdContainers.concat(panelBoxes); this.hiddenList = []; for (let i = 0; i < actorToHide.length; i++) { @@ -284,100 +329,183 @@ var AreaManager = new Lang.Class({ } }, - toggleDrawing: function() { - if (this.activeArea) { - let activeIndex = this.areas.indexOf(this.activeArea); - let activeContainer = this.activeArea.get_parent(); - let save = activeIndex == Main.layoutManager.primaryIndex && this.settings.get_boolean('persistent-drawing'); - - if (this.hiddenList) - this.togglePanelAndDockOpacity(); - - Main.popModal(this.activeArea); - this.removeInternalKeybindings(); - this.activeArea.reactive = false; - this.activeArea.leaveDrawingMode(save); - this.activeArea = null; - - activeContainer.get_parent().remove_actor(activeContainer); + toggleContainer: function() { + if (!this.activeArea) + return; + + let activeContainer = this.activeArea.get_parent(); + let activeIndex = this.areas.indexOf(this.activeArea); + + if (activeContainer.get_parent() == Main.uiGroup) { + Main.uiGroup.set_child_at_index(Main.layoutManager.keyboardBox, this.oldKeyboardIndex); + Main.uiGroup.remove_actor(activeContainer); Main.layoutManager._backgroundGroup.insert_child_above(activeContainer, Main.layoutManager._bgManagers[activeIndex].backgroundActor); if (!this.settings.get_boolean("drawing-on-desktop")) activeContainer.hide(); - - setCursor('DEFAULT'); - if (!this.osdDisabled) - Main.osdWindowManager.show(activeIndex, this.leaveGicon, _("Leaving drawing mode"), null); - } else { - // avoid to deal with Meta changes (global.display/global.screen) - let currentIndex = Main.layoutManager.monitors.indexOf(Main.layoutManager.currentMonitor); - let activeContainer = this.areas[currentIndex].get_parent(); - - activeContainer.get_parent().remove_actor(activeContainer); + } else { + Main.layoutManager._backgroundGroup.remove_actor(activeContainer); Main.uiGroup.add_child(activeContainer); - + // move the keyboard above the area to make it available with text entries + this.oldKeyboardIndex = Main.uiGroup.get_children().indexOf(Main.layoutManager.keyboardBox); + Main.uiGroup.set_child_above_sibling(Main.layoutManager.keyboardBox, activeContainer); + } + }, + + toggleModal: function(source) { + if (!this.activeArea) + return; + + this.activeArea.closeMenu(); + + if (Main._findModal(this.activeArea) != -1) { + Main.popModal(this.activeArea); + if (source && source == global.display) + this.showOsd(null, 'touchpad-disabled-symbolic', _("Keyboard and pointer released"), null, null, false); + setCursor('DEFAULT'); + this.activeArea.reactive = false; + this.removeInternalKeybindings(); + } else { // add Shell.ActionMode.NORMAL to keep system keybindings enabled (e.g. Alt + F2 ...) - if (!Main.pushModal(this.areas[currentIndex], { actionMode: DRAWING_ACTION_MODE | Shell.ActionMode.NORMAL })) - return; - this.activeArea = this.areas[currentIndex]; + let actionMode = (this.activeArea.isWriting ? WRITING_ACTION_MODE : DRAWING_ACTION_MODE) | Shell.ActionMode.NORMAL; + if (!Main.pushModal(this.activeArea, { actionMode: actionMode })) + return false; this.addInternalKeybindings(); this.activeArea.reactive = true; - this.activeArea.enterDrawingMode(); + this.activeArea.initPointerCursor(); + if (source && source == global.display) + this.showOsd(null, 'input-touchpad-symbolic', _("Keyboard and pointer grabbed"), null, null, false); + } + + return true; + }, + + toggleDrawing: function() { + if (this.activeArea) { + let activeIndex = this.areas.indexOf(this.activeArea); + let save = activeIndex == Main.layoutManager.primaryIndex && this.settings.get_boolean('persistent-drawing'); - setCursor('POINTING_HAND'); - this.osdDisabled = this.settings.get_boolean('osd-disabled'); - if (!this.osdDisabled) { - // increase OSD display time - let hideTimeoutSave = OsdWindow.HIDE_TIMEOUT; - try { OsdWindow.HIDE_TIMEOUT = 2000; } catch(e) { /* HIDE_TIMEOUT is not exported with 'var' */ } - Main.osdWindowManager.show(currentIndex, this.enterGicon, _("Press Ctrl + F1 for help") + "\n\n" + _("Entering drawing mode"), null); - try { OsdWindow.HIDE_TIMEOUT = hideTimeoutSave; } catch(e) {} + this.showOsd(null, this.leaveGicon, _("Leaving drawing mode")); + this.activeArea.leaveDrawingMode(save); + if (this.hiddenList) + this.togglePanelAndDockOpacity(); + + if (Main._findModal(this.activeArea) != -1) + this.toggleModal(); + this.toggleContainer(); + this.activeArea = null; + } else { + // avoid to deal with Meta changes (global.display/global.screen) + let currentIndex = Main.layoutManager.monitors.indexOf(Main.layoutManager.currentMonitor); + this.activeArea = this.areas[currentIndex]; + this.toggleContainer(); + if (!this.toggleModal()) { + this.toggleContainer(); + this.activeArea = null; + return; } + + this.activeArea.enterDrawingMode(); + this.osdDisabled = this.settings.get_boolean('osd-disabled'); + let label = _("Press %s for help").format(this.activeArea.helper.helpKeyLabel) + "\n\n" + _("Entering drawing mode"); + this.showOsd(null, this.enterGicon, label, null, null, true); } if (this.indicator) - this.indicator.sync(this.activeArea != null); + this.indicator.sync(Boolean(this.activeArea)); }, - // use level -1 to set no level (null) - showOsd: function(emitter, iconName, label, level) { - if (this.osdDisabled) - return; + updateActionMode: function() { + Main.actionMode = (this.activeArea.isWriting ? WRITING_ACTION_MODE : DRAWING_ACTION_MODE) | Shell.ActionMode.NORMAL; + }, + + // Use level -1 to set no level through a signal. + showOsd: function(emitter, icon, label, color, level, long) { let activeIndex = this.areas.indexOf(this.activeArea); - if (activeIndex != -1) { - let maxLevel; - if (level == -1) - level = null; - else if (level > 100) - maxLevel = 2; + if (activeIndex == -1 || this.osdDisabled) + return; + + let hideTimeoutSave; + if (long && GS_VERSION >= '3.28.0') { + hideTimeoutSave = OsdWindow.HIDE_TIMEOUT; + OsdWindow.HIDE_TIMEOUT = HIDE_TIMEOUT_LONG; + } + + let maxLevel; + if (level == -1) + level = null; + else if (level > 100) + maxLevel = 2; + + // GS 3.32- : bar from 0 to 100 + // GS 3.34+ : bar from 0 to 1 + if (level && GS_VERSION > '3.33.0') + level = level / 100; + + if (icon && typeof icon == 'string') + icon = new Gio.ThemedIcon({ name: icon }); + else if (!icon) + icon = this.enterGicon; + + let osdWindow = Main.osdWindowManager._osdWindows[activeIndex]; + + try { + if (!this.osdWindowConstraint) + this.osdWindowConstraint = new OsdWindowConstraint(); - // GS 3.32- : bar from 0 to 100 - // GS 3.34+ : bar from 0 to 1 - if (level && GS_VERSION > '3.33.0') - level = level / 100; - - let icon = iconName && new Gio.ThemedIcon({ name: iconName }); - Main.osdWindowManager.show(activeIndex, icon || this.enterGicon, label, level, maxLevel); - Main.osdWindowManager._osdWindows[activeIndex]._label.get_clutter_text().set_use_markup(true); - - if (level === 0) { - Main.osdWindowManager._osdWindows[activeIndex]._label.add_style_class_name(WARNING_COLOR_STYLE_CLASS_NAME); - // the same label is shared by all GS OSD so the style must be removed after being used - let osdLabelChangedHandler = Main.osdWindowManager._osdWindows[activeIndex]._label.connect('notify::text', () => { - Main.osdWindowManager._osdWindows[activeIndex]._label.remove_style_class_name(WARNING_COLOR_STYLE_CLASS_NAME); - Main.osdWindowManager._osdWindows[activeIndex]._label.disconnect(osdLabelChangedHandler); + if (!osdWindow._box.get_constraint(this.osdWindowConstraint.constructor.name)) { + osdWindow._box.remove_constraint(osdWindow._boxConstraint); + osdWindow._box.add_constraint_with_name(this.osdWindowConstraint.constructor.name, this.osdWindowConstraint); + this.osdWindowConstraint._minSize = osdWindow._boxConstraint._minSize; + osdWindow._boxConstraintOld = osdWindow._boxConstraint; + osdWindow._boxConstraint = this.osdWindowConstraint; + let osdConstraintHandler = osdWindow._box.connect('notify::mapped', (box) => { + if (!box.mapped) { + osdWindow._boxConstraint = osdWindow._boxConstraintOld; + osdWindow._boxConstraint._minSize = this.osdWindowConstraint._minSize; + osdWindow._box.remove_constraint(this.osdWindowConstraint); + osdWindow._box.add_constraint(osdWindow._boxConstraint); + osdWindow._box.disconnect(osdConstraintHandler); + } }); } + } catch(e) { + logError(e); } + + Main.osdWindowManager.show(activeIndex, icon, label, level, maxLevel); + osdWindow._label.get_clutter_text().set_use_markup(true); + + if (color) { + osdWindow._icon.set_style(`color:${color};`); + osdWindow._label.set_style(`color:${color};`); + let osdColorChangedHandler = osdWindow._label.connect('notify::text', () => { + osdWindow._icon.set_style(`color:;`); + osdWindow._label.set_style(`color:;`); + osdWindow._label.disconnect(osdColorChangedHandler); + }); + } + + if (level === 0) { + osdWindow._label.add_style_class_name(WARNING_COLOR_STYLE_CLASS_NAME); + // the same label is shared by all GS OSD so the style must be removed after being used + let osdLabelChangedHandler = osdWindow._label.connect('notify::text', () => { + osdWindow._label.remove_style_class_name(WARNING_COLOR_STYLE_CLASS_NAME); + osdWindow._label.disconnect(osdLabelChangedHandler); + }); + } + + if (hideTimeoutSave) + OsdWindow.HIDE_TIMEOUT = hideTimeoutSave; }, removeAreas: function() { for (let i = 0; i < this.areas.length; i++) { let area = this.areas[i]; + area.disconnect(area.leaveDrawingHandler); + area.disconnect(area.updateActionModeHandler); + area.disconnect(area.showOsdHandler); let container = area.get_parent(); container.get_parent().remove_actor(container); - area.disconnect(area.stopDrawingHandler); - area.disconnect(area.showOsdHandler); - area.disable(); container.destroy(); } this.areas = []; @@ -412,6 +540,7 @@ var AreaManager = new Lang.Class({ if (this.activeArea) this.toggleDrawing(); Main.wm.removeKeybinding('toggle-drawing'); + Main.wm.removeKeybinding('toggle-modal'); Main.wm.removeKeybinding('erase-drawing'); this.removeAreas(); if (this.indicator) @@ -419,7 +548,30 @@ var AreaManager = new Lang.Class({ } }); -var DrawingIndicator = new Lang.Class({ +// The same as the original, without forcing a ratio of 1. +const OsdWindowConstraint = new Lang.Class({ + Name: 'DrawOnYourScreenOsdWindowConstraint', + Extends: OsdWindow.OsdWindowConstraint, + + vfunc_update_allocation: function(actor, actorBox) { + // Clutter will adjust the allocation for margins, + // so add it to our minimum size + let minSize = this._minSize + actor.margin_top + actor.margin_bottom; + let [width, height] = actorBox.get_size(); + + // DO NOT Enforce a ratio of 1 + let newWidth = Math.ceil(Math.max(minSize, width, height)); + let newHeight = Math.ceil(Math.max(minSize, height)); + actorBox.set_size(newWidth, newHeight); + + // Recenter + let [x, y] = actorBox.get_origin(); + actorBox.set_origin(Math.ceil(x + width / 2 - newWidth / 2), + Math.ceil(y + height / 2 - newHeight / 2)); + } +}); + +const DrawingIndicator = new Lang.Class({ Name: 'DrawOnYourScreenIndicator', _init: function() { diff --git a/locale/draw-on-your-screen.pot b/locale/draw-on-your-screen.pot index 2df55f0..ee4bf35 100644 --- a/locale/draw-on-your-screen.pot +++ b/locale/draw-on-your-screen.pot @@ -4,11 +4,13 @@ # This file is distributed under the same license as Draw On Your Screen. # FIRST AUTHOR , YEAR. # +# Some words refer to SVG attributes (font, line, fill rule ...). +# You are free to translate them or not. msgid "" msgstr "" "Project-Id-Version: Draw On Your Screen VERSION\n" "Report-Msgid-Bugs-To: https://framagit.org/abakkk/DrawOnYourScreen/issues\n" -"POT-Creation-Date: 2020-01-03 08:00+0100\n" +"POT-Creation-Date: 2019-03-04 16:40+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,6 +19,19 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +msgid "About" +msgstr "" + +# You are free to translate the extension name, that is displayed in About page, or not. +msgid "Draw On You Screen" +msgstr "" + +msgid "Version %d" +msgstr "" + +msgid "Start drawing with Super+Alt+D and save your beautiful work by taking a screenshot" +msgstr "" + # Add your name here, for example: # (add "\n" as separator if there is many translators) # msgid "translator-credits" @@ -32,47 +47,225 @@ msgstr "" msgid "translator-credits" msgstr "" -#: extension.js -msgid "Leaving drawing mode" +msgid "Preferences" msgstr "" -msgid "Press Ctrl + F1 for help" +msgid "Global" msgstr "" -msgid "Entering drawing mode" +msgid "Enter/leave drawing mode" msgstr "" -#: draw.js -msgid "Free drawing" +# There is a similar text in GNOME Boxes (https://gitlab.gnome.org/GNOME/gnome-boxes/tree/master/po) +msgid "Grab/ungrab keyboard and pointer" msgstr "" -msgid "Line" +msgid "Erase all drawings" msgstr "" -msgid "Ellipse" +msgid "Persistent" msgstr "" -msgid "Rectangle" +msgid "Persistent drawing through session restart" msgstr "" -msgid "Text" +msgid "Drawing on the desktop" msgstr "" -msgid "Fill" +msgid "Draw On Your Screen becomes Draw On Your Desktop" msgstr "" -msgid "Stroke" +msgid "Disable on-screen notifications" msgstr "" -msgid "Dashed line" +msgid "Disable panel indicator" msgstr "" -msgid "Full line" +msgid "Draw" +msgstr "" + +msgid "Left click" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Right click" +msgstr "" + +msgid "Center click" +msgstr "" + +msgid "Increment/decrement line width" +msgstr "" + +msgid "Scroll" +msgstr "" + +msgid "Select color" +msgstr "" + +# %s are key labels (Ctrl+F1 and Ctrl+F9) +msgid "%s … %s" +msgstr "" + +msgid "Ignore pointer movement" +msgstr "" + +# %s is a key label +msgid "%s held" +msgstr "" + +msgid "Leave" +msgstr "" + +msgid "Select eraser (while starting drawing)" +msgstr "" + +msgid "Duplicate (while starting handling)" +msgstr "" + +msgid "Rotate rectangle, polygon, polyline" +msgstr "" + +msgid "Extend circle to ellipse" +msgstr "" + +msgid "Curve line" +msgstr "" + +msgid "Smooth free drawing stroke" +msgstr "" + +msgid "Rotate (while moving)" +msgstr "" + +msgid "Stretch (while resizing)" +msgstr "" + +msgid "Inverse (while mirroring)" +msgstr "" + +msgid "Internal" +msgstr "" + +msgid "(in drawing mode)" +msgstr "" + +msgid "Undo last brushstroke" +msgstr "" + +msgid "Redo last brushstroke" +msgstr "" + +msgid "Erase last brushstroke" +msgstr "" + +msgid "Smooth last brushstroke" +msgstr "" + +msgid "Select line" +msgstr "" + +msgid "Select ellipse" +msgstr "" + +msgid "Select rectangle" +msgstr "" + +msgid "Select polygon" +msgstr "" + +msgid "Select polyline" +msgstr "" + +msgid "Select text" +msgstr "" + +msgid "Select move" +msgstr "" + +msgid "Select resize" +msgstr "" + +msgid "Select mirror" +msgstr "" + +msgid "Toggle fill/stroke" +msgstr "" + +msgid "Increment line width" +msgstr "" + +msgid "Decrement line width" +msgstr "" + +msgid "Increment line width even more" +msgstr "" + +msgid "Decrement line width even more" +msgstr "" + +msgid "Change linejoin" +msgstr "" + +msgid "Change linecap" +msgstr "" + +msgid "Toggle fill rule" +msgstr "" + +msgid "Change font family (generic name)" +msgstr "" + +msgid "Change font weight" +msgstr "" + +msgid "Change font style" +msgstr "" + +msgid "Toggle text alignment" +msgstr "" + +msgid "Hide panel and dock" +msgstr "" + +msgid "Add a drawing background" +msgstr "" + +msgid "Add a grid overlay" +msgstr "" + +msgid "Square drawing area" +msgstr "" + +msgid "Open previous drawing" +msgstr "" + +msgid "Open next drawing" +msgstr "" + +msgid "Save drawing as a SVG file" +msgstr "" + +msgid "Edit style" +msgstr "" + +msgid "Open preferences" +msgstr "" + +msgid "Show help" msgstr "" msgid "" -"Type your text\n" -"and press Enter" +"Default drawing style attributes (color palette, font, line, dash) are defined in an editable css file.\n" +"See “%s”." +msgstr "" + +msgid "" +"When you save elements made with eraser in a SVG file, " +"they are colored with background color, transparent if it is disabled.\n" +"See “%s” or edit the SVG file afterwards." msgstr "" msgid "Screenshot" @@ -102,284 +295,206 @@ msgstr "" msgid "Smooth" msgstr "" -msgid "Dashed" +msgid "Free drawing" +msgstr "" + +msgid "Line" +msgstr "" + +msgid "Ellipse" +msgstr "" + +msgid "Rectangle" +msgstr "" + +msgid "Text" +msgstr "" + +msgid "Polygon" +msgstr "" + +msgid "Polyline" +msgstr "" + +msgid "Move" +msgstr "" + +msgid "Resize" +msgstr "" + +msgid "Mirror" msgstr "" msgid "Color" msgstr "" +msgid "Fill" +msgstr "" + +# fill-rule SVG attribute +msgid "Evenodd" +msgstr "" + +msgid "%d px" +msgstr "" + +# stroke-linejoin SVG attribute +msgid "Miter" +msgstr "" + +# stroke-linejoin and stroke-linecap SVG attribute +msgid "Round" +msgstr "" + +# stroke-linejoin SVG attribute +msgid "Bevel" +msgstr "" + +# stroke-linecap SVG attribute +msgid "Butt" +msgstr "" + +# stroke-linecap SVG attribute +msgid "Square" +msgstr "" + +msgid "Dashed" +msgstr "" + +# generic font-family SVG attribute +msgid "Sans-Serif" +msgstr "" + +# generic font-family SVG attribute +msgid "Serif" +msgstr "" + +# generic font-family SVG attribute +msgid "Monospace" +msgstr "" + +# generic font-family SVG attribute +msgid "Cursive" +msgstr "" + +# generic font-family SVG attribute +msgid "Fantasy" +msgstr "" + +# font-weight SVG attribute +msgid "Thin" +msgstr "" + +# font-weight SVG attribute +msgid "Ultra-light" +msgstr "" + +# font-weight SVG attribute +msgid "Light" +msgstr "" + +# font-weight SVG attribute +msgid "Semi-light" +msgstr "" + +# font-weight SVG attribute +msgid "Book" +msgstr "" + +# font-weight and font-style SVG attribute +msgid "Normal" +msgstr "" + +# font-weight SVG attribute +msgid "Medium" +msgstr "" + +# font-weight SVG attribute +msgid "Semi-bold" +msgstr "" + +# font-weight SVG attribute +msgid "Bold" +msgstr "" + +# font-weight SVG attribute +msgid "Ultra-bold" +msgstr "" + +# font-weight SVG attribute +msgid "Heavy" +msgstr "" + +# font-style SVG attribute +msgid "Italic" +msgstr "" + +# font-style SVG attribute +msgid "Oblique" +msgstr "" + +msgid "Right aligned" +msgstr "" + msgid "Open drawing" msgstr "" msgid "Save drawing" msgstr "" -#: prefs.js - -msgid "Preferences" +msgid "Leaving drawing mode" msgstr "" -msgid "About" +# %s is a key label +msgid "Press %s for help" msgstr "" -# GLOBAL_KEYBINDINGS - -msgid "Enter/leave drawing mode" +msgid "Entering drawing mode" msgstr "" -msgid "Erase all drawings" +# "released" as the opposite of "grabbed" +msgid "Keyboard and pointer released" msgstr "" -# INTERNAL_KEYBINDINGS - -msgid "Undo last brushstroke" -msgstr "" - -msgid "Redo last brushstroke" -msgstr "" - -msgid "Erase last brushstroke" -msgstr "" - -msgid "Smooth last brushstroke" -msgstr "" - -msgid "Select line" -msgstr "" - -msgid "Select ellipse" -msgstr "" - -msgid "Select rectangle" -msgstr "" - -msgid "Select text" -msgstr "" - -msgid "Unselect shape (free drawing)" -msgstr "" - -msgid "Toggle fill/stroke" -msgstr "" - -msgid "Increment line width" -msgstr "" - -msgid "Decrement line width" -msgstr "" - -msgid "Increment line width even more" -msgstr "" - -msgid "Decrement line width even more" -msgstr "" - -msgid "Change linejoin" -msgstr "" - -msgid "Change linecap" -msgstr "" - -# already in draw.js -#msgid "Dashed line" -#msgstr "" - -msgid "Change font family (generic name)" -msgstr "" - -msgid "Change font weight" -msgstr "" - -msgid "Change font style" -msgstr "" - -msgid "Hide panel and dock" -msgstr "" - -msgid "Add a drawing background" -msgstr "" - -msgid "Square drawing area" -msgstr "" - -msgid "Open previous drawing" -msgstr "" - -msgid "Open next drawing" -msgstr "" - -# already in draw.js -#msgid "Save drawing" -#msgstr "" - -msgid "Save drawing as a SVG file" -msgstr "" - -msgid "Edit style" -msgstr "" - -msgid "Show help" -msgstr "" - -# OTHER_SHORTCUTS - -msgid "Draw" -msgstr "" - -msgid "Left click" -msgstr "" - -msgid "Menu" -msgstr "" - -msgid "Right click" -msgstr "" - -msgid "Center click" -msgstr "" - -msgid "Transform shape (when drawing)" -msgstr "" - -msgid "Ctrl key" -msgstr "" - -msgid "Increment/decrement line width" -msgstr "" - -msgid "Scroll" -msgstr "" - -msgid "Select color" -msgstr "" - -msgid "Ctrl+1...9" -msgstr "" - -msgid "Select eraser" -msgstr "" - -msgid "Shift key held" -msgstr "" - -msgid "Ignore pointer movement" -msgstr "" - -msgid "Space key held" -msgstr "" - -msgid "Leave" -msgstr "" - -msgid "Escape key" -msgstr "" - -# About page - -# you are free to translate the extension name -#msgid "Draw On You Screen" -#msgstr "" - -msgid "Version %d" -msgstr "" - -msgid "Start drawing with Super+Alt+D and save your beautiful work by taking a screenshot" -msgstr "" - -# Prefs page - -msgid "Global" -msgstr "" - -msgid "Persistent" -msgstr "" - -msgid "Persistent drawing through session restart" -msgstr "" - -msgid "Drawing on the desktop" -msgstr "" - -msgid "Draw On Your Screen becomes Draw On Your Desktop" -msgstr "" - -msgid "Disable on-screen notifications" -msgstr "" - -msgid "Disable panel indicator" -msgstr "" - -msgid "Internal" -msgstr "" - -msgid "(in drawing mode)" +msgid "Keyboard and pointer grabbed" msgstr "" +# %s is a key label msgid "" -"By pressing Ctrl key during the drawing process, you can:\n" -" . rotate a rectangle or a text area\n" -" . extend and rotate an ellipse\n" -" . curve a line (cubic Bezier curve)\n" -" . smooth a free drawing stroke (you may prefer to smooth the stroke afterward, see “%s”)" +"Press %s to get\n" +"a fourth control point" msgstr "" +msgid "Mark a point of symmetry" +msgstr "" + +msgid "Draw a line of symmetry" +msgstr "" + +# %s is a key label msgid "" -"Default drawing style attributes (color palette, font, line, dash) are defined in an editable css file.\n" -"See “%s”." +"Press %s to mark vertices" msgstr "" +# %s is a key label msgid "" -"Note: When you save elements made with eraser in a SVG file, " -"they are colored with background color, transparent if it is disabled.\n" -"See “%s” or edit the SVG file afterwards." +"Type your text and press %s" +msgstr "" + +# as the alternative to "Fill" +msgid "Outline" +msgstr "" + +msgid "Dashed line" +msgstr "" + +msgid "Full line" +msgstr "" + +msgid "Left aligned" +msgstr "" + +msgid "Nonzero" msgstr "" -# The following words refer to SVG attributes. -# You are free to translate them or not. - -#msgid "Butt" -#msgstr "" - -#msgid "Round" -#msgstr "" - -#msgid "Square" -#msgstr "" - -#msgid "Miter" -#msgstr "" - -#msgid "Bevel" -#msgstr "" - -#msgid "Normal" -#msgstr "" - -#msgid "Bold" -#msgstr "" - -#msgid "Italic" -#msgstr "" - -#msgid "Oblique" -#msgstr "" - -#msgid "Sans-Serif" -#msgstr "" - -#msgid "Serif" -#msgstr "" - -#msgid "Monospace" -#msgstr "" - -#msgid "Cursive" -#msgstr "" - -#msgid "Fantasy" -#msgstr "" - -#msgid "px" -#msgstr "" - diff --git a/metadata.json b/metadata.json index 9412998..49297dc 100644 --- a/metadata.json +++ b/metadata.json @@ -17,5 +17,5 @@ "3.34", "3.36" ], - "version": 6 + "version": 6.1 } diff --git a/prefs.js b/prefs.js index 3971165..ed47d98 100644 --- a/prefs.js +++ b/prefs.js @@ -20,21 +20,24 @@ * along with this program. If not, see . */ +const GLib = imports.gi.GLib; const GObject = imports.gi.GObject; const Gtk = imports.gi.Gtk; const Lang = imports.lang; -const Mainloop = imports.mainloop; +const Config = imports.misc.config; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Convenience = ExtensionUtils.getSettings && ExtensionUtils.initTranslations ? ExtensionUtils : Me.imports.convenience; const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; const _GTK = imports.gettext.domain('gtk30').gettext; +const GS_VERSION = Config.PACKAGE_VERSION; const MARGIN = 10; var GLOBAL_KEYBINDINGS = { 'toggle-drawing': "Enter/leave drawing mode", + 'toggle-modal': "Grab/ungrab keyboard and pointer", 'erase-drawing': "Erase all drawings" }; @@ -44,13 +47,20 @@ var INTERNAL_KEYBINDINGS = { 'delete-last-element' : "Erase last brushstroke", 'smooth-last-element': "Smooth last brushstroke", '-separator-1': '', + 'select-none-shape': "Free drawing", 'select-line-shape': "Select line", 'select-ellipse-shape': "Select ellipse", 'select-rectangle-shape': "Select rectangle", + 'select-polygon-shape': "Select polygon", + 'select-polyline-shape': "Select polyline", 'select-text-shape': "Select text", - 'select-none-shape': "Unselect shape (free drawing)", - 'toggle-fill': "Toggle fill/stroke", + 'select-move-tool': "Select move", + 'select-resize-tool': "Select resize", + 'select-mirror-tool': "Select mirror", '-separator-2': '', + 'toggle-fill': "Toggle fill/outline", + 'toggle-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", @@ -58,33 +68,52 @@ var INTERNAL_KEYBINDINGS = { 'toggle-linejoin': "Change linejoin", 'toggle-linecap': "Change linecap", 'toggle-dash': "Dashed line", - '-separator-3': '', + '-separator-4': '', 'toggle-font-family': "Change font family (generic name)", 'toggle-font-weight': "Change font weight", 'toggle-font-style': "Change font style", - '-separator-4': '', + 'toggle-text-alignment': "Toggle text alignment", + '-separator-5': '', 'toggle-panel-and-dock-visibility': "Hide panel and dock", 'toggle-background': "Add a drawing background", + 'toggle-grid': "Add a grid overlay", 'toggle-square-area': "Square drawing area", - '-separator-5': '', + '-separator-6': '', 'open-previous-json': "Open previous drawing", 'open-next-json': "Open next drawing", 'save-as-json': "Save drawing", 'save-as-svg': "Save drawing as a SVG file", 'open-user-stylesheet': "Edit style", + 'open-preferences': "Open preferences", 'toggle-help': "Show help" }; +if (GS_VERSION < "3.36") + delete INTERNAL_KEYBINDINGS['open-preferences']; + +function getKeyLabel(accel) { + let [keyval, mods] = Gtk.accelerator_parse(accel); + return Gtk.accelerator_get_label(keyval, mods); +} + var OTHER_SHORTCUTS = [ - { desc: "Draw", shortcut: "Left click" }, - { desc: "Menu", shortcut: "Right click" }, - { desc: "Toggle fill/stroke", shortcut: "Center click" }, - { desc: "Transform shape (when drawing)", shortcut: "Ctrl key" }, - { desc: "Increment/decrement line width", shortcut: "Scroll" }, - { desc: "Select color", shortcut: "Ctrl+1...9" }, - { desc: "Select eraser", shortcut: "Shift key held" }, - { desc: "Ignore pointer movement", shortcut: "Space key held" }, - { desc: "Leave", shortcut: "Escape key" } + { desc: "Draw", get shortcut() { return _("Left click"); } }, + { desc: "Menu", get shortcut() { return _("Right click"); } }, + { desc: "Toggle fill/outline", get shortcut() { return _("Center click"); } }, + { desc: "Increment/decrement line width", get shortcut() { return _("Scroll"); } }, + { desc: "Select color", get shortcut() { return _("%s … %s").format(getKeyLabel('1'), getKeyLabel('9')); } }, + { desc: "Ignore pointer movement", get shortcut() { return _("%s held").format(getKeyLabel('space')); } }, + { desc: "Leave", shortcut: getKeyLabel('Escape') }, + { desc: "-separator-1", shortcut: "" }, + { desc: "Select eraser (while starting drawing)", shortcut: getKeyLabel('') }, + { desc: "Duplicate (while starting handling)", shortcut: getKeyLabel('') }, + { desc: "Rotate rectangle, polygon, polyline", shortcut: getKeyLabel('') }, + { desc: "Extend circle to ellipse", shortcut: getKeyLabel('') }, + { desc: "Curve line", shortcut: getKeyLabel('') }, + { desc: "Smooth free drawing stroke", shortcut: getKeyLabel('') }, + { desc: "Rotate (while moving)", shortcut: getKeyLabel('') }, + { desc: "Stretch (while resizing)", shortcut: getKeyLabel('') }, + { desc: "Inverse (while mirroring)", shortcut: getKeyLabel('') } ]; function init() { @@ -94,7 +123,7 @@ function init() { function buildPrefsWidget() { let topStack = new TopStack(); let switcher = new Gtk.StackSwitcher({halign: Gtk.Align.CENTER, visible: true, stack: topStack}); - Mainloop.timeout_add(0, () => { + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { let window = topStack.get_toplevel(); window.resize(720,500); let headerBar = window.get_titlebar(); @@ -266,30 +295,14 @@ const PrefsPage = new GObject.Class({ continue; } let otherBox = new Gtk.Box({ margin_left: MARGIN, margin_right: MARGIN }); - let otherLabel = new Gtk.Label({ label: _(OTHER_SHORTCUTS[i].desc) }); + let otherLabel = new Gtk.Label({ label: _(OTHER_SHORTCUTS[i].desc), use_markup: true }); otherLabel.set_halign(1); - let otherLabel2 = new Gtk.Label({ label: _(OTHER_SHORTCUTS[i].shortcut) }); + let otherLabel2 = new Gtk.Label({ label: OTHER_SHORTCUTS[i].shortcut }); otherBox.pack_start(otherLabel, true, true, 4); otherBox.pack_start(otherLabel2, false, false, 4); listBox.add(otherBox); } - let controlBox = new Gtk.Box({ margin: MARGIN, margin_top: 2*MARGIN }); - let controlLabel = new Gtk.Label({ - wrap: true, - xalign: 0, - use_markup: true, - label: _("By pressing Ctrl key during the drawing process, you can:\n" + - " . rotate a rectangle or a text area\n" + - " . extend and rotate an ellipse\n" + - " . curve a line (cubic Bezier curve)\n" + - " . smooth a free drawing stroke (you may prefer to smooth the stroke afterward, see “%s”)").format(_("Smooth last brushstroke")) - }); - controlLabel.set_halign(1); - controlLabel.get_style_context().add_class('dim-label'); - controlBox.pack_start(controlLabel, true, true, 4); - listBox.add(controlBox); - let internalKeybindingsWidget = new KeybindingsWidget(INTERNAL_KEYBINDINGS, this.settings); internalKeybindingsWidget.margin = MARGIN; listBox.add(internalKeybindingsWidget); @@ -312,7 +325,7 @@ const PrefsPage = new GObject.Class({ wrap: true, xalign: 0, use_markup: true, - label: _("Note: When you save elements made with eraser in a SVG file, " + + label: _("When you save elements made with eraser in a SVG file, " + "they are colored with background color, transparent if it is disabled.\n" + "See “%s” or edit the SVG file afterwards.").format(_("Add a drawing background")) }); diff --git a/schemas/gschemas.compiled b/schemas/gschemas.compiled index eebc890..4ce4994 100644 Binary files a/schemas/gschemas.compiled and b/schemas/gschemas.compiled differ diff --git a/schemas/org.gnome.shell.extensions.draw-on-your-screen.gschema.xml b/schemas/org.gnome.shell.extensions.draw-on-your-screen.gschema.xml index 8890675..767f859 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 @@ -26,6 +26,11 @@ toggle drawing enter or leave drawing mode + + ["<Primary><Alt><Super>d"] + toggle modeless/modal + toggle modeless/modal + ["<Alt><Super>e"] erase drawing @@ -53,8 +58,13 @@ ["<Primary>b"] - toggle background - toggle background + toggle drawing background + toggle drawing background + + + ["<Primary>g"] + toggle grid overlay + toggle grid overlay ["<Primary>h"] @@ -76,6 +86,16 @@ select rectangle select rectangle + + ["<Primary>y"] + select polygon + select polygon + + + ["<Primary>u"] + select polyline + select polyline + ["<Primary>l"] select line @@ -91,13 +111,28 @@ unselect shape (free drawing) unselect shape (free drawing) + + ["<Primary>m"] + select move tool + select move tool + + + ["<Primary>x"] + select resize tool + select resize tool + + + ["<Primary>c"] + select mirror tool + select mirror tool + - ["<Primary>KP_Add"] + KP_Add','plus']]]> increment the line width increment the line width - ["<Primary>KP_Subtract"] + KP_Subtract','minus','minus']]]> decrement the line width decrement the line width @@ -121,6 +156,11 @@ toggle linecap toggle linecap + + KP_Multiply','asterisk','asterisk']]]> + toggle fill rule + toggle fill rule + ["<Primary>period"] toggle dash @@ -191,6 +231,11 @@ toggle font style toggle font style + + ["<Primary><Shift>a"] + toggle text alignment + toggle text alignment + ["<Primary>o"] open user stylesheet to edit style @@ -216,6 +261,11 @@ Open next json file Open next json file + + ["<Primary>comma"] + Open preferences + Open preferences + ["<Primary>F1"] toggle help