diff --git a/NEWS b/NEWS index 9b28481..31c38da 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,17 @@ +v6.3 - September 2020 +===================== + +* Replace user stylesheet with proper drawing settings +* Multi-palettes +* Possibility to add insertable images from the clipboard +* Image directory is configurable +* Thumbnails in "Open drawing" sub-menu +* Drawings can be directly inserted as an image. +* Add a lot of icons in the menu and the OSD notifications +* Group menu items at the bottom +* Add tooltips to menu buttons +* New "Persistent over toggles' setting #42 + v6.2 - August 2020 ================== diff --git a/README.md b/README.md index 83a00b0..88f8f48 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Then save your beautiful work by taking a screenshot. ## Features -* Basic shapes (rectangle, circle, ellipse, line, curve, text, image, free) +* Basic shapes (rectangle, circle, ellipse, line, curve, polygon, polyline, text, image, free) * Basic transformations (move, rotate, resize, stretch, mirror, inverse) * Smooth stroke * Draw over applications @@ -26,7 +26,7 @@ Then save your beautiful work by taking a screenshot. 6. `Super + Alt + D` to test 7. [https://framagit.org/abakkk/DrawOnYourScreen/issues](https://framagit.org/abakkk/DrawOnYourScreen/issues) to say it doesn't work -## Details +## Tips and tricks * Draw arrows: @@ -42,7 +42,11 @@ Then save your beautiful work by taking a screenshot. * Insertable images: - Add your images (jpeg, png, svg) to `~/.local/share/drawOnYourScreen/images/`. + You can insert images (jpeg, png, svg) in your drawings. By default images are sought in `~/.local/share/drawOnYourScreen/images/` but the location is configurable in the preferences. Another way is to copy-past the images from Nautilus or any clipboard source by using the usual `Ctrl + V` shortcut inside the drawing mode. + +* Eraser and SVG: + + There is no eraser in SVG so when you export elements made with the eraser to a SVG file, they are colored with the background color, transparent if it is disabled. See `“Add a drawing background”` or edit the SVG file afterwards. * Screenshot Tool extension: diff --git a/area.js b/area.js index 2684308..d28a846 100644 --- a/area.js +++ b/area.js @@ -1,4 +1,5 @@ /* jslint esversion: 6 */ +/* exported Tools, DrawingArea */ /* * Copyright 2019 Abakkk @@ -37,24 +38,41 @@ const Screenshot = imports.ui.screenshot; const Me = ExtensionUtils.getCurrentExtension(); const Convenience = ExtensionUtils.getSettings ? ExtensionUtils : Me.imports.convenience; -const Extension = Me.imports.extension; const Elements = Me.imports.elements; const Files = Me.imports.files; const Menu = Me.imports.menu; const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; +const pgettext = imports.gettext.domain(Me.metadata['gettext-domain']).pgettext; const CAIRO_DEBUG_EXTENDS = false; const SVG_DEBUG_EXTENDS = false; const TEXT_CURSOR_TIME = 600; // ms +const ELEMENT_GRABBER_TIME = 80; // ms, default is about 16 ms +const GRID_TILES_HORIZONTAL_NUMBER = 30; -const { Shapes, ShapeNames, Transformations, LineCapNames, LineJoinNames, FillRuleNames, - FontWeightNames, FontStyleNames, FontStretchNames, FontVariantNames } = Elements; +const { Shapes, Transformations } = Elements; +const { DisplayStrings } = Menu; + +const FontGenericFamilies = ['Sans-Serif', 'Serif', 'Monospace', 'Cursive', 'Fantasy']; const Manipulations = { MOVE: 100, RESIZE: 101, MIRROR: 102 }; -const ManipulationNames = { 100: "Move", 101: "Resize", 102: "Mirror" }; -var Tools = Object.assign({}, Shapes, Manipulations); -var ToolNames = Object.assign({}, ShapeNames, ManipulationNames); +var Tools = Object.assign({ + getNameOf: function(value) { + return Object.keys(this).find(key => this[key] == value); + } +}, Shapes, Manipulations); +Object.defineProperty(Tools, 'getNameOf', { enumerable: false }); -var FontGenericFamilies = ['Sans-Serif', 'Serif', 'Monospace', 'Cursive', 'Fantasy']; +const getClutterColorFromString = function(string, fallback) { + let [success, color] = Clutter.Color.from_string(string); + color.toString = () => string; + if (success) + return color; + + log(`${Me.metadata.uuid}: "${string}" color cannot be parsed.`); + color = Clutter.Color.get_static(Clutter.StaticColor[fallback.toUpperCase()]); + color.toString = () => fallback.slice(0, 1).toUpperCase() + fallback.slice(1); + return color; +}; // DrawingArea is the widget in which we draw, thanks to Cairo. // It creates and manages a DrawingElement for each "brushstroke". @@ -62,18 +80,12 @@ var FontGenericFamilies = ['Sans-Serif', 'Serif', 'Monospace', 'Cursive', 'Fanta var DrawingArea = new Lang.Class({ Name: 'DrawOnYourScreenDrawingArea', Extends: St.DrawingArea, - Signals: { 'show-osd': { param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_DOUBLE, GObject.TYPE_BOOLEAN] }, - 'show-osd-gicon': { param_types: [Gio.Icon.$gtype, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_DOUBLE, GObject.TYPE_BOOLEAN] }, + Signals: { 'show-osd': { param_types: [Gio.Icon.$gtype, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_DOUBLE, GObject.TYPE_BOOLEAN] }, 'update-action-mode': {}, 'leave-drawing-mode': {} }, _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; @@ -81,16 +93,27 @@ var DrawingArea = new Lang.Class({ this.undoneElements = []; this.currentElement = null; this.currentTool = Shapes.NONE; - this.currentImage = 0; + this.currentImage = null; + this.currentTextRightAligned = Clutter.get_default_text_direction() == Clutter.TextDirection.RTL; + let fontName = St.Settings && St.Settings.get().font_name || Convenience.getSettings('org.gnome.desktop.interface').get_string('font-name'); + this.currentFont = Pango.FontDescription.from_string(fontName); + this.currentFont.unset_fields(Pango.FontMask.SIZE); + this.defaultFontFamily = this.currentFont.get_family(); + this.currentLineWidth = 5; + this.currentLineJoin = Cairo.LineJoin.ROUND; + this.currentLineCap = Cairo.LineCap.ROUND; + this.currentFillRule = Cairo.FillRule.WINDING; this.isSquareArea = false; this.hasGrid = false; this.hasBackground = false; this.textHasCursor = false; this.dashedLine = false; this.fill = false; - this.colors = [Clutter.Color.new(0, 0, 0, 255)]; - this.newThemeAttributes = {}; - this.oldThemeAttributes = {}; + + this.connect('destroy', this._onDestroy.bind(this)); + this.connect('notify::reactive', this._onReactiveChanged.bind(this)); + this.drawingSettingsChangedHandler = Me.drawingSettings.connect('changed', this._onDrawingSettingsChanged.bind(this)); + this._onDrawingSettingsChanged(); if (loadPersistent) this._loadPersistent(); @@ -98,7 +121,7 @@ var DrawingArea = new Lang.Class({ get menu() { if (!this._menu) - this._menu = new Menu.DrawingMenu(this, this.monitor); + this._menu = new Menu.DrawingMenu(this, this.monitor, Tools); return this._menu; }, @@ -123,6 +146,52 @@ var DrawingArea = new Lang.Class({ this._stopElementGrabber(); }, + get currentPalette() { + return this._currentPalette; + }, + + set currentPalette(palette) { + this._currentPalette = palette; + this.colors = palette[1].map(colorString => getClutterColorFromString(colorString, 'white')); + if (!this.colors[0]) + this.colors.push(Clutter.Color.get_static(Clutter.StaticColor.WHITE)); + }, + + get currentImage() { + if (!this._currentImage) + this._currentImage = Files.Images.getNext(this._currentImage); + + return this._currentImage; + }, + + set currentImage(image) { + this._currentImage = image; + }, + + get currentFontFamily() { + return this.currentFont.get_family(); + }, + + set currentFontFamily(family) { + this.currentFont.set_family(family); + }, + + get currentFontStyle() { + return this.currentFont.get_style(); + }, + + set currentFontStyle(style) { + this.currentFont.set_style(style); + }, + + get currentFontWeight() { + return this.currentFont.get_weight(); + }, + + set currentFontWeight(weight) { + this.currentFont.set_weight(weight); + }, + get hasManipulationTool() { // No Object.values method in GS 3.24. return Object.keys(Manipulations).map(key => Manipulations[key]).indexOf(this.currentTool) != -1; @@ -137,27 +206,12 @@ var DrawingArea = new Lang.Class({ this.currentFillRule = evenodd ? Cairo.FillRule.EVEN_ODD : Cairo.FillRule.WINDING; }, - getImages() { - let images = Files.getImages(); - if (!images[this.currentImage]) - this.currentImage = Math.max(images.length - 1, 0); - return images; - }, - - get currentFontFamily() { - return this._currentFontFamily || this.currentThemeFontFamily; - }, - - set currentFontFamily(fontFamily) { - this._currentFontFamily = fontFamily; - }, - get fontFamilies() { if (!this._fontFamilies) { - let pangoFontFamilies = Elements.getPangoFontFamilies().filter(family => { - return family != this.currentThemeFontFamily && FontGenericFamilies.indexOf(family) == -1; + let otherFontFamilies = Elements.getAllFontFamilies().filter(family => { + return family != this.defaultFontFamily && FontGenericFamilies.indexOf(family) == -1; }); - this._fontFamilies = [this.currentThemeFontFamily].concat(FontGenericFamilies, pangoFontFamilies); + this._fontFamilies = [this.defaultFontFamily].concat(FontGenericFamilies, otherFontFamilies); } return this._fontFamilies; }, @@ -181,57 +235,44 @@ var DrawingArea = new Lang.Class({ this.queue_repaint(); }, - _updateStyle: function() { - try { - let themeNode = this.get_theme_node(); - for (let i = 1; i < 10; i++) { - this.colors[i] = themeNode.get_color('-drawing-color' + i); - } - let font = themeNode.get_font(); - this.newThemeAttributes.ThemeFontFamily = font.get_family(); - try { this.newThemeAttributes.FontWeight = font.get_weight(); } catch(e) { this.newThemeAttributes.FontWeight = Pango.Weight.NORMAL; } - this.newThemeAttributes.FontStyle = font.get_style(); - this.newThemeAttributes.FontStretch = font.get_stretch(); - this.newThemeAttributes.FontVariant = font.get_variant(); - this.newThemeAttributes.TextRightAligned = themeNode.get_text_align() == St.TextAlign.RIGHT; - this.newThemeAttributes.LineWidth = themeNode.get_length('-drawing-line-width'); - this.newThemeAttributes.LineJoin = themeNode.get_double('-drawing-line-join'); - this.newThemeAttributes.LineCap = themeNode.get_double('-drawing-line-cap'); - this.newThemeAttributes.FillRule = themeNode.get_double('-drawing-fill-rule'); - this.dashArray = [Math.abs(themeNode.get_length('-drawing-dash-array-on')), Math.abs(themeNode.get_length('-drawing-dash-array-off'))]; - this.dashOffset = themeNode.get_length('-drawing-dash-offset'); - this.gridGap = themeNode.get_length('-grid-overlay-gap'); - this.gridLineWidth = themeNode.get_length('-grid-overlay-line-width'); - this.gridInterlineWidth = themeNode.get_length('-grid-overlay-interline-width'); - this.gridColor = themeNode.get_color('-grid-overlay-color'); - this.squareAreaWidth = themeNode.get_length('-drawing-square-area-width'); - this.squareAreaHeight = themeNode.get_length('-drawing-square-area-height'); - this.activeBackgroundColor = themeNode.get_color('-drawing-background-color'); - } catch(e) { - logError(e); + _onDrawingSettingsChanged: function() { + this.palettes = Me.drawingSettings.get_value('palettes').deep_unpack(); + if (!this.colors) { + if (this.palettes[0]) + this.currentPalette = this.palettes[0]; + else + this.currentPalette = ['Palette', ['White']]; + } + if (!this.currentColor) + this.currentColor = this.colors[0]; + + if (Me.drawingSettings.get_boolean('square-area-auto')) { + this.squareAreaSize = Math.pow(2, 6); + while (this.squareAreaSize * 2 < Math.min(this.monitor.width, this.monitor.height)) + this.squareAreaSize *= 2; + } else { + this.squareAreaSize = Me.drawingSettings.get_uint('square-area-size'); } - for (let i = 1; i < 10; i++) { - this.colors[i] = this.colors[i].alpha ? this.colors[i] : this.colors[0]; + this.areaBackgroundColor = getClutterColorFromString(Me.drawingSettings.get_string('background-color'), 'black'); + + this.gridColor = getClutterColorFromString(Me.drawingSettings.get_string('grid-color'), 'gray'); + if (Me.drawingSettings.get_boolean('grid-line-auto')) { + this.gridLineSpacing = Math.round(this.monitor.width / (5 * GRID_TILES_HORIZONTAL_NUMBER)); + this.gridLineWidth = this.gridLineSpacing / 20; + } else { + this.gridLineSpacing = Me.drawingSettings.get_uint('grid-line-spacing'); + this.gridLineWidth = Math.round(Me.drawingSettings.get_double('grid-line-width') * 100) / 100; } - this.currentColor = this.currentColor || this.colors[1]; - this._fontFamilies = null; - // SVG does not support 'Ultra-heavy' weight (1000) - this.newThemeAttributes.FontWeight = Math.min(this.newThemeAttributes.FontWeight, 900); - this.newThemeAttributes.LineWidth = (this.newThemeAttributes.LineWidth > 0) ? this.newThemeAttributes.LineWidth : 3; - 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.dashOffset = Math.round(Me.drawingSettings.get_double('dash-offset') * 100) / 100; + if (Me.drawingSettings.get_boolean('dash-array-auto')) { + this.dashArray = [0, 0]; + } else { + let on = Math.round(Me.drawingSettings.get_double('dash-array-on') * 100) / 100; + let off = Math.round(Me.drawingSettings.get_double('dash-array-off') * 100) / 100; + this.dashArray = [on, off]; } - this.gridGap = this.gridGap && this.gridGap >= 1 ? this.gridGap : 10; - this.gridLineWidth = this.gridLineWidth || 0.4; - this.gridInterlineWidth = this.gridInterlineWidth || 0.2; - this.gridColor = this.gridColor && this.gridColor.alpha ? this.gridColor : Clutter.Color.new(127, 127, 127, 255); }, _repaint: function(cr) { @@ -269,27 +310,27 @@ var DrawingArea = new Lang.Class({ cr.restore(); } - if (this.reactive && this.hasGrid && this.gridGap && this.gridGap >= 1) { + if (this.reactive && this.hasGrid) { cr.save(); Clutter.cairo_set_source_color(cr, this.gridColor); let [gridX, gridY] = [0, 0]; while (gridX < this.monitor.width / 2) { - cr.setLineWidth((gridX / this.gridGap) % 5 ? this.gridInterlineWidth : this.gridLineWidth); + cr.setLineWidth((gridX / this.gridLineSpacing) % 5 ? this.gridLineWidth / 2 : this.gridLineWidth); cr.moveTo(this.monitor.width / 2 + gridX, 0); cr.lineTo(this.monitor.width / 2 + gridX, this.monitor.height); cr.moveTo(this.monitor.width / 2 - gridX, 0); cr.lineTo(this.monitor.width / 2 - gridX, this.monitor.height); - gridX += this.gridGap; + gridX += this.gridLineSpacing; cr.stroke(); } while (gridY < this.monitor.height / 2) { - cr.setLineWidth((gridY / this.gridGap) % 5 ? this.gridInterlineWidth : this.gridLineWidth); + cr.setLineWidth((gridY / this.gridLineSpacing) % 5 ? this.gridLineWidth / 2 : this.gridLineWidth); cr.moveTo(0, this.monitor.height / 2 + gridY); cr.lineTo(this.monitor.width, this.monitor.height / 2 + gridY); cr.moveTo(0, this.monitor.height / 2 - gridY); cr.lineTo(this.monitor.width, this.monitor.height / 2 - gridY); - gridY += this.gridGap; + gridY += this.gridLineSpacing; cr.stroke(); } cr.restore(); @@ -362,8 +403,9 @@ var DrawingArea = new Lang.Class({ 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); + // Translators: %s is a key label + this.emit('show-osd', Files.Icons.ARC, _("Press %s to get\na fourth control point") + .format(Gtk.accelerator_get_label(Clutter.KEY_Return, 0)), "", -1, true); this.currentElement.addPoint(); this.updatePointerCursor(true); this._redisplay(); @@ -425,8 +467,9 @@ var DrawingArea = new Lang.Class({ } // Reduce computing without notable effect. - if (Math.random() <= 0.75) + if (event.get_time() - (this.elementGrabberTimestamp || 0) < ELEMENT_GRABBER_TIME) return; + this.elementGrabberTimestamp = event.get_time(); let coords = event.get_coords(); let [s, x, y] = this.transform_stage_point(coords[0], coords[1]); @@ -459,7 +502,7 @@ var DrawingArea = new Lang.Class({ 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); + this.emit('show-osd', Files.Icons.TOOL_MIRROR, label, "", -1, true); return; } } @@ -473,6 +516,10 @@ var DrawingArea = new Lang.Class({ if (duplicate) { // deep cloning let copy = new this.grabbedElement.constructor(JSON.parse(JSON.stringify(this.grabbedElement))); + if (this.grabbedElement.color) + copy.color = this.grabbedElement.color; + if (this.grabbedElement.font) + copy.font = this.grabbedElement.font; if (this.grabbedElement.image) copy.image = this.grabbedElement.image; this.elements.push(copy); @@ -560,34 +607,27 @@ var DrawingArea = new Lang.Class({ if (this.currentTool == Shapes.TEXT) { this.currentElement = new Elements.DrawingElement({ shape: this.currentTool, - color: this.currentColor.to_string(), + color: this.currentColor, eraser: eraser, - font: { - family: this.currentFontFamily, - weight: this.currentFontWeight, - style: this.currentFontStyle, - stretch: this.currentFontStretch, - variant: this.currentFontVariant }, - text: _("Text"), + font: this.currentFont.copy(), + // Translators: initial content of the text area + text: pgettext("text-area-content", "Text"), textRightAligned: this.currentTextRightAligned, points: [] }); } else if (this.currentTool == Shapes.IMAGE) { - let images = this.getImages(); - if (!images.length) - return; this.currentElement = new Elements.DrawingElement({ shape: this.currentTool, - color: this.currentColor.to_string(), + color: this.currentColor, eraser: eraser, - image: images[this.currentImage], + image: this.currentImage, operator: this.currentOperator, points: [] }); } else { this.currentElement = new Elements.DrawingElement({ shape: this.currentTool, - color: this.currentColor.to_string(), + color: this.currentColor, eraser: eraser, fill: this.fill, fillRule: this.currentFillRule, @@ -599,9 +639,12 @@ var DrawingArea = new Lang.Class({ this.currentElement.startDrawing(startX, startY); - if (this.currentTool == Shapes.POLYGON || this.currentTool == Shapes.POLYLINE) - this.emit('show-osd', null, _("Press %s to mark vertices") + if (this.currentTool == Shapes.POLYGON || this.currentTool == Shapes.POLYLINE) { + let icon = Files.Icons[this.currentTool == Shapes.POLYGON ? 'TOOL_POLYGON' : 'TOOL_POLYLINE']; + // Translators: %s is a key label + this.emit('show-osd', icon, _("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) @@ -661,8 +704,9 @@ var DrawingArea = new Lang.Class({ let [x, y] = [this.currentElement.x, this.currentElement.y]; this.currentElement.text = ''; this.currentElement.cursorPosition = 0; - this.emit('show-osd', null, _("Type your text and press %s") - .format(Gtk.accelerator_get_label(Clutter.KEY_Escape, 0)), "", -1, true); + // Translators: %s is a key label + this.emit('show-osd', Files.Icons.TOOL_TEXT, _("Type your text and press %s") + .format(Gtk.accelerator_get_label(Clutter.KEY_Escape, 0)), "", -1, true); this._updateTextCursorTimeout(); this.textHasCursor = true; this._redisplay(); @@ -749,7 +793,7 @@ var DrawingArea = new Lang.Class({ setPointerCursor: function(pointerCursorName) { if (!this.currentPointerCursorName || this.currentPointerCursorName != pointerCursorName) { this.currentPointerCursorName = pointerCursorName; - Extension.setCursor(pointerCursorName); + Me.stateObj.areaManager.setCursor(pointerCursorName); } }, @@ -835,7 +879,7 @@ var DrawingArea = new Lang.Class({ toggleBackground: function() { this.hasBackground = !this.hasBackground; - this.get_parent().set_background_color(this.hasBackground ? this.activeBackgroundColor : null); + this.get_parent().set_background_color(this.hasBackground ? this.areaBackgroundColor : null); }, toggleGrid: function() { @@ -846,10 +890,8 @@ var DrawingArea = new Lang.Class({ toggleSquareArea: function() { this.isSquareArea = !this.isSquareArea; if (this.isSquareArea) { - let width = this.squareAreaWidth || this.squareAreaHeight || Math.min(this.monitor.width, this.monitor.height) * 3 / 4; - let height = this.squareAreaHeight || this.squareAreaWidth || Math.min(this.monitor.width, this.monitor.height) * 3 / 4; - this.set_position(Math.floor(this.monitor.width / 2 - width / 2), Math.floor(this.monitor.height / 2 - height / 2)); - this.set_size(width, height); + this.set_position((this.monitor.width - this.squareAreaSize) / 2, (this.monitor.height - this.squareAreaSize) / 2); + this.set_size(this.squareAreaSize, this.squareAreaSize); this.add_style_class_name('draw-on-your-screen-square-area'); } else { this.set_position(0, 0); @@ -858,76 +900,87 @@ var DrawingArea = new Lang.Class({ } }, - switchColor: function() { - this.selectColor((this.currentColor == this.colors[1]) ? 2 : 1); - }, - selectColor: function(index) { + if (!this.colors[index]) + return; + this.currentColor = this.colors[index]; if (this.currentElement) { - this.currentElement.color = this.currentColor.to_string(); + this.currentElement.color = this.currentColor; this._redisplay(); } // Foreground color markup is not displayed since 3.36, use style instead but the transparency is lost. - this.emit('show-osd', null, this.currentColor.to_string(), this.currentColor.to_string().slice(0, 7), -1, false); + this.emit('show-osd', Files.Icons.COLOR, String(this.currentColor), this.currentColor.to_string().slice(0, 7), -1, false); }, selectTool: function(tool) { this.currentTool = tool; - this.emit('show-osd', null, _(ToolNames[tool]), "", -1, false); + this.emit('show-osd', Files.Icons[`TOOL_${Tools.getNameOf(tool)}`] || null, DisplayStrings.Tool[tool], "", -1, false); this.updatePointerCursor(); }, switchFill: function() { this.fill = !this.fill; - this.emit('show-osd', null, this.fill ? _("Fill") : _("Outline"), "", -1, false); - }, - - switchDash: function() { - this.dashedLine = !this.dashedLine; - this.emit('show-osd', null, this.dashedLine ? _("Dashed line") : _("Full line"), "", -1, false); - }, - - incrementLineWidth: function(increment) { - this.currentLineWidth = Math.max(this.currentLineWidth + increment, 0); - this.emit('show-osd', null, _("%d px").format(this.currentLineWidth), "", 2 * this.currentLineWidth, false); - }, - - switchLineJoin: function() { - this.currentLineJoin = this.currentLineJoin == 2 ? 0 : this.currentLineJoin + 1; - this.emit('show-osd', null, _(LineJoinNames[this.currentLineJoin]), "", -1, false); - }, - - switchLineCap: function() { - this.currentLineCap = this.currentLineCap == 2 ? 0 : this.currentLineCap + 1; - this.emit('show-osd', null, _(LineCapNames[this.currentLineCap]), "", -1, false); + let icon = Files.Icons[this.fill ? 'FILL' : 'STROKE']; + this.emit('show-osd', icon, DisplayStrings.getFill(this.fill), "", -1, false); }, switchFillRule: function() { this.currentFillRule = this.currentFillRule == 1 ? 0 : this.currentFillRule + 1; - this.emit('show-osd', null, _(FillRuleNames[this.currentFillRule]), "", -1, false); + let icon = Files.Icons[this.currentEvenodd ? 'FILLRULE_EVENODD' : 'FILLRULE_NONZERO']; + this.emit('show-osd', icon, DisplayStrings.FillRule[this.currentFillRule], "", -1, false); + }, + + switchColorPalette: function(reverse) { + let index = this.palettes.indexOf(this.currentPalette); + if (reverse) + this.currentPalette = index <= 0 ? this.palettes[this.palettes.length - 1] : this.palettes[index - 1]; + else + this.currentPalette = index == this.palettes.length - 1 ? this.palettes[0] : this.palettes[index + 1]; + this.emit('show-osd', Files.Icons.PALETTE, this.currentPalette[0], "", -1, false); + }, + + switchDash: function() { + this.dashedLine = !this.dashedLine; + let icon = Files.Icons[this.dashedLine ? 'DASHED_LINE' : 'FULL_LINE']; + this.emit('show-osd', icon, DisplayStrings.getDashedLine(this.dashedLine), "", -1, false); + }, + + incrementLineWidth: function(increment) { + this.currentLineWidth = Math.max(this.currentLineWidth + increment, 0); + this.emit('show-osd', null, DisplayStrings.getPixels(this.currentLineWidth), "", 2 * this.currentLineWidth, false); + }, + + switchLineJoin: function() { + this.currentLineJoin = this.currentLineJoin == 2 ? 0 : this.currentLineJoin + 1; + this.emit('show-osd', Files.Icons.LINEJOIN, DisplayStrings.LineJoin[this.currentLineJoin], "", -1, false); + }, + + switchLineCap: function() { + this.currentLineCap = this.currentLineCap == 2 ? 0 : this.currentLineCap + 1; + this.emit('show-osd', Files.Icons.LINECAP, DisplayStrings.LineCap[this.currentLineCap], "", -1, false); }, switchFontWeight: function() { - let fontWeights = Object.keys(FontWeightNames).map(key => Number(key)); + let fontWeights = Object.keys(DisplayStrings.FontWeight).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.currentElement.font.set_weight(this.currentFontWeight); this._redisplay(); } - this.emit('show-osd', null, `` + - `${_(FontWeightNames[this.currentFontWeight])}`, "", -1, false); + this.emit('show-osd', Files.Icons.FONT_WEIGHT, `` + + `${DisplayStrings.FontWeight[this.currentFontWeight]}`, "", -1, false); }, switchFontStyle: function() { this.currentFontStyle = this.currentFontStyle == 2 ? 0 : this.currentFontStyle + 1; if (this.currentElement && this.currentElement.font) { - this.currentElement.font.style = this.currentFontStyle; + this.currentElement.font.set_style(this.currentFontStyle); this._redisplay(); } - this.emit('show-osd', null, `` + - `${_(FontStyleNames[this.currentFontStyle])}`, "", -1, false); + this.emit('show-osd', Files.Icons.FONT_STYLE, `` + + `${DisplayStrings.FontStyle[this.currentFontStyle]}`, "", -1, false); }, switchFontFamily: function(reverse) { @@ -937,10 +990,10 @@ var DrawingArea = new Lang.Class({ else this.currentFontFamily = (index == this.fontFamilies.length - 1) ? this.fontFamilies[0] : this.fontFamilies[index + 1]; if (this.currentElement && this.currentElement.font) { - this.currentElement.font.family = this.currentFontFamily; + this.currentElement.font.set_family(this.currentFontFamily); this._redisplay(); } - this.emit('show-osd', null, `${_(this.currentFontFamily)}`, "", -1, false); + this.emit('show-osd', Files.Icons.FONT_FAMILY, `${DisplayStrings.getFontFamily(this.currentFontFamily)}`, "", -1, false); }, switchTextAlignment: function() { @@ -949,16 +1002,23 @@ var DrawingArea = new Lang.Class({ this.currentElement.textRightAligned = this.currentTextRightAligned; this._redisplay(); } - this.emit('show-osd', null, this.currentTextRightAligned ? _("Right aligned") : _("Left aligned"), "", -1, false); + let icon = Files.Icons[this.currentTextRightAligned ? 'RIGHT_ALIGNED' : 'LEFT_ALIGNED']; + this.emit('show-osd', icon, DisplayStrings.getTextAlignment(this.currentTextRightAligned), "", -1, false); }, - switchImageFile: function() { - let images = this.getImages(); - if (!images.length) - return; - if (images.length > 1) - this.currentImage = this.currentImage == images.length - 1 ? 0 : this.currentImage + 1; - this.emit('show-osd-gicon', images[this.currentImage].gicon, images[this.currentImage].toString(), "", -1, false); + switchImageFile: function(reverse) { + this.currentImage = Files.Images[reverse ? 'getPrevious' : 'getNext'](this.currentImage); + if (this.currentImage) + this.emit('show-osd', this.currentImage.gicon, this.currentImage.toString(), "", -1, false); + }, + + pasteImageFiles: function() { + Files.Images.addImagesFromClipboard(lastImage => { + this.currentImage = lastImage; + this.currentTool = Shapes.IMAGE; + this.updatePointerCursor(); + this.emit('show-osd', this.currentImage.gicon, this.currentImage.toString(), "", -1, false); + }); }, toggleHelp: function() { @@ -984,7 +1044,7 @@ var DrawingArea = new Lang.Class({ }, _onDestroy: function() { - this.disconnect(this.reactiveHandler); + Me.drawingSettings.disconnect(this.drawingSettingsChangedHandler); this.erase(); if (this._menu) this._menu.disable(); @@ -1001,11 +1061,10 @@ var DrawingArea = new Lang.Class({ this.buttonPressedHandler = this.connect('button-press-event', this._onButtonPressed.bind(this)); this.keyboardPopupMenuHandler = this.connect('popup-menu', this._onKeyboardPopupMenu.bind(this)); this.scrollHandler = this.connect('scroll-event', this._onScroll.bind(this)); - this.get_parent().set_background_color(this.reactive && this.hasBackground ? this.activeBackgroundColor : null); - this._updateStyle(); + this.get_parent().set_background_color(this.reactive && this.hasBackground ? this.areaBackgroundColor : null); }, - leaveDrawingMode: function(save) { + leaveDrawingMode: function(save, erase) { if (this.stageKeyPressedHandler) { global.stage.disconnect(this.stageKeyPressedHandler); this.stageKeyPressedHandler = null; @@ -1041,14 +1100,49 @@ var DrawingArea = new Lang.Class({ this.currentElement = null; this._stopTextCursorTimeout(); - this._redisplay(); + if (erase) + this.erase(); + else + this._redisplay(); this.closeMenu(); this.get_parent().set_background_color(null); + Files.Images.reset(); if (save) this.savePersistent(); }, - saveAsSvg: function() { + // Used by the menu. + getSvgContentsForJson(json) { + let elements = []; + let elementsContent = ''; + + elements.push(...JSON.parse(json.contents).map(object => { + if (object.color) + object.color = getClutterColorFromString(object.color, 'white'); + if (object.font && typeof object.font == 'string') + object.font = Pango.FontDescription.from_string(object.font); + if (object.image) + object.image = new Files.Image(object.image); + return new Elements.DrawingElement(object); + })); + elements.forEach(element => elementsContent += element.buildSVG('transparent')); + + let prefixes = 'xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"'; + + let getGiconSvgContent = () => { + let size = Math.min(this.monitor.width, this.monitor.height); + let [x, y] = [(this.monitor.width - size) / 2, (this.monitor.height - size) / 2]; + return `${elementsContent}\n`; + }; + + let getImageSvgContent = () => { + return `${elementsContent}\n`; + }; + + return [getGiconSvgContent, getImageSvgContent]; + }, + + exportToSvg: function() { // stop drawing or writing if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) { this._stopWriting(); @@ -1062,7 +1156,7 @@ var DrawingArea = new Lang.Class({ let content = ``; if (SVG_DEBUG_EXTENDS) content = ``; - let backgroundColorString = this.hasBackground ? this.activeBackgroundColor.to_string() : 'transparent'; + let backgroundColorString = this.hasBackground ? String(this.areaBackgroundColor) : 'transparent'; if (backgroundColorString != 'transparent') { content += `\n `; } @@ -1070,19 +1164,10 @@ var DrawingArea = new Lang.Class({ content += `\n `; content += `\n `; } - for (let i = 0; i < this.elements.length; i++) { - content += this.elements[i].buildSVG(backgroundColorString); - } + this.elements.forEach(element => content += element.buildSVG(backgroundColorString)); content += "\n"; - let filename = `${Me.metadata['svg-file-name']} ${Files.getDateString()}.svg`; - let dir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES); - let path = GLib.build_filenamev([dir, filename]); - if (GLib.file_test(path, GLib.FileTest.EXISTS)) - return false; - let success = GLib.file_set_contents(path, content); - - if (success) { + if (Files.saveSvg(content)) { // pass the parent (bgContainer) to Flashspot because coords of this are relative let flashspot = new Screenshot.Flashspot(this.get_parent()); flashspot.fire(); @@ -1095,7 +1180,7 @@ var DrawingArea = new Lang.Class({ } }, - _saveAsJson: function(name, notify, callback) { + _saveAsJson: function(json, notify, callback) { // stop drawing or writing if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) { this._stopWriting(); @@ -1103,46 +1188,31 @@ var DrawingArea = new Lang.Class({ this._stopDrawing(); } - let json = new Files.Json({ name }); - let oldContents; - - if (name == Me.metadata['persistent-file-name']) { - let oldContents = json.contents; - // do not create a file to write just an empty array - if (!oldContents && this.elements.length == 0) - return; - } - // do not use "content = JSON.stringify(this.elements, null, 2);", neither "content = JSON.stringify(this.elements);" // do compromise between disk usage and human readability - let contents = `[\n ` + new Array(...this.elements.map(element => JSON.stringify(element))).join(`,\n\n `) + `\n]`; - - if (name == Me.metadata['persistent-file-name'] && contents == oldContents) - return; + let contents = this.elements.length ? `[\n ` + new Array(...this.elements.map(element => JSON.stringify(element))).join(`,\n\n `) + `\n]` : '[]'; GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { json.contents = 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; - } + this.emit('show-osd', Files.Icons.SAVE, json.name, "", -1, false); + if (!json.isPersistent) + this.currentJson = json; if (callback) callback(); }); }, saveAsJsonWithName: function(name, callback) { - this._saveAsJson(name, false, callback); + this._saveAsJson(Files.Jsons.getNamed(name), false, callback); }, - saveAsJson: function() { - this._saveAsJson(Files.getDateString(), true); + saveAsJson: function(notify, callback) { + this._saveAsJson(Files.Jsons.getDated(), notify, callback); }, savePersistent: function() { - this._saveAsJson(Me.metadata['persistent-file-name']); + this._saveAsJson(Files.Jsons.getPersistent()); }, syncPersistent: function() { @@ -1154,7 +1224,7 @@ var DrawingArea = new Lang.Class({ }, - _loadJson: function(name, notify) { + _loadJson: function(json, notify) { // stop drawing or writing if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.isWriting) { this._stopWriting(); @@ -1164,56 +1234,49 @@ var DrawingArea = new Lang.Class({ this.elements = []; this.currentElement = null; - let contents = (new Files.Json({ name })).contents; - if (!contents) + if (!json.contents) return; - this.elements.push(...JSON.parse(contents).map(object => { + this.elements.push(...JSON.parse(json.contents).map(object => { + if (object.color) + object.color = getClutterColorFromString(object.color, 'white'); + if (object.font && typeof object.font == 'string') + object.font = Pango.FontDescription.from_string(object.font); if (object.image) object.image = new Files.Image(object.image); return new Elements.DrawingElement(object); })); if (notify) - this.emit('show-osd', 'document-open-symbolic', name, "", -1, false); - if (name != Me.metadata['persistent-file-name']) { - this.jsonName = name; - this.lastJsonContents = contents; - } + this.emit('show-osd', Files.Icons.OPEN, json.name, "", -1, false); + if (!json.isPersistent) + this.currentJson = json; }, _loadPersistent: function() { - this._loadJson(Me.metadata['persistent-file-name']); + this._loadJson(Files.Jsons.getPersistent()); }, - loadJson: function(name, notify) { - this._loadJson(name, notify); + loadJson: function(json, notify) { + this._loadJson(json, notify); this._redisplay(); }, - loadNextJson: function() { - let names = Files.getJsons().map(json => json.name); - - if (!names.length) - return; - - let nextName = names[this.jsonName && names.indexOf(this.jsonName) != names.length - 1 ? names.indexOf(this.jsonName) + 1 : 0]; - this.loadJson(nextName, true); + loadPreviousJson: function() { + let json = Files.Jsons.getPrevious(this.currentJson || null); + if (json) + this.loadJson(json, true); }, - loadPreviousJson: function() { - let names = Files.getJsons().map(json => json.name); - - if (!names.length) - return; - - let previousName = names[this.jsonName && names.indexOf(this.jsonName) > 0 ? names.indexOf(this.jsonName) - 1 : names.length - 1]; - this.loadJson(previousName, true); + loadNextJson: function() { + let json = Files.Jsons.getNext(this.currentJson || null); + if (json) + this.loadJson(json, true); }, get drawingContentsHasChanged() { let contents = `[\n ` + new Array(...this.elements.map(element => JSON.stringify(element))).join(`,\n\n `) + `\n]`; - return contents != this.lastJsonContents; + return contents != (this.currentJson && this.currentJson.contents); } }); diff --git a/data/default.css b/data/default.css deleted file mode 100644 index 4a03e08..0000000 --- a/data/default.css +++ /dev/null @@ -1,151 +0,0 @@ -/* - * 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 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: - * 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, - * when toggling 'Square drawing area', the area is sized to 75% of monitor size. You can fix and customize this size - * by uncommenting square-area-width and square-area-height lines. - * - * font: - * 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. - * - * text-align: left or right. - * - */ - -.draw-on-your-screen { - -drawing-line-width: 5px; - -drawing-line-join: 1; - -drawing-line-cap: 1; - -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: 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/arc-symbolic.svg b/data/icons/arc-symbolic.svg new file mode 100644 index 0000000..5da0547 --- /dev/null +++ b/data/icons/arc-symbolic.svg @@ -0,0 +1,7 @@ + + +https://github.com/PapirusDevelopmentTeam/papirus-icon-theme/blob/master/Papirus/symbolic/actions/tool-arc-symbolic.svg +https://www.gnu.org/licenses/gpl-3.0.html + + + diff --git a/data/icons/color-symbolic.svg b/data/icons/color-symbolic.svg index 39ae95d..163f40f 100644 --- a/data/icons/color-symbolic.svg +++ b/data/icons/color-symbolic.svg @@ -7,7 +7,7 @@ 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/document-export-symbolic.svg b/data/icons/document-export-symbolic.svg new file mode 100644 index 0000000..eb158e9 --- /dev/null +++ b/data/icons/document-export-symbolic.svg @@ -0,0 +1,8 @@ + + +https://github.com/PapirusDevelopmentTeam/papirus-icon-theme/blob/master/Papirus/symbolic/actions/document-export-symbolic.svg +https://www.gnu.org/licenses/gpl-3.0.html + + + + diff --git a/data/icons/fill-symbolic.svg b/data/icons/fill-symbolic.svg index 041bd49..0a66829 100644 --- a/data/icons/fill-symbolic.svg +++ b/data/icons/fill-symbolic.svg @@ -1,3 +1,3 @@ - + diff --git a/data/icons/fillrule-evenodd-symbolic.svg b/data/icons/fillrule-evenodd-symbolic.svg index a74de4c..da0de94 100644 --- a/data/icons/fillrule-evenodd-symbolic.svg +++ b/data/icons/fillrule-evenodd-symbolic.svg @@ -1,3 +1,3 @@ - + diff --git a/data/icons/fillrule-nonzero-symbolic.svg b/data/icons/fillrule-nonzero-symbolic.svg index a3b9b2b..be3096c 100644 --- a/data/icons/fillrule-nonzero-symbolic.svg +++ b/data/icons/fillrule-nonzero-symbolic.svg @@ -1,3 +1,3 @@ - + diff --git a/data/icons/full-line-symbolic.svg b/data/icons/full-line-symbolic.svg index 1919ef3..b17a91b 100644 --- a/data/icons/full-line-symbolic.svg +++ b/data/icons/full-line-symbolic.svg @@ -1,3 +1,3 @@ - + diff --git a/data/icons/linecap-symbolic.svg b/data/icons/linecap-symbolic.svg index 0d3e9df..0df89de 100644 --- a/data/icons/linecap-symbolic.svg +++ b/data/icons/linecap-symbolic.svg @@ -1,4 +1,4 @@ - - + + diff --git a/data/icons/linejoin-symbolic.svg b/data/icons/linejoin-symbolic.svg index 28a1f5e..8d5951e 100644 --- a/data/icons/linejoin-symbolic.svg +++ b/data/icons/linejoin-symbolic.svg @@ -1,3 +1,3 @@ - + diff --git a/data/icons/palette-symbolic.svg b/data/icons/palette-symbolic.svg new file mode 100644 index 0000000..7846c46 --- /dev/null +++ b/data/icons/palette-symbolic.svg @@ -0,0 +1,32 @@ + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 +https://svgsilh.com/image/2026954.html +https://creativecommons.org/publicdomain/zero/1.0/ + + + + + diff --git a/data/icons/smooth-symbolic.svg b/data/icons/smooth-symbolic.svg index 903a0f5..483094e 100644 --- a/data/icons/smooth-symbolic.svg +++ b/data/icons/smooth-symbolic.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/data/icons/stroke-symbolic.svg b/data/icons/stroke-symbolic.svg index 78bc817..29ed599 100644 --- a/data/icons/stroke-symbolic.svg +++ b/data/icons/stroke-symbolic.svg @@ -1,3 +1,3 @@ - + diff --git a/data/icons/tool-ellipse-symbolic.svg b/data/icons/tool-ellipse-symbolic.svg new file mode 100644 index 0000000..9d93d08 --- /dev/null +++ b/data/icons/tool-ellipse-symbolic.svg @@ -0,0 +1,7 @@ + + +https://github.com/PapirusDevelopmentTeam/papirus-icon-theme/blob/master/Papirus/symbolic/actions/tool-circle-move-symbolic.svg +https://www.gnu.org/licenses/gpl-3.0.html + + + diff --git a/data/icons/tool-line-symbolic.svg b/data/icons/tool-line-symbolic.svg new file mode 100644 index 0000000..953d9b5 --- /dev/null +++ b/data/icons/tool-line-symbolic.svg @@ -0,0 +1,7 @@ + + +https://github.com/PapirusDevelopmentTeam/papirus-icon-theme/blob/master/Papirus/symbolic/actions/tool-line-symbolic.svg +https://www.gnu.org/licenses/gpl-3.0.html + + + diff --git a/data/icons/tool-mirror-symbolic.svg b/data/icons/tool-mirror-symbolic.svg new file mode 100644 index 0000000..403970b --- /dev/null +++ b/data/icons/tool-mirror-symbolic.svg @@ -0,0 +1,8 @@ + + +https://github.com/PapirusDevelopmentTeam/papirus-icon-theme/blob/master/Papirus/symbolic/actions/view-mirror-symbolic.svg +https://www.gnu.org/licenses/gpl-3.0.html + + + + diff --git a/data/icons/tool-move-symbolic.svg b/data/icons/tool-move-symbolic.svg new file mode 100644 index 0000000..d77f2e3 --- /dev/null +++ b/data/icons/tool-move-symbolic.svg @@ -0,0 +1,14 @@ + + + +Combination of https://github.com/PapirusDevelopmentTeam/papirus-icon-theme/blob/master/Papirus/symbolic/actions/object-move-symbolic.svg +https://www.gnu.org/licenses/gpl-3.0.html +and https://gitlab.gnome.org/World/design/icon-library/-/blob/master/data/resources/icon-dev-kit/rotate-symbolic.svg +https://www.gnu.org/licenses/gpl-3.0.html +optimized with SVGO + + + + + + diff --git a/data/icons/tool-none-symbolic.svg b/data/icons/tool-none-symbolic.svg new file mode 100644 index 0000000..83dad1a --- /dev/null +++ b/data/icons/tool-none-symbolic.svg @@ -0,0 +1,17 @@ + + + +Combination of https://www.svgrepo.com/svg/150374/pencil +https://creativecommons.org/publicdomain/zero/1.0/deed.en +and https://www.svgrepo.com/svg/29291/pencil +https://creativecommons.org/publicdomain/zero/1.0/deed.en + + + + + + + + + + diff --git a/data/icons/tool-polygon-symbolic.svg b/data/icons/tool-polygon-symbolic.svg new file mode 100644 index 0000000..9c1a0b6 --- /dev/null +++ b/data/icons/tool-polygon-symbolic.svg @@ -0,0 +1,8 @@ + + +https://github.com/PapirusDevelopmentTeam/papirus-icon-theme/blob/master/Papirus/symbolic/actions/tool-polygon-symbolic.svg +https://www.gnu.org/licenses/gpl-3.0.html + + + + diff --git a/data/icons/tool-polyline-symbolic.svg b/data/icons/tool-polyline-symbolic.svg new file mode 100644 index 0000000..2f7c894 --- /dev/null +++ b/data/icons/tool-polyline-symbolic.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/data/icons/tool-rectangle-symbolic.svg b/data/icons/tool-rectangle-symbolic.svg new file mode 100644 index 0000000..e7dbfe0 --- /dev/null +++ b/data/icons/tool-rectangle-symbolic.svg @@ -0,0 +1,7 @@ + + +https://github.com/PapirusDevelopmentTeam/papirus-icon-theme/blob/master/Papirus/symbolic/actions/tool-rectangle-symbolic.svg +https://www.gnu.org/licenses/gpl-3.0.html + + + diff --git a/data/icons/tool-resize-symbolic.svg b/data/icons/tool-resize-symbolic.svg new file mode 100644 index 0000000..2f4d37f --- /dev/null +++ b/data/icons/tool-resize-symbolic.svg @@ -0,0 +1,15 @@ + + +https://gitlab.gnome.org/GNOME/adwaita-icon-theme/-/blob/master/Adwaita/scalable/actions/view-fullscreen-symbolic.svg +https://creativecommons.org/licenses/by-sa/3.0/ + + + + + + + + + + + diff --git a/elements.js b/elements.js index c2214c7..b89800d 100644 --- a/elements.js +++ b/elements.js @@ -1,4 +1,5 @@ /* jslint esversion: 6 */ +/* exported Shapes, Transformations, getAllFontFamilies, DrawingElement */ /* * Copyright 2019 Abakkk @@ -26,28 +27,27 @@ const Lang = imports.lang; const Pango = imports.gi.Pango; const PangoCairo = imports.gi.PangoCairo; -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; +var Shapes = { NONE: 0, LINE: 1, ELLIPSE: 2, RECTANGLE: 3, TEXT: 4, POLYGON: 5, POLYLINE: 6, IMAGE: 7 }; +var Transformations = { TRANSLATION: 0, ROTATION: 1, SCALE_PRESERVE: 2, STRETCH: 3, REFLECTION: 4, INVERSION: 5 }; + +var getAllFontFamilies = function() { + return PangoCairo.font_map_get_default().list_families().map(fontFamily => fontFamily.get_name()).sort((a,b) => a.localeCompare(b)); }; -var Shapes = { NONE: 0, LINE: 1, ELLIPSE: 2, RECTANGLE: 3, TEXT: 4, POLYGON: 5, POLYLINE: 6, IMAGE: 7 }; -var ShapeNames = { 0: "Free drawing", 1: "Line", 2: "Ellipse", 3: "Rectangle", 4: "Text", 5: "Polygon", 6: "Polyline", 7: "Image" }; -var Transformations = { TRANSLATION: 0, ROTATION: 1, SCALE_PRESERVE: 2, STRETCH: 3, REFLECTION: 4, INVERSION: 5 }; -var LineCapNames = Object.assign(reverseEnumeration(Cairo.LineCap), { 2: 'Square' }); -var LineJoinNames = reverseEnumeration(Cairo.LineJoin); -var FillRuleNames = { 0: 'Nonzero', 1: 'Evenodd' }; -var FontWeightNames = Object.assign(reverseEnumeration(Pango.Weight), { 200: "Ultra-light", 350: "Semi-light", 600: "Semi-bold", 800: "Ultra-bold" }); -delete FontWeightNames[Pango.Weight.ULTRAHEAVY]; -var FontStyleNames = reverseEnumeration(Pango.Style); -var FontStretchNames = reverseEnumeration(Pango.Stretch); -var FontVariantNames = reverseEnumeration(Pango.Variant); +const getFillRuleSvgName = function(fillRule) { + return fillRule == Cairo.FillRule.EVEN_ODD ? 'evenodd' : 'nonzero'; +}; -var getPangoFontFamilies = function() { - return PangoCairo.font_map_get_default().list_families().map(fontFamily => fontFamily.get_name()).sort((a,b) => a.localeCompare(b)); +const getLineCapSvgName = function(lineCap) { + return lineCap == Cairo.LineCap.BUTT ? 'butt' : + lineCap == Cairo.LineCap.SQUASH ? 'square' : + 'round'; +}; + +const getLineJoinSvgName = function(lineJoin) { + return lineJoin == Cairo.LineJoin.MITER ? 'miter' : + lineJoin == Cairo.LineJoin.BEVEL ? 'bevel' : + 'round'; }; const SVG_DEBUG_SUPERPOSES_CAIRO = false; @@ -80,10 +80,21 @@ const _DrawingElement = new Lang.Class({ if (params.transformations === undefined) this.transformations = []; - if (params.font && params.font.weight === 0) - this.font.weight = 400; - if (params.font && params.font.weight === 1) - this.font.weight = 700; + + if (params.font && !(params.font instanceof Pango.FontDescription)) { + // compatibility with v6.2- + if (params.font.weight === 0) + this.font.weight = 400; + else if (params.font.weight === 1) + this.font.weight = 700; + this.font = new Pango.FontDescription(); + ['family', 'weight', 'style', 'stretch', 'variant'].forEach(attribute => { + if (params.font[attribute] !== undefined) + try { + this.font[`set_${attribute}`](params.font[attribute]); + } catch(e) {} + }); + } if (params.transform && params.transform.center) { let angle = (params.transform.angle || 0) + (params.transform.startAngle || 0); @@ -102,7 +113,7 @@ const _DrawingElement = new Lang.Class({ toJSON: function() { return { shape: this.shape, - color: this.color, + color: this.color.toString(), line: this.line, dash: this.dash, fill: this.fill, @@ -114,11 +125,8 @@ const _DrawingElement = new Lang.Class({ }, buildCairo: function(cr, params) { - if (this.color) { - let [success, color] = Clutter.Color.from_string(this.color); - if (success) - Clutter.cairo_set_source_color(cr, color); - } + if (this.color) + Clutter.cairo_set_source_color(cr, this.color); if (this.showSymmetryElement) { let transformation = this.lastTransformation; @@ -244,63 +252,89 @@ const _DrawingElement = new Lang.Class({ return inElement; }, - buildSVG: function(bgColor) { - let transAttribute = ''; + buildSVG: function(bgcolorString) { + let transforms = []; 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})`; + transforms.push(['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]})`; + transforms.push(['translate', center[0], center[1]]); + transforms.push(['rotate', transformation.angle * RADIAN]); + transforms.push(['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]})`; + transforms.push(['translate', center[0], center[1]]); + transforms.push(['rotate', transformation.angle * RADIAN]); + transforms.push(['scale', transformation.scaleX, transformation.scaleY]); + transforms.push(['rotate', -transformation.angle * RADIAN]); + transforms.push(['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})`; + transforms.push(['translate', transformation.slideX, transformation.slideY]); + transforms.push(['rotate', transformation.angle * RADIAN]); + transforms.push(['scale', transformation.scaleX, transformation.scaleY]); + transforms.push(['rotate', -transformation.angle * RADIAN]); + transforms.push(['translate', -transformation.slideX, -transformation.slideY]); } }); - transAttribute += transAttribute ? '"' : ''; + + let grouped = []; + transforms.forEach((transform, index) => { + let [type, ...values] = transform; + + if (grouped.length && grouped[grouped.length - 1][0] == type) + values.forEach((value, valueIndex) => grouped[grouped.length - 1][valueIndex + 1] += value); + else + grouped.push(transform); + }); - return this._drawSvg(transAttribute); + let filtered = grouped.filter(transform => { + let [type, ...values] = transform; + + if (type == 'scale') + return values.some(value => value != 1); + else + return values.some(value => value != 0); + }); + + let transAttribute = ''; + if (filtered.length) { + transAttribute = ' transform="'; + filtered.forEach((transform, index) => { + let [type, ...values] = transform; + transAttribute += `${index == 0 ? '' : ' '}${type}(${values.map(value => Number(value).toFixed(2))})`; + }); + + transAttribute += '"'; + } + + return this._drawSvg(transAttribute, bgcolorString); }, - _drawSvg: function(transAttribute) { + _drawSvg: function(transAttribute, bgcolorString) { let row = "\n "; let points = this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100]); - let color = this.eraser ? bgColor : this.color; + let color = this.eraser ? bgcolorString : this.color.toString(); let fill = this.fill && !this.isStraightLine; - let attributes = ''; + let attributes = this.eraser ? `class="eraser" ` : ''; if (fill) { - attributes = `fill="${color}"`; + attributes += `fill="${color}"`; if (this.fillRule) - attributes += ` fill-rule="${FillRuleNames[this.fillRule].toLowerCase()}"`; + attributes += ` fill-rule="${getFillRuleSvgName(this.fillRule)}"`; } else { - attributes = `fill="none"`; + 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()}"`; + attributes += ` stroke-linecap="${getLineCapSvgName(this.line.lineCap)}"`; if (this.line.lineJoin && !this.isStraightLine) - attributes += ` stroke-linejoin="${LineJoinNames[this.line.lineJoin].toLowerCase()}"`; + attributes += ` stroke-linejoin="${getLineJoinSvgName(this.line.lineJoin)}"`; if (this.dash && this.dash.active && this.dash.array && this.dash.array[0] && this.dash.array[1]) attributes += ` stroke-dasharray="${this.dash.array[0]} ${this.dash.array[1]}" stroke-dashoffset="${this.dash.offset}"`; - } else { - attributes += ` stroke="none"`; } if (this.shape == Shapes.LINE && points.length == 4) { @@ -594,15 +628,18 @@ const TextElement = new Lang.Class({ Extends: _DrawingElement, toJSON: function() { + // The font size is useless because it is always computed from the points during cairo/svg building. + this.font.unset_fields(Pango.FontMask.SIZE); + return { shape: this.shape, - color: this.color, + color: this.color.toString(), eraser: this.eraser, transformations: this.transformations, text: this.text, lineIndex: this.lineIndex !== undefined ? this.lineIndex : undefined, textRightAligned: this.textRightAligned, - font: this.font, + font: this.font.to_string(), points: this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100]) }; }, @@ -629,15 +666,8 @@ const TextElement = new Lang.Class({ if (this.points.length == 2) { let layout = PangoCairo.create_layout(cr); let fontSize = this.height * 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); + this.font.set_absolute_size(fontSize); + layout.set_font_description(this.font); layout.set_text(this.text, -1); this.textWidth = layout.get_pixel_size()[0]; cr.moveTo(this.x, this.y - layout.get_baseline() / Pango.SCALE); @@ -670,29 +700,29 @@ const TextElement = new Lang.Class({ return cr.inFill(x, y); }, - _drawSvg: function(transAttribute) { + _drawSvg: function(transAttribute, bgcolorString) { let row = "\n "; let [x, y, height] = [Math.round(this.x*100)/100, Math.round(this.y*100)/100, Math.round(this.height*100)/100]; - let color = this.eraser ? bgColor : this.color; - let attributes = ''; + let color = this.eraser ? bgcolorString : this.color.toString(); + let attributes = this.eraser ? `class="eraser" ` : ''; if (this.points.length == 2) { - attributes = `fill="${color}" ` + - `stroke="transparent" ` + - `stroke-opacity="0" ` + - `font-size="${height}"`; - - 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()}"`; + attributes += `fill="${color}" ` + + `font-size="${height}" ` + + `font-family="${this.font.get_family()}"`; + // this.font.to_string() is not valid to fill the svg 'font' shorthand property. + // Each property must be filled separately. + ['Stretch', 'Style', 'Variant'].forEach(attribute => { + let lower = attribute.toLowerCase(); + if (this.font[`get_${lower}`]() != Pango[attribute].NORMAL) { + let font = new Pango.FontDescription(); + font[`set_${lower}`](this.font[`get_${lower}`]()); + attributes += ` font-${lower}="${font.to_string()}"`; + } + }); + if (this.font.get_weight() != Pango.Weight.NORMAL) + attributes += ` font-weight="${this.font.get_weight()}"`; row += `${this.text}`; } @@ -745,7 +775,7 @@ const ImageElement = new Lang.Class({ toJSON: function() { return { shape: this.shape, - color: this.color, + color: this.color.toString(), fill: this.fill, eraser: this.eraser, transformations: this.transformations, @@ -789,10 +819,10 @@ const ImageElement = new Lang.Class({ _drawSvg: function(transAttribute) { let points = this.points; let row = "\n "; - let attributes = ''; + let attributes = this.eraser ? `class="eraser" ` : ''; if (points.length == 2) { - attributes = `fill="none"`; + attributes += `fill="none"`; row += `. */ -const Gio = imports.gi.Gio; -const GLib = imports.gi.GLib; const Lang = imports.lang; const Meta = imports.gi.Meta; const Shell = imports.gi.Shell; const St = imports.gi.St; const Config = imports.misc.config; +const ExtensionUtils = imports.misc.extensionUtils; const Main = imports.ui.main; const OsdWindow = imports.ui.osdWindow; const PanelMenu = imports.ui.panelMenu; -const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Convenience = ExtensionUtils.getSettings && ExtensionUtils.initTranslations ? ExtensionUtils : Me.imports.convenience; const Area = Me.imports.area; +const Files = Me.imports.files; const Helper = Me.imports.helper; const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; @@ -46,102 +46,114 @@ const HIDE_TIMEOUT_LONG = 2500; // ms, default is 1500 ms 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'; - -var manager; +const WARNING_COLOR_STYLE_CLASS_NAME = 'login-dialog-message-warning'; function init() { - Convenience.initTranslations(); + return new Extension(); } -function enable() { - manager = new AreaManager(); -} +const Extension = new Lang.Class({ + Name: 'DrawOnYourScreenExtension', + + _init: function() { + Convenience.initTranslations(); + }, -function disable() { - manager.disable(); - manager = null; -} + enable() { + if (ExtensionUtils.isOutOfDate(Me)) + log(`${Me.metadata.uuid}: GNOME Shell ${Number.parseFloat(GS_VERSION)} is not supported.`); + + Me.settings = Convenience.getSettings(); + Me.internalShortcutSettings = Convenience.getSettings(Me.metadata['settings-schema'] + '.internal-shortcuts'); + Me.drawingSettings = Convenience.getSettings(Me.metadata['settings-schema'] + '.drawing'); + this.areaManager = new AreaManager(); + }, + + disable() { + this.areaManager.disable(); + delete this.areaManager; + delete Me.settings; + delete Me.internalShortcutSettings; + } +}); // AreaManager assigns one DrawingArea per monitor (updateAreas()), // distributes keybinding callbacks to the active area // and handles stylesheet and monitor changes. -var AreaManager = new Lang.Class({ +const AreaManager = new Lang.Class({ Name: 'DrawOnYourScreenAreaManager', _init: function() { - this.settings = Convenience.getSettings(); this.areas = []; this.activeArea = null; - this.enterGicon = new Gio.ThemedIcon({ name: 'applications-graphics-symbolic' }); - this.leaveGicon = new Gio.ThemedIcon({ name: 'application-exit-symbolic' }); Main.wm.addKeybinding('toggle-drawing', - this.settings, + Me.settings, Meta.KeyBindingFlags.NONE, Shell.ActionMode.ALL, this.toggleDrawing.bind(this)); Main.wm.addKeybinding('toggle-modal', - this.settings, + Me.settings, Meta.KeyBindingFlags.NONE, Shell.ActionMode.ALL, this.toggleModal.bind(this)); - Main.wm.addKeybinding('erase-drawing', - this.settings, + Main.wm.addKeybinding('erase-drawings', + Me.settings, Meta.KeyBindingFlags.NONE, Shell.ActionMode.ALL, - this.eraseDrawing.bind(this)); + this.eraseDrawings.bind(this)); this.updateAreas(); this.monitorChangedHandler = Main.layoutManager.connect('monitors-changed', this.updateAreas.bind(this)); this.updateIndicator(); - this.indicatorSettingHandler = this.settings.connect('changed::indicator-disabled', this.updateIndicator.bind(this)); + this.indicatorSettingHandler = Me.settings.connect('changed::indicator-disabled', this.updateIndicator.bind(this)); - this.desktopSettingHandler = this.settings.connect('changed::drawing-on-desktop', this.onDesktopSettingChanged.bind(this)); - this.persistentSettingHandler = this.settings.connect('changed::persistent-drawing', this.onPersistentSettingChanged.bind(this)); - - this.userStyleFile = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir'], 'user.css'])); - - if (this.userStyleFile.query_exists(null)) { - let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); - theme.load_stylesheet(this.userStyleFile); - } - - this.userStyleMonitor = this.userStyleFile.monitor_file(Gio.FileMonitorFlags.WATCH_MOVES, null); - this.userStyleHandler = this.userStyleMonitor.connect('changed', (monitor, file, otherFile, eventType) => { - // 'CHANGED' events are followed by a 'CHANGES_DONE_HINT' event - if (eventType == Gio.FileMonitorEvent.CHANGED || eventType == Gio.FileMonitorEvent.ATTRIBUTE_CHANGED) - return; - - let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); - if (theme.get_custom_stylesheets().indexOf(this.userStyleFile) != -1) - theme.unload_stylesheet(this.userStyleFile); - if (this.userStyleFile.query_exists(null)) - theme.load_stylesheet(this.userStyleFile); - }); + this.desktopSettingHandler = Me.settings.connect('changed::drawing-on-desktop', this.onDesktopSettingChanged.bind(this)); + this.persistentOverRestartsSettingHandler = Me.settings.connect('changed::persistent-over-restarts', this.onPersistentOverRestartsSettingChanged.bind(this)); + this.persistentOverTogglesSettingHandler = Me.settings.connect('changed::persistent-over-toggles', this.onPersistentOverTogglesSettingChanged.bind(this)); + }, + + get persistentOverToggles() { + return Me.settings.get_boolean('persistent-over-toggles'); + }, + + get persistentOverRestarts() { + return Me.settings.get_boolean('persistent-over-toggles') && Me.settings.get_boolean('persistent-over-restarts'); + }, + + get onDesktop() { + return Me.settings.get_boolean('persistent-over-toggles') && Me.settings.get_boolean('drawing-on-desktop'); }, onDesktopSettingChanged: function() { - if (this.settings.get_boolean("drawing-on-desktop")) + if (this.onDesktop) this.areas.forEach(area => area.get_parent().show()); else this.areas.forEach(area => area.get_parent().hide()); }, - onPersistentSettingChanged: function() { - if (this.settings.get_boolean('persistent-drawing')) + onPersistentOverRestartsSettingChanged: function() { + if (this.persistentOverRestarts) this.areas[Main.layoutManager.primaryIndex].syncPersistent(); }, + onPersistentOverTogglesSettingChanged: function() { + if (!this.persistentOverToggles && !this.activeArea) + this.eraseDrawings(); + + this.onPersistentOverRestartsSettingChanged(); + this.onDesktopSettingChanged(); + }, + updateIndicator: function() { if (this.indicator) { this.indicator.disable(); this.indicator = null; } - if (!this.settings.get_boolean('indicator-disabled')) + if (!Me.settings.get_boolean('indicator-disabled')) this.indicator = new DrawingIndicator(); }, @@ -156,13 +168,13 @@ var AreaManager = new Lang.Class({ let monitor = this.monitors[i]; let container = new St.Widget({ name: 'drawOnYourSreenContainer' + i }); let helper = new Helper.DrawingHelper({ name: 'drawOnYourSreenHelper' + i }, monitor); - let loadPersistent = i == Main.layoutManager.primaryIndex && this.settings.get_boolean('persistent-drawing'); + let loadPersistent = i == Main.layoutManager.primaryIndex && this.persistentOverRestarts; let area = new Area.DrawingArea({ name: 'drawOnYourSreenArea' + i }, monitor, helper, loadPersistent); container.add_child(area); container.add_child(helper); Main.layoutManager._backgroundGroup.insert_child_above(container, Main.layoutManager._bgManagers[i].backgroundActor); - if (!this.settings.get_boolean("drawing-on-desktop")) + if (!this.onDesktop) container.hide(); container.set_position(monitor.x, monitor.y); @@ -171,7 +183,6 @@ var AreaManager = new Lang.Class({ area.leaveDrawingHandler = area.connect('leave-drawing-mode', this.toggleDrawing.bind(this)); area.updateActionModeHandler = area.connect('update-action-mode', this.updateActionMode.bind(this)); area.showOsdHandler = area.connect('show-osd', this.showOsd.bind(this)); - area.showOsdGiconHandler = area.connect('show-osd-gicon', this.showOsd.bind(this)); this.areas.push(area); } }, @@ -187,12 +198,14 @@ var AreaManager = new Lang.Class({ 'decrement-line-width': () => this.activeArea.incrementLineWidth(-1), 'increment-line-width-more': () => this.activeArea.incrementLineWidth(5), 'decrement-line-width-more': () => this.activeArea.incrementLineWidth(-5), + 'paste-image-files': this.activeArea.pasteImageFiles.bind(this.activeArea), 'switch-linejoin': this.activeArea.switchLineJoin.bind(this.activeArea), 'switch-linecap': this.activeArea.switchLineCap.bind(this.activeArea), 'switch-fill-rule': this.activeArea.switchFillRule.bind(this.activeArea), 'switch-dash' : this.activeArea.switchDash.bind(this.activeArea), 'switch-fill' : this.activeArea.switchFill.bind(this.activeArea), - 'switch-image-file' : this.activeArea.switchImageFile.bind(this.activeArea), + 'switch-image-file' : this.activeArea.switchImageFile.bind(this.activeArea, false), + 'switch-image-file-reverse' : this.activeArea.switchImageFile.bind(this.activeArea, true), 'select-none-shape': () => this.activeArea.selectTool(Area.Tools.NONE), 'select-line-shape': () => this.activeArea.selectTool(Area.Tools.LINE), 'select-ellipse-shape': () => this.activeArea.selectTool(Area.Tools.ELLIPSE), @@ -208,27 +221,28 @@ var AreaManager = new Lang.Class({ // available when writing this.internalKeybindings2 = { - 'save-as-svg': this.activeArea.saveAsSvg.bind(this.activeArea), - 'save-as-json': this.activeArea.saveAsJson.bind(this.activeArea), + 'export-to-svg': this.activeArea.exportToSvg.bind(this.activeArea), + 'save-as-json': this.activeArea.saveAsJson.bind(this.activeArea, true, null), 'open-previous-json': this.activeArea.loadPreviousJson.bind(this.activeArea), 'open-next-json': this.activeArea.loadNextJson.bind(this.activeArea), '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), - 'reverse-switch-font-family': this.activeArea.switchFontFamily.bind(this.activeArea, true), + 'switch-color-palette': this.activeArea.switchColorPalette.bind(this.activeArea, false), + 'switch-color-palette-reverse': this.activeArea.switchColorPalette.bind(this.activeArea, true), 'switch-font-family': this.activeArea.switchFontFamily.bind(this.activeArea, false), + 'switch-font-family-reverse': this.activeArea.switchFontFamily.bind(this.activeArea, true), 'switch-font-weight': this.activeArea.switchFontWeight.bind(this.activeArea), 'switch-font-style': this.activeArea.switchFontStyle.bind(this.activeArea), 'switch-text-alignment': this.activeArea.switchTextAlignment.bind(this.activeArea), 'toggle-panel-and-dock-visibility': this.togglePanelAndDockOpacity.bind(this), 'toggle-help': this.activeArea.toggleHelp.bind(this.activeArea), - 'open-user-stylesheet': this.openUserStyleFile.bind(this), 'open-preferences': this.openPreferences.bind(this) }; for (let key in this.internalKeybindings1) { Main.wm.addKeybinding(key, - this.settings, + Me.internalShortcutSettings, Meta.KeyBindingFlags.NONE, DRAWING_ACTION_MODE, this.internalKeybindings1[key]); @@ -236,7 +250,7 @@ var AreaManager = new Lang.Class({ for (let key in this.internalKeybindings2) { Main.wm.addKeybinding(key, - this.settings, + Me.internalShortcutSettings, Meta.KeyBindingFlags.NONE, DRAWING_ACTION_MODE | WRITING_ACTION_MODE, this.internalKeybindings2[key]); @@ -245,10 +259,10 @@ var AreaManager = new Lang.Class({ for (let i = 1; i < 10; i++) { let iCaptured = i; Main.wm.addKeybinding('select-color' + i, - this.settings, + Me.internalShortcutSettings, Meta.KeyBindingFlags.NONE, DRAWING_ACTION_MODE | WRITING_ACTION_MODE, - () => this.activeArea.selectColor(iCaptured)); + this.activeArea.selectColor.bind(this.activeArea, iCaptured - 1)); } }, @@ -272,27 +286,10 @@ var AreaManager = new Lang.Class({ } }, - openUserStyleFile: function() { - if (!this.userStyleFile.query_exists(null)) { - if (!this.userStyleFile.get_parent().query_exists(null)) - this.userStyleFile.get_parent().make_directory_with_parents(null); - let defaultStyleFile = Me.dir.get_child('data').get_child('default.css'); - if (!defaultStyleFile.query_exists(null)) - return; - let success = defaultStyleFile.copy(this.userStyleFile, Gio.FileCopyFlags.NONE, null, null); - if (!success) - return; - } - - Gio.AppInfo.launch_default_for_uri(this.userStyleFile.get_uri(), global.create_app_launch_context(0, -1)); - if (this.activeArea) - this.toggleDrawing(); - }, - - eraseDrawing: function() { + eraseDrawings: function() { for (let i = 0; i < this.areas.length; i++) this.areas[i].erase(); - if (this.settings.get_boolean('persistent-drawing')) + if (this.persistentOverRestarts) this.areas[Main.layoutManager.primaryIndex].savePersistent(); }, @@ -345,7 +342,7 @@ var AreaManager = new Lang.Class({ 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")) + if (!this.onDesktop) activeContainer.hide(); } else { Main.layoutManager._backgroundGroup.remove_actor(activeContainer); @@ -365,8 +362,9 @@ var AreaManager = new Lang.Class({ 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'); + // Translators: "released" as the opposite of "grabbed" + this.showOsd(null, Files.Icons.UNGRAB, _("Keyboard and pointer released"), null, null, false); + this.setCursor('DEFAULT'); this.activeArea.reactive = false; this.removeInternalKeybindings(); } else { @@ -378,7 +376,7 @@ var AreaManager = new Lang.Class({ this.activeArea.reactive = true; this.activeArea.initPointerCursor(); if (source && source == global.display) - this.showOsd(null, 'input-touchpad-symbolic', _("Keyboard and pointer grabbed"), null, null, false); + this.showOsd(null, Files.Icons.GRAB, _("Keyboard and pointer grabbed"), null, null, false); } return true; @@ -387,10 +385,11 @@ var AreaManager = new Lang.Class({ toggleDrawing: function() { if (this.activeArea) { let activeIndex = this.areas.indexOf(this.activeArea); - let save = activeIndex == Main.layoutManager.primaryIndex && this.settings.get_boolean('persistent-drawing'); + let save = activeIndex == Main.layoutManager.primaryIndex && this.persistentOverRestarts; + let erase = !this.persistentOverToggles; - this.showOsd(null, this.leaveGicon, _("Leaving drawing mode")); - this.activeArea.leaveDrawingMode(save); + this.showOsd(null, Files.Icons.LEAVE, _("Leaving drawing mode")); + this.activeArea.leaveDrawingMode(save, erase); if (this.hiddenList) this.togglePanelAndDockOpacity(); @@ -410,9 +409,10 @@ var AreaManager = new Lang.Class({ } 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); + this.osdDisabled = Me.settings.get_boolean('osd-disabled'); + // Translators: %s is a key label + let label = "" + _("Press %s for help").format(this.activeArea.helper.helpKeyLabel) + "\n\n" + _("Entering drawing mode"); + this.showOsd(null, Files.Icons.ENTER, label, null, null, true); } if (this.indicator) @@ -446,10 +446,8 @@ var AreaManager = new Lang.Class({ 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; + if (!icon) + icon = Files.Icons.ENTER; let osdWindow = Main.osdWindowManager._osdWindows[activeIndex]; @@ -503,13 +501,20 @@ var AreaManager = new Lang.Class({ OsdWindow.HIDE_TIMEOUT = hideTimeoutSave; }, + setCursor: function(cursorName) { + // check display or screen (API changes) + if (global.display.set_cursor) + global.display.set_cursor(Meta.Cursor[cursorName]); + else if (global.screen && global.screen.set_cursor) + global.screen.set_cursor(Meta.Cursor[cursorName]); + }, + 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); - area.disconnect(area.showOsdGiconHandler); let container = area.get_parent(); container.get_parent().remove_actor(container); container.destroy(); @@ -518,37 +523,35 @@ var AreaManager = new Lang.Class({ }, disable: function() { - if (this.userStyleHandler && this.userStyleMonitor) { - this.userStyleMonitor.disconnect(this.userStyleHandler); - this.userStyleHandler = null; - } - if (this.userStyleMonitor) { - this.userStyleMonitor.cancel(); - this.userStyleMonitor = null; - } if (this.monitorChangedHandler) { Main.layoutManager.disconnect(this.monitorChangedHandler); this.monitorChangedHandler = null; } if (this.indicatorSettingHandler) { - this.settings.disconnect(this.indicatorSettingHandler); + Me.settings.disconnect(this.indicatorSettingHandler); this.indicatorSettingHandler = null; } if (this.desktopSettingHandler) { - this.settings.disconnect(this.desktopSettingHandler); + Me.settings.disconnect(this.desktopSettingHandler); this.desktopSettingHandler = null; } - if (this.persistentSettingHandler) { - this.settings.disconnect(this.persistentSettingHandler); - this.persistentSettingHandler = null; + if (this.persistentOverTogglesSettingHandler) { + Me.settings.disconnect(this.persistentOverTogglesSettingHandler); + this.persistentOverTogglesSettingHandler = null; + } + if (this.persistentOverRestartsSettingHandler) { + Me.settings.disconnect(this.persistentOverRestartsSettingHandler); + this.persistentOverRestartsSettingHandler = null; } if (this.activeArea) this.toggleDrawing(); Main.wm.removeKeybinding('toggle-drawing'); Main.wm.removeKeybinding('toggle-modal'); - Main.wm.removeKeybinding('erase-drawing'); + Main.wm.removeKeybinding('erase-drawings'); this.removeAreas(); + Files.Images.disable(); + Files.Jsons.disable(); if (this.indicator) this.indicator.disable(); } @@ -601,12 +604,3 @@ const DrawingIndicator = new Lang.Class({ } }); -function setCursor(cursorName) { - // check display or screen (API changes) - if (global.display.set_cursor) - global.display.set_cursor(Meta.Cursor[cursorName]); - else if (global.screen && global.screen.set_cursor) - global.screen.set_cursor(Meta.Cursor[cursorName]); -} - - diff --git a/files.js b/files.js index 0001cc4..22a31bc 100644 --- a/files.js +++ b/files.js @@ -1,4 +1,5 @@ /* jslint esversion: 6 */ +/* exported Icons, Image, Images, Json, Jsons, getDateString, saveSvg */ /* * Copyright 2019 Abakkk @@ -26,13 +27,54 @@ const GdkPixbuf = imports.gi.GdkPixbuf; const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const Lang = imports.lang; +const St = imports.gi.St; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); -const EXAMPLE_IMAGES = Me.dir.get_child('data').get_child('images'); -const USER_IMAGES = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir'], 'images'])); +const EXAMPLE_IMAGE_DIRECTORY = Me.dir.get_child('data').get_child('images'); +const DEFAULT_USER_IMAGE_LOCATION = GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir'], 'images']); +const Clipboard = St.Clipboard.get_default(); +const CLIPBOARD_TYPE = St.ClipboardType.CLIPBOARD; +const ICON_DIR = Me.dir.get_child('data').get_child('icons'); +const ICON_NAMES = [ + 'arc', 'color', 'dashed-line', 'document-export', 'fillrule-evenodd', 'fillrule-nonzero', 'fill', 'full-line', 'linecap', 'linejoin', 'palette', 'smooth', 'stroke', + 'tool-ellipse', 'tool-line', 'tool-mirror', 'tool-move', 'tool-none', 'tool-polygon', 'tool-polyline', 'tool-rectangle', 'tool-resize', +]; +const ThemedIconNames = { + ENTER: 'applications-graphics', LEAVE: 'application-exit', + GRAB: 'input-touchpad', UNGRAB: 'touchpad-disabled', + OPEN: 'document-open', SAVE: 'document-save', + FONT_FAMILY: 'font-x-generic', FONT_STYLE: 'format-text-italic', FONT_WEIGHT:'format-text-bold', + LEFT_ALIGNED: 'format-justify-left', RIGHT_ALIGNED: 'format-justify-right', + TOOL_IMAGE: 'insert-image', TOOL_TEXT: 'insert-text', +}; -// wrapper around an image file +var Icons = {}; + +ICON_NAMES.forEach(name => { + Object.defineProperty(Icons, name.toUpperCase().replace(/-/gi, '_'), { + get: function() { + if (!this[`_${name}`]) { + let file = Gio.File.new_for_path(ICON_DIR.get_child(`${name}-symbolic.svg`).get_path()); + this[`_${name}`] = file.query_exists(null) ? new Gio.FileIcon({ file }) : new Gio.ThemedIcon({ name: 'action-unavailable-symbolic' }); + } + return this[`_${name}`]; + } + }); +}); + +Object.keys(ThemedIconNames).forEach(key => { + Object.defineProperty(Icons, key, { + get: function() { + if (!this[`_${key}`]) + this[`_${key}`] = new Gio.ThemedIcon({ name: `${ThemedIconNames[key]}-symbolic` }); + return this[`_${key}`]; + } + }); +}); + +// Wrapper around image data. If not subclassed, it is used when loading in the area an image element for a drawing file (.json) +// and it takes { displayName, contentType, base64, hash } as params. var Image = new Lang.Class({ Name: 'DrawOnYourScreenImage', @@ -54,29 +96,9 @@ var Image = new Lang.Class({ }; }, - // only called from menu so file exists - get gicon() { - if (!this._gicon) - this._gicon = new Gio.FileIcon({ file: this.file }); - return this._gicon; - }, - get bytes() { - if (!this._bytes) { - if (this.file) - try { - // load_bytes available in GLib 2.56+ - this._bytes = this.file.load_bytes(null)[0]; - } catch(e) { - let [success_, contents] = this.file.load_contents(null); - if (contents instanceof Uint8Array) - this._bytes = ByteArray.toGBytes(contents); - else - this._bytes = contents.toGBytes(); - } - else - this._bytes = new GLib.Bytes(GLib.base64_decode(this.base64)); - } + if (!this._bytes) + this._bytes = new GLib.Bytes(GLib.base64_decode(this.base64)); return this._bytes; }, @@ -124,34 +146,207 @@ var Image = new Lang.Class({ } }); -var getImages = function() { - let images = []; +// Add a gicon generator to Image. It is used with image files and it takes { file, info } as params. +const ImageWithGicon = new Lang.Class({ + Name: 'DrawOnYourScreenImageWithGicon', + Extends: Image, - [EXAMPLE_IMAGES, USER_IMAGES].forEach(directory => { - let enumerator; - try { - enumerator = directory.enumerate_children('standard::display-name,standard::content-type', Gio.FileQueryInfoFlags.NONE, null); - } catch(e) { - return; + get displayName() { + return this.info.get_display_name(); + }, + + get contentType() { + return this.info.get_content_type(); + }, + + get thumbnailFile() { + if (!this._thumbnailFile) { + if (this.info.has_attribute('thumbnail::path') && this.info.get_attribute_boolean('thumbnail::is-valid')) { + let thumbnailPath = this.info.get_attribute_as_string('thumbnail::path'); + this._thumbnailFile = Gio.File.new_for_path(thumbnailPath); + } } + return this._thumbnailFile || null; + }, + + get gicon() { + if (!this._gicon) + this._gicon = new Gio.FileIcon({ file: this.thumbnailFile || this.file }); + return this._gicon; + }, + + // use only thumbnails in menu (memory) + get thumbnailGicon() { + if (this.contentType != 'image/svg+xml' && !this.thumbnailFile) + return null; - let fileInfo = enumerator.next_file(null); - while (fileInfo) { - if (fileInfo.get_content_type().indexOf('image') == 0) - images.push(new Image({ file: enumerator.get_child(fileInfo), contentType: fileInfo.get_content_type(), displayName: fileInfo.get_display_name() })); - fileInfo = enumerator.next_file(null); + return this.gicon; + }, + + get bytes() { + if (!this._bytes) { + try { + // load_bytes available in GLib 2.56+ + this._bytes = this.file.load_bytes(null)[0]; + } catch(e) { + let [, contents] = this.file.load_contents(null); + if (contents instanceof Uint8Array) + this._bytes = ByteArray.toGBytes(contents); + else + this._bytes = contents.toGBytes(); + } } - enumerator.close(null); - }); + return this._bytes; + } +}); + +// It is directly generated from a Json object, without an image file. It takes { bytes, displayName, gicon } as params. +const ImageFromJson = new Lang.Class({ + Name: 'DrawOnYourScreenImageFromJson', + Extends: Image, + contentType: 'image/svg+xml', - images.sort((a, b) => { - return a.displayName.localeCompare(b.displayName); - }); + get bytes() { + return this._bytes; + }, - return images; + set bytes(bytes) { + this._bytes = bytes; + } +}); + +// Access images with getPrevious, getNext, getSorted or by iterating over it. +var Images = { + _images: [], + _clipboardImages: [], + _upToDate: false, + + disable: function() { + this._images = []; + this._clipboardImages = []; + this._upToDate = false; + }, + + _clipboardImagesContains: function(file) { + return this._clipboardImages.some(image => image.file.equal(file)); + }, + + // Firstly iterate over the extension directory that contains Example.svg, + // secondly iterate over the directory that was configured by the user in prefs, + // finally iterate over the images pasted from the clipboard. + [Symbol.iterator]: function() { + if (this._upToDate) + return this._images.concat(this._clipboardImages)[Symbol.iterator](); + + this._upToDate = true; + let oldImages = this._images; + let newImages = this._images = []; + let clipboardImagesContains = this._clipboardImagesContains.bind(this); + let clipboardIterator = this._clipboardImages[Symbol.iterator](); + + return { + getExampleEnumerator: function() { + try { + return EXAMPLE_IMAGE_DIRECTORY.enumerate_children('standard::,thumbnail::', Gio.FileQueryInfoFlags.NONE, null); + } catch(e) { + return this.getUserEnumerator(); + } + }, + + getUserEnumerator: function() { + try { + let userLocation = Me.drawingSettings.get_string('image-location') || DEFAULT_USER_IMAGE_LOCATION; + let userDirectory = Gio.File.new_for_commandline_arg(userLocation); + return userDirectory.enumerate_children('standard::,thumbnail::', Gio.FileQueryInfoFlags.NONE, null); + } catch(e) { + return null; + } + }, + + get enumerator() { + if (this._enumerator === undefined) + this._enumerator = this.getExampleEnumerator(); + else if (this._enumerator && this._enumerator.get_container().equal(EXAMPLE_IMAGE_DIRECTORY) && this._enumerator.is_closed()) + this._enumerator = this.getUserEnumerator(); + else if (this._enumerator && this._enumerator.is_closed()) + this._enumerator = null; + + return this._enumerator; + }, + + next: function() { + if (!this.enumerator) + return clipboardIterator.next(); + + let info = this.enumerator.next_file(null); + if (!info) { + this.enumerator.close(null); + return this.next(); + } + + let file = this.enumerator.get_child(info); + + if (info.get_content_type().indexOf('image') == 0 && !clipboardImagesContains(file)) { + let image = oldImages.find(oldImage => oldImage.file.equal(file)) || new ImageWithGicon({ file, info }); + newImages.push(image); + return { value: image, done: false }; + } else { + return this.next(); + } + } + }; + }, + + getSorted: function() { + return [...this].sort((a, b) => a.toString().localeCompare(b.toString())); + }, + + getNext: function(currentImage) { + let images = this.getSorted(); + let index = currentImage && currentImage.file ? images.findIndex(image => image.file.equal(currentImage.file)) : -1; + return images[index == images.length - 1 ? 0 : index + 1] || null; + }, + + getPrevious: function(currentImage) { + let images = this.getSorted(); + let index = currentImage && currentImage.file ? images.findIndex(image => image.file.equal(currentImage.file)) : -1; + return images[index <= 0 ? images.length - 1 : index - 1] || null; + }, + + reset: function() { + this._upToDate = false; + }, + + addImagesFromClipboard: function(callback) { + Clipboard.get_text(CLIPBOARD_TYPE, (clipBoard, text) => { + if (!text) + return; + + let lines = text.split('\n'); + if (lines[0] == 'x-special/nautilus-clipboard') + lines = lines.slice(2); + + let images = lines.filter(line => !!line) + .map(line => Gio.File.new_for_commandline_arg(line)) + .filter(file => file.query_exists(null)) + .map(file => [file, file.query_info('standard::,thumbnail::', Gio.FileQueryInfoFlags.NONE, null)]) + .filter(pair => pair[1].get_content_type().indexOf('image') == 0) + .map(pair => new ImageWithGicon({ file: pair[0], info: pair[1] })); + + // Prevent duplicated + images.filter(image => !this._clipboardImagesContains(image.file)) + .forEach(image => this._clipboardImages.push(image)); + + if (images.length) { + this.reset(); + let lastFile = images[images.length - 1].file; + callback(this._clipboardImages.find(image => image.file.equal(lastFile))); + } + }); + } }; -// wrapper around a json file +// Wrapper around a json file (drawing saves). var Json = new Lang.Class({ Name: 'DrawOnYourScreenJson', @@ -160,6 +355,10 @@ var Json = new Lang.Class({ this[key] = params[key]; }, + get isPersistent() { + return this.name == Me.metadata['persistent-file-name']; + }, + toString: function() { return this.displayName || this.name; }, @@ -169,10 +368,10 @@ var Json = new Lang.Class({ }, get file() { - if (!this._file && this.name) + if (!this._file) this._file = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir'], `${this.name}.json`])); - return this._file || null; + return this._file; }, set file(file) { @@ -180,62 +379,181 @@ var Json = new Lang.Class({ }, get contents() { - let success_, contents; - try { - [success_, contents] = this.file.load_contents(null); - if (contents instanceof Uint8Array) - contents = ByteArray.toString(contents); - } catch(e) { - return null; + if (this._contents === undefined) { + try { + [, this._contents] = this.file.load_contents(null); + if (this._contents instanceof Uint8Array) + this._contents = ByteArray.toString(this._contents); + } catch(e) { + this._contents = null; + } } - return contents; + + return this._contents; }, set contents(contents) { + if (this.isPersistent && (this.contents == contents || !this.contents && contents == '[]')) + return; + try { this.file.replace_contents(contents, null, false, Gio.FileCreateFlags.NONE, null); } catch(e) { this.file.get_parent().make_directory_with_parents(null); this.file.replace_contents(contents, null, false, Gio.FileCreateFlags.NONE, null); } + + this._contents = contents; + }, + + addSvgContents: function(getGiconSvgContent, getImageSvgContent) { + let giconSvgBytes = new GLib.Bytes(getGiconSvgContent()); + this.gicon = Gio.BytesIcon.new(giconSvgBytes); + this.getImageSvgBytes = () => new GLib.Bytes(getImageSvgContent()); + }, + + get image() { + if (!this._image) + this._image = new ImageFromJson({ bytes: this.getImageSvgBytes(), gicon: this.gicon, displayName: this.displayName }); + + return this._image; } }); -var getJsons = function() { - let directory = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir']])); +// Access jsons with getPersistent, getDated, getNamed, getPrevious, getNext, getSorted or by iterating over it. +var Jsons = { + _jsons: [], + _upToDate: false, - let enumerator; - try { - enumerator = directory.enumerate_children('standard::name,standard::display-name,standard::content-type,time::modified', Gio.FileQueryInfoFlags.NONE, null); - } catch(e) { - return []; - } - - let jsons = []; - let fileInfo = enumerator.next_file(null); - while (fileInfo) { - if (fileInfo.get_content_type().indexOf('json') != -1 && fileInfo.get_name() != `${Me.metadata['persistent-file-name']}.json`) { - let file = enumerator.get_child(fileInfo); - jsons.push(new Json({ - file, - name: fileInfo.get_name().slice(0, -5), - displayName: fileInfo.get_display_name().slice(0, -5), - // fileInfo.get_modification_date_time: Gio 2.62+ - modificationUnixTime: fileInfo.get_attribute_uint64('time::modified') - })); + disable: function() { + if (this._monitor) { + this._monitor.disconnect(this._monitorHandler); + this._monitor.cancel(); } - fileInfo = enumerator.next_file(null); + + delete this._monitor; + delete this._persistent; + + this._jsons = []; + this._upToDate = false; + }, + + _updateMonitor: function() { + if (this._monitor) + return; + + let directory = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir']])); + this._monitor = directory.monitor(Gio.FileMonitorFlags.NONE, null); + this._monitorHandler = this._monitor.connect('changed', (monitor, file) => { + if (file.get_basename() != `${Me.metadata['persistent-file-name']}.json` && file.get_basename().indexOf('.goutputstream')) + this.reset(); + }); + }, + + [Symbol.iterator]: function() { + if (this._upToDate) + return this._jsons[Symbol.iterator](); + + this._updateMonitor(); + this._upToDate = true; + let newJsons = this._jsons = []; + + return { + get enumerator() { + if (this._enumerator === undefined) { + try { + let directory = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir']])); + this._enumerator = directory.enumerate_children('standard::name,standard::display-name,standard::content-type,time::modified', Gio.FileQueryInfoFlags.NONE, null); + } catch(e) { + this._enumerator = null; + } + } + + return this._enumerator; + }, + + next: function() { + if (!this.enumerator || this.enumerator.is_closed()) + return { done: true }; + + let info = this.enumerator.next_file(null); + if (!info) { + this.enumerator.close(null); + return this.next(); + } + + let file = this.enumerator.get_child(info); + + if (info.get_content_type().indexOf('json') != -1 && info.get_name() != `${Me.metadata['persistent-file-name']}.json`) { + let json = new Json({ + file, name: info.get_name().slice(0, -5), + displayName: info.get_display_name().slice(0, -5), + // info.get_modification_date_time: Gio 2.62+ + modificationUnixTime: info.get_attribute_uint64('time::modified') + }); + + newJsons.push(json); + return { value: json, done: false }; + } else { + return this.next(); + } + } + }; + }, + + getSorted: function() { + return [...this].sort((a, b) => b.modificationUnixTime - a.modificationUnixTime); + }, + + getNext: function(currentJson) { + let jsons = this.getSorted(); + let index = currentJson ? jsons.findIndex(json => json.name == currentJson.name) : -1; + return jsons[index == jsons.length - 1 ? 0 : index + 1] || null; + }, + + getPrevious: function(currentJson) { + let jsons = this.getSorted(); + let index = currentJson ? jsons.findIndex(json => json.name == currentJson.name) : -1; + return jsons[index <= 0 ? jsons.length - 1 : index - 1] || null; + }, + + getPersistent: function() { + if (!this._persistent) + this._persistent = new Json({ name: Me.metadata['persistent-file-name'] }); + + return this._persistent; + }, + + getDated: function() { + return new Json({ name: getDateString() }); + }, + + getNamed: function(name) { + return [...this].find(json => json.name == name) || new Json({ name }); + }, + + reset: function() { + this._upToDate = false; } - enumerator.close(null); - - jsons.sort((a, b) => { - return b.modificationUnixTime - a.modificationUnixTime; - }); - - return jsons; }; var getDateString = function() { let date = GLib.DateTime.new_now_local(); return `${date.format("%F")} ${date.format("%X")}`; }; + +var saveSvg = function(content) { + let filename = `${Me.metadata['svg-file-name']} ${getDateString()}.svg`; + let dir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES); + let path = GLib.build_filenamev([dir, filename]); + let file = Gio.File.new_for_path(path); + if (file.query_exists(null)) + return false; + + try { + return file.replace_contents(content, null, false, Gio.FileCreateFlags.NONE, null)[0]; + } catch(e) { + return false; + } +}; + diff --git a/helper.js b/helper.js index e3bfe2e..444ff93 100644 --- a/helper.js +++ b/helper.js @@ -1,4 +1,5 @@ /* jslint esversion: 6 */ +/* exported DrawingHelper */ /* * Copyright 2019 Abakkk @@ -25,24 +26,19 @@ const Lang = imports.lang; const St = imports.gi.St; const Config = imports.misc.config; +const ExtensionUtils = imports.misc.extensionUtils; const Tweener = imports.ui.tweener; -const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Convenience = ExtensionUtils.getSettings ? ExtensionUtils : Me.imports.convenience; -const Prefs = Me.imports.prefs; +const Shortcuts = Me.imports.shortcuts; const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; const GS_VERSION = Config.PACKAGE_VERSION; const HELPER_ANIMATION_TIME = 0.25; const MEDIA_KEYS_SCHEMA = 'org.gnome.settings-daemon.plugins.media-keys'; -const MEDIA_KEYS_KEYS = { - 'screenshot': "Screenshot", - 'screenshot-clip': "Screenshot to clipboard", - 'area-screenshot': "Area screenshot", - 'area-screenshot-clip': "Area screenshot to clipboard" -}; +const MEDIA_KEYS_KEYS = ['screenshot', 'screenshot-clip', 'area-screenshot', 'area-screenshot-clip']; // DrawingHelper provides the "help osd" (Ctrl + F1) // It uses the same texts as in prefs @@ -55,24 +51,27 @@ 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)); + this.settingsHandler = Me.settings.connect('changed', this._onSettingsChanged.bind(this)); + this.internalShortcutsettingsHandler = Me.internalShortcutSettings.connect('changed', this._onSettingsChanged.bind(this)); + this.connect('destroy', () => { + Me.settings.disconnect(this.settingsHandler); + Me.internalShortcutSettings.disconnect(this.internalShortcutsettingsHandler); + }); }, - _onSettingChanged: function(settings, key) { + _onSettingsChanged: function(settings, key) { if (key == 'toggle-help') this._updateHelpKeyLabel(); if (this.vbox) { this.vbox.destroy(); - this.vbox = null; + delete this.vbox; } }, _updateHelpKeyLabel: function() { - let [keyval, mods] = Gtk.accelerator_parse(this.settings.get_strv('toggle-help')[0]); + let [keyval, mods] = Gtk.accelerator_parse(Me.internalShortcutSettings.get_strv('toggle-help')[0] || ''); this._helpKeyLabel = Gtk.accelerator_get_label(keyval, mods); }, @@ -88,63 +87,72 @@ var DrawingHelper = new Lang.Class({ this.add_actor(this.vbox); this.vbox.add_child(new St.Label({ text: _("Global") })); - for (let settingKey in Prefs.GLOBAL_KEYBINDINGS) { - let hbox = new St.BoxLayout({ vertical: false }); - if (settingKey.indexOf('-separator-') != -1) { + Shortcuts.GLOBAL_KEYBINDINGS.forEach((settingKeys, index) => { + if (index) + this.vbox.add_child(new St.BoxLayout({ vertical: false, style_class: 'draw-on-your-screen-helper-separator' })); + + settingKeys.forEach(settingKey => { + if (!Me.settings.get_strv(settingKey)[0]) + return; + + let hbox = new St.BoxLayout({ vertical: false }); + let [keyval, mods] = Gtk.accelerator_parse(Me.settings.get_strv(settingKey)[0] || ''); + hbox.add_child(new St.Label({ text: Me.settings.settings_schema.get_key(settingKey).get_summary() })); + hbox.add_child(new St.Label({ text: Gtk.accelerator_get_label(keyval, mods), x_expand: true })); this.vbox.add_child(hbox); - continue; - } - if (!this.settings.get_strv(settingKey)[0]) - continue; - let [keyval, mods] = Gtk.accelerator_parse(this.settings.get_strv(settingKey)[0]); - hbox.add_child(new St.Label({ text: _(Prefs.GLOBAL_KEYBINDINGS[settingKey]) })); - hbox.add_child(new St.Label({ text: Gtk.accelerator_get_label(keyval, mods), x_expand: true })); - this.vbox.add_child(hbox); - } + }); + }); + this.vbox.add_child(new St.BoxLayout({ vertical: false, style_class: 'draw-on-your-screen-helper-separator' })); this.vbox.add_child(new St.Label({ text: _("Internal") })); - for (let i = 0; i < Prefs.OTHER_SHORTCUTS.length; i++) { - if (Prefs.OTHER_SHORTCUTS[i].desc.indexOf('-separator-') != -1) { + Shortcuts.OTHERS.forEach((pairs, index) => { + if (index) this.vbox.add_child(new St.BoxLayout({ vertical: false, style_class: 'draw-on-your-screen-helper-separator' })); - continue; - } - let hbox = new St.BoxLayout({ vertical: false }); - hbox.add_child(new St.Label({ text: _(Prefs.OTHER_SHORTCUTS[i].desc) })); - hbox.add_child(new St.Label({ text: Prefs.OTHER_SHORTCUTS[i].shortcut, x_expand: true })); - hbox.get_children()[0].get_clutter_text().set_use_markup(true); - this.vbox.add_child(hbox); - } + + pairs.forEach(pair => { + let [action, shortcut] = pair; + let hbox = new St.BoxLayout({ vertical: false }); + hbox.add_child(new St.Label({ text: action })); + hbox.add_child(new St.Label({ text: shortcut, x_expand: true })); + hbox.get_children()[0].get_clutter_text().set_use_markup(true); + this.vbox.add_child(hbox); + }); + }); this.vbox.add_child(new St.BoxLayout({ vertical: false, style_class: 'draw-on-your-screen-helper-separator' })); - for (let settingKey in Prefs.INTERNAL_KEYBINDINGS) { - if (settingKey.indexOf('-separator-') != -1) { + Shortcuts.INTERNAL_KEYBINDINGS.forEach((settingKeys, index) => { + if (index) this.vbox.add_child(new St.BoxLayout({ vertical: false, style_class: 'draw-on-your-screen-helper-separator' })); - continue; - } - let hbox = new St.BoxLayout({ vertical: false }); - if (!this.settings.get_strv(settingKey)[0]) - continue; - let [keyval, mods] = Gtk.accelerator_parse(this.settings.get_strv(settingKey)[0]); - hbox.add_child(new St.Label({ text: _(Prefs.INTERNAL_KEYBINDINGS[settingKey]) })); - hbox.add_child(new St.Label({ text: Gtk.accelerator_get_label(keyval, mods), x_expand: true })); - this.vbox.add_child(hbox); - } + + settingKeys.forEach(settingKey => { + if (!Me.internalShortcutSettings.get_strv(settingKey)[0]) + return; + + let hbox = new St.BoxLayout({ vertical: false }); + let [keyval, mods] = Gtk.accelerator_parse(Me.internalShortcutSettings.get_strv(settingKey)[0] || ''); + hbox.add_child(new St.Label({ text: Me.internalShortcutSettings.settings_schema.get_key(settingKey).get_summary() })); + hbox.add_child(new St.Label({ text: Gtk.accelerator_get_label(keyval, mods), x_expand: true })); + this.vbox.add_child(hbox); + }); + }); let mediaKeysSettings; try { mediaKeysSettings = Convenience.getSettings(MEDIA_KEYS_SCHEMA); } catch(e) { return; } + + this.vbox.add_child(new St.BoxLayout({ vertical: false, style_class: 'draw-on-your-screen-helper-separator' })); this.vbox.add_child(new St.Label({ text: _("System") })); - for (let settingKey in MEDIA_KEYS_KEYS) { + for (let settingKey of MEDIA_KEYS_KEYS) { if (!mediaKeysSettings.settings_schema.has_key(settingKey)) continue; let shortcut = GS_VERSION < '3.33.0' ? mediaKeysSettings.get_string(settingKey) : mediaKeysSettings.get_strv(settingKey)[0]; if (!shortcut) continue; - let [keyval, mods] = Gtk.accelerator_parse(shortcut); + let [keyval, mods] = Gtk.accelerator_parse(shortcut || ''); let hbox = new St.BoxLayout({ vertical: false }); - hbox.add_child(new St.Label({ text: _(MEDIA_KEYS_KEYS[settingKey]) })); + hbox.add_child(new St.Label({ text: mediaKeysSettings.settings_schema.get_key(settingKey).get_summary() })); hbox.add_child(new St.Label({ text: Gtk.accelerator_get_label(keyval, mods), x_expand: true })); this.vbox.add_child(hbox); } diff --git a/locale/POTFILES.in b/locale/POTFILES.in new file mode 100644 index 0000000..3a9591f --- /dev/null +++ b/locale/POTFILES.in @@ -0,0 +1,8 @@ +# xgettext --from-code=UTF-8 --add-comments="Translators: " --no-location --package-name="Draw On Your Screen" --msgid-bugs-address="https://framagit.org/abakkk/DrawOnYourScreen/issues" -f locale/POTFILES.in +area.js +extension.js +helper.js +menu.js +prefs.js +shortcuts.js +schemas/org.gnome.shell.extensions.draw-on-your-screen.gschema.xml diff --git a/locale/draw-on-your-screen.pot b/locale/draw-on-your-screen.pot index 15ef699..e94b500 100644 --- a/locale/draw-on-your-screen.pot +++ b/locale/draw-on-your-screen.pot @@ -8,9 +8,9 @@ # You are free to translate them or not. msgid "" msgstr "" -"Project-Id-Version: Draw On Your Screen VERSION\n" +"Project-Id-Version: Draw On Your Screen\n" "Report-Msgid-Bugs-To: https://framagit.org/abakkk/DrawOnYourScreen/issues\n" -"POT-Creation-Date: 2019-03-04 16:40+0100\n" +"POT-Creation-Date: 2020-09-17 22:27+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -19,66 +19,344 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -msgid "About" +#. Translators: %s is a key label +#, javascript-format +msgid "" +"Press %s to get\n" +"a fourth control point" msgstr "" -# You are free to translate the extension name, that is displayed in About page, or not. -msgid "Draw On You Screen" +msgid "Mark a point of symmetry" msgstr "" -msgid "Version %d" +msgid "Draw a line of symmetry" msgstr "" -msgid "Start drawing with Super+Alt+D and save your beautiful work by taking a screenshot" +#. Translators: initial content of the text area +msgctxt "text-area-content" +msgid "Text" msgstr "" -# Add your name here, for example: -# (add "\n" as separator if there is many translators) -# msgid "translator-credits" -# msgstr "Me" -# or, with mail: -# msgid "translator-credits" -# msgstr "Me" -# or, with page: -# msgid "translator-credits" -# msgstr "Me" -# else keep it empty. -# It will be displayed in about page -msgid "translator-credits" +#. Translators: %s is a key label +#, javascript-format +msgid "Press %s to mark vertices" msgstr "" -msgid "Preferences" +#. Translators: %s is a key label +#, javascript-format +msgid "Type your text and press %s" +msgstr "" + +#. Translators: "released" as the opposite of "grabbed" +msgid "Keyboard and pointer released" +msgstr "" + +msgid "Keyboard and pointer grabbed" +msgstr "" + +msgid "Leaving drawing mode" +msgstr "" + +#. Translators: %s is a key label +#, javascript-format +msgid "Press %s for help" +msgstr "" + +msgid "Entering drawing mode" msgstr "" msgid "Global" msgstr "" -msgid "Enter/leave drawing mode" +msgid "Internal" msgstr "" -# There is a similar text in GNOME Boxes (https://gitlab.gnome.org/GNOME/gnome-boxes/tree/master/po) -msgid "Grab/ungrab keyboard and pointer" +msgid "System" msgstr "" -msgid "Erase all drawings" +msgid "Dashed line" msgstr "" -msgid "Persistent" +#. Translators: as the alternative to "Dashed line" +msgid "Full line" msgstr "" -msgid "Persistent drawing through session restart" +msgid "Fill" msgstr "" -msgid "Drawing on the desktop" +#. Translators: as the alternative to "Fill" +msgid "Outline" msgstr "" -msgid "Draw On Your Screen becomes Draw On Your Desktop" +#. Translators: fill-rule SVG attribute +msgid "Nonzero" msgstr "" -msgid "Disable on-screen notifications" +msgid "Evenodd" msgstr "" -msgid "Disable panel indicator" +#. Translators: generic font-family SVG attribute +msgctxt "font-family" +msgid "Sans-Serif" +msgstr "" + +msgctxt "font-family" +msgid "Serif" +msgstr "" + +msgctxt "font-family" +msgid "Monospace" +msgstr "" + +msgctxt "font-family" +msgid "Cursive" +msgstr "" + +msgctxt "font-family" +msgid "Fantasy" +msgstr "" + +#. Translators: font-style SVG attribute +msgctxt "font-style" +msgid "Normal" +msgstr "" + +msgctxt "font-style" +msgid "Oblique" +msgstr "" + +msgctxt "font-style" +msgid "Italic" +msgstr "" + +#. Translators: font-weight SVG attribute +msgctxt "font-weight" +msgid "Thin" +msgstr "" + +msgctxt "font-weight" +msgid "Ultra Light" +msgstr "" + +msgctxt "font-weight" +msgid "Light" +msgstr "" + +msgctxt "font-weight" +msgid "Semi Light" +msgstr "" + +msgctxt "font-weight" +msgid "Book" +msgstr "" + +msgctxt "font-weight" +msgid "Normal" +msgstr "" + +msgctxt "font-weight" +msgid "Medium" +msgstr "" + +msgctxt "font-weight" +msgid "Semi Bold" +msgstr "" + +msgctxt "font-weight" +msgid "Bold" +msgstr "" + +msgctxt "font-weight" +msgid "Ultra Bold" +msgstr "" + +msgctxt "font-weight" +msgid "Heavy" +msgstr "" + +msgctxt "font-weight" +msgid "Ultra Heavy" +msgstr "" + +#. Translators: stroke-linecap SVG attribute +msgctxt "stroke-linecap" +msgid "Butt" +msgstr "" + +msgctxt "stroke-linecap" +msgid "Round" +msgstr "" + +msgctxt "stroke-linecap" +msgid "Square" +msgstr "" + +#. Translators: stroke-linejoin SVG attribute +msgctxt "stroke-linejoin" +msgid "Miter" +msgstr "" + +msgctxt "stroke-linejoin" +msgid "Round" +msgstr "" + +msgctxt "stroke-linejoin" +msgid "Bevel" +msgstr "" + +#. Translators: value in pixel unit (e.g. "5 px") +#, javascript-format +msgid "%f px" +msgstr "" + +#. Translators: text alignment +msgid "Right aligned" +msgstr "" + +msgid "Left aligned" +msgstr "" + +msgctxt "drawing-tool" +msgid "Free drawing" +msgstr "" + +msgctxt "drawing-tool" +msgid "Line" +msgstr "" + +msgctxt "drawing-tool" +msgid "Ellipse" +msgstr "" + +msgctxt "drawing-tool" +msgid "Rectangle" +msgstr "" + +msgctxt "drawing-tool" +msgid "Text" +msgstr "" + +msgctxt "drawing-tool" +msgid "Polygon" +msgstr "" + +msgctxt "drawing-tool" +msgid "Polyline" +msgstr "" + +msgctxt "drawing-tool" +msgid "Image" +msgstr "" + +msgctxt "drawing-tool" +msgid "Move" +msgstr "" + +msgctxt "drawing-tool" +msgid "Resize" +msgstr "" + +msgctxt "drawing-tool" +msgid "Mirror" +msgstr "" + +msgid "Undo" +msgstr "" + +msgid "Redo" +msgstr "" + +msgid "Erase" +msgstr "" + +msgid "Smooth" +msgstr "" + +msgid "Open drawing" +msgstr "" + +msgid "Save drawing as…" +msgstr "" + +msgid "Palette" +msgstr "" + +msgid "Color" +msgstr "" + +msgid "Type a name" +msgstr "" + +#. Translators: "Preferences" page in preferences +msgid "Preferences" +msgstr "" + +#. Translators: "Drawing" page in preferences +msgid "Drawing" +msgstr "" + +#. Translators: "About" page in preferences +msgid "About" +msgstr "" + +#. Translators: you are free to translate the extension name, that is displayed in About page, or not +msgid "Draw On You Screen" +msgstr "" + +#. Translators: version number in "About" page +#, javascript-format +msgid "Version %f" +msgstr "" + +#. Translators: you are free to translate the extension description, that is displayed in About page, or not +msgid "" +"Start drawing with Super+Alt+D and save your beautiful work by taking a " +"screenshot" +msgstr "" + +#. Translators: add your name here or keep it empty, it will be displayed in about page, e.g. +#. msgstr "" +#. "translator1\n" +#. "translator2\n" +#. "translator3" +msgid "translator-credits" +msgstr "" + +msgid "Palettes" +msgstr "" + +msgid "Add a new palette" +msgstr "" + +msgid "Area" +msgstr "" + +msgid "Auto" +msgstr "" + +msgid "Grid overlay line" +msgstr "" + +msgid "Tools" +msgstr "" + +msgid "Dash array" +msgstr "" + +msgid "Reset settings" +msgstr "" + +msgid "Rename the palette" +msgstr "" + +msgid "Remove the palette" +msgstr "" + +#. Translators: default name of a new palette +msgid "New palette" +msgstr "" + +msgid "In drawing mode" msgstr "" msgid "Draw" @@ -102,17 +380,19 @@ msgstr "" msgid "Scroll" msgstr "" +#. Translators: %s are key labels (Ctrl+F1 and Ctrl+F9) msgid "Select color" msgstr "" -# %s are key labels (Ctrl+F1 and Ctrl+F9) +#, javascript-format msgid "%s … %s" msgstr "" +#. Translators: %s is a key label msgid "Ignore pointer movement" msgstr "" -# %s is a key label +#, javascript-format msgid "%s held" msgstr "" @@ -137,6 +417,9 @@ msgstr "" msgid "Smooth free drawing outline" msgstr "" +msgid "Unlock image ratio" +msgstr "" + msgid "Rotate (while moving)" msgstr "" @@ -146,73 +429,224 @@ msgstr "" msgid "Inverse (while mirroring)" msgstr "" -msgid "Internal" +msgid "Drawing on the desktop" msgstr "" -msgid "(in drawing mode)" +msgid "Draw On Your Screen becomes Draw On Your Desktop" msgstr "" -msgid "Undo last brushstroke" +msgid "Erase all drawings" msgstr "" -msgid "Redo last brushstroke" +msgid "Disable panel indicator" msgstr "" -msgid "Erase last brushstroke" +msgid "Disable on-screen notifications" msgstr "" -msgid "Smooth last brushstroke" +msgid "Persistent over toggles" msgstr "" -msgid "Select line" +msgid "Drawing remains when toggling drawing mode" msgstr "" -msgid "Select ellipse" +msgid "Persistent over restarts" msgstr "" -msgid "Select rectangle" +msgid "Drawing is automatically saved to a file" msgstr "" -msgid "Select polygon" +msgid "Enter/leave drawing mode" msgstr "" -msgid "Select polyline" +#. Translators: 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 "Select image" +msgid "Background color" msgstr "" -msgid "Select text" +msgid "The color of the drawing area background" msgstr "" -msgid "Select move" +msgid "Automatic dash array" msgstr "" -msgid "Select resize" +msgid "Compute the lengths from the line width" msgstr "" -msgid "Select mirror" +msgid "Dash array on" msgstr "" -msgid "Toggle fill/outline" +msgid "The dash length in pixels" msgstr "" -msgid "Increment line width" +msgid "Dash array off" +msgstr "" + +msgid "The gap between the dashes in pixels" +msgstr "" + +msgid "Dash offset" +msgstr "" + +msgid "The dash offset in pixels" +msgstr "" + +msgid "Grid overlay color" +msgstr "" + +msgid "The color of the lines" +msgstr "" + +msgid "Automatic grid overlay line" +msgstr "" + +msgid "Compute the lengths from the screen size" +msgstr "" + +msgid "Grid overlay line spacing" +msgstr "" + +msgid "The gap between lines in pixels" +msgstr "" + +msgid "Grid overlay line width" +msgstr "" + +msgid "The line width in pixels" +msgstr "" + +msgid "Image location" +msgstr "" + +msgid "The location of the directory in which the image tool picks" +msgstr "" + +msgid "Color palettes" +msgstr "" + +msgid "The palettes of drawing colors" +msgstr "" + +msgid "Automatic square area size" +msgstr "" + +msgid "Compute the area size from the screen size" +msgstr "" + +msgid "Square area size" +msgstr "" + +msgid "The size of the area in pixels" msgstr "" msgid "Decrement line width" msgstr "" -msgid "Increment line width even more" -msgstr "" - msgid "Decrement line width even more" msgstr "" -msgid "Change linejoin" +msgid "Erase last brushstroke" msgstr "" -msgid "Change linecap" +msgid "Export drawing to a SVG file" +msgstr "" + +msgid "Increment line width" +msgstr "" + +msgid "Increment line width even more" +msgstr "" + +msgid "Open next drawing" +msgstr "" + +msgid "Open preferences" +msgstr "" + +msgid "Open previous drawing" +msgstr "" + +msgid "Add images from the clipboard" +msgstr "" + +msgid "Redo last brushstroke" +msgstr "" + +msgid "Save drawing" +msgstr "" + +msgid "Select color 1" +msgstr "" + +msgid "Select color 2" +msgstr "" + +msgid "Select color 3" +msgstr "" + +msgid "Select color 4" +msgstr "" + +msgid "Select color 5" +msgstr "" + +msgid "Select color 6" +msgstr "" + +msgid "Select color 7" +msgstr "" + +msgid "Select color 8" +msgstr "" + +msgid "Select color 9" +msgstr "" + +msgid "Select ellipse tool" +msgstr "" + +msgid "Select image tool" +msgstr "" + +msgid "Select line tool" +msgstr "" + +msgid "Select mirror tool" +msgstr "" + +msgid "Select move tool" +msgstr "" + +msgid "Select free drawing" +msgstr "" + +msgid "Select polygon tool" +msgstr "" + +msgid "Select polyline tool" +msgstr "" + +msgid "Select rectangle tool" +msgstr "" + +msgid "Select resize tool" +msgstr "" + +msgid "Select text tool" +msgstr "" + +msgid "Smooth last brushstroke" +msgstr "" + +msgid "Change color palette" +msgstr "" + +msgid "Change color palette (reverse)" +msgstr "" + +msgid "Toggle fill/outline" msgstr "" msgid "Toggle fill rule" @@ -224,289 +658,42 @@ msgstr "" msgid "Change font family (reverse)" msgstr "" -msgid "Change font weight" -msgstr "" - msgid "Change font style" msgstr "" +msgid "Change font weight" +msgstr "" + +msgid "Change image" +msgstr "" + +msgid "Change image (reverse)" +msgstr "" + +msgid "Change linecap" +msgstr "" + +msgid "Change linejoin" +msgstr "" + msgid "Toggle text alignment" msgstr "" -msgid "Change image file" -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 "" -"Default drawing style attributes (color palette, font, line, dash) are defined in an editable css file.\n" -"See “%s”." +msgid "Hide panel and dock" 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." +#. Translators: It is an action: "Make the drawing area a square" +msgid "Square drawing area" msgstr "" -msgid "Screenshot" +msgid "Undo last brushstroke" msgstr "" - -msgid "Screenshot to clipboard" -msgstr "" - -msgid "Area screenshot" -msgstr "" - -msgid "Area screenshot to clipboard" -msgstr "" - -msgid "System" -msgstr "" - -msgid "Undo" -msgstr "" - -msgid "Redo" -msgstr "" - -msgid "Erase" -msgstr "" - -msgid "Smooth" -msgstr "" - -msgid "Free drawing" -msgstr "" - -msgid "Line" -msgstr "" - -msgid "Ellipse" -msgstr "" - -msgid "Rectangle" -msgstr "" - -msgid "Text" -msgstr "" - -msgid "Polygon" -msgstr "" - -msgid "Polyline" -msgstr "" - -msgid "Image" -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 "" - -msgid "Leaving drawing mode" -msgstr "" - -# %s is a key label -msgid "Press %s for help" -msgstr "" - -msgid "Entering drawing mode" -msgstr "" - -# "released" as the opposite of "grabbed" -msgid "Keyboard and pointer released" -msgstr "" - -msgid "Keyboard and pointer grabbed" -msgstr "" - -# %s is a key label -msgid "" -"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 "" -"Press %s to mark vertices" -msgstr "" - -# %s is a key label -msgid "" -"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 "" - - diff --git a/menu.js b/menu.js index 911f082..86a6032 100644 --- a/menu.js +++ b/menu.js @@ -1,4 +1,5 @@ /* jslint esversion: 6 */ +/* exported DisplayStrings, DrawingMenu */ /* * Copyright 2019 Abakkk @@ -21,7 +22,6 @@ */ const Clutter = imports.gi.Clutter; -const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const GObject = imports.gi.GObject; const Gtk = imports.gi.Gtk; @@ -30,44 +30,121 @@ const St = imports.gi.St; const BoxPointer = imports.ui.boxpointer; const Config = imports.misc.config; +const Dash = imports.ui.dash; const Main = imports.ui.main; const PopupMenu = imports.ui.popupMenu; const Slider = imports.ui.slider; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); -const Area = Me.imports.area; -const Elements = Me.imports.elements; -const Extension = Me.imports.extension; const Files = Me.imports.files; const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; +const pgettext = imports.gettext.domain(Me.metadata['gettext-domain']).pgettext; const GS_VERSION = Config.PACKAGE_VERSION; - -const ICON_DIR = Me.dir.get_child('data').get_child('icons'); -const SMOOTH_ICON_PATH = ICON_DIR.get_child('smooth-symbolic.svg').get_path(); -const COLOR_ICON_PATH = ICON_DIR.get_child('color-symbolic.svg').get_path(); -const FILL_ICON_PATH = ICON_DIR.get_child('fill-symbolic.svg').get_path(); -const STROKE_ICON_PATH = ICON_DIR.get_child('stroke-symbolic.svg').get_path(); -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(); - // 150 labels with font-family style take ~15Mo const FONT_FAMILY_STYLE = true; +// use 'login-dialog-message-warning' class in order to get GS theme warning color (default: #f57900) +const WARNING_COLOR_STYLE_CLASS_NAME = 'login-dialog-message-warning'; const getActor = function(object) { return GS_VERSION < '3.33.0' ? object.actor : object; }; +const getSummary = function(settingKey) { + return Me.internalShortcutSettings.settings_schema.get_key(settingKey).get_summary(); +}; + +// Used by both menu and osd notifications. +var DisplayStrings = { + getDashedLine: function(dashed) { + return dashed ? _("Dashed line") : + // Translators: as the alternative to "Dashed line" + _("Full line"); + }, + + getFill: function(fill) { + return fill ? _("Fill") : + // Translators: as the alternative to "Fill" + _("Outline"); + }, + + get FillRule() { + if (!this._fillRules) + // Translators: fill-rule SVG attribute + this._fillRules = { 0: _("Nonzero"), 1: _("Evenodd") }; + return this._fillRules; + }, + + getFontFamily: function(family) { + if (!this._fontGenericFamilies) + // Translators: generic font-family SVG attribute + this._fontGenericFamilies = { 'Sans-Serif': pgettext("font-family", "Sans-Serif"), 'Serif': pgettext("font-family", "Serif"), + 'Monospace': pgettext("font-family", "Monospace"), 'Cursive': pgettext("font-family", "Cursive"), + 'Fantasy': pgettext("font-family", "Fantasy") }; + return this._fontGenericFamilies[family] || family; + }, + + get FontStyle() { + if (!this._fontStyles) + // Translators: font-style SVG attribute + this._fontStyles = { 0: pgettext("font-style", "Normal"), 1: pgettext("font-style", "Oblique"), 2: pgettext("font-style", "Italic") }; + return this._fontStyles; + }, + + FontStyleMarkup: { 0: 'normal', 1: 'oblique', 2: 'italic' }, + + get FontWeight() { + if (!this._fontWeights) + // Translators: font-weight SVG attribute + this._fontWeights = { 100: pgettext("font-weight", "Thin"), 200: pgettext("font-weight", "Ultra Light"), 300: pgettext("font-weight", "Light"), + 350: pgettext("font-weight", "Semi Light"), 380: pgettext("font-weight", "Book"), 400: pgettext("font-weight", "Normal"), + 500: pgettext("font-weight", "Medium"), 600: pgettext("font-weight", "Semi Bold"), 700: pgettext("font-weight", "Bold"), + 800: pgettext("font-weight", "Ultra Bold"), 900: pgettext("font-weight", "Heavy"), 1000: pgettext("font-weight", "Ultra Heavy") }; + return this._fontWeights; + }, + + get LineCap() { + if (!this._lineCaps) + // Translators: stroke-linecap SVG attribute + this._lineCaps = { 0: pgettext("stroke-linecap", "Butt"), 1: pgettext("stroke-linecap", "Round"), 2: pgettext("stroke-linecap", "Square") }; + return this._lineCaps; + }, + + get LineJoin() { + if (!this._lineJoins) + // Translators: stroke-linejoin SVG attribute + this._lineJoins = { 0: pgettext("stroke-linejoin", "Miter"), 1: pgettext("stroke-linejoin", "Round"), 2: pgettext("stroke-linejoin", "Bevel") }; + return this._lineJoins; + }, + + getPixels(value) { + // Translators: value in pixel unit (e.g. "5 px") + return _("%f px").format(value); + }, + + getTextAlignment: function(rightAligned) { + // Translators: text alignment + return rightAligned ? _("Right aligned") : _("Left aligned"); + }, + + get Tool() { + if (!this._tools) + this._tools = { 0: pgettext("drawing-tool", "Free drawing"), 1: pgettext("drawing-tool", "Line"), 2: pgettext("drawing-tool", "Ellipse"), + 3: pgettext("drawing-tool", "Rectangle"), 4: pgettext("drawing-tool", "Text"), 5: pgettext("drawing-tool", "Polygon"), + 6: pgettext("drawing-tool", "Polyline"), 7: pgettext("drawing-tool", "Image"), + 100: pgettext("drawing-tool", "Move"), 101: pgettext("drawing-tool", "Resize"), 102: pgettext("drawing-tool", "Mirror") }; + return this._tools; + } +}; + var DrawingMenu = new Lang.Class({ Name: 'DrawOnYourScreenDrawingMenu', - _init: function(area, monitor) { + _init: function(area, monitor, drawingTools) { this.area = area; + this.drawingTools = drawingTools; + let side = Clutter.get_default_text_direction() == Clutter.TextDirection.RTL ? St.Side.RIGHT : St.Side.LEFT; this.menu = new PopupMenu.PopupMenu(Main.layoutManager.dummyCursor, 0.25, side); this.menuManager = new PopupMenu.PopupMenuManager(GS_VERSION < '3.33.0' ? { actor: this.area } : this.area); @@ -94,20 +171,11 @@ var DrawingMenu = new Lang.Class({ this.saveDrawingSubMenu.close(); menuCloseFunc.bind(this.menu)(animate); }; - - this.colorIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(COLOR_ICON_PATH) }); - this.smoothIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(SMOOTH_ICON_PATH) }); - this.strokeIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(STROKE_ICON_PATH) }); - this.fillIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(FILL_ICON_PATH) }); - this.fillRuleNonzeroIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(FILLRULE_NONZERO_ICON_PATH) }); - this.fillRuleEvenoddIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(FILLRULE_EVENODD_ICON_PATH) }); - this.linejoinIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(LINEJOIN_ICON_PATH) }); - this.linecapIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(LINECAP_ICON_PATH) }); - this.fullLineIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(FULL_LINE_ICON_PATH) }); - this.dashedLineIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(DASHED_LINE_ICON_PATH) }); }, disable: function() { + delete this.area; + delete this.drawingTools; this.menuManager.removeMenu(this.menu); Main.layoutManager.uiGroup.remove_actor(this.menu.actor); this.menu.destroy(); @@ -154,106 +222,101 @@ var DrawingMenu = new Lang.Class({ _redisplay: function() { this.menu.removeAll(); - this.actionButtons = []; - let groupItem = new PopupMenu.PopupBaseMenuItem({ reactive: false, can_focus: false, style_class: "draw-on-your-screen-menu-group-item" }); - getActor(groupItem).add_child(this._createActionButton(_("Undo"), this.area.undo.bind(this.area), 'edit-undo-symbolic')); - getActor(groupItem).add_child(this._createActionButton(_("Redo"), this.area.redo.bind(this.area), 'edit-redo-symbolic')); - getActor(groupItem).add_child(this._createActionButton(_("Erase"), this.area.deleteLastElement.bind(this.area), 'edit-clear-all-symbolic')); - getActor(groupItem).add_child(this._createActionButton(_("Smooth"), this.area.smoothLastElement.bind(this.area), this.smoothIcon)); + let groupItem = new PopupMenu.PopupBaseMenuItem({ reactive: false, can_focus: false, style_class: 'draw-on-your-screen-menu-group-item' }); + this.undoButton = new ActionButton(_("Undo"), 'edit-undo-symbolic', this.area.undo.bind(this.area), this._updateActionSensitivity.bind(this)); + this.redoButton = new ActionButton(_("Redo"), 'edit-redo-symbolic', this.area.redo.bind(this.area), this._updateActionSensitivity.bind(this)); + this.eraseButton = new ActionButton(_("Erase"), 'edit-clear-all-symbolic', this.area.deleteLastElement.bind(this.area), this._updateActionSensitivity.bind(this)); + this.smoothButton = new ActionButton(_("Smooth"), Files.Icons.SMOOTH, this.area.smoothLastElement.bind(this.area), this._updateActionSensitivity.bind(this)); + this.eraseButton.child.add_style_class_name('draw-on-your-screen-menu-destructive-button'); + this.smoothButton.child.add_style_class_name('draw-on-your-screen-menu-destructive-button'); + getActor(groupItem).add_child(this.undoButton); + getActor(groupItem).add_child(this.redoButton); + getActor(groupItem).add_child(this.eraseButton); + getActor(groupItem).add_child(this.smoothButton); this.menu.addMenuItem(groupItem); this._addSeparator(this.menu, true); - this._addSubMenuItem(this.menu, 'document-edit-symbolic', Area.ToolNames, this.area, 'currentTool', this._updateSectionVisibility.bind(this)); - this.colorItem = this._addColorSubMenuItem(this.menu); - this.fillItem = this._addSwitchItem(this.menu, _("Fill"), this.strokeIcon, this.fillIcon, this.area, 'fill', this._updateSectionVisibility.bind(this)); + this.toolItem = this._addToolSubMenuItem(this.menu, this._updateSectionVisibility.bind(this)); + this.paletteItem = this._addPaletteSubMenuItem(this.menu, Files.Icons.PALETTE); + this.colorItem = this._addColorSubMenuItem(this.menu, Files.Icons.COLOR); + this.fillItem = this._addSwitchItem(this.menu, DisplayStrings.getFill(true), Files.Icons.STROKE, Files.Icons.FILL, 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.fillRuleItem = this._addSwitchItem(this.fillSection, DisplayStrings.FillRule[1], Files.Icons.FILLRULE_NONZERO, Files.Icons.FILLRULE_EVENODD, this.area, 'currentEvenodd'); this.menu.addMenuItem(this.fillSection); this._addSeparator(this.menu); let lineSection = new PopupMenu.PopupMenuSection(); this._addSliderItem(lineSection, this.area, 'currentLineWidth'); - this._addSubMenuItem(lineSection, this.linejoinIcon, Elements.LineJoinNames, this.area, 'currentLineJoin'); - this._addSubMenuItem(lineSection, this.linecapIcon, Elements.LineCapNames, this.area, 'currentLineCap'); - this._addSwitchItem(lineSection, _("Dashed"), this.fullLineIcon, this.dashedLineIcon, this.area, 'dashedLine'); + this._addSubMenuItem(lineSection, Files.Icons.LINEJOIN, DisplayStrings.LineJoin, this.area, 'currentLineJoin'); + this._addSubMenuItem(lineSection, Files.Icons.LINECAP, DisplayStrings.LineCap, this.area, 'currentLineCap'); + this._addSwitchItem(lineSection, DisplayStrings.getDashedLine(true), Files.Icons.FULL_LINE, Files.Icons.DASHED_LINE, this.area, 'dashedLine'); this._addSeparator(lineSection); this.menu.addMenuItem(lineSection); lineSection.itemActivated = () => {}; this.lineSection = lineSection; let fontSection = new PopupMenu.PopupMenuSection(); - this._addFontFamilySubMenuItem(fontSection, 'font-x-generic-symbolic'); - this._addSubMenuItem(fontSection, 'format-text-bold-symbolic', Elements.FontWeightNames, this.area, 'currentFontWeight'); - this._addSubMenuItem(fontSection, 'format-text-italic-symbolic', Elements.FontStyleNames, this.area, 'currentFontStyle'); - this._addSwitchItem(fontSection, _("Right aligned"), 'format-justify-left-symbolic', 'format-justify-right-symbolic', this.area, 'currentTextRightAligned'); + this._addFontFamilySubMenuItem(fontSection, Files.Icons.FONT_FAMILY); + this._addSubMenuItem(fontSection, Files.Icons.FONT_WEIGHT, DisplayStrings.FontWeight, this.area, 'currentFontWeight'); + this._addSubMenuItem(fontSection, Files.Icons.FONT_STYLE, DisplayStrings.FontStyle, this.area, 'currentFontStyle'); + this._addSwitchItem(fontSection, DisplayStrings.getTextAlignment(true), Files.Icons.LEFT_ALIGNED, Files.Icons.RIGHT_ALIGNED, this.area, 'currentTextRightAligned'); this._addSeparator(fontSection); this.menu.addMenuItem(fontSection); fontSection.itemActivated = () => {}; this.fontSection = fontSection; let imageSection = new PopupMenu.PopupMenuSection(); - let images = this.area.getImages(); - if (images.length) { - if (this.area.currentImage > images.length - 1) - this.area.currentImage = images.length - 1; - this._addSubMenuItem(imageSection, null, images, this.area, 'currentImage'); - } + this.imageItem = this._addImageSubMenuItem(imageSection); this._addSeparator(imageSection); this.menu.addMenuItem(imageSection); imageSection.itemActivated = () => {}; this.imageSection = imageSection; - let manager = Extension.manager; - this._addSimpleSwitchItem(this.menu, _("Hide panel and dock"), manager.hiddenList ? true : false, manager.togglePanelAndDockOpacity.bind(manager)); - this._addSimpleSwitchItem(this.menu, _("Add a drawing background"), this.area.hasBackground, this.area.toggleBackground.bind(this.area)); - 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)); + let areaManager = Me.stateObj.areaManager; + this._addSimpleSwitchItem(this.menu, getSummary('toggle-panel-and-dock-visibility'), !!areaManager.hiddenList, areaManager.togglePanelAndDockOpacity.bind(areaManager)); + this._addSimpleSwitchItem(this.menu, getSummary('toggle-background'), this.area.hasBackground, this.area.toggleBackground.bind(this.area)); + this._addSimpleSwitchItem(this.menu, getSummary('toggle-grid'), this.area.hasGrid, this.area.toggleGrid.bind(this.area)); + this._addSimpleSwitchItem(this.menu, getSummary('toggle-square-area'), this.area.isSquareArea, this.area.toggleSquareArea.bind(this.area)); this._addSeparator(this.menu); this._addDrawingNameItem(this.menu); - this._addOpenDrawingSubMenuItem(this.menu); - this._addSaveDrawingSubMenuItem(this.menu); + this._addOpenDrawingSubMenuItem(this.menu, _("Open drawing"), 'document-open-symbolic'); + this._addSaveDrawingSubMenuItem(this.menu, _("Save drawing as…"), 'document-save-as-symbolic'); + this._addSeparator(this.menu); - this.menu.addAction(_("Save drawing as a SVG file"), this.area.saveAsSvg.bind(this.area), 'image-x-generic-symbolic'); - this.menu.addAction(_("Edit style"), manager.openUserStyleFile.bind(manager), 'document-page-setup-symbolic'); - this.menu.addAction(_("Show help"), () => { this.close(); this.area.toggleHelp(); }, 'preferences-desktop-keyboard-shortcuts-symbolic'); + groupItem = new PopupMenu.PopupBaseMenuItem({ reactive: false, can_focus: false, style_class: 'draw-on-your-screen-menu-group-item' }); + this.saveButton = new ActionButton(getSummary('save-as-json'), 'document-save-symbolic', this.area.saveAsJson.bind(this.area, false, this._onDrawingSaved.bind(this)), null); + this.svgButton = new ActionButton(getSummary('export-to-svg'), Files.Icons.DOCUMENT_EXPORT, this.area.exportToSvg.bind(this.area), null); + this.prefsButton = new ActionButton(getSummary('open-preferences'), 'document-page-setup-symbolic', areaManager.openPreferences.bind(areaManager), null); + this.helpButton = new ActionButton(getSummary('toggle-help'), 'preferences-desktop-keyboard-shortcuts-symbolic', () => { this.close(); this.area.toggleHelp(); }, null); + getActor(groupItem).add_child(this.saveButton); + getActor(groupItem).add_child(this.svgButton); + getActor(groupItem).add_child(this.prefsButton); + getActor(groupItem).add_child(this.helpButton); + this.menu.addMenuItem(groupItem); this._updateActionSensitivity(); this._updateSectionVisibility(); }, - // from system.js (GS 3.34-) - _createActionButton: function(accessibleName, callback, icon) { - let button = new St.Button({ track_hover: true, - x_align: Clutter.ActorAlign.CENTER, - accessible_name: accessibleName, - // use 'popup-menu' and 'popup-menu-item' style classes to provide theme colors - style_class: 'system-menu-action popup-menu-item popup-menu' }); - button.child = new St.Icon(typeof icon == 'string' ? { icon_name: icon } : { gicon: icon }); - button.connect('clicked', () => { - callback(); - this._updateActionSensitivity(); - }); - button.bind_property('reactive', button, 'can_focus', GObject.BindingFlags.DEFAULT); - this.actionButtons.push(button); - return new St.Bin({ child: button, x_expand: true }); - }, - _updateActionSensitivity: function() { - let [undoButton, redoButton, eraseButton, smoothButton] = this.actionButtons; - undoButton.reactive = this.area.elements.length > 0; - redoButton.reactive = this.area.undoneElements.length > 0; - eraseButton.reactive = this.area.elements.length > 0; - smoothButton.reactive = this.area.elements.length > 0 && this.area.elements[this.area.elements.length - 1].shape == Area.Tools.NONE; + this.undoButton.child.reactive = this.area.elements.length > 0; + this.redoButton.child.reactive = this.area.undoneElements.length > 0; + this.eraseButton.child.reactive = this.area.elements.length > 0; + this.smoothButton.child.reactive = this.area.elements.length > 0 && this.area.elements[this.area.elements.length - 1].shape == this.drawingTools.NONE; + this.saveButton.child.reactive = this.area.elements.length > 0; + this.svgButton.child.reactive = this.area.elements.length > 0; + this.saveDrawingSubMenuItem.setSensitive(this.area.elements.length > 0); }, _updateSectionVisibility: function() { - let [isText, isImage] = [this.area.currentTool == Area.Tools.TEXT, this.area.currentTool == Area.Tools.IMAGE]; + let [isText, isImage] = [this.area.currentTool == this.drawingTools.TEXT, this.area.currentTool == this.drawingTools.IMAGE]; this.lineSection.actor.visible = !isText && !isImage; this.fontSection.actor.visible = isText; this.imageSection.actor.visible = isImage; this.colorItem.setSensitive(!isImage); + this.paletteItem.setSensitive(!isImage); this.fillItem.setSensitive(!isText && !isImage); this.fillSection.setSensitive(!isText && !isImage); @@ -269,18 +332,14 @@ var DrawingMenu = new Lang.Class({ item.icon = new St.Icon({ style_class: 'popup-menu-icon' }); getActor(item).insert_child_at_index(item.icon, 1); let icon = target[targetProperty] ? iconTrue : iconFalse; - if (icon && icon instanceof GObject.Object && GObject.type_is_a(icon, Gio.Icon)) + if (icon) item.icon.set_gicon(icon); - else if (icon) - item.icon.set_icon_name(icon); item.connect('toggled', (item, state) => { target[targetProperty] = state; let icon = target[targetProperty] ? iconTrue : iconFalse; - if (icon && icon instanceof GObject.Object && GObject.type_is_a(icon, Gio.Icon)) + if (icon) item.icon.set_gicon(icon); - else if (icon) - item.icon.set_icon_name(icon); if (onToggled) onToggled(); }); @@ -296,26 +355,26 @@ var DrawingMenu = new Lang.Class({ _addSliderItem: function(menu, target, targetProperty) { let item = new PopupMenu.PopupBaseMenuItem({ activate: false }); - let label = new St.Label({ text: _("%d px").format(target[targetProperty]), style_class: 'draw-on-your-screen-menu-slider-label' }); + let label = new St.Label({ text: DisplayStrings.getPixels(target[targetProperty]), style_class: 'draw-on-your-screen-menu-slider-label' }); let slider = new Slider.Slider(target[targetProperty] / 50); if (GS_VERSION < '3.33.0') { slider.connect('value-changed', (slider, value, property) => { target[targetProperty] = Math.max(Math.round(value * 50), 0); - label.set_text(target[targetProperty] + " px"); + label.set_text(DisplayStrings.getPixels(target[targetProperty])); if (target[targetProperty] === 0) - label.add_style_class_name(Extension.WARNING_COLOR_STYLE_CLASS_NAME); + label.add_style_class_name(WARNING_COLOR_STYLE_CLASS_NAME); else - label.remove_style_class_name(Extension.WARNING_COLOR_STYLE_CLASS_NAME); + label.remove_style_class_name(WARNING_COLOR_STYLE_CLASS_NAME); }); } else { slider.connect('notify::value', () => { target[targetProperty] = Math.max(Math.round(slider.value * 50), 0); - label.set_text(target[targetProperty] + " px"); + label.set_text(DisplayStrings.getPixels(target[targetProperty])); if (target[targetProperty] === 0) - label.add_style_class_name(Extension.WARNING_COLOR_STYLE_CLASS_NAME); + label.add_style_class_name(WARNING_COLOR_STYLE_CLASS_NAME); else - label.remove_style_class_name(Extension.WARNING_COLOR_STYLE_CLASS_NAME); + label.remove_style_class_name(WARNING_COLOR_STYLE_CLASS_NAME); }); } @@ -327,94 +386,139 @@ var DrawingMenu = new Lang.Class({ menu.addMenuItem(item); }, - _addSubMenuItem: function(menu, icon, obj, target, targetProperty, callback) { - if (targetProperty == 'currentImage') - icon = obj[target[targetProperty]].gicon; - let item = new PopupMenu.PopupSubMenuMenuItem(_(String(obj[target[targetProperty]])), icon ? true : false); - if (icon && icon instanceof GObject.Object && GObject.type_is_a(icon, Gio.Icon)) - item.icon.set_gicon(icon); - else if (icon) - item.icon.set_icon_name(icon); + _addSubMenuItem: function(menu, icon, obj, target, targetProperty) { + let item = new PopupMenu.PopupSubMenuMenuItem(String(obj[target[targetProperty]]), icon ? true : false); - item.menu.itemActivated = () => { - item.menu.close(); - }; + item.icon.set_gicon(icon); + item.menu.itemActivated = item.menu.close; GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { - for (let i in obj) { - let text; - if (targetProperty == 'currentFontWeight') - text = `${_(obj[i])}`; - else if (targetProperty == 'currentFontStyle') - text = `${_(obj[i])}`; - else - text = _(String(obj[i])); + Object.keys(obj).forEach(key => { + let text = targetProperty == 'currentFontWeight' ? `${obj[key]}` : + targetProperty == 'currentFontStyle' ? `${obj[key]}` : + String(obj[key]); - let iCaptured = Number(i); let subItem = item.menu.addAction(text, () => { - item.label.set_text(_(String(obj[iCaptured]))); - target[targetProperty] = iCaptured; - if (targetProperty == 'currentImage') - item.icon.set_gicon(obj[iCaptured].gicon); - if (callback) - callback(); + item.label.set_text(String(obj[key])); + target[targetProperty] = Number(key); }); subItem.label.get_clutter_text().set_use_markup(true); getActor(subItem).connect('key-focus-in', updateSubMenuAdjustment); + }); + return GLib.SOURCE_REMOVE; + }); + + menu.addMenuItem(item); + }, + + _addToolSubMenuItem: function(menu, callback) { + let item = new PopupMenu.PopupSubMenuMenuItem('', true); + item.update = () => { + item.label.set_text(DisplayStrings.Tool[this.area.currentTool]); + let toolName = this.drawingTools.getNameOf(this.area.currentTool); + item.icon.set_gicon(Files.Icons[`TOOL_${toolName}`]); + }; + item.update(); + + item.menu.itemActivated = item.menu.close; + + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + Object.keys(DisplayStrings.Tool).forEach(key => { + let text = DisplayStrings.Tool[key]; + let toolName = this.drawingTools.getNameOf(key); + let subItemIcon = Files.Icons[`TOOL_${toolName}`]; + let subItem = item.menu.addAction(text, () => { + this.area.currentTool = Number(key); + item.update(); + callback(); + }, subItemIcon); + + subItem.label.get_clutter_text().set_use_markup(true); + getActor(subItem).connect('key-focus-in', updateSubMenuAdjustment); // change the display order of tools - if (obj == Area.ToolNames && i == Area.Tools.POLYGON) - item.menu.moveMenuItem(subItem, 4); - else if (obj == Area.ToolNames && i == Area.Tools.POLYLINE) - item.menu.moveMenuItem(subItem, 5); - } + if (key == this.drawingTools.POLYGON) + item.menu.moveMenuItem(subItem, Number(this.drawingTools.TEXT)); + else if (key == this.drawingTools.POLYLINE) + item.menu.moveMenuItem(subItem, Number(this.drawingTools.TEXT) + 1); + }); return GLib.SOURCE_REMOVE; }); - menu.addMenuItem(item); - }, - - _addColorSubMenuItem: function(menu) { - let item = new PopupMenu.PopupSubMenuMenuItem(_("Color"), true); - item.icon.set_gicon(this.colorIcon); - item.icon.set_style(`color:${this.area.currentColor.to_string().slice(0, 7)};`); - item.menu.itemActivated = () => { - item.menu.close(); - }; - - GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { - for (let i = 1; i < this.area.colors.length; i++) { - let text = this.area.colors[i].to_string(); - let iCaptured = i; - let colorItem = item.menu.addAction(text, () => { - this.area.currentColor = this.area.colors[iCaptured]; - item.icon.set_style(`color:${this.area.currentColor.to_string().slice(0, 7)};`); - }); - // Foreground color markup is not displayed since 3.36, use style instead but the transparency is lost. - colorItem.label.set_style(`color:${this.area.colors[i].to_string().slice(0, 7)};`); - getActor(colorItem).connect('key-focus-in', updateSubMenuAdjustment); - } - return GLib.SOURCE_REMOVE; - }); menu.addMenuItem(item); return item; }, - _addFontFamilySubMenuItem: function(menu, icon) { - let item = new PopupMenu.PopupSubMenuMenuItem(this.area.currentFontFamily, true); - item.icon.set_icon_name(icon); + _addPaletteSubMenuItem: function(menu, icon) { + let text = _(this.area.currentPalette[0] || "Palette"); + let item = new PopupMenu.PopupSubMenuMenuItem(text, true); + item.icon.set_gicon(icon); - item.menu.itemActivated = () => { - item.menu.close(); - }; + item.menu.itemActivated = item.menu.close; + + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + this.area.palettes.forEach(palette => { + let [name, colors] = palette; + if (!colors[0]) + return; + + let subItem = item.menu.addAction(_(name || "Palette"), () => { + item.label.set_text(_(name || "Palette")); + this.area.currentPalette = palette; + this._populateColorSubMenu(); + }); + getActor(subItem).connect('key-focus-in', updateSubMenuAdjustment); + }); + return GLib.SOURCE_REMOVE; + }); + + menu.addMenuItem(item); + return item; + }, + + _addColorSubMenuItem: function(menu, icon) { + let item = new PopupMenu.PopupSubMenuMenuItem(_("Color"), true); + this.colorSubMenu = item.menu; + item.icon.set_gicon(icon); + item.icon.set_style(`color:${this.area.currentColor.to_string().slice(0, 7)};`); + + item.menu.itemActivated = item.menu.close; + + this._populateColorSubMenu(); + menu.addMenuItem(item); + return item; + }, + + _populateColorSubMenu: function() { + this.colorSubMenu.removeAll(); + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + this.area.colors.forEach(color => { + let text = String(color); + let subItem = this.colorSubMenu.addAction(text, () => { + this.area.currentColor = color; + this.colorItem.icon.set_style(`color:${color.to_string().slice(0, 7)};`); + }); + // Foreground color markup is not displayed since 3.36, use style instead but the transparency is lost. + subItem.label.set_style(`color:${color.to_string().slice(0, 7)};`); + getActor(subItem).connect('key-focus-in', updateSubMenuAdjustment); + }); + return GLib.SOURCE_REMOVE; + }); + }, + + _addFontFamilySubMenuItem: function(menu, icon) { + let item = new PopupMenu.PopupSubMenuMenuItem(DisplayStrings.getFontFamily(this.area.currentFontFamily), true); + item.icon.set_gicon(icon); + + item.menu.itemActivated = item.menu.close; item.menu.openOld = item.menu.open; item.menu.open = (animate) => { if (!item.menu.isOpen && item.menu.isEmpty()) { this.area.fontFamilies.forEach(family => { - let subItem = item.menu.addAction(_(family), () => { - item.label.set_text(_(family)); + let subItem = item.menu.addAction(DisplayStrings.getFontFamily(family), () => { + item.label.set_text(DisplayStrings.getFontFamily(family)); this.area.currentFontFamily = family; }); if (FONT_FAMILY_STYLE) @@ -428,6 +532,34 @@ var DrawingMenu = new Lang.Class({ menu.addMenuItem(item); }, + _addImageSubMenuItem: function(menu, images) { + let item = new PopupMenu.PopupSubMenuMenuItem('', true); + item.update = () => { + item.label.set_text(this.area.currentImage.toString()); + item.icon.set_gicon(this.area.currentImage.gicon); + }; + item.update(); + + item.menu.itemActivated = item.menu.close; + + item.menu.openOld = item.menu.open; + item.menu.open = (animate) => { + if (!item.menu.isOpen && item.menu.isEmpty()) { + Files.Images.getSorted().forEach(image => { + let subItem = item.menu.addAction(image.toString(), () => { + this.area.currentImage = image; + item.update(); + }, image.thumbnailGicon || undefined); + getActor(subItem).connect('key-focus-in', updateSubMenuAdjustment); + }); + } + item.menu.openOld(); + }; + + menu.addMenuItem(item); + return item; + }, + _addDrawingNameItem: function(menu) { this.drawingNameMenuItem = new PopupMenu.PopupMenuItem('', { reactive: false, activate: false }); this.drawingNameMenuItem.setSensitive(false); @@ -436,24 +568,22 @@ var DrawingMenu = new Lang.Class({ }, _updateDrawingNameMenuItem: function() { - getActor(this.drawingNameMenuItem).visible = this.area.jsonName ? true : false; - if (this.area.jsonName) { + getActor(this.drawingNameMenuItem).visible = this.area.currentJson ? true : false; + if (this.area.currentJson) { let prefix = this.area.drawingContentsHasChanged ? "* " : ""; - this.drawingNameMenuItem.label.set_text(`${prefix}${this.area.jsonName}`); + this.drawingNameMenuItem.label.set_text(`${prefix}${this.area.currentJson.name}`); this.drawingNameMenuItem.label.get_clutter_text().set_use_markup(true); } }, - _addOpenDrawingSubMenuItem: function(menu) { - let item = new PopupMenu.PopupSubMenuMenuItem(_("Open drawing"), true); + _addOpenDrawingSubMenuItem: function(menu, label, icon) { + let item = new PopupMenu.PopupSubMenuMenuItem(label, true); this.openDrawingSubMenuItem = item; this.openDrawingSubMenu = item.menu; - item.setSensitive(Boolean(Files.getJsons().length)); - item.icon.set_icon_name('document-open-symbolic'); + item.setSensitive(Boolean(Files.Jsons.getSorted().length)); + item.icon.set_icon_name(icon); - item.menu.itemActivated = () => { - item.menu.close(); - }; + item.menu.itemActivated = item.menu.close; item.menu.openOld = item.menu.open; item.menu.open = (animate) => { @@ -467,13 +597,16 @@ var DrawingMenu = new Lang.Class({ _populateOpenDrawingSubMenu: function() { this.openDrawingSubMenu.removeAll(); - let jsons = Files.getJsons(); - jsons.forEach(json => { + Files.Jsons.getSorted().forEach(json => { + if (!json.gicon) + json.addSvgContents(...this.area.getSvgContentsForJson(json)); + let subItem = this.openDrawingSubMenu.addAction(`${String(json)}`, () => { - this.area.loadJson(json.name); + this.area.loadJson(json); this._updateDrawingNameMenuItem(); - this._updateSaveDrawingSubMenuItemSensitivity(); - }); + this._updateActionSensitivity(); + }, json.gicon); + subItem.label.get_clutter_text().set_use_markup(true); getActor(subItem).connect('key-focus-in', updateSubMenuAdjustment); @@ -483,10 +616,22 @@ var DrawingMenu = new Lang.Class({ }); getActor(subItem).add_child(expander); - let deleteButton = new St.Button({ style_class: 'draw-on-your-screen-menu-delete-button', + let insertButton = new St.Button({ style_class: 'button draw-on-your-screen-menu-inline-button', + child: new St.Icon({ icon_name: 'insert-image-symbolic', + style_class: 'popup-menu-icon' }) }); + getActor(subItem).add_child(insertButton); + + insertButton.connect('clicked', () => { + this.area.currentImage = json.image; + this.imageItem.update(); + this.area.currentTool = this.drawingTools.IMAGE; + this.toolItem.update(); + this._updateSectionVisibility(); + }); + + let deleteButton = new St.Button({ style_class: 'button draw-on-your-screen-menu-inline-button draw-on-your-screen-menu-destructive-button', child: new St.Icon({ icon_name: 'edit-delete-symbolic', - style_class: 'popup-menu-icon', - x_align: Clutter.ActorAlign.END }) }); + style_class: 'popup-menu-icon' }) }); getActor(subItem).add_child(deleteButton); deleteButton.connect('clicked', () => { @@ -499,16 +644,13 @@ var DrawingMenu = new Lang.Class({ this.openDrawingSubMenuItem.setSensitive(!this.openDrawingSubMenu.isEmpty()); }, - _addSaveDrawingSubMenuItem: function(menu) { - let item = new PopupMenu.PopupSubMenuMenuItem(_("Save drawing"), true); + _addSaveDrawingSubMenuItem: function(menu, label, icon) { + let item = new PopupMenu.PopupSubMenuMenuItem(label, true); this.saveDrawingSubMenuItem = item; - this._updateSaveDrawingSubMenuItemSensitivity(); this.saveDrawingSubMenu = item.menu; - item.icon.set_icon_name('document-save-symbolic'); + item.icon.set_icon_name(icon); - item.menu.itemActivated = () => { - item.menu.close(); - }; + item.menu.itemActivated = item.menu.close; item.menu.openOld = item.menu.open; item.menu.open = (animate) => { @@ -530,13 +672,14 @@ var DrawingMenu = new Lang.Class({ _populateSaveDrawingSubMenu: function() { this.saveDrawingSubMenu.removeAll(); - let saveEntry = new DrawingMenuEntry({ initialTextGetter: Files.getDateString, - entryActivateCallback: (text) => { - this.area.saveAsJsonWithName(text, this._onDrawingSaved.bind(this)); - this.saveDrawingSubMenu.toggle(); - }, - invalidStrings: [Me.metadata['persistent-file-name'], '/'], - primaryIconName: 'insert-text' }); + let saveEntry = new Entry({ initialTextGetter: () => this.area.currentJson ? this.area.currentJson.name : "", + hint_text: _("Type a name"), + entryActivateCallback: (text) => { + this.area.saveAsJsonWithName(text, this._onDrawingSaved.bind(this)); + this.saveDrawingSubMenu.toggle(); + }, + invalidStrings: [Me.metadata['persistent-file-name'], '/'], + primaryIconName: 'insert-text' }); this.saveDrawingSubMenu.addMenuItem(saveEntry.item); }, @@ -568,8 +711,51 @@ const updateSubMenuAdjustment = function(itemActor) { adjustment.set_value(newScrollValue); }; +// An action button that uses upstream dash item tooltips. +const ActionButton = new Lang.Class({ + Name: 'DrawOnYourScreenDrawingMenuActionButton', + Extends: St.Bin, + _labelShowing: false, + _resetHoverTimeoutId: 0, + _showLabelTimeoutId: 0, + showLabel: Dash.DashItemContainer.prototype.showLabel, + hideLabel: Dash.DashItemContainer.prototype.hideLabel, + _syncLabel: Dash.Dash.prototype._syncLabel, + + _init: function(name, icon, callback, callbackAfter) { + this._labelText = name; + + let button = new St.Button({ track_hover: true, + x_align: Clutter.ActorAlign.CENTER, + accessible_name: name, + // use 'popup-menu' and 'popup-menu-item' style classes to provide theme colors + //style_class: 'system-menu-action popup-menu-item popup-menu' }); + style_class: 'button draw-on-your-screen-menu-action-button' }); + button.child = new St.Icon(typeof icon == 'string' ? { icon_name: icon } : { gicon: icon }); + button.connect('clicked', () => { + callback(); + if (callbackAfter) + callbackAfter(); + }); + button.bind_property('reactive', button, 'can_focus', GObject.BindingFlags.DEFAULT); + button.connect('notify::hover', () => this._syncLabel(this)); + + this.parent({ child: button, x_expand: true }); + }, + + get label() { + if (!this._label) { + this._label = new St.Label({ style_class: 'dash-label' }); + Main.layoutManager.uiGroup.add_actor(this._label); + this.connect('destroy', () => this._label.destroy()); + } + + return this._label; + } +}); + // based on searchItem.js, https://github.com/leonardo-bartoli/gnome-shell-extension-Recents -const DrawingMenuEntry = new Lang.Class({ +const Entry = new Lang.Class({ Name: 'DrawOnYourScreenDrawingMenuEntry', _init: function(params) { @@ -582,6 +768,7 @@ const DrawingMenuEntry = new Lang.Class({ this.itemActor = GS_VERSION < '3.33.0' ? this.item.actor : this.item; this.entry = new St.Entry({ + hint_text: params.hint_text || "", style_class: 'search-entry draw-on-your-screen-menu-entry', track_hover: true, reactive: true, diff --git a/metadata.json b/metadata.json index 4690c5b..c581739 100644 --- a/metadata.json +++ b/metadata.json @@ -17,5 +17,5 @@ "3.34", "3.36" ], - "version": 6.2 + "version": 6.3 } diff --git a/prefs.js b/prefs.js index 8ff013d..30369ea 100644 --- a/prefs.js +++ b/prefs.js @@ -1,4 +1,5 @@ /* jslint esversion: 6 */ +/* exported init, buildPrefsWidget */ /* * Copyright 2019 Abakkk @@ -20,104 +21,27 @@ * along with this program. If not, see . */ +const Atk = imports.gi.Atk; +const Gdk = imports.gi.Gdk; +const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const GObject = imports.gi.GObject; const Gtk = imports.gi.Gtk; -const Lang = imports.lang; -const 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 Shortcuts = Me.imports.shortcuts; +const gettext = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; +const _ = function(string) { + if (!string) + return ""; + return gettext(string); +}; 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" -}; - -var INTERNAL_KEYBINDINGS = { - 'undo': "Undo last brushstroke", - 'redo': "Redo last brushstroke", - '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-image-shape': "Select image", - 'select-move-tool': "Select move", - 'select-resize-tool': "Select resize", - 'select-mirror-tool': "Select mirror", - '-separator-2': '', - 'switch-fill': "Toggle fill/outline", - 'switch-fill-rule': "Toggle fill rule", - '-separator-3': '', - 'increment-line-width': "Increment line width", - 'decrement-line-width': "Decrement line width", - 'increment-line-width-more': "Increment line width even more", - 'decrement-line-width-more': "Decrement line width even more", - 'switch-linejoin': "Change linejoin", - 'switch-linecap': "Change linecap", - 'switch-dash': "Dashed line", - '-separator-4': '', - 'switch-font-family': "Change font family", - 'reverse-switch-font-family': "Change font family (reverse)", - 'switch-font-weight': "Change font weight", - 'switch-font-style': "Change font style", - 'switch-text-alignment': "Toggle text alignment", - 'switch-image-file': "Change image file", - '-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-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", 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 outline", shortcut: getKeyLabel('') }, - { desc: "Rotate (while moving)", shortcut: getKeyLabel('') }, - { desc: "Stretch (while resizing)", shortcut: getKeyLabel('') }, - { desc: "Inverse (while mirroring)", shortcut: getKeyLabel('') } -]; +const ROWBOX_MARGIN_PARAMS = { margin_top: MARGIN / 2, margin_bottom: MARGIN / 2, margin_left: MARGIN, margin_right: MARGIN }; function init() { Convenience.initTranslations(); @@ -128,7 +52,6 @@ function buildPrefsWidget() { let switcher = new Gtk.StackSwitcher({halign: Gtk.Align.CENTER, visible: true, stack: topStack}); GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { let window = topStack.get_toplevel(); - window.resize(720,500); let headerBar = window.get_titlebar(); headerBar.custom_title = switcher; return false; @@ -146,8 +69,13 @@ const TopStack = new GObject.Class({ _init: function(params) { this.parent({ transition_type: 1, transition_duration: 500, expand: true }); this.prefsPage = new PrefsPage(); + // Translators: "Preferences" page in preferences this.add_titled(this.prefsPage, 'prefs', _("Preferences")); + this.drawingPage = new DrawingPage(); + // Translators: "Drawing" page in preferences + this.add_titled(this.drawingPage, 'drawing', _("Drawing")); this.aboutPage = new AboutPage(); + // Translators: "About" page in preferences this.add_titled(this.aboutPage, 'about', _("About")); } }); @@ -158,14 +86,17 @@ const AboutPage = new GObject.Class({ Extends: Gtk.ScrolledWindow, _init: function(params) { - this.parent(); + this.parent({ hscrollbar_policy: Gtk.PolicyType.NEVER }); - let vbox= new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, margin: MARGIN*3 }); + let vbox= new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, margin: MARGIN * 3 }); this.add(vbox); - let name = " " + _(Me.metadata.name) + ""; - let version = _("Version %d").format(Me.metadata.version); - let description = _(Me.metadata.description); + // Translators: you are free to translate the extension name, that is displayed in About page, or not + let name = " " + _("Draw On You Screen") + ""; + // Translators: version number in "About" page + let version = _("Version %f").format(Me.metadata.version); + // Translators: you are free to translate the extension description, that is displayed in About page, or not + let description = _("Start drawing with Super+Alt+D and save your beautiful work by taking a screenshot"); let link = "" + Me.metadata.url + ""; let licenceName = _GTK("GNU General Public License, version 2 or later"); let licenceLink = "https://www.gnu.org/licenses/old-licenses/gpl-2.0.html"; @@ -176,7 +107,7 @@ const AboutPage = new GObject.Class({ vbox.add(aboutLabel); - let creditBox = new Gtk.Box({ orientation: Gtk.Orientation.HORIZONTAL, margin: 2*MARGIN }); + let creditBox = new Gtk.Box({ orientation: Gtk.Orientation.HORIZONTAL, margin: 2 * MARGIN }); let leftBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL }); let rightBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL }); let leftLabel = new Gtk.Label({ wrap: true, valign: 1, halign: 2, justify: 1, use_markup: true, label: "" + _GTK("Created by") + "" }); @@ -187,6 +118,11 @@ const AboutPage = new GObject.Class({ creditBox.pack_start(rightBox, true, true, 5); vbox.add(creditBox); + // Translators: add your name here or keep it empty, it will be displayed in about page, e.g. + // msgstr "" + // "translator1\n" + // "translator2\n" + // "translator3" if (_("translator-credits") != "translator-credits" && _("translator-credits") != "") { leftBox.pack_start(new Gtk.Label(), false, false, 0); rightBox.pack_start(new Gtk.Label(), false, false, 0); @@ -196,7 +132,235 @@ const AboutPage = new GObject.Class({ rightBox.pack_start(rightLabel, false, false, 0); } } +}); + +const DrawingPage = new GObject.Class({ + Name: 'DrawOnYourScreenDrawingPage', + GTypeName: 'DrawOnYourScreenDrawingPage', + Extends: Gtk.ScrolledWindow, + + _init: function(params) { + this.parent({ hscrollbar_policy: Gtk.PolicyType.NEVER }); + + this.settings = Convenience.getSettings(Me.metadata['settings-schema'] + '.drawing'); + this.schema = this.settings.settings_schema; + + let box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, margin: 3 * MARGIN, spacing: 3 * MARGIN }); + this.add(box); + + let palettesFrame = new Frame({ label: _("Palettes") }); + box.add(palettesFrame); + + let palettesScrolledWindow = new Gtk.ScrolledWindow({ vscrollbar_policy: Gtk.PolicyType.NEVER, margin_top: MARGIN / 2, margin_bottom: MARGIN / 2 }); + palettesFrame.add(palettesScrolledWindow); + this.palettesListBox = new Gtk.ListBox({ selection_mode: 0, hexpand: true }); + this.palettesListBox.get_style_context().add_class('background'); + this.palettesListBox.get_accessible().set_name(this.schema.get_key('palettes').get_summary()); + this.palettesListBox.get_accessible().set_description(this.schema.get_key('palettes').get_description()); + palettesScrolledWindow.add(this.palettesListBox); + + this.settings.connect('changed::palettes', this._updatePalettes.bind(this)); + this._updatePalettes(); + + this.addBox = new Gtk.Box(ROWBOX_MARGIN_PARAMS); + this.addBox.margin_bottom = MARGIN; // add space for the scrollbar + let addButton = Gtk.Button.new_from_icon_name('list-add-symbolic', Gtk.IconSize.BUTTON); + addButton.set_tooltip_text(_("Add a new palette")); + this.addBox.pack_start(addButton, true, true, 4); + addButton.connect('clicked', this._addNewPalette.bind(this)); + this.palettesListBox.add(this.addBox); + this.addBox.get_parent().set_activatable(false); + + let areaFrame = new Frame({ label: _("Area") }); + box.add(areaFrame); + + let areaListBox = new Gtk.ListBox({ selection_mode: 0, hexpand: true, margin_top: MARGIN / 2, margin_bottom: MARGIN / 2 }); + areaListBox.get_style_context().add_class('background'); + areaFrame.add(areaListBox); + + let squareAreaRow = new PrefRow({ label: this.schema.get_key('square-area-size').get_summary() }); + let squareAreaAutoButton = new Gtk.CheckButton({ label: _("Auto"), + name: this.schema.get_key('square-area-auto').get_summary(), + tooltip_text: this.schema.get_key('square-area-auto').get_description() }); + let squareAreaSizeButton = new PixelSpinButton({ width_chars: 5, digits: 0, step: 1, + range: this.schema.get_key('square-area-size').get_range(), + name: this.schema.get_key('square-area-size').get_summary(), + tooltip_text: this.schema.get_key('square-area-size').get_description() }); + this.settings.bind('square-area-auto', squareAreaAutoButton, 'active', 0); + this.settings.bind('square-area-size', squareAreaSizeButton, 'value', 0); + squareAreaAutoButton.bind_property('active', squareAreaSizeButton, 'sensitive', GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.INVERT_BOOLEAN); + squareAreaRow.addWidget(squareAreaAutoButton); + squareAreaRow.addWidget(squareAreaSizeButton); + areaListBox.add(squareAreaRow); + + let backgroundColorRow = new PrefRow({ label: this.schema.get_key('background-color').get_summary() }); + let backgroundColorButton = new ColorStringButton({ use_alpha: true, show_editor: true, + name: this.schema.get_key('background-color').get_summary(), + tooltip_text: this.schema.get_key('background-color').get_description() }); + this.settings.bind('background-color', backgroundColorButton, 'color-string', 0); + backgroundColorRow.addWidget(backgroundColorButton); + areaListBox.add(backgroundColorRow); + + let gridLineRow = new PrefRow({ label: _("Grid overlay line") }); + let gridLineAutoButton = new Gtk.CheckButton({ label: _("Auto"), + name: this.schema.get_key('grid-line-auto').get_summary(), + tooltip_text: this.schema.get_key('grid-line-auto').get_description() }); + let gridLineWidthButton = new PixelSpinButton({ width_chars: 5, digits: 1, step: 0.1, + range: this.schema.get_key('grid-line-width').get_range(), + name: this.schema.get_key('grid-line-width').get_summary(), + tooltip_text: this.schema.get_key('grid-line-width').get_description() }); + let gridLineSpacingButton = new PixelSpinButton({ width_chars: 5, digits: 1, step: 1, + range: this.schema.get_key('grid-line-spacing').get_range(), + name: this.schema.get_key('grid-line-spacing').get_summary(), + tooltip_text: this.schema.get_key('grid-line-spacing').get_description() }); + this.settings.bind('grid-line-auto', gridLineAutoButton, 'active', 0); + this.settings.bind('grid-line-width', gridLineWidthButton, 'value', 0); + this.settings.bind('grid-line-spacing', gridLineSpacingButton, 'value', 0); + gridLineAutoButton.bind_property('active', gridLineWidthButton, 'sensitive', GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.INVERT_BOOLEAN); + gridLineAutoButton.bind_property('active', gridLineSpacingButton, 'sensitive', GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.INVERT_BOOLEAN); + gridLineRow.addWidget(gridLineAutoButton); + gridLineRow.addWidget(gridLineWidthButton); + gridLineRow.addWidget(gridLineSpacingButton); + areaListBox.add(gridLineRow); + + let gridColorRow = new PrefRow({ label: this.schema.get_key('grid-color').get_summary() }); + let gridColorButton = new ColorStringButton({ use_alpha: true, show_editor: true, + name: this.schema.get_key('grid-color').get_summary(), + tooltip_text: this.schema.get_key('grid-color').get_description() }); + this.settings.bind('grid-color', gridColorButton, 'color-string', 0); + gridColorRow.addWidget(gridColorButton); + areaListBox.add(gridColorRow); + + let toolsFrame = new Frame({ label: _("Tools") }); + box.add(toolsFrame); + + let toolsListBox = new Gtk.ListBox({ selection_mode: 0, hexpand: true, margin_top: MARGIN / 2, margin_bottom: MARGIN / 2 }); + toolsListBox.get_style_context().add_class('background'); + toolsFrame.add(toolsListBox); + + let dashArrayRow = new PrefRow({ label: _("Dash array") }); + let dashArrayAutoButton = new Gtk.CheckButton({ label: _("Auto"), + name: this.schema.get_key('dash-array-auto').get_summary(), + tooltip_text: this.schema.get_key('dash-array-auto').get_description() }); + let dashArrayOnButton = new PixelSpinButton({ width_chars: 5, digits: 1, step: 0.1, + range: this.schema.get_key('dash-array-on').get_range(), + name: this.schema.get_key('dash-array-on').get_summary(), + tooltip_text: this.schema.get_key('dash-array-on').get_description() }); + let dashArrayOffButton = new PixelSpinButton({ width_chars: 5, digits: 1, step: 0.1, + range: this.schema.get_key('dash-array-off').get_range(), + name: this.schema.get_key('dash-array-off').get_summary(), + tooltip_text: this.schema.get_key('dash-array-off').get_description() }); + this.settings.bind('dash-array-auto', dashArrayAutoButton, 'active', 0); + this.settings.bind('dash-array-on', dashArrayOnButton, 'value', 0); + this.settings.bind('dash-array-off', dashArrayOffButton, 'value', 0); + dashArrayAutoButton.bind_property('active', dashArrayOnButton, 'sensitive', GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.INVERT_BOOLEAN); + dashArrayAutoButton.bind_property('active', dashArrayOffButton, 'sensitive', GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.INVERT_BOOLEAN); + dashArrayRow.addWidget(dashArrayAutoButton); + dashArrayRow.addWidget(dashArrayOnButton); + dashArrayRow.addWidget(dashArrayOffButton); + toolsListBox.add(dashArrayRow); + + let dashOffsetRow = new PrefRow({ label: this.schema.get_key('dash-offset').get_summary() }); + let dashOffsetButton = new PixelSpinButton({ width_chars: 5, digits: 1, step: 0.1, + range: this.schema.get_key('dash-offset').get_range(), + name: this.schema.get_key('dash-offset').get_summary(), + tooltip_text: this.schema.get_key('dash-offset').get_description() }); + this.settings.bind('dash-offset', dashOffsetButton, 'value', 0); + dashOffsetRow.addWidget(dashOffsetButton); + toolsListBox.add(dashOffsetRow); + + let imageLocationRow = new PrefRow({ label: this.schema.get_key('image-location').get_summary() }); + let imageLocationButton = new FileChooserButton({ action: Gtk.FileChooserAction.SELECT_FOLDER, + name: this.schema.get_key('image-location').get_summary(), + tooltip_text: this.schema.get_key('image-location').get_description() }); + this.settings.bind('image-location', imageLocationButton, 'location', 0); + imageLocationRow.addWidget(imageLocationButton); + toolsListBox.add(imageLocationRow); + + let resetButton = new Gtk.Button({ label: _("Reset settings"), halign: Gtk.Align.CENTER }); + resetButton.get_style_context().add_class('destructive-action'); + resetButton.connect('clicked', () => this.schema.list_keys().forEach(key => this.settings.reset(key))); + box.add(resetButton); + }, + _updatePalettes: function() { + this.palettes = this.settings.get_value('palettes').deep_unpack(); + this.palettesListBox.get_children().filter(row=> row.get_child() != this.addBox) + .slice(this.palettes.length) + .forEach(row => this.palettesListBox.remove(row)); + let paletteBoxes = this.palettesListBox.get_children().map(row => row.get_child()).filter(child => child != this.addBox); + + this.palettes.forEach((palette, paletteIndex) => { + let [name, colors] = palette; + let paletteBox; + + if (paletteBoxes[paletteIndex]) { + paletteBox = paletteBoxes[paletteIndex]; + let nameEntry = paletteBox.get_children()[0]; + if (nameEntry.get_text() !== _(name)) { + GObject.signal_handler_block(nameEntry, nameEntry.paletteNameChangedHandler); + nameEntry.set_text(_(name)); + GObject.signal_handler_unblock(nameEntry, nameEntry.paletteNameChangedHandler); + } + } else { + let nameEntry = new Gtk.Entry({ text: name, halign: Gtk.Align.START, tooltip_text: _("Rename the palette") }); + nameEntry.paletteNameChangedHandler = nameEntry.connect('changed', this._onPaletteNameChanged.bind(this, paletteIndex)); + let removeButton = Gtk.Button.new_from_icon_name('list-remove-symbolic', Gtk.IconSize.BUTTON); + removeButton.set_tooltip_text(_("Remove the palette")); + removeButton.connect('clicked', this._removePalette.bind(this, paletteIndex)); + paletteBox = new Gtk.Box(ROWBOX_MARGIN_PARAMS); + paletteBox.pack_start(nameEntry, true, true, 4); + paletteBox.pack_start(new Gtk.Box({ spacing: 4 }), false, false, 4); + paletteBox.pack_start(removeButton, false, false, 4); + this.palettesListBox.insert(paletteBox, paletteIndex); + paletteBox.get_parent().set_activatable(false); + } + + colors.splice(9); + while (colors.length < 9) + colors.push('transparent'); + + let colorsBox = paletteBox.get_children()[1]; + let colorButtons = colorsBox.get_children(); + colors.forEach((color, colorIndex) => { + if (colorButtons[colorIndex]) { + colorButtons[colorIndex].color_string = color; + } else { + let colorButton = new ColorStringButton({ color_string: color, use_alpha: true, show_editor: true, halign: Gtk.Align.START, hexpand: false }); + colorButton.connect('notify::color-string', this._onPaletteColorChanged.bind(this, paletteIndex, colorIndex)); + colorsBox.add(colorButton); + } + }); + + paletteBox.show_all(); + }); + }, + + _savePalettes: function() { + this.settings.set_value('palettes', new GLib.Variant('a(sas)', this.palettes)); + }, + + _onPaletteNameChanged: function(index, entry) { + this.palettes[index][0] = entry.get_text(); + this._savePalettes(); + }, + + _onPaletteColorChanged: function(paletteIndex, colorIndex, colorButton) { + this.palettes[paletteIndex][1][colorIndex] = colorButton.get_rgba().to_string(); + this._savePalettes(); + }, + + _addNewPalette: function() { + let colors = Array(9).fill('Black'); + // Translators: default name of a new palette + this.palettes.push([_("New palette"), colors]); + this._savePalettes(); + }, + + _removePalette: function(paletteIndex) { + this.palettes.splice(paletteIndex, 1); + this._savePalettes(); + } }); const PrefsPage = new GObject.Class({ @@ -205,143 +369,276 @@ const PrefsPage = new GObject.Class({ Extends: Gtk.ScrolledWindow, _init: function(params) { - this.parent(); + this.parent({ hscrollbar_policy: Gtk.PolicyType.NEVER }); - this.settings = Convenience.getSettings(); + let settings = Convenience.getSettings(); + let schema = settings.settings_schema; + let internalShortcutSettings = Convenience.getSettings(Me.metadata['settings-schema'] + '.internal-shortcuts'); - let box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, margin: MARGIN*3 }); + let box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, margin: MARGIN * 3, spacing: 3 * MARGIN }); this.add(box); - let globalFrame = new Gtk.Frame({ label_yalign: 1.0 }); - globalFrame.set_label_widget(new Gtk.Label({ margin_bottom: MARGIN/2, use_markup: true, label: "" + _("Global") + "" })); + let globalFrame = new Frame({ label: _("Global") }); box.add(globalFrame); - let listBox = new Gtk.ListBox({ selection_mode: 0, hexpand: true, margin_top: MARGIN/2, margin_bottom: MARGIN/2 }); + let listBox = new Gtk.ListBox({ selection_mode: 0, hexpand: true, margin_top: MARGIN, margin_bottom: MARGIN / 2 }); + listBox.get_style_context().add_class('background'); globalFrame.add(listBox); - let styleContext = listBox.get_style_context(); - styleContext.add_class('background'); + Shortcuts.GLOBAL_KEYBINDINGS.forEach((settingKeys, index) => { + if (index) + listBox.add(new Gtk.Box(ROWBOX_MARGIN_PARAMS)); + + let globalKeybindingsRow = new Gtk.ListBoxRow({ activatable: false }); + let globalKeybindingsWidget = new KeybindingsWidget(settingKeys, settings); + globalKeybindingsRow.add(globalKeybindingsWidget); + listBox.add(globalKeybindingsRow); + }); - let globalKeybindingsWidget = new KeybindingsWidget(GLOBAL_KEYBINDINGS, this.settings); - globalKeybindingsWidget.margin = MARGIN; - listBox.add(globalKeybindingsWidget); + let persistentOverTogglesKey = schema.get_key('persistent-over-toggles'); + let persistentOverTogglesRow = new PrefRow({ label: persistentOverTogglesKey.get_summary(), desc: persistentOverTogglesKey.get_description() }); + let persistentOverTogglesSwitch = new Gtk.Switch(); + settings.bind('persistent-over-toggles', persistentOverTogglesSwitch, 'active', 0); + persistentOverTogglesRow.addWidget(persistentOverTogglesSwitch, true); + listBox.add(persistentOverTogglesRow); - let persistentBox = new Gtk.Box({ margin_top: MARGIN/2, margin_bottom: MARGIN/2, margin_left: MARGIN, margin_right: MARGIN }); - let persistentLabelBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL }); - let persistentLabel1 = new Gtk.Label({label: _("Persistent")}); - let persistentLabel2 = new Gtk.Label({ use_markup: true, halign: 1, wrap: true, xalign: 0, label: "" + _("Persistent drawing through session restart") + "" }); - persistentLabel1.set_halign(1); - persistentLabel2.get_style_context().add_class('dim-label'); - persistentLabelBox.pack_start(persistentLabel1, true, true, 0); - persistentLabelBox.pack_start(persistentLabel2, true, true, 0); - let persistentSwitch = new Gtk.Switch({valign: 3}); - this.settings.bind('persistent-drawing', persistentSwitch, 'active', 0); - persistentBox.pack_start(persistentLabelBox, true, true, 4); - persistentBox.pack_start(persistentSwitch, false, false, 4); - listBox.add(persistentBox); + let persistentOverRestartsKey = schema.get_key('persistent-over-restarts'); + let persistentOverRestartsRow = new PrefRow({ label: persistentOverRestartsKey.get_summary(), desc: persistentOverRestartsKey.get_description() }); + let persistentOverRestartsSwitch = new Gtk.Switch(); + settings.bind('persistent-over-restarts', persistentOverRestartsSwitch, 'active', 0); + persistentOverRestartsRow.addWidget(persistentOverRestartsSwitch, true); + persistentOverTogglesSwitch.bind_property('active', persistentOverRestartsSwitch, 'sensitive', GObject.BindingFlags.SYNC_CREATE); + listBox.add(persistentOverRestartsRow); - let desktopBox = new Gtk.Box({ margin_top: MARGIN/2, margin_bottom: MARGIN/2, margin_left: MARGIN, margin_right: MARGIN }); - let desktopLabelBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL }); - let desktopLabel1 = new Gtk.Label({label: _("Drawing on the desktop")}); - let desktopLabel2 = new Gtk.Label({ use_markup: true, halign: 1, wrap: true, xalign: 0, label: "" + _("Draw On Your Screen becomes Draw On Your Desktop") + "" }); - desktopLabel1.set_halign(1); - desktopLabel2.get_style_context().add_class('dim-label'); - desktopLabelBox.pack_start(desktopLabel1, true, true, 0); - desktopLabelBox.pack_start(desktopLabel2, true, true, 0); - let desktopSwitch = new Gtk.Switch({valign: 3}); - this.settings.bind('drawing-on-desktop', desktopSwitch, 'active', 0); - desktopBox.pack_start(desktopLabelBox, true, true, 4); - desktopBox.pack_start(desktopSwitch, false, false, 4); - listBox.add(desktopBox); + let desktopKey = schema.get_key('drawing-on-desktop'); + let desktopRow = new PrefRow({ label: desktopKey.get_summary(), desc: desktopKey.get_description() }); + let desktopSwitch = new Gtk.Switch(); + settings.bind('drawing-on-desktop', desktopSwitch, 'active', 0); + desktopRow.addWidget(desktopSwitch, true); + persistentOverTogglesSwitch.bind_property('active', desktopSwitch, 'sensitive', GObject.BindingFlags.SYNC_CREATE); + listBox.add(desktopRow); - let osdBox = new Gtk.Box({ margin_top: MARGIN/2, margin_bottom: MARGIN/2, margin_left: MARGIN, margin_right: MARGIN }); - let osdLabelBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL }); - let osdLabel1 = new Gtk.Label({label: _("Disable on-screen notifications")}); - osdLabel1.set_halign(1); - osdLabelBox.pack_start(osdLabel1, true, true, 0); - let osdSwitch = new Gtk.Switch({valign: 3}); - this.settings.bind('osd-disabled', osdSwitch, 'active', 0); - osdBox.pack_start(osdLabelBox, true, true, 4); - osdBox.pack_start(osdSwitch, false, false, 4); - listBox.add(osdBox); + let osdKey = schema.get_key('osd-disabled'); + let osdRow = new PrefRow({ label: osdKey.get_summary(), desc: osdKey.get_description() }); + let osdSwitch = new Gtk.Switch(); + settings.bind('osd-disabled', osdSwitch, 'active', 0); + osdRow.addWidget(osdSwitch, true); + listBox.add(osdRow); - let indicatorBox = new Gtk.Box({ margin_top: MARGIN/2, margin_bottom: MARGIN/2, margin_left: MARGIN, margin_right: MARGIN }); - let indicatorLabelBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL }); - let indicatorLabel1 = new Gtk.Label({label: _("Disable panel indicator")}); - indicatorLabel1.set_halign(1); - indicatorLabelBox.pack_start(indicatorLabel1, true, true, 0); - let indicatorSwitch = new Gtk.Switch({valign: 3}); - this.settings.bind('indicator-disabled', indicatorSwitch, 'active', 0); - indicatorBox.pack_start(indicatorLabelBox, true, true, 4); - indicatorBox.pack_start(indicatorSwitch, false, false, 4); - listBox.add(indicatorBox); + let indicatorKey = schema.get_key('indicator-disabled'); + let indicatorRow = new PrefRow({ label: indicatorKey.get_summary(), desc: indicatorKey.get_description() }); + let indicatorSwitch = new Gtk.Switch(); + settings.bind('indicator-disabled', indicatorSwitch, 'active', 0); + indicatorRow.addWidget(indicatorSwitch, true); + listBox.add(indicatorRow); - let children = listBox.get_children(); - for (let i = 0; i < children.length; i++) { - if (children[i].activatable) - children[i].set_activatable(false); - } - - let internalFrame = new Gtk.Frame({ margin_top: 3*MARGIN, label_yalign: 1.0 }); - internalFrame.set_label_widget(new Gtk.Label({ margin_bottom: MARGIN/2, use_markup: true, label: "" + _("Internal") + " " + _("(in drawing mode)") })); + let internalFrame = new Frame({ label: _("Internal"), desc: _("In drawing mode") }); box.add(internalFrame); - listBox = new Gtk.ListBox({ selection_mode: 0, hexpand: true, margin_top: MARGIN }); + listBox = new Gtk.ListBox({ selection_mode: 0, hexpand: true, margin_top: MARGIN, margin_bottom: MARGIN }); + listBox.get_style_context().add_class('background'); internalFrame.add(listBox); - styleContext = listBox.get_style_context(); - styleContext.add_class('background'); + Shortcuts.OTHERS.forEach((pairs, index) => { + if (index) + listBox.add(new Gtk.Box(ROWBOX_MARGIN_PARAMS)); + + pairs.forEach(pair => { + let [action, shortcut] = pair; + let otherBox = new Gtk.Box({ margin_left: MARGIN, margin_right: MARGIN }); + let otherLabel = new Gtk.Label({ label: action, use_markup: true }); + otherLabel.set_halign(1); + let otherLabel2 = new Gtk.Label({ label: shortcut }); + otherBox.pack_start(otherLabel, true, true, 4); + otherBox.pack_start(otherLabel2, false, false, 4); + listBox.add(otherBox); + }); + }); - for (let i = 0; i < OTHER_SHORTCUTS.length; i++) { - if (OTHER_SHORTCUTS[i].desc.indexOf('-separator-') != -1) { - listBox.add(new Gtk.Box({ margin_top: MARGIN, margin_left: MARGIN, margin_right: MARGIN })); - continue; + listBox.add(new Gtk.Box(ROWBOX_MARGIN_PARAMS)); + + Shortcuts.INTERNAL_KEYBINDINGS.forEach((settingKeys, index) => { + if (index) + listBox.add(new Gtk.Box(ROWBOX_MARGIN_PARAMS)); + + let internalKeybindingsWidget = new KeybindingsWidget(settingKeys, internalShortcutSettings); + listBox.add(internalKeybindingsWidget); + }); + + listBox.get_children().forEach(row => row.set_activatable(false)); + + let resetButton = new Gtk.Button({ label: _("Reset settings"), halign: Gtk.Align.CENTER }); + resetButton.get_style_context().add_class('destructive-action'); + resetButton.connect('clicked', () => { + internalShortcutSettings.settings_schema.list_keys().forEach(key => internalShortcutSettings.reset(key)); + settings.settings_schema.list_keys().forEach(key => settings.reset(key)); + }); + box.add(resetButton); + } +}); + +const Frame = new GObject.Class({ + Name: 'DrawOnYourScreenFrame', + GTypeName: 'DrawOnYourScreenFrame', + Extends: Gtk.Frame, + + _init: function(params) { + let labelWidget = new Gtk.Label({ margin_bottom: MARGIN / 2, use_markup: true, label: `${params.label}` }); + this.parent({ label_yalign: 1.0, label_widget: labelWidget }); + + if (params.desc) { + labelWidget.set_tooltip_text(params.desc); + this.get_accessible().set_description(params.desc); + } + } +}); + +const PrefRow = new GObject.Class({ + Name: 'DrawOnYourScreenPrefRow', + GTypeName: 'DrawOnYourScreenPrefRow', + Extends: Gtk.ListBoxRow, + + _init: function(params) { + this.parent({ activatable: false }); + + let hbox = new Gtk.Box(ROWBOX_MARGIN_PARAMS); + this.add(hbox); + + let labelBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL }); + hbox.pack_start(labelBox, true, true, 4); + + this.widgetBox = new Gtk.Box({ spacing: 4 }); + hbox.pack_start(this.widgetBox, false, false, 4); + + this.label = new Gtk.Label({ use_markup: true, label: params.label, halign: Gtk.Align.START }); + labelBox.pack_start(this.label, true, true, 0); + if (params.desc) { + this.desc = new Gtk.Label({ use_markup: true, label: `${params.desc}`, halign: Gtk.Align.START, wrap: true, xalign: 0 }); + this.desc.get_style_context().add_class('dim-label'); + labelBox.pack_start(this.desc, true, true, 0); + this.widgetBox.set_valign(Gtk.Align.START); + } + }, + + addWidget: function(widget, setRelationship) { + this.widgetBox.add(widget); + + if (widget.name) + widget.get_accessible().set_name(widget.name); + + if (setRelationship) { + this.label.get_accessible().add_relationship(Atk.RelationType.LABEL_FOR, widget.get_accessible()); + widget.get_accessible().add_relationship(Atk.RelationType.LABELLED_BY, this.label.get_accessible()); + + if (this.desc) { + this.desc.get_accessible().add_relationship(Atk.RelationType.DESCRIPTION_FOR, widget.get_accessible()); + widget.get_accessible().add_relationship(Atk.RelationType.DESCRIBED_BY, this.desc.get_accessible()); } - let otherBox = new Gtk.Box({ margin_left: MARGIN, margin_right: MARGIN }); - 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 }); - otherBox.pack_start(otherLabel, true, true, 4); - otherBox.pack_start(otherLabel2, false, false, 4); - listBox.add(otherBox); + } + } +}); + +const PixelSpinButton = new GObject.Class({ + Name: 'DrawOnYourScreenPixelSpinButton', + GTypeName: 'DrawOnYourScreenPixelSpinButton', + Extends: Gtk.SpinButton, + Properties: { + 'range': GObject.param_spec_variant('range', 'range', 'GSettings range', + GLib.VariantType.new('(sv)'), null, GObject.ParamFlags.WRITABLE), + + 'step': GObject.ParamSpec.double('step', 'step', 'step increment', + GObject.ParamFlags.WRITABLE, + 0, 1000, 1) + }, + + set range(range) { + let [type, variant] = range.deep_unpack(); + if (type == 'range') { + let [min, max] = variant.deep_unpack(); + this.adjustment.set_lower(min); + this.adjustment.set_upper(max); + } + }, + + set step(step) { + this.adjustment.set_step_increment(step); + this.adjustment.set_page_increment(step * 10); + }, + + // Add 'px' unit. + vfunc_output: function() { + this.text = _("%f px").format(Number(this.value).toFixed(2)); + return true; + }, + + // Prevent accidental scrolling. + vfunc_scroll_event: function(event) { + return this.has_focus ? this.parent(event) : Gdk.EVENT_PROPAGATE; + } +}); + +// A color button that can be easily bound with a color string setting. +const ColorStringButton = new GObject.Class({ + Name: 'DrawOnYourScreenColorStringButton', + GTypeName: 'DrawOnYourScreenColorStringButton', + Extends: Gtk.ColorButton, + Properties: { + 'color-string': GObject.ParamSpec.string('color-string', 'colorString', 'A string that describes the color', + GObject.ParamFlags.READWRITE, 'black') + }, + + get color_string() { + return this._color_string || 'black'; + }, + + set color_string(colorString) { + this._color_string = colorString; + + let newRgba = new Gdk.RGBA(); + newRgba.parse(colorString); + this.set_rgba(newRgba); + }, + + // Do nothing if the new color is equivalent to the old color (e.g. "black" and "rgb(0,0,0)"). + vfunc_color_set(args) { + let oldRgba = new Gdk.RGBA(); + oldRgba.parse(this.color_string); + + if (!this.rgba.equal(oldRgba)) { + this._color_string = this.rgba.to_string(); + this.notify('color-string'); + } + } +}); + +const FileChooserButton = new GObject.Class({ + Name: 'DrawOnYourScreenFileChooserButton', + GTypeName: 'DrawOnYourScreenFileChooserButton', + Extends: Gtk.FileChooserButton, + Properties: { + 'location': GObject.ParamSpec.string('location', 'location', 'location', + GObject.ParamFlags.READWRITE, '') + }, + + get location() { + return this.get_file().get_path(); + }, + + set location(location) { + if (!location) { + this.unselect_all(); + if (this.get_file()) + this.set_file(Gio.File.new_for_path('aFileThatDoesNotExist')); + return; } - let internalKeybindingsWidget = new KeybindingsWidget(INTERNAL_KEYBINDINGS, this.settings); - internalKeybindingsWidget.margin = MARGIN; - listBox.add(internalKeybindingsWidget); - - let styleBox = new Gtk.Box({ margin: MARGIN }); - let styleLabel = new Gtk.Label({ - wrap: true, - xalign: 0, - use_markup: true, - label: _("Default drawing style attributes (color palette, font, line, dash) are defined in an editable css file.\n" + - "See “%s”.").format(_("Edit style")) - }); - styleLabel.set_halign(1); - styleLabel.get_style_context().add_class('dim-label'); - styleBox.pack_start(styleLabel, true, true, 4); - listBox.add(styleBox); - - let noteBox = new Gtk.Box({ margin: MARGIN }); - let noteLabel = new Gtk.Label({ - wrap: true, - xalign: 0, - use_markup: true, - 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")) - }); - noteLabel.set_halign(1); - noteLabel.get_style_context().add_class('dim-label'); - noteBox.pack_start(noteLabel, true, true, 4); - listBox.add(noteBox); - - children = listBox.get_children(); - for (let i = 0; i < children.length; i++) { - if (children[i].activatable) - children[i].set_activatable(false); - } + let file = Gio.File.new_for_commandline_arg(location); + if (file.query_exists(null)) + this.set_file(file); + }, + + vfunc_file_set: function(args) { + this.notify('location'); } }); @@ -351,11 +648,11 @@ const KeybindingsWidget = new GObject.Class({ GTypeName: 'DrawOnYourScreenKeybindingsWidget', Extends: Gtk.Box, - _init: function(keybindings, settings) { - this.parent(); + _init: function(settingKeys, settings) { + this.parent(ROWBOX_MARGIN_PARAMS); this.set_orientation(Gtk.Orientation.VERTICAL); - this._keybindings = keybindings; + this._settingKeys = settingKeys; this._settings = settings; this._columns = { @@ -436,17 +733,27 @@ const KeybindingsWidget = new GObject.Class({ this.keybinding_column = keybinding_column; this.action_column = action_column; + this._settings.connect('changed', this._onSettingsChanged.bind(this)); this._refresh(); }, + + // Support the case where all the settings has been reset. + _onSettingsChanged: function() { + if (this._refreshTimeout) + GLib.source_remove(this._refreshTimeout); + + this._refreshTimeout = GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + this._refreshTimeout = 0; + this._refresh(); + }); + }, _refresh: function() { this._store.clear(); - for(let settings_key in this._keybindings) { - if (settings_key.indexOf('-separator-') != -1) - continue; + this._settingKeys.forEach(settingKey => { let [key, mods] = Gtk.accelerator_parse( - this._settings.get_strv(settings_key)[0] + this._settings.get_strv(settingKey)[0] || '' ); let iter = this._store.append(); @@ -458,12 +765,12 @@ const KeybindingsWidget = new GObject.Class({ this._columns.KEY ], [ - settings_key, - _(this._keybindings[settings_key]), + settingKey, + this._settings.settings_schema.get_key(settingKey).get_summary(), mods, key ] ); - } + }); } }); diff --git a/schemas/gschemas.compiled b/schemas/gschemas.compiled index 92584ee..76b17ef 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 4fe2746..d7a9158 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 @@ -1,290 +1,342 @@ - + + + false - move drawing on desktop - move drawing on desktop + Drawing on the desktop + Draw On Your Screen becomes Draw On Your Desktop]]> - - false - persistent drawing - persistent drawing - - - false - disable OSD notifications - disable on-screen notifications + + ["<Alt><Super>e"] + Erase all drawings false - disable panel indicator - disable panel indicator + Disable panel indicator + + + false + Disable on-screen notifications + + + true + Persistent over toggles + Drawing remains when toggling drawing mode + + + false + Persistent over restarts + Drawing is automatically saved to a file ["<Alt><Super>d"] - toggle drawing - enter or leave drawing mode + Enter/leave drawing mode ["<Primary><Alt><Super>d"] - toggle modeless/modal - toggle modeless/modal + + Grab/ungrab keyboard and pointer + - - ["<Alt><Super>e"] - erase drawing - erase drawing + + + + "#2e2e2e" + Background color + The color of the drawing area background - - ["<Primary>z"] - undo - undo + + true + Automatic dash array + Compute the lengths from the line width - - ["<Primary><Shift>z"] - redo - redo + + + 5 + Dash array on + The dash length in pixels - - ["Delete"] - delete last element - delete last element + + + 15 + Dash array off + The gap between the dashes in pixels - - ["<Primary>equal"] - smooth last brushstroke - smooth last brushstroke + + + 0 + Dash offset + The dash offset in pixels - - ["<Primary>b"] - toggle drawing background - toggle drawing background + + "Gray" + Grid overlay color + The color of the lines - - ["<Primary>g"] - toggle grid overlay - toggle grid overlay + + true + Automatic grid overlay line + Compute the lengths from the screen size - - ["<Primary>h"] - hide or show panel and dock - hide or show panel and dock + + + 10 + Grid overlay line spacing + The gap between lines in pixels - - ["<Primary>n"] - toggle square area - toggle square area + + + 0.5 + Grid overlay line width + The line width in pixels - - ["<Primary>e"] - select cercle - select a cercle + + "" + Image location + The location of the directory in which the image tool picks - - ["<Primary>r"] - select rectangle - select rectangle + + + [ + ("Palette", ["HotPink","Cyan","yellow","Orangered","Chartreuse","DarkViolet","White","Gray","Black"]), + ("GNOME HIG lighter", ["rgb(153,193,241)","rgb(143,240,164)","rgb(249,240,107)","rgb(255,190,111)","rgb(246,97,81)","rgb(220,138,221)","rgb(205,171,143)","rgb(255,255,255)","rgb(119,118,123)"]), + ("GNOME HIG light", ["rgb(98,160,241)","rgb(87,227,137)","rgb(248,228,92)","rgb(255,163,72)","rgb(237,51,59)","rgb(192,97,203)","rgb(181,131,90)","rgb(246,245,244)","rgb(94,92,100)"]), + ("GNOME HIG normal", ["rgb(53,132,228)","rgb(51,209,122)","rgb(246,211,45)","rgb(255,120,0)","rgb(224,27,36)","rgb(145,65,172)","rgb(152,106,68)","rgb(222,221,218)","rgb(61,56,70)"]), + ("GNOME HIG dark", ["rgb(28,113,216)","rgb(46,194,126)","rgb(245,194,17)","rgb(230,97,0)","rgb(192,28,40)","rgb(129,61,156)","rgb(134,94,60)","rgb(192,191,188)","rgb(36,31,49)"]), + ("GNOME HIG darker", ["rgb(26,095,180)","rgb(38,162,105)","rgb(229,165,10)","rgb(198,70,0)","rgb(165,29,45)","rgb(97,53,131)","rgb(99,69,44)","rgb(154,153,150)","rgb(0,0,0)"]) + ] + + Color palettes + The palettes of drawing colors - - ["<Primary>y"] - select polygon - select polygon + + true + Automatic square area size + Compute the area size from the screen size - - ["<Primary>u"] - select polyline - select polyline - - - ["<Primary>l"] - select line - select a line - - - ["<Primary>t"] - select text - select text - - - ["<Primary>i"] - select image - select image - - - ["<Primary>p"] - 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 - - - KP_Add','plus']]]> - increment the line width - increment the line width + + + 512 + Square area size + The size of the area in pixels + + KP_Subtract','minus','minus']]]> - decrement the line width - decrement the line width - - - ["<Primary><Shift>KP_Add"] - increment the line width even more - increment the line width even more + Decrement line width ["<Primary><Shift>KP_Subtract"] - decrement the line width even more - decrement the line width even more + Decrement line width even more - - ["<Primary>j"] - switch linejoin - switch linejoin + + ["Delete"] + Erase last brushstroke - - ["<Primary>k"] - switch linecap - switch linecap + + ["<Primary><Alt>s"] + Export drawing to a SVG file - - KP_Multiply','asterisk','asterisk']]]> - switch fill rule - switch fill rule + + KP_Add','plus']]]> + Increment line width - - ["<Primary>period"] - switch dash - switch dash - - - ["<Primary>a"] - switch fill - switch fill - - - KP_1','1']]]> - select color1 - select color1 - - - KP_2','2']]]> - select color2 - select color2 - - - KP_3','3']]]> - select color3 - select color3 - - - KP_4','4']]]> - select color4 - select color4 - - - KP_5','5']]]> - select color5 - select color5 - - - KP_6','6']]]> - select color6 - select color6 - - - KP_7','7']]]> - select color7 - select color7 - - - KP_8','8']]]> - select color8 - select color8 - - - KP_9','9']]]> - select color9 - select color9 - - - ["<Primary>f"] - switch font family - switch font family - - - ["<Primary><Shift>f"] - switch font family (reverse) - switch font family (reverse) - - - ["<Primary>w"] - switch font weight - switch font weight - - - ["<Primary><Shift>w"] - switch font style - switch font style - - - ["<Primary><Shift>a"] - switch text alignment - switch text alignment - - - ["<Primary><Shift>i"] - switch image file - switch image file - - - ["<Primary>o"] - open user stylesheet to edit style - open user stylesheet to edit style - - - ["<Primary><Shift>s"] - Save drawing as a svg file - Save drawing as a svg file - - - ["<Primary>s"] - Save drawing as a json file - Save drawing as a json file - - - ["<Primary>Page_Down"] - Open previous json file - Open previous json file + + ["<Primary><Shift>KP_Add"] + Increment line width even more - ["<Primary>Page_Up"] - Open next json file - Open next json file + ["<Primary>o"] + Open next drawing ["<Primary>comma"] Open preferences - Open preferences + + + ["<Primary><Shift>o"] + Open previous drawing + + + ["<Primary>v"] + Add images from the clipboard + + + ["<Primary><Shift>z"] + Redo last brushstroke + + + ["<Primary>s"] + Save drawing + + + KP_1','1']]]> + Select color 1 + + + + KP_2','2']]]> + Select color 2 + + + KP_3','3']]]> + Select color 3 + + + KP_4','4']]]> + Select color 4 + + + KP_5','5']]]> + Select color 5 + + + KP_6','6']]]> + Select color 6 + + + KP_7','7']]]> + Select color 7 + + + KP_8','8']]]> + Select color 8 + + + KP_9','9']]]> + Select color 9 + + + ["<Primary>e"] + Select ellipse tool + + + ["<Primary>i"] + Select image tool + + + ["<Primary>l"] + Select line tool + + + ["<Primary>c"] + Select mirror tool + + + ["<Primary>m"] + Select move tool + + + ["<Primary>p"] + Select free drawing + + + ["<Primary>y"] + Select polygon tool + + + ["<Primary>u"] + Select polyline tool + + + ["<Primary>r"] + Select rectangle tool + + + ["<Primary>x"] + Select resize tool + + + ["<Primary>t"] + Select text tool + + + ["<Primary>equal"] + Smooth last brushstroke + + + ["<Primary>KP_Divide","<Primary>slash"] + Change color palette + + + ["<Primary><Shift>KP_Divide","<Primary><Shift>slash"] + Change color palette (reverse) + + + ["<Primary>period"] + Dashed line + + + ["<Primary>a"] + Toggle fill/outline + + + KP_Multiply','asterisk','asterisk']]]> + Toggle fill rule + + + ["<Primary>f"] + Change font family + + + ["<Primary><Shift>f"] + Change font family (reverse) + + + ["<Primary><Alt>w"] + Change font style + + + ["<Primary>w"] + Change font weight + + + ["<Primary><Alt>i"] + Change image + + + ["<Primary><Alt><Shift>i"] + Change image (reverse) + + + ["<Primary>k"] + Change linecap + + + ["<Primary>j"] + Change linejoin + + + ["<Primary><Alt>a"] + Toggle text alignment + + + ["<Primary>b"] + Add a drawing background + + + ["<Primary>g"] + Add a grid overlay ["<Primary>F1"] - toggle help - toggle help + Show help + + + ["<Primary>h"] + Hide panel and dock + + + ["<Primary>n"] + + Square drawing area + + + ["<Primary>z"] + Undo last brushstroke diff --git a/shortcuts.js b/shortcuts.js new file mode 100644 index 0000000..488485a --- /dev/null +++ b/shortcuts.js @@ -0,0 +1,104 @@ +/* jslint esversion: 6 */ +/* exported GLOBAL_KEYBINDINGS, INTERNAL_KEYBINDINGS, OTHERS */ + +/* + * Copyright 2019 Abakkk + * + * This file is part of DrawOnYourScreen, a drawing extension for GNOME Shell. + * https://framagit.org/abakkk/DrawOnYourScreen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +const Gtk = imports.gi.Gtk; + +const GS_VERSION = imports.misc.config.PACKAGE_VERSION; +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 internalShortcutsSchema = Convenience.getSettings(Me.metadata['settings-schema'] + '.internal-shortcuts').settings_schema; + +const getKeyLabel = function(accel) { + let [keyval, mods] = Gtk.accelerator_parse(accel); + return Gtk.accelerator_get_label(keyval, mods); +}; + +// The setting keys of the "org.gnome.shell.extensions.draw-on-your-screen" schema. +var GLOBAL_KEYBINDINGS = [ + ['toggle-drawing', 'toggle-modal', 'erase-drawings'], +]; + +// The setting keys of the "org.gnome.shell.extensions.draw-on-your-screen.internal-shortcuts" schema. +var INTERNAL_KEYBINDINGS = [ + ['undo', 'redo', 'delete-last-element', 'smooth-last-element'], + ['select-none-shape', 'select-line-shape', 'select-ellipse-shape', 'select-rectangle-shape', 'select-polygon-shape', 'select-polyline-shape', + 'select-text-shape', 'select-image-shape', 'select-move-tool', 'select-resize-tool', 'select-mirror-tool'], + ['switch-fill', 'switch-fill-rule', 'switch-color-palette', 'switch-color-palette-reverse'], + ['increment-line-width', 'increment-line-width-more', 'decrement-line-width', 'decrement-line-width-more', + 'switch-linejoin', 'switch-linecap', 'switch-dash'], + ['switch-font-family', 'switch-font-family-reverse', 'switch-font-weight', 'switch-font-style', 'switch-text-alignment'], + ['switch-image-file', 'switch-image-file-reverse', 'paste-image-files'], + ['toggle-panel-and-dock-visibility', 'toggle-background', 'toggle-grid', 'toggle-square-area'], + ['open-next-json', 'open-previous-json', 'save-as-json', 'export-to-svg', 'open-preferences', 'toggle-help'], +]; + +if (GS_VERSION < '3.36') { + // Remove 'open-preferences' keybinding. + INTERNAL_KEYBINDINGS.forEach(settingKeys => { + let index = settingKeys.indexOf('open-preferences'); + if (index != -1) + settingKeys.splice(index, 1); + }); +} + +const getOthers = function() { + return [ + [ + [_("Draw"), _("Left click")], + [_("Menu"), _("Right click")], + [internalShortcutsSchema.get_key('switch-fill').get_summary(), _("Center click")], + [_("Increment/decrement line width"), _("Scroll")], + // Translators: %s are key labels (Ctrl+F1 and Ctrl+F9) + [_("Select color"), _("%s … %s").format(getKeyLabel('1'), getKeyLabel('9'))], + // Translators: %s is a key label + [_("Ignore pointer movement"), _("%s held").format(getKeyLabel('space'))], + [_("Leave"), getKeyLabel('Escape')], + ], [ + [_("Select eraser (while starting drawing)"), getKeyLabel('')], + [_("Duplicate (while starting handling)"), getKeyLabel('')], + [_("Rotate rectangle, polygon, polyline"), getKeyLabel('')], + [_("Extend circle to ellipse"), getKeyLabel('')], + [_("Curve line"), getKeyLabel('')], + [_("Smooth free drawing outline"), getKeyLabel('')], + [_("Unlock image ratio"), getKeyLabel('')], + [_("Rotate (while moving)"), getKeyLabel('')], + [_("Stretch (while resizing)"), getKeyLabel('')], + [_("Inverse (while mirroring)"), getKeyLabel('')], + ], + ]; +}; + +let _OTHERS; +// Equivalent to "var OTHERS = [[ ... ]]", but as a getter so the translations are got after the initTranslations call. +// 'this' is the module. +Object.defineProperty(this, 'OTHERS', { + get: function() { + if (!_OTHERS) + _OTHERS = getOthers(); + return _OTHERS; + } +}); + diff --git a/stylesheet.css b/stylesheet.css index 8da1267..768f8ca 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -1,7 +1,3 @@ -@import "./data/default.css"; - -/* The following styles don't affect the drawing */ - /* square area */ .draw-on-your-screen-square-area { @@ -48,7 +44,7 @@ padding-bottom: .3em; } -.draw-on-your-screen-menu .popup-menu-icon { +.draw-on-your-screen-menu .popup-menu-item > .popup-menu-icon { icon-size: 1em; /* default: 1.09 */ padding-top: 0.03em; } @@ -80,23 +76,20 @@ margin-top: 0; } -/* system-menu-action: from GS 3.34- */ -.draw-on-your-screen-menu .system-menu-action { - min-width: 0; - border: none; - border-radius: 32px; - padding: 12px; - margin: 0; +.draw-on-your-screen-menu-destructive-button:hover { + color: #e01b24; /* upstream destructive color, light: #e01b24, dark: #b2161d */ } -.draw-on-your-screen-menu .system-menu-action:hover, -.draw-on-your-screen-menu .system-menu-action:focus { - border: none; +/* override .button upstream style class */ +.draw-on-your-screen-menu-action-button { + min-height: 0; + min-width: 0; + border-radius: 32px; padding: 12px; } -.draw-on-your-screen-menu .system-menu-action > StIcon { - icon-size: 16px; +.draw-on-your-screen-menu-action-button > StIcon { + icon-size: 1em; } .draw-on-your-screen-menu-slider-label { @@ -124,8 +117,13 @@ padding: 0.35em 0.57em; } -.draw-on-your-screen-menu-delete-button:hover { - color: #f57900; +/* override .button upstream style class */ +.draw-on-your-screen-menu-inline-button { + min-height: 1px; + padding: 2px 4px; /* default 3px 24px */ } - - + +.draw-on-your-screen-menu-inline-button .popup-menu-icon { + icon-size: 0.85em; /* default 1.09 */ +} +