/* jslint esversion: 6 */
/*
* Copyright 2019 Abakkk
*
* This file is part of DrawOnYourScreen, a drawing extension for GNOME Shell.
* https://framagit.org/abakkk/DrawOnYourScreen
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
const ByteArray = imports.byteArray;
const Cairo = imports.cairo;
const Clutter = imports.gi.Clutter;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;
const PangoMatrix = imports.gi.Pango.Matrix;
const Shell = imports.gi.Shell;
const St = imports.gi.St;
const BoxPointer = imports.ui.boxpointer;
const Config = imports.misc.config;
const Main = imports.ui.main;
const PopupMenu = imports.ui.popupMenu;
const Slider = imports.ui.slider;
const Screenshot = imports.ui.screenshot;
const Tweener = imports.ui.tweener;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Convenience = ExtensionUtils.getSettings ? ExtensionUtils : Me.imports.convenience;
const Extension = Me.imports.extension;
const Prefs = Me.imports.prefs;
const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext;
const GS_VERSION = Config.PACKAGE_VERSION;
const CAIRO_DEBUG_EXTENDS = false;
const SVG_DEBUG_EXTENDS = false;
const SVG_DEBUG_SUPERPOSES_CAIRO = false;
const TEXT_CURSOR_TIME = 600; // ms
const ICON_DIR = Me.dir.get_child('data').get_child('icons');
const FILL_ICON_PATH = ICON_DIR.get_child('fill-symbolic.svg').get_path();
const STROKE_ICON_PATH = ICON_DIR.get_child('stroke-symbolic.svg').get_path();
const LINEJOIN_ICON_PATH = ICON_DIR.get_child('linejoin-symbolic.svg').get_path();
const LINECAP_ICON_PATH = ICON_DIR.get_child('linecap-symbolic.svg').get_path();
const FILLRULE_NONZERO_ICON_PATH = ICON_DIR.get_child('fillrule-nonzero-symbolic.svg').get_path();
const FILLRULE_EVENODD_ICON_PATH = ICON_DIR.get_child('fillrule-evenodd-symbolic.svg').get_path();
const DASHED_LINE_ICON_PATH = ICON_DIR.get_child('dashed-line-symbolic.svg').get_path();
const FULL_LINE_ICON_PATH = ICON_DIR.get_child('full-line-symbolic.svg').get_path();
const Shapes = { NONE: 0, LINE: 1, ELLIPSE: 2, RECTANGLE: 3, TEXT: 4, POLYGON: 5, POLYLINE: 6 };
const Manipulations = { MOVE: 100, RESIZE: 101, MIRROR: 102 };
var Tools = Object.assign({}, Shapes, Manipulations);
const TextStates = { WRITTEN: 0, DRAWING: 1, WRITING: 2 };
const Transformations = { TRANSLATION: 0, ROTATION: 1, SCALE_PRESERVE: 2, STRETCH: 3, REFLECTION: 4, INVERSION: 5 };
const ToolNames = { 0: "Free drawing", 1: "Line", 2: "Ellipse", 3: "Rectangle", 4: "Text", 5: "Polygon", 6: "Polyline", 100: "Move", 101: "Resize", 102: "Mirror" };
const LineCapNames = { 0: 'Butt', 1: 'Round', 2: 'Square' };
const LineJoinNames = { 0: 'Miter', 1: 'Round', 2: 'Bevel' };
const FillRuleNames = { 0: 'Nonzero', 1: 'Evenodd' };
const FontWeightNames = { 0: 'Normal', 1: 'Bold' };
const FontStyleNames = { 0: 'Normal', 1: 'Italic', 2: 'Oblique' };
const FontFamilyNames = { 0: 'Default', 1: 'Sans-Serif', 2: 'Serif', 3: 'Monospace', 4: 'Cursive', 5: 'Fantasy' };
const getDateString = function() {
let date = GLib.DateTime.new_now_local();
return `${date.format("%F")} ${date.format("%X")}`;
};
const getJsonFiles = function() {
let directory = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir']]));
let enumerator;
try {
enumerator = directory.enumerate_children('standard::name,standard::display-name,standard::content-type,time::modified', Gio.FileQueryInfoFlags.NONE, null);
} catch(e) {
return [];
}
let jsonFiles = [];
let fileInfo = enumerator.next_file(null);
while (fileInfo) {
if (fileInfo.get_content_type().indexOf('json') != -1 && fileInfo.get_name() != `${Me.metadata['persistent-file-name']}.json`) {
let file = enumerator.get_child(fileInfo);
jsonFiles.push({ name: fileInfo.get_name().slice(0, -5),
displayName: fileInfo.get_display_name().slice(0, -5),
// fileInfo.get_modification_date_time: Gio 2.62+
modificationUnixTime: fileInfo.get_attribute_uint64('time::modified'),
delete: () => file.delete(null) });
}
fileInfo = enumerator.next_file(null);
}
enumerator.close(null);
jsonFiles.sort((a, b) => {
return b.modificationUnixTime - a.modificationUnixTime;
});
return jsonFiles;
};
// DrawingArea is the widget in which we draw, thanks to Cairo.
// It creates and manages a DrawingElement for each "brushstroke".
// It handles pointer/mouse/(touch?) events and some keyboard events.
var DrawingArea = new Lang.Class({
Name: 'DrawOnYourScreenDrawingArea',
Extends: St.DrawingArea,
Signals: { 'show-osd': { param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_DOUBLE] },
'stop-drawing': {} },
_init: function(params, monitor, helper, loadPersistent) {
this.parent({ style_class: 'draw-on-your-screen', name: params.name});
this.settings = Convenience.getSettings();
this.monitor = monitor;
this.helper = helper;
this.elements = [];
this.undoneElements = [];
this.currentElement = null;
this.currentTool = Shapes.NONE;
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)];
if (loadPersistent)
this._loadPersistent();
},
get menu() {
if (!this._menu)
this._menu = new DrawingMenu(this, this.monitor);
return this._menu;
},
get currentTool() {
return this._currentTool;
},
set currentTool(tool) {
this._currentTool = tool;
if (Object.values(Manipulations).indexOf(tool) != -1)
this._startElementGrabber();
else
this._stopElementGrabber();
},
_redisplay: function() {
// force area to emit 'repaint'
this.queue_repaint();
},
_updateStyle: function() {
try {
let themeNode = this.get_theme_node();
for (let i = 1; i < 10; i++) {
this.colors[i] = themeNode.get_color('-drawing-color' + i);
}
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.currentFillRule = 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');
let font = themeNode.get_font();
this.fontFamily = font.get_family();
this.currentFontWeight = font.get_weight();
this.currentFontStyle = font.get_style();
this.gridGap = themeNode.get_length('-grid-overlay-gap') || 10;
this.gridLineWidth = themeNode.get_length('-grid-overlay-line-width') || 0.4;
this.gridInterlineWidth = themeNode.get_length('-grid-overlay-interline-width') || 0.2;
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');
} 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.currentFillRule = ([0, 1].indexOf(this.currentFillRule) != -1) ? this.currentFillRule : Cairo.FillRule.WINDING;
this.currentFontFamilyId = 0;
this.currentFontWeight = this.currentFontWeight > 500 ? 1 : 0 ;
// font style enum order of Cairo and Pango are different
this.currentFontStyle = this.currentFontStyle == 2 ? 1 : ( this.currentFontStyle == 1 ? 2 : 0);
this.gridGap = this.gridGap && this.gridGap >= 1 ? this.gridGap : 10;
this.gridLineWidth = this.gridLineWidth || 0.4;
this.gridInterlineWidth = this.gridInterlineWidth || 0.2;
this.gridColor = this.gridColor && this.gridColor.alpha ? this.gridColor : Clutter.Color.new(127, 127, 127, 255);
},
vfunc_repaint: function() {
let cr = this.get_context();
if (CAIRO_DEBUG_EXTENDS) {
cr.scale(0.5, 0.5);
cr.translate(this.monitor.width, this.monitor.height);
}
for (let i = 0; i < this.elements.length; i++) {
cr.save();
this.elements[i].buildCairo(cr, { showTextRectangle: this.grabbedElement && this.grabbedElement == this.elements[i],
drawTextRectangle: this.grabPoint ? true : false });
if (this.grabPoint)
this._searchElementToGrab(cr, this.elements[i]);
if (this.elements[i].fill && !this.elements[i].isStraightLine) {
cr.fillPreserve();
if (this.elements[i].shape == Shapes.NONE || this.elements[i].shape == Shapes.LINE)
cr.closePath();
}
cr.stroke();
cr.restore();
}
if (this.currentElement) {
cr.save();
this.currentElement.buildCairo(cr, { showTextCursor: this.textHasCursor,
showTextRectangle: this.currentElement.shape == Shapes.TEXT && this.currentElement.textState == TextStates.DRAWING,
dummyStroke: this.currentElement.fill && this.currentElement.line.lineWidth == 0 });
cr.stroke();
cr.restore();
}
if (this.isInDrawingMode && this.hasGrid && this.gridGap && this.gridGap >= 1) {
cr.save();
Clutter.cairo_set_source_color(cr, this.gridColor);
let [gridX, gridY] = [this.gridGap, this.gridGap];
while (gridX < this.monitor.width) {
cr.setLineWidth((gridX / this.gridGap) % 5 ? this.gridInterlineWidth : this.gridLineWidth);
cr.moveTo(gridX, 0);
cr.lineTo(gridX, this.monitor.height);
gridX += this.gridGap;
cr.stroke();
}
while (gridY < this.monitor.height) {
cr.setLineWidth((gridY / this.gridGap) % 5 ? this.gridInterlineWidth : this.gridLineWidth);
cr.moveTo(0, gridY);
cr.lineTo(this.monitor.width, gridY);
gridY += this.gridGap;
cr.stroke();
}
cr.restore();
}
cr.$dispose();
},
_onButtonPressed: function(actor, event) {
if (this.spaceKeyPressed)
return Clutter.EVENT_PROPAGATE;
let button = event.get_button();
let [x, y] = event.get_coords();
let controlPressed = event.has_control_modifier();
let shiftPressed = event.has_shift_modifier();
if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.currentElement.textState == TextStates.WRITING) {
// finish writing
this._stopWriting();
}
if (this.helper.visible) {
// hide helper
this.helper.hideHelp();
return Clutter.EVENT_STOP;
}
if (button == 1) {
if (Object.values(Manipulations).indexOf(this.currentTool) != -1) {
if (this.grabbedElement)
this._startTransforming(x, y, controlPressed, shiftPressed);
} else {
this._startDrawing(x, y, shiftPressed);
}
return Clutter.EVENT_STOP;
} else if (button == 2) {
this.toggleFill();
} else if (button == 3) {
this._stopDrawing();
this.menu.open(x, y);
return Clutter.EVENT_STOP;
}
return Clutter.EVENT_PROPAGATE;
},
_onKeyboardPopupMenu: function() {
this._stopDrawing();
if (this.helper.visible)
this.helper.hideHelp();
this.menu.popup();
return Clutter.EVENT_STOP;
},
_onStageKeyPressed: function(actor, event) {
if (event.get_key_symbol() == Clutter.KEY_space)
this.spaceKeyPressed = true;
return Clutter.EVENT_PROPAGATE;
},
_onStageKeyReleased: function(actor, event) {
if (event.get_key_symbol() == Clutter.KEY_space)
this.spaceKeyPressed = false;
return Clutter.EVENT_PROPAGATE;
},
_onKeyPressed: function(actor, event) {
if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.currentElement.textState == TextStates.WRITING) {
if (event.get_key_symbol() == Clutter.KEY_Escape) {
// finish writing
this._stopWriting();
} else if (event.get_key_symbol() == Clutter.KEY_BackSpace) {
this.currentElement.text = this.currentElement.text.slice(0, -1);
this._updateTextCursorTimeout();
} else if (event.has_control_modifier() && event.get_key_symbol() == 118) {
// Ctrl + V
St.Clipboard.get_default().get_text(St.ClipboardType.CLIPBOARD, (clipBoard, clipText) => {
this.currentElement.text += clipText;
this._updateTextCursorTimeout();
this._redisplay();
});
} else if (event.get_key_symbol() == Clutter.KEY_Return || event.get_key_symbol() == 65421) {
// Clutter.KEY_Return is "Enter" and 65421 is KP_Enter
// start a new line
let startNewLine = true;
this._stopWriting(startNewLine);
} else if (event.has_control_modifier()){
// it is a shortcut, do not write text
return Clutter.EVENT_PROPAGATE;
} else {
let unicode = event.get_key_unicode();
this.currentElement.text += unicode;
this._updateTextCursorTimeout();
}
this._redisplay();
return Clutter.EVENT_STOP;
} else if (this.currentElement && this.currentElement.shape == Shapes.LINE) {
if (event.get_key_symbol() == Clutter.KEY_Return || event.get_key_symbol() == 65421 || event.get_key_symbol() == 65507) {
// 65507 is 'Ctrl' key alone
if (this.currentElement.points.length == 2)
this.emit('show-osd', null, _("Press %s to get a fourth control point")
.format(Gtk.accelerator_get_label(Clutter.KEY_Return, 0)), "", -1);
this.currentElement.addPoint();
this.updatePointerCursor(true);
this._redisplay();
return Clutter.EVENT_STOP;
} else {
return Clutter.EVENT_PROPAGATE;
}
} else if (this.currentElement &&
(this.currentElement.shape == Shapes.POLYGON || this.currentElement.shape == Shapes.POLYLINE) &&
(event.get_key_symbol() == Clutter.KEY_Return || event.get_key_symbol() == 65421)) {
this.currentElement.addPoint();
return Clutter.EVENT_STOP;
} else if (event.get_key_symbol() == Clutter.KEY_Escape) {
if (this.helper.visible)
this.helper.hideHelp();
else
this.emit('stop-drawing');
return Clutter.EVENT_STOP;
} else {
return Clutter.EVENT_PROPAGATE;
}
},
_onScroll: function(actor, event) {
if (this.helper.visible)
return Clutter.EVENT_PROPAGATE;
let direction = event.get_scroll_direction();
if (direction == Clutter.ScrollDirection.UP)
this.incrementLineWidth(1);
else if (direction == Clutter.ScrollDirection.DOWN)
this.incrementLineWidth(-1);
else
return Clutter.EVENT_PROPAGATE;
return Clutter.EVENT_STOP;
},
_searchElementToGrab: function(cr, element) {
if (element.getContainsPoint(cr, this.grabPoint[0], this.grabPoint[1]))
this.grabbedElement = element;
else if (this.grabbedElement == element)
this.grabbedElement = null;
if (element == this.elements[this.elements.length - 1])
// All elements have been tested, the winner is the last.
this.updatePointerCursor();
},
_startElementGrabber: function() {
this.elementGrabberHandler = this.connect('motion-event', (actor, event) => {
if (this.motionHandler || this.grabbedElementLocked) {
this.grabPoint = null;
return;
}
// Reduce computing without notable effect.
if (Math.random() <= 0.75)
return;
let coords = event.get_coords();
let [s, x, y] = this.transform_stage_point(coords[0], coords[1]);
if (!s)
return;
this.grabPoint = [x, y];
this.grabbedElement = null;
// this._redisplay calls this._searchElementToGrab.
this._redisplay();
});
},
_stopElementGrabber: function() {
if (this.elementGrabberHandler) {
this.disconnect(this.elementGrabberHandler);
this.grabPoint = null;
this.elementGrabberHandler = null;
}
},
_startTransforming: function(stageX, stageY, controlPressed, duplicate) {
let [success, startX, startY] = this.transform_stage_point(stageX, stageY);
if (!success)
return;
if (this.currentTool == Manipulations.MIRROR) {
this.grabbedElementLocked = !this.grabbedElementLocked;
if (this.grabbedElementLocked) {
this.updatePointerCursor();
let label = controlPressed ? _("Mark a point of symmetry") : _("Draw a line of symmetry");
this.emit('show-osd', null, label, "", -1);
return;
}
}
this.grabPoint = null;
this.buttonReleasedHandler = this.connect('button-release-event', (actor, event) => {
this._stopTransforming();
});
if (duplicate) {
// deep cloning
let copy = new DrawingElement(JSON.parse(JSON.stringify(this.grabbedElement)));
this.elements.push(copy);
this.grabbedElement = copy;
}
if (this.currentTool == Manipulations.MOVE)
this.grabbedElement.startTransformation(startX, startY, controlPressed ? Transformations.ROTATION : Transformations.TRANSLATION);
else if (this.currentTool == Manipulations.RESIZE)
this.grabbedElement.startTransformation(startX, startY, controlPressed ? Transformations.STRETCH : Transformations.SCALE_PRESERVE);
else if (this.currentTool == Manipulations.MIRROR) {
this.grabbedElement.startTransformation(startX, startY, controlPressed ? Transformations.INVERSION : Transformations.REFLECTION);
this._redisplay();
}
this.motionHandler = this.connect('motion-event', (actor, event) => {
if (this.spaceKeyPressed)
return;
let coords = event.get_coords();
let [s, x, y] = this.transform_stage_point(coords[0], coords[1]);
if (!s)
return;
let controlPressed = event.has_control_modifier();
this._updateTransforming(x, y, controlPressed);
});
},
_updateTransforming: function(x, y, controlPressed) {
if (controlPressed && this.grabbedElement.lastTransformation.type == Transformations.TRANSLATION) {
this.grabbedElement.stopTransformation();
this.grabbedElement.startTransformation(x, y, Transformations.ROTATION);
} else if (!controlPressed && this.grabbedElement.lastTransformation.type == Transformations.ROTATION) {
this.grabbedElement.stopTransformation();
this.grabbedElement.startTransformation(x, y, Transformations.TRANSLATION);
}
if (controlPressed && this.grabbedElement.lastTransformation.type == Transformations.SCALE_PRESERVE) {
this.grabbedElement.stopTransformation();
this.grabbedElement.startTransformation(x, y, Transformations.STRETCH);
} else if (!controlPressed && this.grabbedElement.lastTransformation.type == Transformations.STRETCH) {
this.grabbedElement.stopTransformation();
this.grabbedElement.startTransformation(x, y, Transformations.SCALE_PRESERVE);
}
if (controlPressed && this.grabbedElement.lastTransformation.type == Transformations.REFLECTION) {
this.grabbedElement.transformations.pop();
this.grabbedElement.startTransformation(x, y, Transformations.INVERSION);
} else if (!controlPressed && this.grabbedElement.lastTransformation.type == Transformations.INVERSION) {
this.grabbedElement.transformations.pop();
this.grabbedElement.startTransformation(x, y, Transformations.REFLECTION);
}
this.grabbedElement.updateTransformation(x, y);
this._redisplay();
},
_stopTransforming: function() {
if (this.motionHandler) {
this.disconnect(this.motionHandler);
this.motionHandler = null;
}
if (this.buttonReleasedHandler) {
this.disconnect(this.buttonReleasedHandler);
this.buttonReleasedHandler = null;
}
this.grabbedElement.stopTransformation();
this.grabbedElement = null;
this.grabbedElementLocked = false;
this._redisplay();
},
_startDrawing: function(stageX, stageY, eraser) {
let [success, startX, startY] = this.transform_stage_point(stageX, stageY);
if (!success)
return;
this.buttonReleasedHandler = this.connect('button-release-event', (actor, event) => {
this._stopDrawing();
});
this.currentElement = new DrawingElement ({
shape: this.currentTool,
color: this.currentColor.to_string(),
line: { lineWidth: this.currentLineWidth, lineJoin: this.currentLineJoin, lineCap: this.currentLineCap },
dash: { active: this.dashedLine, array: this.dashedLine ? [this.dashArray[0] || this.currentLineWidth, this.dashArray[1] || this.currentLineWidth * 3] : [0, 0] , offset: this.dashOffset },
fill: this.fill,
fillRule: this.currentFillRule,
eraser: eraser,
transform: { active: false, center: [0, 0], angle: 0, startAngle: 0, ratio: 1 },
text: '',
font: { family: (this.currentFontFamilyId == 0 ? this.fontFamily : FontFamilyNames[this.currentFontFamilyId]), weight: this.currentFontWeight, style: this.currentFontStyle },
points: []
});
if (this.currentTool == Shapes.TEXT) {
this.currentElement.fill = false;
this.currentElement.text = _("Text");
this.currentElement.textState = TextStates.DRAWING;
}
this.currentElement.startDrawing(startX, startY);
if (this.currentTool == Shapes.POLYGON || this.currentTool == Shapes.POLYLINE)
this.emit('show-osd', null, _("Press %s to mark vertices").format(Gtk.accelerator_get_label(Clutter.KEY_Return, 0)), "", -1);
this.motionHandler = this.connect('motion-event', (actor, event) => {
if (this.spaceKeyPressed)
return;
let coords = event.get_coords();
let [s, x, y] = this.transform_stage_point(coords[0], coords[1]);
if (!s)
return;
let controlPressed = event.has_control_modifier();
this._updateDrawing(x, y, controlPressed);
});
},
_updateDrawing: function(x, y, controlPressed) {
if (!this.currentElement)
return;
this.currentElement.updateDrawing(x, y, controlPressed);
this._redisplay();
this.updatePointerCursor(controlPressed);
},
_stopDrawing: function() {
if (this.motionHandler) {
this.disconnect(this.motionHandler);
this.motionHandler = null;
}
if (this.buttonReleasedHandler) {
this.disconnect(this.buttonReleasedHandler);
this.buttonReleasedHandler = null;
}
// skip when a polygon has not at least 3 points
if (this.currentElement && this.currentElement.shape == Shapes.POLYGON && this.currentElement.points.length < 3)
this.currentElement = null;
if (this.currentElement)
this.currentElement.stopDrawing();
if (this.currentElement && this.currentElement.points.length >= 2) {
if (this.currentElement.shape == Shapes.TEXT && this.currentElement.textState == TextStates.DRAWING) {
// start writing
this.currentElement.textState = TextStates.WRITING;
this.currentElement.text = '';
this.emit('show-osd', null, _("Type your text and press %s").format(Gtk.accelerator_get_label(Clutter.KEY_Escape, 0)), "", -1);
this._updateTextCursorTimeout();
this.textHasCursor = true;
this._redisplay();
this.updatePointerCursor();
return;
}
this.elements.push(this.currentElement);
}
this.currentElement = null;
this._redisplay();
this.updatePointerCursor();
},
_stopWriting: function(startNewLine) {
this.currentElement.textState = TextStates.WRITTEN;
if (this.currentElement.text.length > 0)
this.elements.push(this.currentElement);
if (startNewLine && this.currentElement.points.length == 2) {
this.currentElement.lineIndex = this.currentElement.lineIndex || 0;
// copy object, the original keep existing in this.elements
this.currentElement = Object.create(this.currentElement);
this.currentElement.textState = TextStates.WRITING;
this.currentElement.lineIndex ++;
let height = Math.abs(this.currentElement.points[1][1] - this.currentElement.points[0][1]);
// define a new 'points' array, the original keep existing in this.elements
this.currentElement.points = [
[this.currentElement.points[0][0], this.currentElement.points[0][1] + height],
[this.currentElement.points[1][0], this.currentElement.points[1][1] + height]
];
this.currentElement.text = "";
} else {
this.currentElement = null;
this._stopTextCursorTimeout();
}
this._redisplay();
},
setPointerCursor: function(pointerCursorName) {
if (!this.currentPointerCursorName || this.currentPointerCursorName != pointerCursorName) {
this.currentPointerCursorName = pointerCursorName;
Extension.setCursor(pointerCursorName);
}
},
updatePointerCursor: function(controlPressed) {
if (this.currentTool == Manipulations.MIRROR && this.grabbedElementLocked)
this.setPointerCursor('CROSSHAIR');
else if (Object.values(Manipulations).indexOf(this.currentTool) != -1)
this.setPointerCursor(this.grabbedElement ? 'MOVE_OR_RESIZE_WINDOW' : 'DEFAULT');
else if (!this.currentElement || (this.currentElement.shape == Shapes.TEXT && this.currentElement.textState == TextStates.WRITING))
this.setPointerCursor(this.currentTool == Shapes.NONE ? 'POINTING_HAND' : 'CROSSHAIR');
else if (this.currentElement.shape != Shapes.NONE && controlPressed)
this.setPointerCursor('MOVE_OR_RESIZE_WINDOW');
},
initPointerCursor: function() {
this.currentPointerCursorName = null;
this.updatePointerCursor();
},
_stopTextCursorTimeout: function() {
if (this.textCursorTimeoutId) {
GLib.source_remove(this.textCursorTimeoutId);
this.textCursorTimeoutId = null;
}
this.textHasCursor = false;
},
_updateTextCursorTimeout: function() {
this._stopTextCursorTimeout();
this.textCursorTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, TEXT_CURSOR_TIME, () => {
this.textHasCursor = !this.textHasCursor;
this._redisplay();
return GLib.SOURCE_CONTINUE;
});
},
erase: function() {
this.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._stopTextCursorTimeout();
} else {
this.elements.pop();
}
this._redisplay();
},
undo: function() {
if (this.elements.length > 0)
this.undoneElements.push(this.elements.pop());
this._redisplay();
},
redo: function() {
if (this.undoneElements.length > 0)
this.elements.push(this.undoneElements.pop());
this._redisplay();
},
smoothLastElement: function() {
if (this.elements.length > 0 && this.elements[this.elements.length - 1].shape == Shapes.NONE) {
this.elements[this.elements.length - 1].smoothAll();
this._redisplay();
}
},
toggleBackground: function() {
this.hasBackground = !this.hasBackground;
this.get_parent().set_background_color(this.hasBackground ? this.activeBackgroundColor : null);
},
toggleGrid: function() {
this.hasGrid = !this.hasGrid;
this._redisplay();
},
toggleSquareArea: function() {
this.isSquareArea = !this.isSquareArea;
if (this.isSquareArea) {
let width = this.squareAreaWidth || this.squareAreaHeight || Math.min(this.monitor.width, this.monitor.height) * 3 / 4;
let height = this.squareAreaHeight || this.squareAreaWidth || Math.min(this.monitor.width, this.monitor.height) * 3 / 4;
this.set_position(Math.floor(this.monitor.width / 2 - width / 2), Math.floor(this.monitor.height / 2 - height / 2));
this.set_size(width, height);
this.add_style_class_name('draw-on-your-screen-square-area');
} else {
this.set_position(0, 0);
this.set_size(this.monitor.width, this.monitor.height);
this.remove_style_class_name('draw-on-your-screen-square-area');
}
},
toggleColor: function() {
this.selectColor((this.currentColor == this.colors[1]) ? 2 : 1);
},
selectColor: function(index) {
this.currentColor = this.colors[index];
if (this.currentElement) {
this.currentElement.color = this.currentColor.to_string();
this._redisplay();
}
// Foreground color markup is not displayed since 3.36, use style instead but the transparency is lost.
this.emit('show-osd', null, this.currentColor.to_string(), this.currentColor.to_string().slice(0, 7), -1);
},
selectTool: function(tool) {
this.currentTool = tool;
this.emit('show-osd', null, _(ToolNames[tool]), "", -1);
this.updatePointerCursor();
},
toggleFill: function() {
this.fill = !this.fill;
this.emit('show-osd', null, this.fill ? _("Fill") : _("Stroke"), "", -1);
},
toggleDash: function() {
this.dashedLine = !this.dashedLine;
this.emit('show-osd', null, this.dashedLine ? _("Dashed line") : _("Full line"), "", -1);
},
incrementLineWidth: function(increment) {
this.currentLineWidth = Math.max(this.currentLineWidth + increment, 0);
this.emit('show-osd', null, _("%d px").format(this.currentLineWidth), "", 2 * this.currentLineWidth);
},
toggleLineJoin: function() {
this.currentLineJoin = this.currentLineJoin == 2 ? 0 : this.currentLineJoin + 1;
this.emit('show-osd', null, _(LineJoinNames[this.currentLineJoin]), "", -1);
},
toggleLineCap: function() {
this.currentLineCap = this.currentLineCap == 2 ? 0 : this.currentLineCap + 1;
this.emit('show-osd', null, _(LineCapNames[this.currentLineCap]), "", -1);
},
toggleFillRule: function() {
this.currentFillRule = this.currentFillRule == 1 ? 0 : this.currentFillRule + 1;
this.emit('show-osd', null, _(FillRuleNames[this.currentFillRule]), "", -1);
},
toggleFontWeight: function() {
this.currentFontWeight = this.currentFontWeight == 1 ? 0 : this.currentFontWeight + 1;
if (this.currentElement) {
this.currentElement.font.weight = this.currentFontWeight;
this._redisplay();
}
this.emit('show-osd', null, `${_(FontWeightNames[this.currentFontWeight])}`, "", -1);
},
toggleFontStyle: function() {
this.currentFontStyle = this.currentFontStyle == 2 ? 0 : this.currentFontStyle + 1;
if (this.currentElement) {
this.currentElement.font.style = this.currentFontStyle;
this._redisplay();
}
this.emit('show-osd', null, `${_(FontStyleNames[this.currentFontStyle])}`, "", -1);
},
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.emit('show-osd', null, `${_(currentFontFamily)}`, "", -1);
},
toggleHelp: function() {
if (this.helper.visible)
this.helper.hideHelp();
else
this.helper.showHelp();
},
enterDrawingMode: function() {
this.isInDrawingMode = true;
this.stageKeyPressedHandler = global.stage.connect('key-press-event', this._onStageKeyPressed.bind(this));
this.stageKeyReleasedHandler = global.stage.connect('key-release-event', this._onStageKeyReleased.bind(this));
this.keyPressedHandler = this.connect('key-press-event', this._onKeyPressed.bind(this));
this.buttonPressedHandler = this.connect('button-press-event', this._onButtonPressed.bind(this));
this._onKeyboardPopupMenuHandler = this.connect('popup-menu', this._onKeyboardPopupMenu.bind(this));
this.scrollHandler = this.connect('scroll-event', this._onScroll.bind(this));
this.get_parent().set_background_color(this.hasBackground ? this.activeBackgroundColor : null);
if (this.hasGrid)
// redisplay to show the grid before updating the style because _updateStyle is long.
this._redisplay();
this._updateStyle();
},
leaveDrawingMode: function(save) {
this.isInDrawingMode = false;
if (this.stageKeyPressedHandler) {
global.stage.disconnect(this.stageKeyPressedHandler);
this.stageKeyPressedHandler = null;
}
if (this.stageKeyReleasedHandler) {
global.stage.disconnect(this.stageKeyReleasedHandler);
this.stageKeyReleasedHandler = null;
}
if (this.keyPressedHandler) {
this.disconnect(this.keyPressedHandler);
this.keyPressedHandler = null;
}
if (this.buttonPressedHandler) {
this.disconnect(this.buttonPressedHandler);
this.buttonPressedHandler = null;
}
if (this._onKeyboardPopupMenuHandler) {
this.disconnect(this._onKeyboardPopupMenuHandler);
this._onKeyboardPopupMenuHandler = null;
}
if (this.elementGrabberHandler) {
this.disconnect(this.elementGrabberHandler);
this.elementGrabberHandler = 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._stopTextCursorTimeout();
this.currentTool = Shapes.NONE;
this.dashedLine = false;
this.fill = false;
this._redisplay();
if (this._menu)
this._menu.close();
this.get_parent().set_background_color(null);
if (save)
this.savePersistent();
},
saveAsSvg: function() {
// stop drawing or writing
if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.currentElement.textState == TextStates.WRITING) {
this._stopWriting();
} else if (this.currentElement && this.currentElement.shape != Shapes.TEXT) {
this._stopDrawing();
}
let content = `