diff --git a/convenience.js b/convenience.js new file mode 100644 index 0000000..9b3ced5 --- /dev/null +++ b/convenience.js @@ -0,0 +1,92 @@ +/* + Copyright (c) 2011-2012, Giovanni Campagna + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the GNOME nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +const Gettext = imports.gettext; +const Gio = imports.gi.Gio; + +const Config = imports.misc.config; +const ExtensionUtils = imports.misc.extensionUtils; + +/** + * initTranslations: + * @domain: (optional): the gettext domain to use + * + * Initialize Gettext to load translations from extensionsdir/locale. + * If @domain is not provided, it will be taken from metadata['gettext-domain'] + */ +function initTranslations(domain) { + let extension = ExtensionUtils.getCurrentExtension(); + + domain = domain || extension.metadata['gettext-domain']; + + // check if this extension was built with "make zip-file", and thus + // has the locale files in a subfolder + // otherwise assume that extension has been installed in the + // same prefix as gnome-shell + let localeDir = extension.dir.get_child('locale'); + if (localeDir.query_exists(null)) + Gettext.bindtextdomain(domain, localeDir.get_path()); + else + Gettext.bindtextdomain(domain, Config.LOCALEDIR); +} + +/** + * getSettings: + * @schema: (optional): the GSettings schema id + * + * Builds and return a GSettings schema for @schema, using schema files + * in extensionsdir/schemas. If @schema is not provided, it is taken from + * metadata['settings-schema']. + */ +function getSettings(schema) { + let extension = ExtensionUtils.getCurrentExtension(); + + schema = schema || extension.metadata['settings-schema']; + + const GioSSS = Gio.SettingsSchemaSource; + + // check if this extension was built with "make zip-file", and thus + // has the schema files in a subfolder + // otherwise assume that extension has been installed in the + // same prefix as gnome-shell (and therefore schemas are available + // in the standard folders) + let schemaDir = extension.dir.get_child('schemas'); + let schemaSource; + if (schemaDir.query_exists(null)) + schemaSource = GioSSS.new_from_directory(schemaDir.get_path(), + GioSSS.get_default(), + false); + else + schemaSource = GioSSS.get_default(); + + let schemaObj = schemaSource.lookup(schema, true); + if (!schemaObj) + throw new Error('Schema ' + schema + ' could not be found for extension ' + + extension.metadata.uuid + '. Please check your installation.'); + + return new Gio.Settings({ settings_schema: schemaObj }); +} + diff --git a/draw.js b/draw.js new file mode 100644 index 0000000..f3581cf --- /dev/null +++ b/draw.js @@ -0,0 +1,690 @@ +/* jslint esversion: 6 */ + +/* + * Copyright 2019 Abakkk + * + * This file is part of DrowOnYourScreen, 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 Cairo = imports.cairo; +const Clutter = imports.gi.Clutter; +const GLib = imports.gi.GLib; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; +const Mainloop = imports.mainloop; +const Signals = imports.signals; +const St = imports.gi.St; +const Screenshot = imports.ui.screenshot; +const Tweener = imports.ui.tweener; + +const ExtensionUtils = imports.misc.extensionUtils; +const Extension = ExtensionUtils.getCurrentExtension(); +const Convenience = Extension.imports.convenience; +const Prefs = Extension.imports.prefs; +const _ = imports.gettext.domain(Extension.metadata["gettext-domain"]).gettext; + +var Shapes = { NONE: 0, LINE: 1, ELLIPSE: 2, RECTANGLE: 3, TEXT: 4 }; +var ShapeNames = { 0: "Free drawing", 1: "Line", 2: "Circle", 3: "Rectangle", 4: "Text" }; +var LineCapNames = { 0: 'Butt', 1: 'Round', 2: 'Square' }; +var LineJoinNames = { 0: 'Miter', 1: 'Round', 2: 'Bevel' }; +var FontWeightNames = { 0: 'Normal', 1: 'Bold' }; +var FontStyleNames = { 0: 'Normal', 1: 'Italic', 2: 'Oblique' }; +var FontFamilyNames = { 0: 'Default', 1: 'Sans-Serif', 2: 'Serif', 3: 'Monospace', 4: 'Cursive', 5: 'Fantasy' }; + +// DrawingArea is the widget in which we draw, thanks to Cairo. +// It creates and manages a DrawingElement for each "brushstroke". +// It handles pointer/mouse/(touch?) events and some keyboard events. +var DrawingArea = new Lang.Class({ + Name: 'DrawingArea', + Extends: St.DrawingArea, + + _init: function(params, helper) { + this.parent({ style_class: 'draw-on-your-screen', name: params && params.name ? params.name : ""}); + + // 'style-changed' is emitted when 'this' is added to an actor + // ('this' needs to be in the stage to query theme_node) + this.connect('style-changed', this._onStyleChanged.bind(this)); + this.connect('repaint', this._repaint.bind(this)); + + this.emitter = new DrawingAreaEmitter(); + this.helper = helper; + + this.elements = []; + this.undoneElements = []; + this.currentElement = null; + this.currentShape = Shapes.NONE; + this.hasBackground = false; + this.textHasCursor = false; + this.dashedLine = false; + this.colors = [Clutter.Color.new(0, 0, 0, 255)]; + }, + + _redisplay: function() { + // force area to emit 'repaint' + this.queue_repaint(); + }, + + _onStyleChanged: function() { + try { + let themeNode = this.get_theme_node(); + for (let i = 1; i < 10; i++) { + this.colors[i] = themeNode.get_color('-drawing-color' + i); + } + this.activeBackgroundColor = themeNode.get_color('-drawing-background-color'); + this.currentLineWidth = themeNode.get_length('-drawing-line-width'); + this.currentLineJoin = themeNode.get_double('-drawing-line-join'); + this.currentLineCap = themeNode.get_double('-drawing-line-cap'); + this.dashArray = [themeNode.get_length('-drawing-dash-array-on'), themeNode.get_length('-drawing-dash-array-off')]; + this.dashOffset = themeNode.get_length('-drawing-dash-offset'); + let font = themeNode.get_font(); + this.fontFamily = font.get_family(); + this.currentFontWeight = font.get_weight(); + this.currentFontStyle = font.get_style(); + } catch(e) { + logError(e); + } + + for (let i = 1; i < 10; i++) { + this.colors[i] = this.colors[i].alpha ? this.colors[i] : this.colors[0]; + } + this.currentColor = this.colors[1]; + + this.currentLineWidth = (this.currentLineWidth > 0) ? this.currentLineWidth : 3; + this.currentLineJoin = ([0, 1, 2].indexOf(this.currentLineJoin) != -1) ? this.currentLineJoin : Cairo.LineJoin.ROUND; + this.currentLineCap = ([0, 1, 2].indexOf(this.currentLineCap) != -1) ? this.currentLineCap : Cairo.LineCap.ROUND; + this.currentFontFamilyId = 0; + this.currentFontWeight = this.currentFontWeight > 500 ? 1 : 0 ; + // font style enum order of Cairo and Pango are different + this.currentFontStyle = this.currentFontStyle == 2 ? 1 : ( this.currentFontStyle == 1 ? 2 : 0); + }, + + _repaint: function(area) { + let cr = area.get_context(); + + for (let i = 0; i < this.elements.length; i++) { + this.elements[i].buildCairo(cr, false); + + if (this.elements[i].fill && this.elements[i].shape != Shapes.LINE) + cr.fill(); + else + cr.stroke(); + } + + if (this.currentElement) { + this.currentElement.buildCairo(cr, this.textHasCursor); + cr.stroke(); + } + + cr.$dispose(); + }, + + _onButtonPressed: function(actor, event) { + let button = event.get_button(); + let [x, y] = event.get_coords(); + let shiftPressed = event.has_shift_modifier(); + + // stop writing + if (this.currentElement && this.currentElement.shape == Shapes.TEXT) { + this._stopWriting(); + } + + // hide helper + if (this.helper.visible && button != 2) { + this.helper.hideHelp(); + return Clutter.EVENT_STOP; + } + + if (button == 1) { + this._startDrawing(x, y, false, shiftPressed); + return Clutter.EVENT_STOP; + } else if (button == 2) { + this.toggleShape(); + } else if (button == 3) { + this._startDrawing(x, y, true, shiftPressed); + return Clutter.EVENT_STOP; + } + + return Clutter.EVENT_PROPAGATE; + }, + + _onKeyPressed: function(actor, event) { + if (event.get_key_symbol() == Clutter.Escape) { + this.emitter.emit('stop-drawing'); + this.erase(); + return Clutter.EVENT_STOP; + } else if (this.currentElement && this.currentElement.shape == Shapes.TEXT) { + if (event.get_key_symbol() == Clutter.KEY_BackSpace) { + this.currentElement.text = this.currentElement.text.slice(0, -1); + this._updateCursorTimeout(); + } else if (event.has_control_modifier() && event.get_key_symbol() == 118) { + // Ctrl + V + St.Clipboard.get_default().get_text(St.ClipboardType.CLIPBOARD, (clipBoard, clipText) => { + this.currentElement.text += clipText; + this._updateCursorTimeout(); + this._redisplay(); + }); + return Clutter.EVENT_STOP; + } else if (event.get_key_symbol() == Clutter.KEY_Return || event.get_key_symbol() == 65421) { + // stop writing + // Clutter.KEY_Return is "Enter" and 65421 is KP_Enter + this._stopWriting(); + } else { + let unicode = event.get_key_unicode(); + this.currentElement.text += unicode; + this._updateCursorTimeout(); + } + this._redisplay(); + return Clutter.EVENT_STOP; + } else { + return Clutter.EVENT_PROPAGATE; + } + }, + + _onScroll: function(actor, event) { + if (this.helper.visible) + return Clutter.EVENT_PROPAGATE; + let direction = event.get_scroll_direction(); + if (direction == Clutter.ScrollDirection.UP) + this.incrementLineWidth(1); + else if (direction == Clutter.ScrollDirection.DOWN) + this.incrementLineWidth(-1); + else + return Clutter.EVENT_PROPAGATE; + return Clutter.EVENT_STOP; + }, + + _startDrawing: function(stageX, stageY, fill, eraser) { + let [success, startX, startY] = this.transform_stage_point(stageX, stageY); + + if (!success) + return; + + this.buttonReleasedHandler = this.connect('button-release-event', (actor, event) => { + this._stopDrawing(); + }); + + this.currentElement = new DrawingElement ({ + color: this.currentColor, + line: { lineWidth: this.currentLineWidth, lineJoin: this.currentLineJoin, lineCap: this.currentLineCap }, + dash: { array: this.dashedLine ? this.dashArray : [0, 0] , offset: this.dashedLine ? this.dashOffset : 0 }, + fill: fill, + eraser: eraser, + shape: this.currentShape == Shapes.TEXT ? Shapes.RECTANGLE : this.currentShape, + text: '', + font: { family: (this.currentFontFamilyId == 0 ? this.fontFamily : FontFamilyNames[this.currentFontFamilyId]), weight: this.currentFontWeight, style: this.currentFontStyle }, + points: [[startX, startY]] + }); + + this.motionHandler = this.connect('motion-event', (actor, event) => { + let coords = event.get_coords(); + let [s, x, y] = this.transform_stage_point(coords[0], coords[1]); + if (!s) + return; + this._updateDrawing(x, y); + }); + }, + + _stopDrawing: function() { + if (this.motionHandler) { + this.disconnect(this.motionHandler); + this.motionHandler = null; + } + if (this.buttonReleasedHandler) { + this.disconnect(this.buttonReleasedHandler); + this.buttonReleasedHandler = null; + } + + // start writing + if (this.currentShape == Shapes.TEXT && this.currentElement) { + this.currentElement.shape = Shapes.TEXT; + this.currentElement.text = ''; + this.emitter.emit('show-osd', _("Type your text\nand press Enter"), null); + this._updateCursorTimeout(); + this._redisplay(); + return; + } + + if (this.currentElement) { + this.elements.push(this.currentElement); + } + this.currentElement = null; + this._redisplay(); + }, + + _updateDrawing: function(x, y) { + if (!this.currentElement) + return; + if (this.currentElement.shape == Shapes.NONE) + this.currentElement.points.push([x, y]); + else + this.currentElement.points[1] = [x, y]; + this._redisplay(); + }, + + _stopWriting: function() { + this.elements.push(this.currentElement); + this.currentElement = null; + this._stopCursorTimeout(); + this._redisplay(); + }, + + _stopCursorTimeout: function() { + if (this.cursorTimeoutId) { + Mainloop.source_remove(this.cursorTimeoutId); + this.cursorTimeoutId = null; + } + this.textHasCursor = false; + }, + + _updateCursorTimeout: function() { + this._stopCursorTimeout(); + this.cursorTimeoutId = Mainloop.timeout_add(600, () => { + this.textHasCursor = !this.textHasCursor; + this._redisplay(); + return true; + }); + }, + + erase: function() { + this.elements = []; + this.undoneElements = []; + this.currentElement = null; + this._redisplay(); + }, + + deleteLastElement: function() { + if (this.currentElement) { + if (this.motionHandler) { + this.disconnect(this.motionHandler); + this.motionHandler = null; + } + if (this.buttonReleasedHandler) { + this.disconnect(this.buttonReleasedHandler); + this.buttonReleasedHandler = null; + } + this.currentElement = null; + this._stopCursorTimeout(); + } else { + this.elements.pop(); + } + this._redisplay(); + }, + + undo: function() { + if (this.elements.length > 0) + this.undoneElements.push(this.elements.pop()); + this._redisplay(); + }, + + redo: function() { + if (this.undoneElements.length > 0) + this.elements.push(this.undoneElements.pop()); + this._redisplay(); + }, + + toggleBackground: function() { + this.hasBackground = !this.hasBackground; + this.get_parent().set_background_color(this.hasBackground ? this.activeBackgroundColor : null); + }, + + toggleColor: function() { + this.selectColor((this.currentColor == this.colors[1]) ? 2 : 1); + }, + + selectColor: function(index) { + this.currentColor = this.colors[index]; + if (this.currentElement) { + this.currentElement.color = this.currentColor; + this._redisplay(); + } + this.emitter.emit('show-osd', this.currentColor.to_string(), null); + }, + + selectShape: function(shape) { + this.currentShape = shape; + this.emitter.emit('show-osd', _(ShapeNames[shape]), null); + }, + + toggleShape: function() { + this.selectShape((this.currentShape == Object.keys(Shapes).length - 1) ? 0 : this.currentShape + 1); + }, + + toggleDash: function() { + this.dashedLine = !this.dashedLine; + this.emitter.emit('show-osd', this.dashedLine ? _("Dashed line") : _("Full line"), null); + }, + + incrementLineWidth: function(increment) { + this.currentLineWidth = Math.max(this.currentLineWidth + increment, 1); + this.emitter.emit('show-osd', this.currentLineWidth + "px", this.currentLineWidth); + }, + + toggleLineJoin: function() { + this.currentLineJoin = this.currentLineJoin == 2 ? 0 : this.currentLineJoin + 1; + this.emitter.emit('show-osd', LineJoinNames[this.currentLineJoin], null); + }, + + toggleLineCap: function() { + this.currentLineCap = this.currentLineCap == 2 ? 0 : this.currentLineCap + 1; + this.emitter.emit('show-osd', LineCapNames[this.currentLineCap], null); + }, + + toggleFontWeight: function() { + this.currentFontWeight = this.currentFontWeight == 1 ? 0 : this.currentFontWeight + 1; + if (this.currentElement) { + this.currentElement.font.weight = this.currentFontWeight; + this._redisplay(); + } + this.emitter.emit('show-osd', FontWeightNames[this.currentFontWeight], null); + }, + + toggleFontStyle: function() { + this.currentFontStyle = this.currentFontStyle == 2 ? 0 : this.currentFontStyle + 1; + if (this.currentElement) { + this.currentElement.font.style = this.currentFontStyle; + this._redisplay(); + } + this.emitter.emit('show-osd', FontStyleNames[this.currentFontStyle], null); + }, + + toggleFontFamily: function() { + this.currentFontFamilyId = this.currentFontFamilyId == 5 ? 0 : this.currentFontFamilyId + 1; + let currentFontFamily = this.currentFontFamilyId == 0 ? this.fontFamily : FontFamilyNames[this.currentFontFamilyId]; + if (this.currentElement) { + this.currentElement.font.family = currentFontFamily; + this._redisplay(); + } + this.emitter.emit('show-osd',currentFontFamily , null); + }, + + toggleHelp: function() { + if (this.helper.visible) + this.helper.hideHelp(); + else + this.helper.showHelp(); + }, + + enterDrawingMode: function() { + this.keyPressedHandler = this.connect('key-press-event', this._onKeyPressed.bind(this)); + this.buttonPressedHandler = this.connect('button-press-event', this._onButtonPressed.bind(this)); + this.scrollHandler = this.connect('scroll-event', this._onScroll.bind(this)); + this.selectShape(Shapes.NONE); + this.get_parent().set_background_color(this.hasBackground ? this.activeBackgroundColor : null); + }, + + leaveDrawingMode: function() { + if (this.keyPressedHandler) { + this.disconnect(this.keyPressedHandler); + this.keyPressedHandler = null; + } + if (this.buttonPressedHandler) { + this.disconnect(this.buttonPressedHandler); + this.buttonPressedHandler = null; + } + if (this.motionHandler) { + this.disconnect(this.motionHandler); + this.motionHandler = null; + } + if (this.buttonReleasedHandler) { + this.disconnect(this.buttonReleasedHandler); + this.buttonReleasedHandler = null; + } + if (this.scrollHandler) { + this.disconnect(this.scrollHandler); + this.scrollHandler = null; + } + + if (this.helper.visible) + this.helper.hideHelp(); + + this.currentElement = null; + this._stopCursorTimeout(); + this.dashedLine = false; + this._redisplay(); + this.get_parent().set_background_color(null); + }, + + save: function() { + // stop drawing or writing + if (this.currentElement && this.currentElement.shape == Shapes.TEXT) { + this._stopWriting(); + } else if (this.currentElement && this.currentShape != Shapes.TEXT) { + this._stopDrawing(); + } + + let content = ``; + let backgroundColorString = this.hasBackground ? this.activeBackgroundColor.to_string() : 'transparent'; + if (backgroundColorString != 'transparent') { + content += `\n `; + } + for (let i = 0; i < this.elements.length; i++) { + content += this.elements[i].buildSVG(backgroundColorString); + } + content += "\n"; + + let date = GLib.DateTime.new_now_local(); + let filename = `DrawOnYourScreen ${date.format("%F")} ${date.format("%X")}.svg`; + let dir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES); + let path = GLib.build_filenamev([dir, filename]); + if (GLib.file_test(path, GLib.FileTest.EXISTS)) + return false; + let success = GLib.file_set_contents(path, content); + + if (success) { + // pass the parent (bgContainer) to Flashspot because coords of this are relative + let flashspot = new Screenshot.Flashspot(this.get_parent()); + flashspot.fire(); + global.play_theme_sound(0, 'screen-capture', "Save as SVG", null); + } + }, + + disable: function() { + if (this.theme && this.customStylesheetsChangedHandler) { + this.theme.disconnect(this.customStylesheetsChangedHandler); + this.customStylesheetsChangedHandler = null; + } + this.erase(); + } +}); + +var DrawingAreaEmitter = new Lang.Class({ + Name: 'DrawingAreaEmitter', + + _init: function() { + } +}); +Signals.addSignalMethods(DrawingAreaEmitter.prototype); + + +// DrawingElement represents a "brushstroke". +// It can be converted into a cairo path as well as a svg element. +// See DrawingArea._startDrawing() to know its params. +var DrawingElement = new Lang.Class({ + Name: 'DrawingElement', + + _init: function(params) { + for (let key in params) + this[key] = params[key]; + }, + + buildCairo: function(cr, showTextCursor) { + cr.setLineCap(this.line.lineCap); + cr.setLineJoin(this.line.lineJoin); + cr.setLineWidth(this.line.lineWidth); + + if (this.dash.array[0] > 0 && this.dash.array[1] > 0) + cr.setDash(this.dash.array, this.dash.offset); + else + cr.setDash([1000000], 0); + + if (this.eraser) + cr.setOperator(Cairo.Operator.CLEAR); + else + cr.setOperator(Cairo.Operator.OVER); + + Clutter.cairo_set_source_color(cr, this.color); + + let [points, shape] = [this.points, this.shape]; + + if (shape == Shapes.NONE || shape == Shapes.LINE) { + cr.moveTo(points[0][0], points[0][1]); + for (let j = 1; j < points.length; j++) { + cr.lineTo(points[j][0], points[j][1]); + } + } else if (shape == Shapes.ELLIPSE && points.length == 2) { + let r = Math.hypot(points[1][0] - points[0][0], points[1][1] - points[0][1]); + cr.arc(points[0][0], points[0][1], r, 0, 2 * Math.PI); + + } else if (shape == Shapes.RECTANGLE && points.length == 2) { + cr.rectangle(points[0][0], points[0][1], points[1][0] - points[0][0], points[1][1] - points[0][1]); + } else if (shape == Shapes.TEXT && points.length == 2) { + cr.selectFontFace(this.font.family, this.font.style, this.font.weight); + cr.setFontSize(Math.abs(points[1][1] - points[0][1])); + cr.moveTo(Math.min(points[0][0], points[1][0]), Math.max(points[0][1], points[1][1])); + cr.showText((showTextCursor) ? (this.text + "_") : this.text); + } + }, + + buildSVG: function(bgColor) { + let row = "\n "; + let points = this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100]); + let color = this.eraser ? bgColor : this.color.to_string(); + let attributes = `fill="${this.fill ? color : 'transparent'}" ` + + `stroke="${this.fill ? 'transparent' : color}" ` + + `stroke-width="${this.line.lineWidth}" ` + + `stroke-linecap="${LineCapNames[this.line.lineCap].toLowerCase()}" ` + + `stroke-linejoin="${LineJoinNames[this.line.lineJoin].toLowerCase()}"`; + + if (this.dash.array[0] > 0 && this.dash.array[1] > 0) + attributes += ` stroke-dasharray="${this.dash.array[0]} ${this.dash.array[1]}" stroke-dashoffset="${this.dash.offset}"`; + + if (this.shape == Shapes.NONE || this.shape == Shapes.LINE) { + row += ``; + } else if (this.shape == Shapes.ELLIPSE && points.length == 2) { + let r = Math.hypot(points[1][0] - points[0][0], points[1][1] - points[0][1]); + row += ``; + } else if (this.shape == Shapes.RECTANGLE && points.length == 2) { + row += ``; + } else if (this.shape == Shapes.TEXT && points.length == 2) { + attributes = `fill="${color}" ` + + `stroke="transparent" ` + + `font-family="${this.font.family}" ` + + `font-size="${Math.abs(points[1][1] - points[0][1])}" ` + + `font-weight="${FontWeightNames[this.font.weight].toLowerCase()}" ` + + `font-style="${FontStyleNames[this.font.style].toLowerCase()}"`; + + row += `${this.text}`; + } + + return row; + } +}); + +var HELPER_ANIMATION_TIME = 0.25; + +// DrawingHelper provides the "help osd" (Ctrl + F1) +// It uses the same texts as in prefs +var DrawingHelper = new Lang.Class({ + Name: 'DrawingHelper', + Extends: St.ScrollView, + + _init: function(params, monitor) { + this.parent(params); + this.monitor = monitor; + this.hide(); + this.vbox = new St.BoxLayout({ style_class: 'osd-window draw-on-your-screen-helper', vertical: true }); + this.add_actor(this.vbox); + this.vbox.add(new St.Label({ text: _("Global") })); + + let settings = Convenience.getSettings(); + + for (let settingKey in Prefs.GLOBAL_KEYBINDINGS) { + let hbox = new St.BoxLayout({ vertical: false }); + if (settingKey.indexOf('-separator-') != -1) { + this.vbox.add(hbox); + continue; + } + let [keyval, mods] = Gtk.accelerator_parse(settings.get_strv(settingKey)[0]); + hbox.add(new St.Label({ text: _(Prefs.GLOBAL_KEYBINDINGS[settingKey]) })); + hbox.add(new St.Label({ text: Gtk.accelerator_get_label(keyval, mods) }), { expand: true }); + this.vbox.add(hbox); + } + + this.vbox.add(new St.Label({ text: _("Internal") })); + + for (let desc in Prefs.OTHER_SHORTCUTS) { + if (desc.indexOf('-separator-') != -1) { + this.vbox.add(new St.BoxLayout({ vertical: false, style_class: 'draw-on-your-screen-separator' })); + continue; + } + let hbox = new St.BoxLayout({ vertical: false }); + hbox.add(new St.Label({ text: _(desc) })); + hbox.add(new St.Label({ text: _(Prefs.OTHER_SHORTCUTS[desc]) }), { expand: true }); + this.vbox.add(hbox); + } + + this.vbox.add(new St.BoxLayout({ vertical: false, style_class: 'draw-on-your-screen-separator' })); + + for (let settingKey in Prefs.INTERNAL_KEYBINDINGS) { + if (settingKey.indexOf('-separator-') != -1) { + this.vbox.add(new St.BoxLayout({ vertical: false, style_class: 'draw-on-your-screen-separator' })); + continue; + } + let hbox = new St.BoxLayout({ vertical: false }); + let [keyval, mods] = Gtk.accelerator_parse(settings.get_strv(settingKey)[0]); + hbox.add(new St.Label({ text: _(Prefs.INTERNAL_KEYBINDINGS[settingKey]) })); + hbox.add(new St.Label({ text: Gtk.accelerator_get_label(keyval, mods) }), { expand: true }); + this.vbox.add(hbox); + } + }, + + showHelp: function() { + this.opacity = 0; + this.show(); + + let maxHeight = this.monitor.height*(3/4); + if (this.height > maxHeight) + this.vscrollbar_policy = Gtk.PolicyType.ALWAYS; + else + this.vscrollbar_policy = Gtk.PolicyType.NEVER; + this.set_height(Math.min(this.height, maxHeight)); + this.set_position(this.monitor.x + Math.floor(this.monitor.width / 2 - this.width / 2), + this.monitor.y + Math.floor(this.monitor.height / 2 - this.height / 2)); + + Tweener.removeTweens(this); + Tweener.addTween(this, { opacity: 255, + time: HELPER_ANIMATION_TIME, + transition: 'easeOutQuad', + onComplete: null }); + }, + + hideHelp: function() { + Tweener.removeTweens(this); + Tweener.addTween(this, { opacity: 0, + time: HELPER_ANIMATION_TIME, + transition: 'easeOutQuad', + onComplete: this.hide.bind(this) }); + + }, +}); diff --git a/extension.js b/extension.js new file mode 100644 index 0000000..b95021f --- /dev/null +++ b/extension.js @@ -0,0 +1,278 @@ +/* jslint esversion: 6 */ + +/* + * Copyright 2019 Abakkk + * + * This file is part of DrowOnYourScreen, 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 Gio = imports.gi.Gio; +const Lang = imports.lang; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; +const St = imports.gi.St; +const Main = imports.ui.main; +const OsdWindow = imports.ui.osdWindow; +const Extension = imports.misc.extensionUtils.getCurrentExtension(); +const Convenience = Extension.imports.convenience; +const Draw = Extension.imports.draw; +const _ = imports.gettext.domain(Extension.metadata["gettext-domain"]).gettext; + +let manager; + +function init() { + Convenience.initTranslations(); +} + +function enable() { + manager = new AreaManager(); +} + +function disable() { + manager.disable(); + manager = null; +} + +// 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({ + Name: 'AreaManager', + + _init: function() { + this.areas = []; + this.drawingHandlers = []; + 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', + Convenience.getSettings(), + Meta.KeyBindingFlags.NONE, + Shell.ActionMode.ALL, + this.toggleDrawing.bind(this)); + + Main.wm.addKeybinding('erase-drawing', + Convenience.getSettings(), + Meta.KeyBindingFlags.NONE, + Shell.ActionMode.ALL, + this.eraseDrawing.bind(this)); + + this.updateAreas(); + this.monitorChangedHandler = Main.layoutManager.connect('monitors-changed', this.updateAreas.bind(this)); + + if (Extension.stylesheet) { + this.stylesheetMonitor = Extension.stylesheet.monitor(Gio.FileMonitorFlags.NONE, null); + this.stylesheetChangedHandler = this.stylesheetMonitor.connect('changed', (monitor, file, otherFile, eventType) => { + if ((eventType != 0 && eventType != 3) || !Extension.stylesheet.query_exists(null)) + return; + let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); + theme.unload_stylesheet(Extension.stylesheet); + theme.load_stylesheet(Extension.stylesheet); + }); + } + }, + + updateAreas: function() { + if (this.activeArea) + this.toggleDrawing(); + this.removeAreas(); + + this.monitors = Main.layoutManager.monitors; + + for (let i = 0; i < this.monitors.length; i++) { + let monitor = this.monitors[i]; + let helper = new Draw.DrawingHelper({ name: 'drawOnYourSreenHelper' + i }, monitor); + let bgContainer = new St.Bin({ name: 'drawOnYourSreenContainer' + i }); + let area = new Draw.DrawingArea({ name: 'drawOnYourSreenArea' + i }, helper); + bgContainer.set_child(area); + Main.uiGroup.add_actor(bgContainer); + Main.uiGroup.add_actor(helper); + bgContainer.set_position(monitor.x, monitor.y); + bgContainer.set_size(monitor.width, monitor.height); + area.set_position(monitor.x, monitor.y); + area.set_size(monitor.width, monitor.height); + this.drawingHandlers.push(area.emitter.connect('stop-drawing', this.toggleDrawing.bind(this))); + this.drawingHandlers.push(area.emitter.connect('show-osd', this.showOsd.bind(this))); + this.areas.push(area); + } + }, + + addInternalKeybindings: function() { + this.internalKeybindings = { + 'undo': this.activeArea.undo.bind(this.activeArea), + 'redo': this.activeArea.redo.bind(this.activeArea), + 'delete-last-element': this.activeArea.deleteLastElement.bind(this.activeArea), + 'save-as-svg': this.activeArea.save.bind(this.activeArea), + 'toggle-background': this.activeArea.toggleBackground.bind(this.activeArea), + 'increment-line-width': () => this.activeArea.incrementLineWidth(1), + 'decrement-line-width': () => this.activeArea.incrementLineWidth(-1), + 'increment-line-width-more': () => this.activeArea.incrementLineWidth(5), + 'decrement-line-width-more': () => this.activeArea.incrementLineWidth(-5), + 'toggle-linejoin': this.activeArea.toggleLineJoin.bind(this.activeArea), + 'toggle-linecap': this.activeArea.toggleLineCap.bind(this.activeArea), + 'toggle-dash' : this.activeArea.toggleDash.bind(this.activeArea), + 'select-none-shape': () => this.activeArea.selectShape(Draw.Shapes.NONE), + 'select-line-shape': () => this.activeArea.selectShape(Draw.Shapes.LINE), + 'select-ellipse-shape': () => this.activeArea.selectShape(Draw.Shapes.ELLIPSE), + 'select-rectangle-shape': () => this.activeArea.selectShape(Draw.Shapes.RECTANGLE), + 'select-text-shape': () => this.activeArea.selectShape(Draw.Shapes.TEXT), + 'toggle-font-family': this.activeArea.toggleFontFamily.bind(this.activeArea), + 'toggle-font-weight': this.activeArea.toggleFontWeight.bind(this.activeArea), + 'toggle-font-style': this.activeArea.toggleFontStyle.bind(this.activeArea), + 'toggle-panel-and-dock-visibility': this.togglePanelAndDockOpacity.bind(this), + 'toggle-help': this.activeArea.toggleHelp.bind(this.activeArea), + 'open-stylesheet': this.openStylesheetFile.bind(this) + }; + + for (let key in this.internalKeybindings) { + Main.wm.addKeybinding(key, + Convenience.getSettings(), + Meta.KeyBindingFlags.NONE, + 256, + this.internalKeybindings[key]); + } + + for (let i = 1; i < 10; i++) { + Main.wm.addKeybinding('select-color' + i, + Convenience.getSettings(), + Meta.KeyBindingFlags.NONE, + 256, + () => this.activeArea.selectColor(i)); + } + }, + + removeInternalKeybindings: function() { + for (let key in this.internalKeybindings) { + Main.wm.removeKeybinding(key); + } + + for (let i = 1; i < 10; i++) { + Main.wm.removeKeybinding('select-color' + i); + } + }, + + openStylesheetFile: function() { + if (Extension.stylesheet && Extension.stylesheet.query_exists(null)) + Gio.AppInfo.launch_default_for_uri(Extension.stylesheet.get_uri(), global.create_app_launch_context(0, -1)); + if (this.activeArea) + this.toggleDrawing(); + }, + + eraseDrawing: function() { + for (let i = 0; i < this.areas.length; i++) { + this.areas[i].erase(); + } + }, + + togglePanelAndDockOpacity: function() { + if (this.hiddenList) { + for (let i = 0; i < this.hiddenList.length; i++) { + this.hiddenList[i].actor.set_opacity(this.hiddenList[i].oldOpacity); + } + this.hiddenList = null; + } else { + let activeIndex = this.areas.indexOf(this.activeArea); + + // dash-to-dock + let dtdContainers = Main.uiGroup.get_children().filter((actor) => { + return actor.name && actor.name == 'dashtodockContainer' && + actor._delegate && + actor._delegate._monitorIndex !== undefined && + actor._delegate._monitorIndex == activeIndex; + }); + + // for simplicity, we assume that main dash-to-panel panel is displayed on primary monitor + // and we hide all secondary panels together if the active area is not on the primary + let name = activeIndex == Main.layoutManager.primaryIndex ? 'panelBox' : 'dashtopanelSecondaryPanelBox'; + let panelBoxes = Main.uiGroup.get_children().filter((actor) => { + return actor.name && actor.name == name; + }); + + + let actorToHide = dtdContainers.concat(panelBoxes); + this.hiddenList = []; + for (let i = 0; i < actorToHide.length; i++) { + this.hiddenList.push({ actor: actorToHide[i], oldOpacity: actorToHide[i].get_opacity() }); + actorToHide[i].set_opacity(0); + } + } + }, + + toggleDrawing: function() { + if (this.activeArea) { + if (this.hiddenList) + this.togglePanelAndDockOpacity(); + Main.popModal(this.activeArea); + let activeIndex = this.areas.indexOf(this.activeArea); + this.removeInternalKeybindings(); + this.activeArea.reactive = false; + this.activeArea.leaveDrawingMode(); + this.activeArea = null; + global.display.set_cursor(Meta.Cursor.DEFAULT); + Main.osdWindowManager.show(activeIndex, this.leaveGicon, _("Leaving drawing mode"), null); + } else { + // avoid to deal with Meta changes (global.display/global.screen) + let currentIndex = Main.layoutManager.monitors.indexOf(Main.layoutManager.currentMonitor); + // 256 is a custom Shell.ActionMode + if (!Main.pushModal(this.areas[currentIndex], { actionMode: 256 | 1 })) + return; + this.activeArea = this.areas[currentIndex]; + this.addInternalKeybindings(); + this.activeArea.reactive = true; + this.activeArea.enterDrawingMode(); + global.display.set_cursor(Meta.Cursor.POINTING_HAND); + // increase OSD display time + let hideTimeoutSave = OsdWindow.HIDE_TIMEOUT; + OsdWindow.HIDE_TIMEOUT = 2000; + Main.osdWindowManager.show(currentIndex, this.enterGicon, _("Press Ctrl + F1 for help") + "\n\n" + _("Entering drawing mode"), null); + OsdWindow.HIDE_TIMEOUT = hideTimeoutSave; + } + }, + + showOsd: function(emitter, label, level, maxLevel) { + let activeIndex = this.areas.indexOf(this.activeArea); + if (activeIndex != -1) + Main.osdWindowManager.show(activeIndex, this.enterGicon, label, level, maxLevel); + }, + + removeAreas: function() { + for (let i = 0; i < this.areas.length; i++) { + let area = this.areas[i]; + Main.uiGroup.remove_actor(area.get_parent()); + area.emitter.disconnect(this.drawingHandlers[i]); + area.disable(); + area.get_parent().destroy(); + } + this.areas = []; + this.drawingHandlers = []; + }, + + disable: function() { + if (this.stylesheetChangedHandler) { + this.stylesheetMonitor.disconnect(this.stylesheetChangedHandler); + this.stylesheetChangedHandler = null; + } + if (this.activeArea) + this.toggleDrawing(); + Main.wm.removeKeybinding('toggle-drawing'); + Main.wm.removeKeybinding('erase-drawing'); + this.removeAreas(); + } +}); + + diff --git a/locale/draw-on-your-screen.pot b/locale/draw-on-your-screen.pot new file mode 100644 index 0000000..401e176 --- /dev/null +++ b/locale/draw-on-your-screen.pot @@ -0,0 +1,208 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-03-04 16:40+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: extension.js +msgid "Leaving drawing mode" +msgstr "" + +msgid "Press Ctrl + F1 for help" +msgstr "" + +msgid "Entering drawing mode" +msgstr "" + +#: draw.js +msgid "Free drawing" +msgstr "" + +msgid "Line" +msgstr "" + +msgid "Circle" +msgstr "" + +msgid "Rectangle" +msgstr "" + +msgid "Text" +msgstr "" + +msgid "Dashed line" +msgstr "" + +msgid "Full line" +msgstr "" + +msgid "" +"Type your text\n" +"and press Enter" +msgstr "" + +#: prefs.js +msgid "Enter/leave drawing mode" +msgstr "" + +msgid "Erase all drawings" +msgstr "" + +msgid "Undo last brushstroke" +msgstr "" + +msgid "Redo last brushstroke" +msgstr "" + +msgid "Erase last brushstroke" +msgstr "" + +msgid "Increment line width" +msgstr "" + +msgid "Decrement line width" +msgstr "" + +msgid "Increment line width even more" +msgstr "" + +msgid "Decrement line width even more" +msgstr "" + +msgid "Change linejoin" +msgstr "" + +msgid "Change linecap" +msgstr "" + +msgid "Dashed line" +msgstr "" + +msgid "Select line" +msgstr "" + +msgid "Select circle" +msgstr "" + +msgid "Select rectangle" +msgstr "" + +msgid "Select text" +msgstr "" + +msgid "Unselect shape (free drawing)" +msgstr "" + +msgid "Change font family (generic name)" +msgstr "" + +msgid "Change font weight" +msgstr "" + +msgid "Change font style" +msgstr "" + +msgid "Hide panel and dock" +msgstr "" + +msgid "Add a drawing background" +msgstr "" + +msgid "Save drawing as a SVG file" +msgstr "" + +msgid "Open stylesheet.css" +msgstr "" + +msgid "Show help" +msgstr "" + +msgid "Draw" +msgstr "" + +msgid "Left click" +msgstr "" + +msgid "Draw by filling in" +msgstr "" + +msgid "Right click" +msgstr "" + +msgid "Toggle shape" +msgstr "" + +msgid "Center click" +msgstr "" + +msgid "Increment/decrement line width" +msgstr "" + +msgid "Scroll" +msgstr "" + +msgid "Select color" +msgstr "" + +msgid "Ctrl+1...9" +msgstr "" + +msgid "Select eraser" +msgstr "" + +msgid "Shift key held" +msgstr "" + +msgid "Leave and erase all drawings" +msgstr "" + +msgid "Escape key" +msgstr "" + +msgid "" +"Start drawing with Super+Alt+D\n" +"Then save your beautiful work by taking a screenshot" +msgstr "" + +msgid "Global" +msgstr "" + +msgid "Internal" +msgstr "" + +msgid "(in drawing mode)" +msgstr "" + +msgid "Change the style" +msgstr "" + +msgid "See stylesheet.css" +msgstr "" + +msgid "" +"Note: When you save elements made with eraser in a SVG file,\n" +"they are colored with background color, transparent if it is disabled.\n" +"(See \"Add a drawing background\" or edit the SVG file afterwards)" +msgstr "" + +msgid "" +"This program comes with ABSOLUTELY NO WARRANTY.\n" +"See the GNU General Public License, version 2 or later for details." +msgstr "" + +msgid "Credits" +msgstr "" diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..95a7f08 --- /dev/null +++ b/metadata.json @@ -0,0 +1,14 @@ +{ + "name": "Draw On You Screen", + "description": "Start drawing with Super+Alt+D and save your beautiful work by taking a screenshot", + "version": 1, + "uuid": "drawOnYourScreen@abakkk.framagit.org", + "url": "https://framagit.org/abakkk/DrawOnYourScreen", + "settings-schema": "org.gnome.shell.extensions.draw-on-your-screen", + "gettext-domain": "draw-on-your-screen", + "shell-version": [ + "3.26", + "3.28", + "3.30" + ] +} diff --git a/prefs.js b/prefs.js new file mode 100644 index 0000000..e459618 --- /dev/null +++ b/prefs.js @@ -0,0 +1,333 @@ +/* jslint esversion: 6 */ + +/* + * Copyright 2019 Abakkk + * + * This file is part of DrowOnYourScreen, 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 GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; + +const ExtensionUtils = imports.misc.extensionUtils; +const Extension = ExtensionUtils.getCurrentExtension(); +const Convenience = Extension.imports.convenience; +const Metadata = Extension.metadata; +const _ = imports.gettext.domain(Extension.metadata["gettext-domain"]).gettext; + +const MARGIN = 10; + +var GLOBAL_KEYBINDINGS = { + 'toggle-drawing': "Enter/leave drawing mode", + 'erase-drawing': "Erase all drawings" +}; + +var INTERNAL_KEYBINDINGS = { + 'undo': "Undo last brushstroke", + 'redo': "Redo last brushstroke", + 'delete-last-element' : "Erase last brushstroke", + '-separator-1': '', + 'increment-line-width': "Increment line width", + 'decrement-line-width': "Decrement line width", + 'increment-line-width-more': "Increment line width even more", + 'decrement-line-width-more': "Decrement line width even more", + 'toggle-linejoin': "Change linejoin", + 'toggle-linecap': "Change linecap", + 'toggle-dash': "Dashed line", + '-separator-2': '', + 'select-line-shape': "Select line", + 'select-ellipse-shape': "Select circle", + 'select-rectangle-shape': "Select rectangle", + 'select-text-shape': "Select text", + 'select-none-shape': "Unselect shape (free drawing)", + '-separator-3': '', + 'toggle-font-family': "Change font family (generic name)", + 'toggle-font-weight': "Change font weight", + 'toggle-font-style': "Change font style", + '-separator-4': '', + 'toggle-panel-and-dock-visibility': "Hide panel and dock", + 'toggle-background': "Add a drawing background", + '-separator-5': '', + 'save-as-svg': "Save drawing as a SVG file", + 'open-stylesheet': "Open stylesheet.css", + 'toggle-help': "Show help" +}; + +var OTHER_SHORTCUTS = { + "Draw": "Left click", + "Draw by filling in": "Right click", + "Toggle shape": "Center click", + "Increment/decrement line width": "Scroll", + "Select color": "Ctrl+1...9", + "Select eraser": "Shift key held", + "Leave and erase all drawings": "Escape key" +}; + +function init() { + Convenience.initTranslations(); +} + +function buildPrefsWidget() { + let prefsPage = new PrefsPage(); + prefsPage.show_all(); + return prefsPage; +} + +const PrefsPage = new GObject.Class({ + Name: 'PrefsPage', + GTypeName: 'PrefsPage', + Extends: Gtk.ScrolledWindow, + + _init: function(params) { + this.parent(); + + this.settings = Convenience.getSettings(); + + let box = new Gtk.Box({orientation: Gtk.Orientation.VERTICAL, margin: MARGIN*3 }); + this.add(box); + + let textBox1 = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, margin: MARGIN }); + let text1 = new Gtk.Label({ wrap: true, justify: 2, use_markup: true, + label: _("Start drawing with Super+Alt+D\nThen save your beautiful work by taking a screenshot") }); + textBox1.pack_start(text1, false, false, 0); + box.add(textBox1); + + let listBox = new Gtk.ListBox({ selection_mode: 0, hexpand: true, margin_top: 2*MARGIN, margin_bottom: 2*MARGIN }); + box.add(listBox); + + let styleContext = listBox.get_style_context(); + styleContext.add_class('background'); + + let globalTitleBox = new Gtk.Box({ margin: MARGIN }); + let globalTitleLabel = new Gtk.Label({ use_markup: true, label: "" + _("Global") + " :" }); + globalTitleLabel.set_halign(1); + globalTitleBox.pack_start(globalTitleLabel, true, true, 4); + listBox.add(globalTitleBox); + + let globalKeybindingsWidget = new KeybindingsWidget(GLOBAL_KEYBINDINGS, this.settings); + globalKeybindingsWidget.margin = MARGIN; + listBox.add(globalKeybindingsWidget); + this.addSeparator(listBox); + + let internalTitleBox = new Gtk.Box({ margin: MARGIN }); + let internalTitleLabel = new Gtk.Label({ use_markup: true, label: "" + _("Internal") + " " + _("(in drawing mode)") + " :" }); + internalTitleLabel.set_halign(1); + internalTitleBox.pack_start(internalTitleLabel, true, true, 4); + listBox.add(internalTitleBox); + + listBox.add(new Gtk.Box({ margin_top: MARGIN/2, margin_left: MARGIN, margin_right: MARGIN })); + + for (let desc in OTHER_SHORTCUTS) { + if (desc.indexOf('-separator-') != -1) { + listBox.add(new Gtk.Box({ margin_top: MARGIN, margin_left: MARGIN, margin_right: MARGIN })); + continue; + } + let otherBox = new Gtk.Box({ margin_left: MARGIN, margin_right: MARGIN }); + let otherLabel = new Gtk.Label({ label: _(desc) }); + otherLabel.set_halign(1); + let otherLabel2 = new Gtk.Label({ label: _(OTHER_SHORTCUTS[desc]) }); + otherBox.pack_start(otherLabel, true, true, 4); + otherBox.pack_start(otherLabel2, false, false, 4); + listBox.add(otherBox); + } + + listBox.add(new Gtk.Box({ margin_top: MARGIN, margin_left: MARGIN, margin_right: MARGIN })); + + let internalKeybindingsWidget = new KeybindingsWidget(INTERNAL_KEYBINDINGS, this.settings); + internalKeybindingsWidget.margin = MARGIN; + listBox.add(internalKeybindingsWidget); + + let styleBox = new Gtk.Box({ margin_top: MARGIN, margin_left: MARGIN, margin_right: MARGIN, margin_bottom:MARGIN }); + let styleLabel = new Gtk.Label({ label: _("Change the style") }); + styleLabel.set_halign(1); + let styleLabel2 = new Gtk.Label({ label: _("See stylesheet.css") }); + styleBox.pack_start(styleLabel, true, true, 4); + styleBox.pack_start(styleLabel2, false, false, 4); + listBox.add(styleBox); + + let noteBox = new Gtk.Box({ margin_top: MARGIN, margin_left: MARGIN, margin_right: MARGIN, margin_bottom:MARGIN }); + let noteLabel = new Gtk.Label({ + use_markup: true, + label: _("Note: When you save elements made with eraser in a SVG file,\nthey are colored with background color, transparent if it is disabled.\n(See \"Add a drawing background\" or edit the SVG file afterwards)") + }); + noteLabel.set_halign(1); + //let noteLabel2 = new Gtk.Label({ label: _("See notesheet.css") }); + noteBox.pack_start(noteLabel, true, true, 4); + //noteBox.pack_start(noteLabel2, false, false, 4); + listBox.add(noteBox); + + this.addSeparator(listBox); + + let licence = _("This program comes with ABSOLUTELY NO WARRANTY.\nSee the GNU General Public License, version 2 or later for details."); + + let textBox2 = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL }); + let text2 = new Gtk.Label({ wrap: true, justify: 2, use_markup: true, + label: "Version" + " " + Metadata.version +"\n\n" + "" + Metadata.url + "" + "\n\n" + licence + "\n" }); + textBox2.pack_start(text2, false, false, 0); + + let creditBox = new Gtk.Box({ orientation: Gtk.Orientation.HORIZONTAL }); + 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: "" + _("Credits") + ":" }); + let rightLabel = new Gtk.Label({ wrap: true, valign: 1, halign: 1, justify: 0, use_markup: true, label: "Abakkk" }); + leftBox.pack_start(leftLabel, true, true, 0); + rightBox.pack_start(rightLabel, true, true, 0); + creditBox.pack_start(leftBox, true, true, 5); + creditBox.pack_start(rightBox, true, true, 5); + textBox2.pack_start(creditBox, false, false, 0); + + box.add(textBox2); + + let children = listBox.get_children(); + for (let i = 0; i < children.length; i++) { + if (children[i].activatable) + children[i].set_activatable(false); + } + }, + + addSeparator: function(container) { + let separatorRow = new Gtk.ListBoxRow({sensitive: false}); + separatorRow.add(new Gtk.Separator({ margin: MARGIN })); + container.add(separatorRow); + } +}); + +// this code comes from Sticky Notes View by Sam Bull, https://extensions.gnome.org/extension/568/notes/ +const KeybindingsWidget = new GObject.Class({ + Name: 'Keybindings.Widget', + GTypeName: 'KeybindingsWidget', + Extends: Gtk.Box, + + _init: function(keybindings, settings) { + this.parent(); + this.set_orientation(Gtk.Orientation.VERTICAL); + + this._keybindings = keybindings; + this._settings = settings; + + this._columns = { + NAME: 0, + ACCEL_NAME: 1, + MODS: 2, + KEY: 3 + }; + + this._store = new Gtk.ListStore(); + this._store.set_column_types([ + GObject.TYPE_STRING, + GObject.TYPE_STRING, + GObject.TYPE_INT, + GObject.TYPE_INT + ]); + + this._tree_view = new Gtk.TreeView({ + model: this._store, + hexpand: false, + vexpand: false + }); + this._tree_view.set_activate_on_single_click(false); + this._tree_view.get_selection().set_mode(Gtk.SelectionMode.SINGLE); + + let action_renderer = new Gtk.CellRendererText(); + let action_column = new Gtk.TreeViewColumn({ + title: "", + expand: true, + }); + action_column.pack_start(action_renderer, true); + action_column.add_attribute(action_renderer, 'text', 1); + this._tree_view.append_column(action_column); + + let keybinding_renderer = new Gtk.CellRendererAccel({ + editable: true, + accel_mode: Gtk.CellRendererAccelMode.GTK, + xalign: 1 + }); + keybinding_renderer.connect('accel-edited', + Lang.bind(this, function(renderer, iter, key, mods) { + let value = Gtk.accelerator_name(key, mods); + let [success, iterator ] = + this._store.get_iter_from_string(iter); + + if(!success) { + printerr("Can't change keybinding"); + } + + let name = this._store.get_value(iterator, 0); + + this._store.set( + iterator, + [this._columns.MODS, this._columns.KEY], + [mods, key] + ); + this._settings.set_strv(name, [value]); + }) + ); + + let keybinding_column = new Gtk.TreeViewColumn({ + title: "", + }); + keybinding_column.pack_end(keybinding_renderer, false); + keybinding_column.add_attribute( + keybinding_renderer, + 'accel-mods', + this._columns.MODS + ); + keybinding_column.add_attribute( + keybinding_renderer, + 'accel-key', + this._columns.KEY + ); + this._tree_view.append_column(keybinding_column); + this._tree_view.columns_autosize(); + this._tree_view.set_headers_visible(false); + + this.add(this._tree_view); + this.keybinding_column = keybinding_column; + this.action_column = action_column; + + this._refresh(); + }, + + _refresh: function() { + this._store.clear(); + + for(let settings_key in this._keybindings) { + if (settings_key.indexOf('-separator-') != -1) + continue; + let [key, mods] = Gtk.accelerator_parse( + this._settings.get_strv(settings_key)[0] + ); + + let iter = this._store.append(); + this._store.set(iter, + [ + this._columns.NAME, + this._columns.ACCEL_NAME, + this._columns.MODS, + this._columns.KEY + ], + [ + settings_key, + _(this._keybindings[settings_key]), + mods, + key + ] + ); + } + } +}); diff --git a/schemas/gschemas.compiled b/schemas/gschemas.compiled new file mode 100644 index 0000000..0d32dc8 Binary files /dev/null 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 new file mode 100644 index 0000000..c018f29 --- /dev/null +++ b/schemas/org.gnome.shell.extensions.draw-on-your-screen.gschema.xml @@ -0,0 +1,175 @@ + + + + + ["<Alt><Super>d"] + toggle drawing + enter or leave drawing mode + + + ["<Alt><Super>e"] + erase drawing + erase drawing + + + ["<Primary>z"] + undo + undo + + + ["<Primary><Shift>z"] + redo + redo + + + ["Delete"] + delete last element + delete last element + + + ["<Primary>b"] + toggle background + toggle background + + + ["<Primary>h"] + hide or show panel and dock + hide or show panel and dock + + + ["<Primary>e"] + select cercle + select a cercle + + + ["<Primary>r"] + select rectangle + select rectangle + + + ["<Primary>l"] + select line + select a line + + + ["<Primary>t"] + select text + select text + + + ["<Primary>u"] + unselect shape (free drawing) + unselect shape (free drawing) + + + ["<Primary>KP_Add"] + increment the line width + increment the line width + + + ["<Primary>KP_Subtract"] + decrement the line width + decrement the line width + + + ["<Primary>Page_Up"] + increment the line width even more + increment the line width even more + + + ["<Primary>Page_Down"] + decrement the line width even more + decrement the line width even more + + + ["<Primary>j"] + toggle linejoin + toggle linejoin + + + ["<Primary>k"] + toggle linecap + toggle linecap + + + ["<Primary>a"] + toggle dash + toggle dash + + + 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"] + toggle font family + toggle font family + + + ["<Primary>w"] + toggle font weight + toggle font weight + + + ["<Primary>i"] + toggle font style + toggle font style + + + ["<Primary>o"] + open stylesheet + open stylesheet + + + ["<Primary>s"] + Save drawing as a svg file + Save drawing as a svg file + + + ["<Primary>F1"] + toggle help + toggle help + + + diff --git a/stylesheet.css b/stylesheet.css new file mode 100644 index 0000000..15bb5af --- /dev/null +++ b/stylesheet.css @@ -0,0 +1,73 @@ +/* + * Except for the font, + * you don't need to restart the extension. + * Just save this file and the changes will be applied for your next brushstroke. + * + * line-join (no string): + * 0 : miter, 1 : round, 2 : bevel + * line-cap (no string): + * 0 : butt, 1 : round, 2 : square + * + * dash: + * dash-array-on is the length of dashes (no dashes if 0, you can put 0.1 to get dots or square according to line-cap) + * dash-array-off is the length of gaps (no dashes if 0) + * + * 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 + * weight <= 500 (or lighter, normal, medium) is rendered as normal + * weight > 500 (or bolder, bold) is rendered as bold + * oblique and italic style support depends on the font family and seem to be rendered identically + * + */ + +.draw-on-your-screen { + -drawing-line-width: 5px; + -drawing-line-join: 1; + -drawing-line-cap: 1; + -drawing-dash-array-on: 5px; + -drawing-dash-array-off: 15px; + -drawing-dash-offset: 0px; + -drawing-color1: HotPink; + -drawing-color2: Cyan; + -drawing-color3: yellow; + -drawing-color4: Orangered; + -drawing-color5: Chartreuse; + -drawing-color6: DarkViolet; + -drawing-color7: #ffffff; + -drawing-color8: rgba(130, 130, 130, 0.3); + -drawing-color9: rgb(0, 0, 0); + -drawing-background-color: #2e3436; /* GS osd_bg_color: #2e3436, GTK Adwaita-dark theme_base_color: #2d2c2e */ + font-family: Cantarell; + font-weight: normal; + font-style: normal; +} + +/*********************************************/ + +/* + * The following styles don't affect the drawing, + * but the "Ctrl + F1" on-screen-display + * + */ + + .draw-on-your-screen-helper { + margin: 0; + spacing: 0.5em; + } + + .draw-on-your-screen-helper StBoxLayout { + spacing: 5em; + } + + .draw-on-your-screen-helper StBoxLayout StLabel { + text-align: right; + font-weight: normal; + } + + .draw-on-your-screen-separator { + min-height: 0.6em; + } + +