Merge branch 'dev' into 'master'

v6.2

See merge request abakkk/DrawOnYourScreen!13
This commit is contained in:
abakkk 2020-08-23 11:19:46 +02:00
commit 0fe72b7746
17 changed files with 3414 additions and 2650 deletions

14
NEWS
View File

@ -1,5 +1,15 @@
v6.2 - August 2020
==================
* Show the text entry when ibusCandidatePopup is used
* Regroup first menu items
* Center grid overlay
* Image shape
* Extend font family choices
* Fix sub-menu scroll view adjustment
v6.1 - June 2020
=================
================
* Fix empty media-keys settings case #28
* Fix label color in OSD and menu #31
@ -20,7 +30,7 @@ v6.1 - June 2020
* Attributes are now persistent through drawing mode toggling #27
v6 - March 2020
=================
===============
* GS 3.36 compatibility

View File

@ -7,7 +7,7 @@ Then save your beautiful work by taking a screenshot.
## Features
* Basic shapes (rectangle, circle, ellipse, line, curve, text, free)
* Basic shapes (rectangle, circle, ellipse, line, curve, text, image, free)
* Basic transformations (move, rotate, resize, stretch, mirror, inverse)
* Smooth stroke
* Draw over applications
@ -40,6 +40,10 @@ Then save your beautiful work by taking a screenshot.
![How to duplicate an element](https://framagit.org/abakkk/DrawOnYourScreen/-/raw/ressources/duplicate.webm)
* Insertable images:
Add your images (jpeg, png, svg) to `~/.local/share/drawOnYourScreen/images/`.
* Screenshot Tool extension:
[Screenshot Tool](https://extensions.gnome.org/extension/1112/screenshot-tool/) is a convenient extension to “create, copy, store and upload screenshots”. In order to select a screenshoot area with your pointer while keeping the drawing in place, you need first to tell DrawOnYourScreen to ungrab the pointer (`Ctrl + Super + Alt + D`).

1219
area.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
<svg viewBox="0 0 576 576" xmlns="http://www.w3.org/2000/svg">
<rect fill="#555" stroke="none" x="99.06" y="116.46" width="426.79" height="88.9" transform="translate(-1.13,-26.30) translate(297.56,177.87) rotate(-17.00) translate(-297.56,-177.87) translate(-14.89,16.96)"/>
<rect fill="#555" stroke="none" x="382.92" y="219.17" width="137.39" height="55" transform="translate(-7.43,-27.23) translate(449.62,218.30) rotate(-16.48) translate(-449.62,-218.30) translate(-2,-19.20)"/>
<rect fill="#555" stroke="none" x="99.06" y="116.46" width="426.79" height="88.9" transform="translate(0, 284.75) rotate(180) scale(1, -1) rotate(-180) translate(0, -284.75) translate(-1.13,-26.30) translate(297.56,177.87) rotate(-17.00) translate(-297.56,-177.87) translate(-14.89,16.96)"/>
<rect fill="#555" stroke="none" x="382.92" y="219.17" width="137.39" height="55" transform="translate(0, 284.02) rotate(180) scale(1, -1) rotate(-180) translate(0, -284.02) translate(-7.43,-27.23) translate(449.62,218.30) rotate(-16.48) translate(-449.62,-218.30) translate(-2,-19.20)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

43
data/images/Example.svg Normal file
View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<!-- https://commons.wikimedia.org/wiki/File:Bananas.svg -->
<!-- public domain -->
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://web.resource.org/cc/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="559.62469" height="373.29596" id="svg2" sodipodi:version="0.32" inkscape:version="0.44" version="1.0" sodipodi:docbase="C:\Documents and Settings\Elembis\My Documents\Images\Shartak\Shartak.com" sodipodi:docname="bananas3.svg" inkscape:export-filename="C:\Documents and Settings\Elembis\My Documents\Images\Shartak\Shartak.com\banana.png" inkscape:export-xdpi="25.797226" inkscape:export-ydpi="25.797226">
<defs id="defs4"/>
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" gridtolerance="10000" guidetolerance="10" objecttolerance="10" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="1" inkscape:cx="257.85399" inkscape:cy="231.60006" inkscape:document-units="px" inkscape:current-layer="layer1" inkscape:window-width="1024" inkscape:window-height="721" inkscape:window-x="-4" inkscape:window-y="-4"/>
<metadata id="metadata7">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
</cc:Work>
</rdf:RDF>
</metadata>
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(-66.38047,-391.3222)">
<g id="g9893" inkscape:transform-center-x="88.0002" inkscape:transform-center-y="113.00018">
<path transform="translate(-17.85713,318.6479)" sodipodi:nodetypes="csccccc" id="path17976" d="M 543,119 C 543,119 547,177 531,189 C 515,201 397,320 397,320 L 419,345 L 562,248 L 573,121 L 543,119 z " style="fill:#ffc701;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
<path sodipodi:nodetypes="csccccc" id="path18863" d="M 525.14247,437.64826 C 525.14247,437.64826 529.14247,495.64826 513.14247,507.64826 C 497.14247,519.64826 379.14247,638.64826 379.14247,638.64826 L 401.14247,663.64826 L 544.14247,566.64826 L 555.14247,439.64826 L 525.14247,437.64826 z " style="opacity:0.7;fill:black;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
</g>
<g id="g2783">
<path id="path6424" d="M 519.9114,399.65406 L 483.38679,454.45323 C 483.38679,454.45323 454.12626,461.72103 439.29443,470.94146 C 424.46259,480.16189 377.85779,514.19114 274.74769,506.89067 C 171.63759,499.59021 156.32242,477.55739 133.97629,476.75109 C 111.63016,475.9448 77.83775,479.82705 69.64709,486.06507 C 61.45643,492.3031 71.25003,511.09366 71.25003,511.09366 C 71.25003,511.09366 101.21736,578.4951 210.30564,606.14897 C 319.39391,633.80284 448.5453,576.70729 448.5453,576.70729 L 538.74205,411.07361 L 519.9114,399.65406 z " style="fill:#ffc701;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
<path id="path1895" d="M 517.00522,404.00971 L 483.38022,454.44721 C 483.38022,454.44721 454.1183,461.72678 439.28647,470.94721 C 424.45462,480.16764 377.86532,514.18518 274.75522,506.88471 C 171.64512,499.58425 156.3201,477.56601 133.97397,476.75971 C 111.62785,475.95343 77.85213,479.83419 69.66147,486.07221 C 69.2897,486.35536 68.96174,486.67778 68.66147,487.00971 C 87.94596,484.21174 116.37515,484.78053 136.38022,488.41596 C 174.77085,495.39252 209.97775,518.65573 299.50522,516.22846 C 426.73709,512.73998 441.6649,473.10304 459.38022,468.41596 C 484.00837,461.89991 530.48444,452.34626 517.00522,404.00971 z " style="fill:white;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;opacity:0.4"/>
<path id="path9086" d="M 106.96213,554.70796 C 128.49383,573.41467 161.44348,593.76255 210.30593,606.14918 C 222.07021,609.13143 234.0755,611.11073 246.11929,612.29962 L 284.83188,604.45269 C 220.5418,614.51673 142.31641,575.18761 106.96213,554.70796 z " style="opacity:0.4;fill:black;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
</g>
<g id="g9941">
<path transform="translate(-17.85713,318.6479)" inkscape:transform-center-y="181" inkscape:transform-center-x="126" sodipodi:nodetypes="cssssssssc" id="path12644" d="M 546,120 C 546,120 567,175 561,189 C 555,203 520,283 455,313 C 390,343 223,339 219,331 C 215,323 219,407 320,438 C 421,469 550.08904,401.09271 573,379 C 602,351 657,279 641,225 C 625,171 613,187 604,172 C 595,157 585,128 585,116 C 585,104 547,120 546,120 z " style="opacity:1;fill:#ffc701;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
<path id="path8981" d="M 533.7244,527.63751 C 518.52971,557.28537 486.49949,608.85189 437.13065,631.63751 C 372.13066,661.63751 205.13065,657.63751 201.13065,649.63751 C 200.13065,647.63751 199.64628,651.40313 201.0369,658.63751 L 201.06815,658.76251 L 203.13065,665.63751 C 203.13064,665.63751 202.9682,665.65254 202.75565,665.73126 C 202.85933,666.09273 202.98841,666.48596 203.0994,666.85626 C 203.2463,667.34553 203.37675,667.82087 203.5369,668.32501 C 203.82494,669.23173 204.17292,670.18568 204.50565,671.13751 C 204.61749,671.45746 204.70108,671.78151 204.81815,672.10626 C 205.04405,672.73471 205.32226,673.36688 205.56815,674.01251 C 205.8517,674.75499 206.10087,675.49879 206.4119,676.26251 C 206.6323,676.80515 206.89597,677.36609 207.13065,677.91876 C 207.44564,678.65842 207.75805,679.38112 208.0994,680.13751 C 208.26157,680.49795 208.4311,680.86725 208.5994,681.23126 C 209.69117,683.59259 210.89044,686.02408 212.25565,688.51251 C 212.29126,688.57728 212.34485,688.63516 212.38065,688.70001 C 212.39669,688.72911 212.39582,688.76464 212.4119,688.79376 C 212.77389,689.44818 213.15496,690.10127 213.5369,690.76251 C 213.57214,690.82361 213.59523,690.88885 213.63065,690.95001 C 214.03425,691.64604 214.45442,692.3411 214.88065,693.04376 C 215.33612,693.79463 215.80496,694.56759 216.2869,695.32501 C 216.29464,695.33717 216.3104,695.34409 216.31815,695.35626 C 216.77977,696.08134 217.23774,696.81355 217.7244,697.54376 C 217.72759,697.54854 217.75246,697.53898 217.75565,697.54376 C 258.55477,715.44577 403.2244,681.10626 468.13065,633.63751 C 513.82083,600.22229 528.80654,553.79612 533.7244,527.63751 z " style="opacity:0.6;fill:black;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
<path id="path17089" d="M 559.79937,430.17885 C 558.18509,430.20703 556.38497,430.42195 554.48687,430.74135 C 560.75792,449.99636 590.83405,543.70808 588.14312,561.6476 C 585.14312,581.6476 553.30943,654.34782 501.14312,685.6476 C 466.14312,706.6476 390.14312,739.6476 363.14312,744.6476 C 346.05557,747.81196 312.61613,752.46521 286.48687,751.11635 C 291.4351,753.10006 296.63167,754.95597 302.14312,756.6476 C 403.14312,787.6476 532.23216,719.74031 555.14312,697.6476 C 584.14312,669.6476 639.14312,597.6476 623.14312,543.6476 C 607.14312,489.6476 595.14312,505.6476 586.14312,490.6476 C 577.14312,475.6476 567.14312,446.6476 567.14312,434.6476 C 567.14312,431.2726 564.14254,430.10304 559.79937,430.17885 z " style="opacity:0.4;fill:black;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
</g>
<g id="g9903">
<path sodipodi:nodetypes="cssccccc" id="path1933" d="M 533.14287,448.6479 C 533.14287,448.6479 514.14287,467.6479 519.14287,485.6479 C 524.14287,503.6479 541.14287,547.6479 478.14287,597.6479 C 415.14287,647.6479 232.14287,734.6479 109.14287,639.6479 C 74.14287,599.6479 94.14287,573.6479 94.14287,573.6479 C 94.14287,573.6479 114.12992,570.75757 122.14287,571.6479 C 132.14287,568.6479 179.14287,571.6479 223.14287,573.6479 C 282.05327,572.49008 443.14287,497.6479 474.14287,468.6479 C 474.14287,468.6479 512.14287,422.6479 513.14287,414.6479 C 514.14287,406.6479 540.14287,430.6479 541.14287,437.6479 C 542.14287,444.6479 533.14287,447.6479 533.14287,448.6479 z " style="fill:#ffc701;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
<path sodipodi:nodetypes="cssccccsscc" id="path7201" d="M 514.60028,413.04467 C 513.79169,413.14037 513.25653,413.63842 513.13153,414.63842 C 512.13153,422.63842 474.13153,468.63841 474.13153,468.63842 C 443.13153,497.63842 282.04191,572.4806 223.13152,573.63842 C 179.13152,571.63842 132.13152,568.63842 122.13152,571.63842 C 114.11858,570.74809 94.13152,573.63842 94.13152,573.63842 C 94.13152,573.63842 93.7656,574.16602 93.22527,575.07592 C 104.18437,578.28284 124.12588,582.75014 156.38152,583.60717 C 230.77215,585.58373 262.00653,581.54467 326.50653,556.41967 C 394.13686,530.0753 484.82418,496.21955 520.38153,415.35717 C 517.97095,413.81698 515.90107,412.89071 514.60028,413.04467 z " style="opacity:0.4;fill:white;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
<path id="path4646" d="M 518.58062,482.8981 C 510.39544,511.03311 477.26645,585.47414 350.14312,628.6481 C 194.69709,681.44109 156.81818,651.08254 106.61187,636.61685 C 107.43132,637.61484 108.2539,638.63184 109.14312,639.6481 C 232.14312,734.6481 415.14312,647.6481 478.14312,597.6481 C 541.14312,547.6481 524.14312,503.6481 519.14312,485.6481 C 518.88851,484.73149 518.71644,483.81726 518.58062,482.8981 z " style="opacity:0.4;fill:black;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
</g>
<g id="g9946">
<path sodipodi:nodetypes="czsssz" id="path23294" d="M 534.47367,391.42537 C 528.72367,389.92537 508.71616,405.72925 509.06846,411.02017 C 509.42029,416.30405 528.53454,433.43637 540.64281,437.57407 C 544.64281,438.57407 566.64281,436.57407 567.64281,432.57407 C 568.64281,428.57407 568.64281,411.57407 564.64281,407.57407 C 560.64281,403.57407 541.16441,393.17078 534.47367,391.42537 z " style="fill:#884a13;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
<path sodipodi:nodetypes="czsssz" id="path22407" d="M 534.47374,391.42502 C 528.72374,389.92502 508.71623,405.7289 509.06853,411.01982 C 509.42036,416.3037 528.53461,433.43602 540.64288,437.57372 C 544.64288,438.57372 566.64288,436.57372 567.64288,432.57372 C 568.64288,428.57372 568.64288,411.57372 564.64288,407.57372 C 560.64288,403.57372 541.16448,393.17043 534.47374,391.42502 z " style="fill:#884a13;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
<path id="path25958" d="M 533.23672,391.3354 C 526.30206,391.85508 508.75019,406.06266 509.08047,411.0229 C 509.222,413.14841 512.41917,417.17996 516.95547,421.5229 C 517.24178,413.18494 521.37892,401.08827 540.67422,393.7729 C 538.17765,392.66554 536.00097,391.82417 534.48672,391.42915 C 534.12734,391.3354 533.69903,391.30075 533.23672,391.3354 z " style="opacity:0.3;fill:white;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
<path id="path26854" d="M 564.58087,407.52257 C 564.73328,425.7813 549.04695,433.48138 538.20587,436.61632 C 539.04145,436.98636 539.85952,437.31721 540.64337,437.58507 C 544.64338,438.58507 566.64337,436.58507 567.64337,432.58507 C 568.64338,428.58507 568.64337,411.58507 564.64337,407.58507 C 564.62587,407.56758 564.59896,407.54031 564.58087,407.52257 z " style="opacity:0.4;fill:black;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

2569
draw.js

File diff suppressed because it is too large Load Diff

907
elements.js Normal file
View File

@ -0,0 +1,907 @@
/* 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 <http://www.gnu.org/licenses/>.
*/
const Cairo = imports.cairo;
const Clutter = imports.gi.Clutter;
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 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);
var getPangoFontFamilies = function() {
return PangoCairo.font_map_get_default().list_families().map(fontFamily => fontFamily.get_name()).sort((a,b) => a.localeCompare(b));
};
const SVG_DEBUG_SUPERPOSES_CAIRO = false;
const RADIAN = 180 / Math.PI; // degree
const INVERSION_CIRCLE_RADIUS = 12; // px
const REFLECTION_TOLERANCE = 5; // px, to select vertical and horizontal directions
const STRETCH_TOLERANCE = Math.PI / 8; // rad, to select vertical and horizontal directions
const MIN_REFLECTION_LINE_LENGTH = 10; // px
const MIN_TRANSLATION_DISTANCE = 1; // px
const MIN_ROTATION_ANGLE = Math.PI / 1000; // rad
const MIN_DRAWING_SIZE = 3; // px
var DrawingElement = function(params) {
return params.shape == Shapes.TEXT ? new TextElement(params) :
params.shape == Shapes.IMAGE ? new ImageElement(params) :
new _DrawingElement(params);
};
// DrawingElement represents a "brushstroke".
// It can be converted into a cairo path as well as a svg element.
// See DrawingArea._startDrawing() to know its params.
const _DrawingElement = new Lang.Class({
Name: 'DrawOnYourScreenDrawingElement',
_init: function(params) {
for (let key in params)
this[key] = params[key];
// compatibility with json generated by old extension versions
if (params.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.transform && params.transform.center) {
let angle = (params.transform.angle || 0) + (params.transform.startAngle || 0);
if (angle)
this.transformations.push({ type: Transformations.ROTATION, angle: angle });
}
if (params.shape == Shapes.ELLIPSE && params.transform && params.transform.ratio && params.transform.ratio != 1 && params.points.length >= 2) {
let [ratio, p0, p1] = [params.transform.ratio, params.points[0], params.points[1]];
// Add a fake point that will give the right ellipse ratio when building the element.
this.points.push([ratio * (p1[0] - p0[0]) + p0[0], ratio * (p1[1] - p0[1]) + p0[1]]);
}
delete this.transform;
},
// toJSON is called by JSON.stringify
toJSON: function() {
return {
shape: this.shape,
color: this.color,
line: this.line,
dash: this.dash,
fill: this.fill,
fillRule: this.fillRule,
eraser: this.eraser,
transformations: this.transformations,
points: this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100])
};
},
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.showSymmetryElement) {
let transformation = this.lastTransformation;
setDummyStroke(cr);
if (transformation.type == Transformations.REFLECTION) {
cr.moveTo(transformation.startX, transformation.startY);
cr.lineTo(transformation.endX, transformation.endY);
} else {
cr.arc(transformation.endX, transformation.endY, INVERSION_CIRCLE_RADIUS, 0, 2 * Math.PI);
}
cr.stroke();
}
if (this.line) {
cr.setLineCap(this.line.lineCap);
cr.setLineJoin(this.line.lineJoin);
cr.setLineWidth(this.line.lineWidth);
}
if (this.fillRule)
cr.setFillRule(this.fillRule);
if (this.dash && this.dash.active && this.dash.array && this.dash.array[0] && this.dash.array[1])
cr.setDash(this.dash.array, this.dash.offset);
if (this.eraser)
cr.setOperator(Cairo.Operator.CLEAR);
else
cr.setOperator(Cairo.Operator.OVER);
if (params.dummyStroke)
setDummyStroke(cr);
if (SVG_DEBUG_SUPERPOSES_CAIRO) {
Clutter.cairo_set_source_color(cr, Clutter.Color.new(255, 0, 0, 255));
cr.setLineWidth(this.line.lineWidth / 2 || 1);
}
this.transformations.slice(0).reverse().forEach(transformation => {
if (transformation.type == Transformations.TRANSLATION) {
cr.translate(transformation.slideX, transformation.slideY);
} else if (transformation.type == Transformations.ROTATION) {
let center = this._getTransformedCenter(transformation);
cr.translate(center[0], center[1]);
cr.rotate(transformation.angle);
cr.translate(-center[0], -center[1]);
} else if (transformation.type == Transformations.SCALE_PRESERVE || transformation.type == Transformations.STRETCH) {
let center = this._getTransformedCenter(transformation);
cr.translate(center[0], center[1]);
cr.rotate(transformation.angle);
cr.scale(transformation.scaleX, transformation.scaleY);
cr.rotate(-transformation.angle);
cr.translate(-center[0], -center[1]);
} else if (transformation.type == Transformations.REFLECTION || transformation.type == Transformations.INVERSION) {
cr.translate(transformation.slideX, transformation.slideY);
cr.rotate(transformation.angle);
cr.scale(transformation.scaleX, transformation.scaleY);
cr.rotate(-transformation.angle);
cr.translate(-transformation.slideX, -transformation.slideY);
}
});
this._drawCairo(cr, params);
cr.identityMatrix();
},
_drawCairo: function(cr, params) {
let [points, shape] = [this.points, this.shape];
if (shape == Shapes.LINE && points.length == 3) {
cr.moveTo(points[0][0], points[0][1]);
cr.curveTo(points[0][0], points[0][1], points[1][0], points[1][1], points[2][0], points[2][1]);
} else if (shape == Shapes.LINE && points.length == 4) {
cr.moveTo(points[0][0], points[0][1]);
cr.curveTo(points[1][0], points[1][1], points[2][0], points[2][1], points[3][0], points[3][1]);
} else if (shape == Shapes.NONE || shape == Shapes.LINE) {
cr.moveTo(points[0][0], points[0][1]);
for (let j = 1; j < points.length; j++) {
cr.lineTo(points[j][0], points[j][1]);
}
} else if (shape == Shapes.ELLIPSE && points.length >= 2) {
let radius = Math.hypot(points[1][0] - points[0][0], points[1][1] - points[0][1]);
let ratio = 1;
if (points[2]) {
ratio = Math.hypot(points[2][0] - points[0][0], points[2][1] - points[0][1]) / radius;
cr.translate(points[0][0], points[0][1]);
cr.scale(ratio, 1);
cr.translate(-points[0][0], -points[0][1]);
cr.arc(points[0][0], points[0][1], radius, 0, 2 * Math.PI);
cr.translate(points[0][0], points[0][1]);
cr.scale(1 / ratio, 1);
cr.translate(-points[0][0], -points[0][1]);
} else
cr.arc(points[0][0], points[0][1], radius, 0, 2 * Math.PI);
} else if (shape == Shapes.RECTANGLE && points.length == 2) {
cr.rectangle(points[0][0], points[0][1], points[1][0] - points[0][0], points[1][1] - points[0][1]);
} else if ((shape == Shapes.POLYGON || shape == Shapes.POLYLINE) && points.length >= 2) {
cr.moveTo(points[0][0], points[0][1]);
for (let j = 1; j < points.length; j++) {
cr.lineTo(points[j][0], points[j][1]);
}
if (shape == Shapes.POLYGON)
cr.closePath();
}
},
getContainsPoint: function(cr, x, y) {
cr.save();
cr.setLineWidth(Math.max(this.line.lineWidth, 25));
cr.setDash([], 0);
// Check whether the point is inside/on/near the element.
let inElement = cr.inStroke(x, y) || this.fill && cr.inFill(x, y);
cr.restore();
return inElement;
},
buildSVG: function(bgColor) {
let transAttribute = '';
this.transformations.slice(0).reverse().forEach(transformation => {
transAttribute += transAttribute ? ' ' : ' transform="';
let center = this._getTransformedCenter(transformation);
if (transformation.type == Transformations.TRANSLATION) {
transAttribute += `translate(${transformation.slideX},${transformation.slideY})`;
} else if (transformation.type == Transformations.ROTATION) {
transAttribute += `translate(${center[0]},${center[1]}) `;
transAttribute += `rotate(${transformation.angle * RADIAN}) `;
transAttribute += `translate(${-center[0]},${-center[1]})`;
} else if (transformation.type == Transformations.SCALE_PRESERVE || transformation.type == Transformations.STRETCH) {
transAttribute += `translate(${center[0]},${center[1]}) `;
transAttribute += `rotate(${transformation.angle * RADIAN}) `;
transAttribute += `scale(${transformation.scaleX},${transformation.scaleY}) `;
transAttribute += `rotate(${-transformation.angle * RADIAN}) `;
transAttribute += `translate(${-center[0]},${-center[1]})`;
} else if (transformation.type == Transformations.REFLECTION || transformation.type == Transformations.INVERSION) {
transAttribute += `translate(${transformation.slideX}, ${transformation.slideY}) `;
transAttribute += `rotate(${transformation.angle * RADIAN}) `;
transAttribute += `scale(${transformation.scaleX}, ${transformation.scaleY}) `;
transAttribute += `rotate(${-transformation.angle * RADIAN}) `;
transAttribute += `translate(${-transformation.slideX}, ${-transformation.slideY})`;
}
});
transAttribute += transAttribute ? '"' : '';
return this._drawSvg(transAttribute);
},
_drawSvg: function(transAttribute) {
let row = "\n ";
let points = this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100]);
let color = this.eraser ? bgColor : this.color;
let fill = this.fill && !this.isStraightLine;
let attributes = '';
if (fill) {
attributes = `fill="${color}"`;
if (this.fillRule)
attributes += ` fill-rule="${FillRuleNames[this.fillRule].toLowerCase()}"`;
} else {
attributes = `fill="none"`;
}
if (this.line && this.line.lineWidth) {
attributes += ` stroke="${color}"` +
` stroke-width="${this.line.lineWidth}"`;
if (this.line.lineCap)
attributes += ` stroke-linecap="${LineCapNames[this.line.lineCap].toLowerCase()}"`;
if (this.line.lineJoin && !this.isStraightLine)
attributes += ` stroke-linejoin="${LineJoinNames[this.line.lineJoin].toLowerCase()}"`;
if (this.dash && this.dash.active && this.dash.array && this.dash.array[0] && this.dash.array[1])
attributes += ` stroke-dasharray="${this.dash.array[0]} ${this.dash.array[1]}" stroke-dashoffset="${this.dash.offset}"`;
} else {
attributes += ` stroke="none"`;
}
if (this.shape == Shapes.LINE && points.length == 4) {
row += `<path ${attributes} d="M${points[0][0]} ${points[0][1]}`;
row += ` C ${points[1][0]} ${points[1][1]}, ${points[2][0]} ${points[2][1]}, ${points[3][0]} ${points[3][1]}`;
row += `${fill ? 'z' : ''}"${transAttribute}/>`;
} else if (this.shape == Shapes.LINE && points.length == 3) {
row += `<path ${attributes} d="M${points[0][0]} ${points[0][1]}`;
row += ` C ${points[0][0]} ${points[0][1]}, ${points[1][0]} ${points[1][1]}, ${points[2][0]} ${points[2][1]}`;
row += `${fill ? 'z' : ''}"${transAttribute}/>`;
} else if (this.shape == Shapes.LINE) {
row += `<line ${attributes} x1="${points[0][0]}" y1="${points[0][1]}" x2="${points[1][0]}" y2="${points[1][1]}"${transAttribute}/>`;
} else if (this.shape == Shapes.NONE) {
row += `<path ${attributes} d="M${points[0][0]} ${points[0][1]}`;
for (let i = 1; i < points.length; i++)
row += ` L ${points[i][0]} ${points[i][1]}`;
row += `${fill ? 'z' : ''}"${transAttribute}/>`;
} else if (this.shape == Shapes.ELLIPSE && points.length == 3) {
let ry = Math.hypot(points[1][0] - points[0][0], points[1][1] - points[0][1]);
let rx = Math.hypot(points[2][0] - points[0][0], points[2][1] - points[0][1]);
row += `<ellipse ${attributes} cx="${points[0][0]}" cy="${points[0][1]}" rx="${rx}" ry="${ry}"${transAttribute}/>`;
} 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 += `<circle ${attributes} cx="${points[0][0]}" cy="${points[0][1]}" r="${r}"${transAttribute}/>`;
} else if (this.shape == Shapes.RECTANGLE && points.length == 2) {
row += `<rect ${attributes} x="${Math.min(points[0][0], points[1][0])}" y="${Math.min(points[0][1], points[1][1])}" ` +
`width="${Math.abs(points[1][0] - points[0][0])}" height="${Math.abs(points[1][1] - points[0][1])}"${transAttribute}/>`;
} else if (this.shape == Shapes.POLYGON && points.length >= 3) {
row += `<polygon ${attributes} points="`;
for (let i = 0; i < points.length; i++)
row += ` ${points[i][0]},${points[i][1]}`;
row += `"${transAttribute}/>`;
} else if (this.shape == Shapes.POLYLINE && points.length >= 2) {
row += `<polyline ${attributes} points="`;
for (let i = 0; i < points.length; i++)
row += ` ${points[i][0]},${points[i][1]}`;
row += `"${transAttribute}/>`;
}
return row;
},
get lastTransformation() {
if (!this.transformations.length)
return null;
return this.transformations[this.transformations.length - 1];
},
get isStraightLine() {
return this.shape == Shapes.LINE && this.points.length == 2;
},
smoothAll: function() {
for (let i = 0; i < this.points.length; i++) {
this._smooth(i);
}
},
addPoint: function() {
if (this.shape == Shapes.POLYGON || this.shape == Shapes.POLYLINE) {
// copy last point
let [lastPoint, secondToLastPoint] = [this.points[this.points.length - 1], this.points[this.points.length - 2]];
if (!getNearness(secondToLastPoint, lastPoint, MIN_DRAWING_SIZE))
this.points.push([lastPoint[0], lastPoint[1]]);
} else if (this.shape == Shapes.LINE) {
if (this.points.length == 2) {
this.points[2] = this.points[1];
} else if (this.points.length == 3) {
this.points[3] = this.points[2];
this.points[2] = this.points[1];
}
}
},
startDrawing: function(startX, startY) {
this.points.push([startX, startY]);
if (this.shape == Shapes.POLYGON || this.shape == Shapes.POLYLINE)
this.points.push([startX, startY]);
},
updateDrawing: function(x, y, transform) {
let points = this.points;
if (x == points[points.length - 1][0] && y == points[points.length - 1][1])
return;
transform = transform || this.transformations.length >= 1;
if (this.shape == Shapes.NONE) {
points.push([x, y]);
if (transform)
this._smooth(points.length - 1);
} else if ((this.shape == Shapes.RECTANGLE || this.shape == Shapes.POLYGON || this.shape == Shapes.POLYLINE) && transform) {
if (points.length < 2)
return;
let center = this._getOriginalCenter();
this.transformations[0] = { type: Transformations.ROTATION,
angle: getAngle(center[0], center[1], points[points.length - 1][0], points[points.length - 1][1], x, y) };
} else if (this.shape == Shapes.ELLIPSE && transform) {
if (points.length < 2)
return;
points[2] = [x, y];
let center = this._getOriginalCenter();
this.transformations[0] = { type: Transformations.ROTATION,
angle: getAngle(center[0], center[1], center[0] + 1, center[1], x, y) };
} else if (this.shape == Shapes.POLYGON || this.shape == Shapes.POLYLINE) {
points[points.length - 1] = [x, y];
} else {
points[1] = [x, y];
}
},
stopDrawing: function() {
// skip when the size is too small to be visible (3px) (except for free drawing)
if (this.shape != Shapes.NONE && this.points.length >= 2) {
let lastPoint = this.points[this.points.length - 1];
let secondToLastPoint = this.points[this.points.length - 2];
if (getNearness(secondToLastPoint, lastPoint, MIN_DRAWING_SIZE))
this.points.pop();
}
if (this.transformations[0] && this.transformations[0].type == Transformations.ROTATION &&
Math.abs(this.transformations[0].angle) < MIN_ROTATION_ANGLE)
this.transformations.shift();
},
startTransformation: function(startX, startY, type) {
if (type == Transformations.TRANSLATION)
this.transformations.push({ startX: startX, startY: startY, type: type, slideX: 0, slideY: 0 });
else if (type == Transformations.ROTATION)
this.transformations.push({ startX: startX, startY: startY, type: type, angle: 0 });
else if (type == Transformations.SCALE_PRESERVE || type == Transformations.STRETCH)
this.transformations.push({ startX: startX, startY: startY, type: type, scaleX: 1, scaleY: 1, angle: 0 });
else if (type == Transformations.REFLECTION)
this.transformations.push({ startX: startX, startY: startY, endX: startX, endY: startY, type: type,
scaleX: 1, scaleY: 1, slideX: 0, slideY: 0, angle: 0 });
else if (type == Transformations.INVERSION)
this.transformations.push({ startX: startX, startY: startY, endX: startX, endY: startY, type: type,
scaleX: -1, scaleY: -1, slideX: startX, slideY: startY,
angle: Math.PI + Math.atan(startY / (startX || 1)) });
if (type == Transformations.REFLECTION || type == Transformations.INVERSION)
this.showSymmetryElement = true;
},
updateTransformation: function(x, y) {
let transformation = this.lastTransformation;
if (transformation.type == Transformations.TRANSLATION) {
transformation.slideX = x - transformation.startX;
transformation.slideY = y - transformation.startY;
} else if (transformation.type == Transformations.ROTATION) {
let center = this._getTransformedCenter(transformation);
transformation.angle = getAngle(center[0], center[1], transformation.startX, transformation.startY, x, y);
} else if (transformation.type == Transformations.SCALE_PRESERVE) {
let center = this._getTransformedCenter(transformation);
let scale = Math.hypot(x - center[0], y - center[1]) / Math.hypot(transformation.startX - center[0], transformation.startY - center[1]) || 1;
[transformation.scaleX, transformation.scaleY] = [scale, scale];
} else if (transformation.type == Transformations.STRETCH) {
let center = this._getTransformedCenter(transformation);
let startAngle = getAngle(center[0], center[1], center[0] + 1, center[1], transformation.startX, transformation.startY);
let vertical = Math.abs(Math.sin(startAngle)) >= Math.sin(Math.PI / 2 - STRETCH_TOLERANCE);
let horizontal = Math.abs(Math.cos(startAngle)) >= Math.cos(STRETCH_TOLERANCE);
let scale = Math.hypot(x - center[0], y - center[1]) / Math.hypot(transformation.startX - center[0], transformation.startY - center[1]) || 1;
transformation.scaleX = vertical ? 1 : scale;
transformation.scaleY = !vertical ? 1 : scale;
transformation.angle = vertical || horizontal ? 0 : getAngle(center[0], center[1], center[0] + 1, center[1], x, y);
} else if (transformation.type == Transformations.REFLECTION) {
[transformation.endX, transformation.endY] = [x, y];
if (getNearness([transformation.startX, transformation.startY], [x, y], MIN_REFLECTION_LINE_LENGTH)) {
// do nothing to avoid jumps (no transformation at starting and locked transformation after)
} else if (Math.abs(y - transformation.startY) <= REFLECTION_TOLERANCE && Math.abs(x - transformation.startX) > REFLECTION_TOLERANCE) {
[transformation.scaleX, transformation.scaleY] = [1, -1];
[transformation.slideX, transformation.slideY] = [0, transformation.startY];
transformation.angle = Math.PI;
} else if (Math.abs(x - transformation.startX) <= REFLECTION_TOLERANCE && Math.abs(y - transformation.startY) > REFLECTION_TOLERANCE) {
[transformation.scaleX, transformation.scaleY] = [-1, 1];
[transformation.slideX, transformation.slideY] = [transformation.startX, 0];
transformation.angle = Math.PI;
} else if (x != transformation.startX) {
let tan = (y - transformation.startY) / (x - transformation.startX);
[transformation.scaleX, transformation.scaleY] = [1, -1];
[transformation.slideX, transformation.slideY] = [0, transformation.startY - transformation.startX * tan];
transformation.angle = Math.PI + Math.atan(tan);
} else if (y != transformation.startY) {
let tan = (x - transformation.startX) / (y - transformation.startY);
[transformation.scaleX, transformation.scaleY] = [-1, 1];
[transformation.slideX, transformation.slideY] = [transformation.startX - transformation.startY * tan, 0];
transformation.angle = Math.PI - Math.atan(tan);
}
} else if (transformation.type == Transformations.INVERSION) {
[transformation.endX, transformation.endY] = [x, y];
[transformation.scaleX, transformation.scaleY] = [-1, -1];
[transformation.slideX, transformation.slideY] = [x, y];
transformation.angle = Math.PI + Math.atan(y / (x || 1));
}
},
stopTransformation: function() {
// Clean transformations
let transformation = this.lastTransformation;
if (transformation.type == Transformations.REFLECTION || transformation.type == Transformations.INVERSION)
this.showSymmetryElement = false;
if (transformation.type == Transformations.REFLECTION &&
getNearness([transformation.startX, transformation.startY], [transformation.endX, transformation.endY], MIN_REFLECTION_LINE_LENGTH) ||
transformation.type == Transformations.TRANSLATION && Math.hypot(transformation.slideX, transformation.slideY) < MIN_TRANSLATION_DISTANCE ||
transformation.type == Transformations.ROTATION && Math.abs(transformation.angle) < MIN_ROTATION_ANGLE) {
this.transformations.pop();
} else {
delete transformation.startX;
delete transformation.startY;
delete transformation.endX;
delete transformation.endY;
}
},
// The figure rotation center before transformations (original).
// this.textWidth is computed during Cairo building.
_getOriginalCenter: function() {
if (!this._originalCenter) {
let points = this.points;
this._originalCenter = this.shape == Shapes.ELLIPSE ? [points[0][0], points[0][1]] :
this.shape == Shapes.LINE && points.length == 4 ? getCurveCenter(points[0], points[1], points[2], points[3]) :
this.shape == Shapes.LINE && points.length == 3 ? getCurveCenter(points[0], points[0], points[1], points[2]) :
points.length >= 3 ? getCentroid(points) :
getNaiveCenter(points);
}
return this._originalCenter;
},
// The figure rotation center, whose position is affected by all transformations done before 'transformation'.
_getTransformedCenter: function(transformation) {
if (!transformation.elementTransformedCenter) {
let matrix = new Pango.Matrix({ xx: 1, xy: 0, yx: 0, yy: 1, x0: 0, y0: 0 });
// Apply transformations to the matrice in reverse order
// because Pango multiply matrices by the left when applying a transformation
this.transformations.slice(0, this.transformations.indexOf(transformation)).reverse().forEach(transformation => {
if (transformation.type == Transformations.TRANSLATION) {
matrix.translate(transformation.slideX, transformation.slideY);
} else if (transformation.type == Transformations.ROTATION) {
// nothing, the center position is preserved.
} else if (transformation.type == Transformations.SCALE_PRESERVE || transformation.type == Transformations.STRETCH) {
// nothing, the center position is preserved.
} else if (transformation.type == Transformations.REFLECTION || transformation.type == Transformations.INVERSION) {
matrix.translate(transformation.slideX, transformation.slideY);
matrix.rotate(-transformation.angle * RADIAN);
matrix.scale(transformation.scaleX, transformation.scaleY);
matrix.rotate(transformation.angle * RADIAN);
matrix.translate(-transformation.slideX, -transformation.slideY);
}
});
let originalCenter = this._getOriginalCenter();
transformation.elementTransformedCenter = matrix.transform_point(originalCenter[0], originalCenter[1]);
}
return transformation.elementTransformedCenter;
},
_smooth: function(i) {
if (i < 2)
return;
this.points[i-1] = [(this.points[i-2][0] + this.points[i][0]) / 2, (this.points[i-2][1] + this.points[i][1]) / 2];
}
});
const TextElement = new Lang.Class({
Name: 'DrawOnYourScreenTextElement',
Extends: _DrawingElement,
toJSON: function() {
return {
shape: this.shape,
color: this.color,
eraser: this.eraser,
transformations: this.transformations,
text: this.text,
lineIndex: this.lineIndex !== undefined ? this.lineIndex : undefined,
textRightAligned: this.textRightAligned,
font: this.font,
points: this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100])
};
},
get x() {
// this.textWidth is computed during Cairo building.
return this.points[1][0] - (this.textRightAligned ? this.textWidth : 0);
},
get y() {
return Math.max(this.points[0][1], this.points[1][1]);
},
get height() {
return Math.abs(this.points[1][1] - this.points[0][1]);
},
// When rotating grouped lines, lineOffset is used to retrieve the rotation center of the first line.
get lineOffset() {
return (this.lineIndex || 0) * this.height;
},
_drawCairo: function(cr, params) {
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);
layout.set_text(this.text, -1);
this.textWidth = layout.get_pixel_size()[0];
cr.moveTo(this.x, this.y - layout.get_baseline() / Pango.SCALE);
layout.set_text(this.text, -1);
PangoCairo.show_layout(cr, layout);
if (params.showTextCursor) {
let cursorPosition = this.cursorPosition == -1 ? this.text.length : this.cursorPosition;
layout.set_text(this.text.slice(0, cursorPosition), -1);
let width = layout.get_pixel_size()[0];
cr.rectangle(this.x + width, this.y,
this.height / 25, - this.height);
cr.fill();
}
if (params.showTextRectangle) {
cr.rectangle(this.x, this.y - this.lineOffset,
this.textWidth, - this.height);
setDummyStroke(cr);
} else if (params.drawTextRectangle) {
cr.rectangle(this.x, this.y,
this.textWidth, - this.height);
// Only draw the rectangle to find the element, not to show it.
cr.setLineWidth(0);
}
}
},
getContainsPoint: function(cr, x, y) {
return cr.inFill(x, y);
},
_drawSvg: function(transAttribute) {
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 = '';
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()}"`;
row += `<text ${attributes} x="${x}" `;
row += `y="${y}"${transAttribute}>${this.text}</text>`;
}
return row;
},
updateDrawing: function(x, y, transform) {
let points = this.points;
if (x == points[points.length - 1][0] && y == points[points.length - 1][1])
return;
transform = transform || this.transformations.length >= 1;
if (transform) {
if (points.length < 2)
return;
let [slideX, slideY] = [x - points[1][0], y - points[1][1]];
points[0] = [points[0][0] + slideX, points[0][1] + slideY];
points[1] = [x, y];
} else {
points[1] = [x, y];
}
},
_getOriginalCenter: function() {
if (!this._originalCenter) {
let points = this.points;
this._originalCenter = this.textWidth ? [points[1][0], Math.max(points[0][1], points[1][1]) - this.lineOffset] :
points.length >= 3 ? getCentroid(points) :
getNaiveCenter(points);
}
return this._originalCenter;
},
});
const ImageElement = new Lang.Class({
Name: 'DrawOnYourScreenImageElement',
Extends: _DrawingElement,
_init: function(params) {
params.fill = false;
this.parent(params);
},
toJSON: function() {
return {
shape: this.shape,
color: this.color,
fill: this.fill,
eraser: this.eraser,
transformations: this.transformations,
image: this.image.toJson(),
preserveAspectRatio: this.preserveAspectRatio,
points: this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100])
};
},
_drawCairo: function(cr, params) {
if (this.points.length < 2)
return;
let points = this.points;
let [x, y] = [Math.min(points[0][0], points[1][0]), Math.min(points[0][1], points[1][1])];
let [width, height] = [Math.abs(points[1][0] - points[0][0]), Math.abs(points[1][1] - points[0][1])];
if (width < 1 || height < 1)
return;
cr.save();
this.image.setCairoSource(cr, x, y, width, height, this.preserveAspectRatio);
cr.rectangle(x, y, width, height);
cr.fill();
cr.restore();
if (params.showTextRectangle) {
cr.rectangle(x, y, width, height);
setDummyStroke(cr);
} else if (params.drawTextRectangle) {
cr.rectangle(x, y, width, height);
// Only draw the rectangle to find the element, not to show it.
cr.setLineWidth(0);
}
},
getContainsPoint: function(cr, x, y) {
return cr.inFill(x, y);
},
_drawSvg: function(transAttribute) {
let points = this.points;
let row = "\n ";
let attributes = '';
if (points.length == 2) {
attributes = `fill="none"`;
row += `<image ${attributes} x="${Math.min(points[0][0], points[1][0])}" y="${Math.min(points[0][1], points[1][1])}" ` +
`width="${Math.abs(points[1][0] - points[0][0])}" height="${Math.abs(points[1][1] - points[0][1])}"${transAttribute} ` +
`preserveAspectRatio="${this.preserveAspectRatio ? 'xMinYMin' : 'none'}" ` +
`id="${this.image.displayName}" xlink:href="data:${this.image.contentType};base64,${this.image.base64}"/>`;
}
return row;
},
updateDrawing: function(x, y, transform) {
let points = this.points;
if (x == points[0][0] || y == points[0][1])
return;
points[1] = [x, y];
this.preserveAspectRatio = !transform;
}
});
const setDummyStroke = function(cr) {
cr.setLineWidth(2);
cr.setLineCap(0);
cr.setLineJoin(0);
cr.setDash([1, 2], 0);
};
const getNearness = function(pointA, pointB, distance) {
return Math.hypot(pointB[0] - pointA[0], pointB[1] - pointA[1]) < distance;
};
// mean of the vertices, ok for regular polygons
const getNaiveCenter = function(points) {
return points.reduce((accumulator, point) => accumulator = [accumulator[0] + point[0], accumulator[1] + point[1]])
.map(coord => coord / points.length);
};
// https://en.wikipedia.org/wiki/Centroid#Of_a_polygon
const getCentroid = function(points) {
let n = points.length;
points.push(points[0]);
let [sA, sX, sY] = [0, 0, 0];
for (let i = 0; i <= n-1; i++) {
let a = points[i][0]*points[i+1][1] - points[i+1][0]*points[i][1];
sA += a;
sX += (points[i][0] + points[i+1][0]) * a;
sY += (points[i][1] + points[i+1][1]) * a;
}
points.pop();
if (sA == 0)
return getNaiveCenter(points);
return [sX / (3 * sA), sY / (3 * sA)];
};
/*
Cubic Bézier:
[0, 1] -> ℝ², P(t) = (1-t)³P₀ + 3t(1-t)²P₁ + 3(1-t)P₂ + t³P₃
general case:
const cubicBezierCoord = function(x0, x1, x2, x3, t) {
return (1-t)**3*x0 + 3*t*(1-t)**2*x1 + 3*t**2*(1-t)*x2 + t**3*x3;
}
const cubicBezierPoint = function(p0, p1, p2, p3, t) {
return [cubicBezier(p0[0], p1[0], p2[0], p3[0], t), cubicBezier(p0[1], p1[1], p2[1], p3[1], t)];
}
Approximatively:
control point: p0 ---- p1 ---- p2 ---- p3 (p2 is not on the curve)
t: 0 ---- 1/3 ---- 2/3 ---- 1
*/
// If the curve has a symmetry axis, it is truly a center (the intersection of the curve and the axis).
// In other cases, it is not a notable point, just a visual approximation.
const getCurveCenter = function(p0, p1, p2, p3) {
if (p0[0] == p1[0] && p0[1] == p1[1])
// p0 = p1, t = 2/3
return [(p1[0] + 6*p1[0] + 12*p2[0] + 8*p3[0]) / 27, (p1[1] + 6*p1[1] + 12*p2[1] + 8*p3[1]) / 27];
else
// t = 1/2
return [(p0[0] + 3*p1[0] + 3*p2[0] + p3[0]) / 8, (p0[1] + 3*p1[1] + 3*p2[1] + p3[1]) / 8];
};
const getAngle = function(xO, yO, xA, yA, xB, yB) {
// calculate angle of rotation in absolute value
// cos(AOB) = (OA.OB)/(||OA||*||OB||) where OA.OB = (xA-xO)*(xB-xO) + (yA-yO)*(yB-yO)
let cos = ((xA - xO)*(xB - xO) + (yA - yO)*(yB - yO)) / (Math.hypot(xA - xO, yA - yO) * Math.hypot(xB - xO, yB - yO));
// acos is defined on [-1, 1] but
// with A == B and imperfect computer calculations, cos may be equal to 1.00000001.
cos = Math.min(Math.max(-1, cos), 1);
let angle = Math.acos( cos );
// determine the sign of the angle
if (xA == xO) {
if (xB > xO)
angle = -angle;
} else {
// equation of OA: y = ax + b
let a = (yA - yO) / (xA - xO);
let b = yA - a*xA;
if (yB < a*xB + b)
angle = - angle;
if (xA < xO)
angle = - angle;
}
return angle;
};

View File

@ -35,7 +35,8 @@ const PanelMenu = imports.ui.panelMenu;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Convenience = ExtensionUtils.getSettings && ExtensionUtils.initTranslations ? ExtensionUtils : Me.imports.convenience;
const Draw = Me.imports.draw;
const Area = Me.imports.area;
const Helper = Me.imports.helper;
const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext;
const GS_VERSION = Config.PACKAGE_VERSION;
@ -154,9 +155,9 @@ var AreaManager = new Lang.Class({
for (let i = 0; i < this.monitors.length; i++) {
let monitor = this.monitors[i];
let container = new St.Widget({ name: 'drawOnYourSreenContainer' + i });
let helper = new Draw.DrawingHelper({ name: 'drawOnYourSreenHelper' + i }, monitor);
let helper = new Helper.DrawingHelper({ name: 'drawOnYourSreenHelper' + i }, monitor);
let loadPersistent = i == Main.layoutManager.primaryIndex && this.settings.get_boolean('persistent-drawing');
let area = new Draw.DrawingArea({ name: 'drawOnYourSreenArea' + i }, monitor, helper, loadPersistent);
let area = new Area.DrawingArea({ name: 'drawOnYourSreenArea' + i }, monitor, helper, loadPersistent);
container.add_child(area);
container.add_child(helper);
@ -170,6 +171,7 @@ var AreaManager = new Lang.Class({
area.leaveDrawingHandler = area.connect('leave-drawing-mode', this.toggleDrawing.bind(this));
area.updateActionModeHandler = area.connect('update-action-mode', this.updateActionMode.bind(this));
area.showOsdHandler = area.connect('show-osd', this.showOsd.bind(this));
area.showOsdGiconHandler = area.connect('show-osd-gicon', this.showOsd.bind(this));
this.areas.push(area);
}
},
@ -185,21 +187,23 @@ var AreaManager = new Lang.Class({
'decrement-line-width': () => this.activeArea.incrementLineWidth(-1),
'increment-line-width-more': () => this.activeArea.incrementLineWidth(5),
'decrement-line-width-more': () => this.activeArea.incrementLineWidth(-5),
'toggle-linejoin': this.activeArea.toggleLineJoin.bind(this.activeArea),
'toggle-linecap': this.activeArea.toggleLineCap.bind(this.activeArea),
'toggle-fill-rule': this.activeArea.toggleFillRule.bind(this.activeArea),
'toggle-dash' : this.activeArea.toggleDash.bind(this.activeArea),
'toggle-fill' : this.activeArea.toggleFill.bind(this.activeArea),
'select-none-shape': () => this.activeArea.selectTool(Draw.Tools.NONE),
'select-line-shape': () => this.activeArea.selectTool(Draw.Tools.LINE),
'select-ellipse-shape': () => this.activeArea.selectTool(Draw.Tools.ELLIPSE),
'select-rectangle-shape': () => this.activeArea.selectTool(Draw.Tools.RECTANGLE),
'select-text-shape': () => this.activeArea.selectTool(Draw.Tools.TEXT),
'select-polygon-shape': () => this.activeArea.selectTool(Draw.Tools.POLYGON),
'select-polyline-shape': () => this.activeArea.selectTool(Draw.Tools.POLYLINE),
'select-move-tool': () => this.activeArea.selectTool(Draw.Tools.MOVE),
'select-resize-tool': () => this.activeArea.selectTool(Draw.Tools.RESIZE),
'select-mirror-tool': () => this.activeArea.selectTool(Draw.Tools.MIRROR)
'switch-linejoin': this.activeArea.switchLineJoin.bind(this.activeArea),
'switch-linecap': this.activeArea.switchLineCap.bind(this.activeArea),
'switch-fill-rule': this.activeArea.switchFillRule.bind(this.activeArea),
'switch-dash' : this.activeArea.switchDash.bind(this.activeArea),
'switch-fill' : this.activeArea.switchFill.bind(this.activeArea),
'switch-image-file' : this.activeArea.switchImageFile.bind(this.activeArea),
'select-none-shape': () => this.activeArea.selectTool(Area.Tools.NONE),
'select-line-shape': () => this.activeArea.selectTool(Area.Tools.LINE),
'select-ellipse-shape': () => this.activeArea.selectTool(Area.Tools.ELLIPSE),
'select-rectangle-shape': () => this.activeArea.selectTool(Area.Tools.RECTANGLE),
'select-text-shape': () => this.activeArea.selectTool(Area.Tools.TEXT),
'select-image-shape': () => this.activeArea.selectTool(Area.Tools.IMAGE),
'select-polygon-shape': () => this.activeArea.selectTool(Area.Tools.POLYGON),
'select-polyline-shape': () => this.activeArea.selectTool(Area.Tools.POLYLINE),
'select-move-tool': () => this.activeArea.selectTool(Area.Tools.MOVE),
'select-resize-tool': () => this.activeArea.selectTool(Area.Tools.RESIZE),
'select-mirror-tool': () => this.activeArea.selectTool(Area.Tools.MIRROR)
};
// available when writing
@ -211,10 +215,11 @@ var AreaManager = new Lang.Class({
'toggle-background': this.activeArea.toggleBackground.bind(this.activeArea),
'toggle-grid': this.activeArea.toggleGrid.bind(this.activeArea),
'toggle-square-area': this.activeArea.toggleSquareArea.bind(this.activeArea),
'toggle-font-family': this.activeArea.toggleFontFamily.bind(this.activeArea),
'toggle-font-weight': this.activeArea.toggleFontWeight.bind(this.activeArea),
'toggle-font-style': this.activeArea.toggleFontStyle.bind(this.activeArea),
'toggle-text-alignment': this.activeArea.toggleTextAlignment.bind(this.activeArea),
'reverse-switch-font-family': this.activeArea.switchFontFamily.bind(this.activeArea, true),
'switch-font-family': this.activeArea.switchFontFamily.bind(this.activeArea, false),
'switch-font-weight': this.activeArea.switchFontWeight.bind(this.activeArea),
'switch-font-style': this.activeArea.switchFontStyle.bind(this.activeArea),
'switch-text-alignment': this.activeArea.switchTextAlignment.bind(this.activeArea),
'toggle-panel-and-dock-visibility': this.togglePanelAndDockOpacity.bind(this),
'toggle-help': this.activeArea.toggleHelp.bind(this.activeArea),
'open-user-stylesheet': this.openUserStyleFile.bind(this),
@ -504,6 +509,7 @@ var AreaManager = new Lang.Class({
area.disconnect(area.leaveDrawingHandler);
area.disconnect(area.updateActionModeHandler);
area.disconnect(area.showOsdHandler);
area.disconnect(area.showOsdGiconHandler);
let container = area.get_parent();
container.get_parent().remove_actor(container);
container.destroy();

241
files.js Normal file
View File

@ -0,0 +1,241 @@
/* 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 <http://www.gnu.org/licenses/>.
*/
const ByteArray = imports.byteArray;
const Gdk = imports.gi.Gdk;
const GdkPixbuf = imports.gi.GdkPixbuf;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Lang = imports.lang;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const EXAMPLE_IMAGES = Me.dir.get_child('data').get_child('images');
const USER_IMAGES = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir'], 'images']));
// wrapper around an image file
var Image = new Lang.Class({
Name: 'DrawOnYourScreenImage',
_init: function(params) {
for (let key in params)
this[key] = params[key];
},
toString: function() {
return this.displayName;
},
toJson: function() {
return {
displayName: this.displayName,
contentType: this.contentType,
base64: this.base64,
hash: this.hash
};
},
// only called from menu so file exists
get gicon() {
if (!this._gicon)
this._gicon = new Gio.FileIcon({ file: this.file });
return this._gicon;
},
get bytes() {
if (!this._bytes) {
if (this.file)
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));
}
return this._bytes;
},
get base64() {
if (!this._base64)
this._base64 = GLib.base64_encode(this.bytes.get_data());
return this._base64;
},
set base64(base64) {
this._base64 = base64;
},
// hash is not used
get hash() {
if (!this._hash)
this._hash = this.bytes.hash();
return this._hash;
},
set hash(hash) {
this._hash = hash;
},
get pixbuf() {
if (!this._pixbuf) {
let stream = Gio.MemoryInputStream.new_from_bytes(this.bytes);
this._pixbuf = GdkPixbuf.Pixbuf.new_from_stream(stream, null);
stream.close(null);
}
return this._pixbuf;
},
getPixbufAtScale: function(width, height) {
let stream = Gio.MemoryInputStream.new_from_bytes(this.bytes);
let pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale(stream, width, height, true, null);
stream.close(null);
return pixbuf;
},
setCairoSource: function(cr, x, y, width, height, preserveAspectRatio) {
let pixbuf = preserveAspectRatio ? this.getPixbufAtScale(width, height)
: this.pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR);
Gdk.cairo_set_source_pixbuf(cr, pixbuf, x, y);
}
});
var getImages = function() {
let images = [];
[EXAMPLE_IMAGES, USER_IMAGES].forEach(directory => {
let enumerator;
try {
enumerator = directory.enumerate_children('standard::display-name,standard::content-type', Gio.FileQueryInfoFlags.NONE, null);
} catch(e) {
return;
}
let fileInfo = enumerator.next_file(null);
while (fileInfo) {
if (fileInfo.get_content_type().indexOf('image') == 0)
images.push(new Image({ file: enumerator.get_child(fileInfo), contentType: fileInfo.get_content_type(), displayName: fileInfo.get_display_name() }));
fileInfo = enumerator.next_file(null);
}
enumerator.close(null);
});
images.sort((a, b) => {
return a.displayName.localeCompare(b.displayName);
});
return images;
};
// wrapper around a json file
var Json = new Lang.Class({
Name: 'DrawOnYourScreenJson',
_init: function(params) {
for (let key in params)
this[key] = params[key];
},
toString: function() {
return this.displayName || this.name;
},
delete: function() {
this.file.delete(null);
},
get file() {
if (!this._file && this.name)
this._file = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir'], `${this.name}.json`]));
return this._file || null;
},
set file(file) {
this._file = file;
},
get contents() {
let success_, contents;
try {
[success_, contents] = this.file.load_contents(null);
if (contents instanceof Uint8Array)
contents = ByteArray.toString(contents);
} catch(e) {
return null;
}
return contents;
},
set contents(contents) {
try {
this.file.replace_contents(contents, null, false, Gio.FileCreateFlags.NONE, null);
} catch(e) {
this.file.get_parent().make_directory_with_parents(null);
this.file.replace_contents(contents, null, false, Gio.FileCreateFlags.NONE, null);
}
}
});
var getJsons = function() {
let directory = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir']]));
let enumerator;
try {
enumerator = directory.enumerate_children('standard::name,standard::display-name,standard::content-type,time::modified', Gio.FileQueryInfoFlags.NONE, null);
} catch(e) {
return [];
}
let jsons = [];
let fileInfo = enumerator.next_file(null);
while (fileInfo) {
if (fileInfo.get_content_type().indexOf('json') != -1 && fileInfo.get_name() != `${Me.metadata['persistent-file-name']}.json`) {
let file = enumerator.get_child(fileInfo);
jsons.push(new Json({
file,
name: fileInfo.get_name().slice(0, -5),
displayName: fileInfo.get_display_name().slice(0, -5),
// fileInfo.get_modification_date_time: Gio 2.62+
modificationUnixTime: fileInfo.get_attribute_uint64('time::modified')
}));
}
fileInfo = enumerator.next_file(null);
}
enumerator.close(null);
jsons.sort((a, b) => {
return b.modificationUnixTime - a.modificationUnixTime;
});
return jsons;
};
var getDateString = function() {
let date = GLib.DateTime.new_now_local();
return `${date.format("%F")} ${date.format("%X")}`;
};

186
helper.js Normal file
View File

@ -0,0 +1,186 @@
/* 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 <http://www.gnu.org/licenses/>.
*/
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;
const St = imports.gi.St;
const Config = imports.misc.config;
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 _ = 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"
};
// DrawingHelper provides the "help osd" (Ctrl + F1)
// It uses the same texts as in prefs
var DrawingHelper = new Lang.Class({
Name: 'DrawOnYourScreenDrawingHelper',
Extends: St.ScrollView,
_init: function(params, monitor) {
params.style_class = 'osd-window draw-on-your-screen-helper';
this.parent(params);
this.monitor = monitor;
this.hide();
this.settings = Convenience.getSettings();
this.settingHandler = this.settings.connect('changed', this._onSettingChanged.bind(this));
this.connect('destroy', () => this.settings.disconnect(this.settingHandler));
},
_onSettingChanged: function(settings, key) {
if (key == 'toggle-help')
this._updateHelpKeyLabel();
if (this.vbox) {
this.vbox.destroy();
this.vbox = null;
}
},
_updateHelpKeyLabel: function() {
let [keyval, mods] = Gtk.accelerator_parse(this.settings.get_strv('toggle-help')[0]);
this._helpKeyLabel = Gtk.accelerator_get_label(keyval, mods);
},
get helpKeyLabel() {
if (!this._helpKeyLabel)
this._updateHelpKeyLabel();
return this._helpKeyLabel;
},
_populate: function() {
this.vbox = new St.BoxLayout({ vertical: true });
this.add_actor(this.vbox);
this.vbox.add_child(new St.Label({ text: _("Global") }));
for (let settingKey in Prefs.GLOBAL_KEYBINDINGS) {
let hbox = new St.BoxLayout({ vertical: false });
if (settingKey.indexOf('-separator-') != -1) {
this.vbox.add_child(hbox);
continue;
}
if (!this.settings.get_strv(settingKey)[0])
continue;
let [keyval, mods] = Gtk.accelerator_parse(this.settings.get_strv(settingKey)[0]);
hbox.add_child(new St.Label({ text: _(Prefs.GLOBAL_KEYBINDINGS[settingKey]) }));
hbox.add_child(new St.Label({ text: Gtk.accelerator_get_label(keyval, mods), x_expand: true }));
this.vbox.add_child(hbox);
}
this.vbox.add_child(new St.Label({ text: _("Internal") }));
for (let i = 0; i < Prefs.OTHER_SHORTCUTS.length; i++) {
if (Prefs.OTHER_SHORTCUTS[i].desc.indexOf('-separator-') != -1) {
this.vbox.add_child(new St.BoxLayout({ vertical: false, style_class: 'draw-on-your-screen-helper-separator' }));
continue;
}
let hbox = new St.BoxLayout({ vertical: false });
hbox.add_child(new St.Label({ text: _(Prefs.OTHER_SHORTCUTS[i].desc) }));
hbox.add_child(new St.Label({ text: Prefs.OTHER_SHORTCUTS[i].shortcut, x_expand: true }));
hbox.get_children()[0].get_clutter_text().set_use_markup(true);
this.vbox.add_child(hbox);
}
this.vbox.add_child(new St.BoxLayout({ vertical: false, style_class: 'draw-on-your-screen-helper-separator' }));
for (let settingKey in Prefs.INTERNAL_KEYBINDINGS) {
if (settingKey.indexOf('-separator-') != -1) {
this.vbox.add_child(new St.BoxLayout({ vertical: false, style_class: 'draw-on-your-screen-helper-separator' }));
continue;
}
let hbox = new St.BoxLayout({ vertical: false });
if (!this.settings.get_strv(settingKey)[0])
continue;
let [keyval, mods] = Gtk.accelerator_parse(this.settings.get_strv(settingKey)[0]);
hbox.add_child(new St.Label({ text: _(Prefs.INTERNAL_KEYBINDINGS[settingKey]) }));
hbox.add_child(new St.Label({ text: Gtk.accelerator_get_label(keyval, mods), x_expand: true }));
this.vbox.add_child(hbox);
}
let mediaKeysSettings;
try { mediaKeysSettings = Convenience.getSettings(MEDIA_KEYS_SCHEMA); } catch(e) { return; }
this.vbox.add_child(new St.Label({ text: _("System") }));
for (let settingKey in MEDIA_KEYS_KEYS) {
if (!mediaKeysSettings.settings_schema.has_key(settingKey))
continue;
let shortcut = GS_VERSION < '3.33.0' ? mediaKeysSettings.get_string(settingKey) : mediaKeysSettings.get_strv(settingKey)[0];
if (!shortcut)
continue;
let [keyval, mods] = Gtk.accelerator_parse(shortcut);
let hbox = new St.BoxLayout({ vertical: false });
hbox.add_child(new St.Label({ text: _(MEDIA_KEYS_KEYS[settingKey]) }));
hbox.add_child(new St.Label({ text: Gtk.accelerator_get_label(keyval, mods), x_expand: true }));
this.vbox.add_child(hbox);
}
},
showHelp: function() {
if (!this.vbox)
this._populate();
this.opacity = 0;
this.show();
let maxHeight = this.monitor.height * 3 / 4;
this.set_height(Math.min(this.height, maxHeight));
this.set_position(Math.floor(this.monitor.width / 2 - this.width / 2),
Math.floor(this.monitor.height / 2 - this.height / 2));
if (this.height == maxHeight)
this.vscrollbar_policy = Gtk.PolicyType.ALWAYS;
else
this.vscrollbar_policy = Gtk.PolicyType.NEVER;
Tweener.removeTweens(this);
Tweener.addTween(this, { opacity: 255,
time: HELPER_ANIMATION_TIME,
transition: 'easeOutQuad',
onComplete: null });
},
hideHelp: function() {
Tweener.removeTweens(this);
Tweener.addTween(this, { opacity: 0,
time: HELPER_ANIMATION_TIME,
transition: 'easeOutQuad',
onComplete: this.hide.bind(this) });
}
});

View File

@ -179,6 +179,9 @@ msgstr ""
msgid "Select polyline"
msgstr ""
msgid "Select image"
msgstr ""
msgid "Select text"
msgstr ""
@ -215,7 +218,10 @@ msgstr ""
msgid "Toggle fill rule"
msgstr ""
msgid "Change font family (generic name)"
msgid "Change font family"
msgstr ""
msgid "Change font family (reverse)"
msgstr ""
msgid "Change font weight"
@ -227,6 +233,9 @@ msgstr ""
msgid "Toggle text alignment"
msgstr ""
msgid "Change image file"
msgstr ""
msgid "Hide panel and dock"
msgstr ""
@ -316,6 +325,9 @@ msgstr ""
msgid "Polyline"
msgstr ""
msgid "Image"
msgstr ""
msgid "Move"
msgstr ""

655
menu.js Normal file
View File

@ -0,0 +1,655 @@
/* 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 <http://www.gnu.org/licenses/>.
*/
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 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 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 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;
const getActor = function(object) {
return GS_VERSION < '3.33.0' ? object.actor : object;
};
var DrawingMenu = new Lang.Class({
Name: 'DrawOnYourScreenDrawingMenu',
_init: function(area, monitor) {
this.area = area;
let side = Clutter.get_default_text_direction() == Clutter.TextDirection.RTL ? St.Side.RIGHT : St.Side.LEFT;
this.menu = new PopupMenu.PopupMenu(Main.layoutManager.dummyCursor, 0.25, side);
this.menuManager = new PopupMenu.PopupMenuManager(GS_VERSION < '3.33.0' ? { actor: this.area } : this.area);
this.menuManager.addMenu(this.menu);
Main.layoutManager.uiGroup.add_actor(this.menu.actor);
this.menu.actor.add_style_class_name('background-menu draw-on-your-screen-menu');
this.menu.actor.set_style('max-height:' + monitor.height + 'px;');
this.menu.actor.hide();
this.hasSeparators = monitor.height >= 750;
// do not close the menu on item activated
this.menu.itemActivated = () => {};
this.menu.connect('open-state-changed', this._onMenuOpenStateChanged.bind(this));
// Case where the menu is closed (escape key) while the save entry clutter_text is active:
// St.Entry clutter_text set the DEFAULT cursor on leave event with a delay and
// overrides the cursor set by area.updatePointerCursor().
// In order to update drawing cursor on menu closed, we need to leave the saveEntry before closing menu.
// Since escape key press event can't be captured easily, the job is done in the menu close function.
let menuCloseFunc = this.menu.close;
this.menu.close = (animate) => {
if (this.saveDrawingSubMenu && this.saveDrawingSubMenu.isOpen)
this.saveDrawingSubMenu.close();
menuCloseFunc.bind(this.menu)(animate);
};
this.colorIcon = new Gio.FileIcon({ file: Gio.File.new_for_path(COLOR_ICON_PATH) });
this.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() {
this.menuManager.removeMenu(this.menu);
Main.layoutManager.uiGroup.remove_actor(this.menu.actor);
this.menu.destroy();
},
_onMenuOpenStateChanged: function(menu, open) {
if (open) {
this.area.setPointerCursor('DEFAULT');
} else {
this.area.updatePointerCursor();
// actionMode has changed, set previous actionMode in order to keep internal shortcuts working
this.area.updateActionMode();
this.area.grab_key_focus();
}
},
popup: function() {
if (this.menu.isOpen) {
this.close();
} else {
this.open();
this.menu.actor.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false);
}
},
open: function(x, y) {
if (this.menu.isOpen)
return;
if (x === undefined || y === undefined)
[x, y] = [this.area.monitor.x + this.area.monitor.width / 2, this.area.monitor.y + this.area.monitor.height / 2];
this._redisplay();
Main.layoutManager.setDummyCursorGeometry(x, y, 0, 0);
let monitor = this.area.monitor;
this.menu._arrowAlignment = (y - monitor.y) / monitor.height;
this.menu.open(BoxPointer.PopupAnimation.NONE);
this.menuManager.ignoreRelease();
},
close: function() {
if (this.menu.isOpen)
this.menu.close();
},
_redisplay: function() {
this.menu.removeAll();
this.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));
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.fillSection = new PopupMenu.PopupMenuSection();
this.fillSection.itemActivated = () => {};
this.fillRuleItem = this._addSwitchItem(this.fillSection, _("Evenodd"), this.fillRuleNonzeroIcon, this.fillRuleEvenoddIcon, this.area, 'currentEvenodd');
this.menu.addMenuItem(this.fillSection);
this._addSeparator(this.menu);
let lineSection = new PopupMenu.PopupMenuSection();
this._addSliderItem(lineSection, this.area, 'currentLineWidth');
this._addSubMenuItem(lineSection, this.linejoinIcon, Elements.LineJoinNames, this.area, 'currentLineJoin');
this._addSubMenuItem(lineSection, this.linecapIcon, Elements.LineCapNames, this.area, 'currentLineCap');
this._addSwitchItem(lineSection, _("Dashed"), this.fullLineIcon, this.dashedLineIcon, this.area, 'dashedLine');
this._addSeparator(lineSection);
this.menu.addMenuItem(lineSection);
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._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._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));
this._addSeparator(this.menu);
this._addDrawingNameItem(this.menu);
this._addOpenDrawingSubMenuItem(this.menu);
this._addSaveDrawingSubMenuItem(this.menu);
this.menu.addAction(_("Save drawing as a SVG file"), this.area.saveAsSvg.bind(this.area), 'image-x-generic-symbolic');
this.menu.addAction(_("Edit style"), manager.openUserStyleFile.bind(manager), 'document-page-setup-symbolic');
this.menu.addAction(_("Show help"), () => { this.close(); this.area.toggleHelp(); }, 'preferences-desktop-keyboard-shortcuts-symbolic');
this._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;
},
_updateSectionVisibility: function() {
let [isText, isImage] = [this.area.currentTool == Area.Tools.TEXT, this.area.currentTool == Area.Tools.IMAGE];
this.lineSection.actor.visible = !isText && !isImage;
this.fontSection.actor.visible = isText;
this.imageSection.actor.visible = isImage;
this.colorItem.setSensitive(!isImage);
this.fillItem.setSensitive(!isText && !isImage);
this.fillSection.setSensitive(!isText && !isImage);
if (this.area.fill)
this.fillSection.actor.show();
else
this.fillSection.actor.hide();
},
_addSwitchItem: function(menu, label, iconFalse, iconTrue, target, targetProperty, onToggled) {
let item = new PopupMenu.PopupSwitchMenuItem(label, target[targetProperty]);
item.icon = new St.Icon({ style_class: 'popup-menu-icon' });
getActor(item).insert_child_at_index(item.icon, 1);
let icon = target[targetProperty] ? iconTrue : iconFalse;
if (icon && icon instanceof GObject.Object && GObject.type_is_a(icon, Gio.Icon))
item.icon.set_gicon(icon);
else if (icon)
item.icon.set_icon_name(icon);
item.connect('toggled', (item, state) => {
target[targetProperty] = state;
let icon = target[targetProperty] ? iconTrue : iconFalse;
if (icon && icon instanceof GObject.Object && GObject.type_is_a(icon, Gio.Icon))
item.icon.set_gicon(icon);
else if (icon)
item.icon.set_icon_name(icon);
if (onToggled)
onToggled();
});
menu.addMenuItem(item);
return item;
},
_addSimpleSwitchItem: function(menu, label, active, onToggled) {
let item = new PopupMenu.PopupSwitchMenuItem(label, active);
item.connect('toggled', onToggled);
menu.addMenuItem(item);
},
_addSliderItem: function(menu, target, targetProperty) {
let item = new PopupMenu.PopupBaseMenuItem({ activate: false });
let label = new St.Label({ text: _("%d px").format(target[targetProperty]), style_class: 'draw-on-your-screen-menu-slider-label' });
let slider = new Slider.Slider(target[targetProperty] / 50);
if (GS_VERSION < '3.33.0') {
slider.connect('value-changed', (slider, value, property) => {
target[targetProperty] = Math.max(Math.round(value * 50), 0);
label.set_text(target[targetProperty] + " px");
if (target[targetProperty] === 0)
label.add_style_class_name(Extension.WARNING_COLOR_STYLE_CLASS_NAME);
else
label.remove_style_class_name(Extension.WARNING_COLOR_STYLE_CLASS_NAME);
});
} else {
slider.connect('notify::value', () => {
target[targetProperty] = Math.max(Math.round(slider.value * 50), 0);
label.set_text(target[targetProperty] + " px");
if (target[targetProperty] === 0)
label.add_style_class_name(Extension.WARNING_COLOR_STYLE_CLASS_NAME);
else
label.remove_style_class_name(Extension.WARNING_COLOR_STYLE_CLASS_NAME);
});
}
getActor(slider).x_expand = true;
getActor(item).add_child(getActor(slider));
getActor(item).add_child(label);
if (slider.onKeyPressEvent)
getActor(item).connect('key-press-event', slider.onKeyPressEvent.bind(slider));
menu.addMenuItem(item);
},
_addSubMenuItem: function(menu, icon, obj, target, targetProperty, callback) {
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);
item.menu.itemActivated = () => {
item.menu.close();
};
GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
for (let i in obj) {
let text;
if (targetProperty == 'currentFontWeight')
text = `<span font_weight="${i}">${_(obj[i])}</span>`;
else if (targetProperty == 'currentFontStyle')
text = `<span font_style="${obj[i].toLowerCase()}">${_(obj[i])}</span>`;
else
text = _(String(obj[i]));
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();
});
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);
}
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);
item.menu.itemActivated = () => {
item.menu.close();
};
item.menu.openOld = item.menu.open;
item.menu.open = (animate) => {
if (!item.menu.isOpen && item.menu.isEmpty()) {
this.area.fontFamilies.forEach(family => {
let subItem = item.menu.addAction(_(family), () => {
item.label.set_text(_(family));
this.area.currentFontFamily = family;
});
if (FONT_FAMILY_STYLE)
subItem.label.set_style(`font-family:${family}`);
getActor(subItem).connect('key-focus-in', updateSubMenuAdjustment);
});
}
item.menu.openOld();
};
menu.addMenuItem(item);
},
_addDrawingNameItem: function(menu) {
this.drawingNameMenuItem = new PopupMenu.PopupMenuItem('', { reactive: false, activate: false });
this.drawingNameMenuItem.setSensitive(false);
menu.addMenuItem(this.drawingNameMenuItem);
this._updateDrawingNameMenuItem();
},
_updateDrawingNameMenuItem: function() {
getActor(this.drawingNameMenuItem).visible = this.area.jsonName ? true : false;
if (this.area.jsonName) {
let prefix = this.area.drawingContentsHasChanged ? "* " : "";
this.drawingNameMenuItem.label.set_text(`<i>${prefix}${this.area.jsonName}</i>`);
this.drawingNameMenuItem.label.get_clutter_text().set_use_markup(true);
}
},
_addOpenDrawingSubMenuItem: function(menu) {
let item = new PopupMenu.PopupSubMenuMenuItem(_("Open drawing"), true);
this.openDrawingSubMenuItem = item;
this.openDrawingSubMenu = item.menu;
item.setSensitive(Boolean(Files.getJsons().length));
item.icon.set_icon_name('document-open-symbolic');
item.menu.itemActivated = () => {
item.menu.close();
};
item.menu.openOld = item.menu.open;
item.menu.open = (animate) => {
if (!item.menu.isOpen)
this._populateOpenDrawingSubMenu();
item.menu.openOld();
};
menu.addMenuItem(item);
},
_populateOpenDrawingSubMenu: function() {
this.openDrawingSubMenu.removeAll();
let jsons = Files.getJsons();
jsons.forEach(json => {
let subItem = this.openDrawingSubMenu.addAction(`<i>${String(json)}</i>`, () => {
this.area.loadJson(json.name);
this._updateDrawingNameMenuItem();
this._updateSaveDrawingSubMenuItemSensitivity();
});
subItem.label.get_clutter_text().set_use_markup(true);
getActor(subItem).connect('key-focus-in', updateSubMenuAdjustment);
let expander = new St.Bin({
style_class: 'popup-menu-item-expander',
x_expand: true,
});
getActor(subItem).add_child(expander);
let deleteButton = new St.Button({ style_class: 'draw-on-your-screen-menu-delete-button',
child: new St.Icon({ icon_name: 'edit-delete-symbolic',
style_class: 'popup-menu-icon',
x_align: Clutter.ActorAlign.END }) });
getActor(subItem).add_child(deleteButton);
deleteButton.connect('clicked', () => {
json.delete();
subItem.destroy();
this.openDrawingSubMenuItem.setSensitive(!this.openDrawingSubMenu.isEmpty());
});
});
this.openDrawingSubMenuItem.setSensitive(!this.openDrawingSubMenu.isEmpty());
},
_addSaveDrawingSubMenuItem: function(menu) {
let item = new PopupMenu.PopupSubMenuMenuItem(_("Save drawing"), true);
this.saveDrawingSubMenuItem = item;
this._updateSaveDrawingSubMenuItemSensitivity();
this.saveDrawingSubMenu = item.menu;
item.icon.set_icon_name('document-save-symbolic');
item.menu.itemActivated = () => {
item.menu.close();
};
item.menu.openOld = item.menu.open;
item.menu.open = (animate) => {
if (!item.menu.isOpen)
this._populateSaveDrawingSubMenu();
item.menu.openOld();
};
menu.addMenuItem(item);
},
_updateSaveDrawingSubMenuItemSensitivity: function() {
this.saveDrawingSubMenuItem.setSensitive(this.area.elements.length > 0);
},
_onDrawingSaved() {
this._updateDrawingNameMenuItem();
this.openDrawingSubMenuItem.setSensitive(true);
},
_populateSaveDrawingSubMenu: function() {
this.saveDrawingSubMenu.removeAll();
let saveEntry = new DrawingMenuEntry({ initialTextGetter: Files.getDateString,
entryActivateCallback: (text) => {
this.area.saveAsJsonWithName(text, this._onDrawingSaved.bind(this));
this.saveDrawingSubMenu.toggle();
},
invalidStrings: [Me.metadata['persistent-file-name'], '/'],
primaryIconName: 'insert-text' });
this.saveDrawingSubMenu.addMenuItem(saveEntry.item);
},
_addSeparator: function(menu, thin) {
if (this.hasSeparators) {
let separatorItem = new PopupMenu.PopupSeparatorMenuItem(' ');
getActor(separatorItem).add_style_class_name('draw-on-your-screen-menu-separator-item');
if (thin)
getActor(separatorItem).add_style_class_name('draw-on-your-screen-menu-thin-separator-item');
menu.addMenuItem(separatorItem);
}
}
});
// based on ApplicationsButton.scrollToButton , https://gitlab.gnome.org/GNOME/gnome-shell-extensions/blob/master/extensions/apps-menu/extension.js
const updateSubMenuAdjustment = function(itemActor) {
let scrollView = itemActor.get_parent().get_parent();
let adjustment = scrollView.get_vscroll_bar().get_adjustment();
let scrollViewAlloc = scrollView.get_allocation_box();
let currentScrollValue = adjustment.get_value();
let height = scrollViewAlloc.y2 - scrollViewAlloc.y1;
let itemActorAlloc = itemActor.get_allocation_box();
let newScrollValue = currentScrollValue;
if (currentScrollValue > itemActorAlloc.y1 - 10)
newScrollValue = itemActorAlloc.y1 - 10;
if (height + currentScrollValue < itemActorAlloc.y2 + 10)
newScrollValue = itemActorAlloc.y2 - height + 10;
if (newScrollValue != currentScrollValue)
adjustment.set_value(newScrollValue);
};
// based on searchItem.js, https://github.com/leonardo-bartoli/gnome-shell-extension-Recents
const DrawingMenuEntry = new Lang.Class({
Name: 'DrawOnYourScreenDrawingMenuEntry',
_init: function(params) {
this.params = params;
this.item = new PopupMenu.PopupBaseMenuItem({ style_class: 'draw-on-your-screen-menu-entry-item',
activate: false,
reactive: true,
can_focus: false });
this.itemActor = GS_VERSION < '3.33.0' ? this.item.actor : this.item;
this.entry = new St.Entry({
style_class: 'search-entry draw-on-your-screen-menu-entry',
track_hover: true,
reactive: true,
can_focus: true,
x_expand: true
});
this.entry.set_primary_icon(new St.Icon({ style_class: 'search-entry-icon',
icon_name: this.params.primaryIconName }));
this.entry.clutter_text.connect('text-changed', this._onTextChanged.bind(this));
this.entry.clutter_text.connect('activate', this._onTextActivated.bind(this));
this.clearIcon = new St.Icon({
style_class: 'search-entry-icon',
icon_name: 'edit-clear-symbolic'
});
this.entry.connect('secondary-icon-clicked', this._reset.bind(this));
getActor(this.item).add_child(this.entry);
getActor(this.item).connect('notify::mapped', (actor) => {
if (actor.mapped) {
this.entry.set_text(this.params.initialTextGetter());
this.entry.clutter_text.grab_key_focus();
}
});
},
_setError: function(hasError) {
if (hasError)
this.entry.add_style_class_name('draw-on-your-screen-menu-entry-error');
else
this.entry.remove_style_class_name('draw-on-your-screen-menu-entry-error');
},
_reset: function() {
this.entry.text = '';
this.entry.clutter_text.set_cursor_visible(true);
this.entry.clutter_text.set_selection(0, 0);
this._setError(false);
},
_onTextActivated: function(clutterText) {
let text = clutterText.get_text();
if (text.length == 0)
return;
if (this._getIsInvalid())
return;
this._reset();
this.params.entryActivateCallback(text);
},
_onTextChanged: function(clutterText) {
let text = clutterText.get_text();
this.entry.set_secondary_icon(text.length ? this.clearIcon : null);
if (text.length)
this._setError(this._getIsInvalid());
},
_getIsInvalid: function() {
for (let i = 0; i < this.params.invalidStrings.length; i++) {
if (this.entry.text.indexOf(this.params.invalidStrings[i]) != -1)
return true;
}
return false;
}
});

View File

@ -17,5 +17,5 @@
"3.34",
"3.36"
],
"version": 6.1
"version": 6.2
}

View File

@ -54,25 +54,28 @@ var INTERNAL_KEYBINDINGS = {
'select-polygon-shape': "Select polygon",
'select-polyline-shape': "Select polyline",
'select-text-shape': "Select text",
'select-image-shape': "Select image",
'select-move-tool': "Select move",
'select-resize-tool': "Select resize",
'select-mirror-tool': "Select mirror",
'-separator-2': '',
'toggle-fill': "Toggle fill/outline",
'toggle-fill-rule': "Toggle fill rule",
'switch-fill': "Toggle fill/outline",
'switch-fill-rule': "Toggle fill rule",
'-separator-3': '',
'increment-line-width': "Increment line width",
'decrement-line-width': "Decrement line width",
'increment-line-width-more': "Increment line width even more",
'decrement-line-width-more': "Decrement line width even more",
'toggle-linejoin': "Change linejoin",
'toggle-linecap': "Change linecap",
'toggle-dash': "Dashed line",
'switch-linejoin': "Change linejoin",
'switch-linecap': "Change linecap",
'switch-dash': "Dashed line",
'-separator-4': '',
'toggle-font-family': "Change font family (generic name)",
'toggle-font-weight': "Change font weight",
'toggle-font-style': "Change font style",
'toggle-text-alignment': "Toggle text alignment",
'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",
@ -392,26 +395,24 @@ const KeybindingsWidget = new GObject.Class({
accel_mode: Gtk.CellRendererAccelMode.GTK,
xalign: 1
});
keybinding_renderer.connect('accel-edited',
Lang.bind(this, function(renderer, iter, key, mods) {
let value = Gtk.accelerator_name(key, mods);
let [success, iterator ] =
this._store.get_iter_from_string(iter);
keybinding_renderer.connect('accel-edited', (renderer, iter, key, mods) => {
let value = Gtk.accelerator_name(key, mods);
let [success, iterator ] =
this._store.get_iter_from_string(iter);
if(!success) {
printerr("Can't change keybinding");
}
if(!success) {
printerr("Can't change keybinding");
}
let name = this._store.get_value(iterator, 0);
let name = this._store.get_value(iterator, 0);
this._store.set(
iterator,
[this._columns.MODS, this._columns.KEY],
[mods, key]
);
this._settings.set_strv(name, [value]);
})
);
this._store.set(
iterator,
[this._columns.MODS, this._columns.KEY],
[mods, key]
);
this._settings.set_strv(name, [value]);
});
let keybinding_column = new Gtk.TreeViewColumn({
title: "",

Binary file not shown.

View File

@ -106,6 +106,11 @@
<summary>select text</summary>
<description>select text</description>
</key>
<key type="as" name="select-image-shape">
<default>["&lt;Primary&gt;i"]</default>
<summary>select image</summary>
<description>select image</description>
</key>
<key type="as" name="select-none-shape">
<default>["&lt;Primary&gt;p"]</default>
<summary>unselect shape (free drawing)</summary>
@ -146,30 +151,30 @@
<summary>decrement the line width even more</summary>
<description>decrement the line width even more</description>
</key>
<key type="as" name="toggle-linejoin">
<key type="as" name="switch-linejoin">
<default>["&lt;Primary&gt;j"]</default>
<summary>toggle linejoin</summary>
<description>toggle linejoin</description>
<summary>switch linejoin</summary>
<description>switch linejoin</description>
</key>
<key type="as" name="toggle-linecap">
<key type="as" name="switch-linecap">
<default>["&lt;Primary&gt;k"]</default>
<summary>toggle linecap</summary>
<description>toggle linecap</description>
<summary>switch linecap</summary>
<description>switch linecap</description>
</key>
<key type="as" name="toggle-fill-rule">
<key type="as" name="switch-fill-rule">
<default><![CDATA[['<Primary>KP_Multiply','<Primary>asterisk','<Primary><Shift>asterisk']]]></default>
<summary>toggle fill rule</summary>
<description>toggle fill rule</description>
<summary>switch fill rule</summary>
<description>switch fill rule</description>
</key>
<key type="as" name="toggle-dash">
<key type="as" name="switch-dash">
<default>["&lt;Primary&gt;period"]</default>
<summary>toggle dash</summary>
<description>toggle dash</description>
<summary>switch dash</summary>
<description>switch dash</description>
</key>
<key type="as" name="toggle-fill">
<key type="as" name="switch-fill">
<default>["&lt;Primary&gt;a"]</default>
<summary>toggle fill</summary>
<description>toggle fill</description>
<summary>switch fill</summary>
<description>switch fill</description>
</key>
<key type="as" name="select-color1">
<default><![CDATA[['<Primary>KP_1','<Primary>1']]]></default>
@ -216,25 +221,35 @@
<summary>select color9</summary>
<description>select color9</description>
</key>
<key type="as" name="toggle-font-family">
<key type="as" name="switch-font-family">
<default>["&lt;Primary&gt;f"]</default>
<summary>toggle font family</summary>
<description>toggle font family</description>
<summary>switch font family</summary>
<description>switch font family</description>
</key>
<key type="as" name="toggle-font-weight">
<key type="as" name="reverse-switch-font-family">
<default>["&lt;Primary&gt;&lt;Shift&gt;f"]</default>
<summary>switch font family (reverse)</summary>
<description>switch font family (reverse)</description>
</key>
<key type="as" name="switch-font-weight">
<default>["&lt;Primary&gt;w"]</default>
<summary>toggle font weight</summary>
<description>toggle font weight</description>
<summary>switch font weight</summary>
<description>switch font weight</description>
</key>
<key type="as" name="toggle-font-style">
<default>["&lt;Primary&gt;i"]</default>
<summary>toggle font style</summary>
<description>toggle font style</description>
<key type="as" name="switch-font-style">
<default>["&lt;Primary&gt;&lt;Shift&gt;w"]</default>
<summary>switch font style</summary>
<description>switch font style</description>
</key>
<key type="as" name="toggle-text-alignment">
<key type="as" name="switch-text-alignment">
<default>["&lt;Primary&gt;&lt;Shift&gt;a"]</default>
<summary>toggle text alignment</summary>
<description>toggle text alignment</description>
<summary>switch text alignment</summary>
<description>switch text alignment</description>
</key>
<key type="as" name="switch-image-file">
<default>["&lt;Primary&gt;&lt;Shift&gt;i"]</default>
<summary>switch image file</summary>
<description>switch image file</description>
</key>
<key type="as" name="open-user-stylesheet">
<default>["&lt;Primary&gt;o"]</default>

View File

@ -58,6 +58,7 @@
}
.draw-on-your-screen-menu-separator-item {
margin-top: 0;
padding-top: 0.14em;
padding-bottom: 0.14em;
}
@ -71,6 +72,33 @@
margin-bottom: 0.2em; /* default 6px */
}
.draw-on-your-screen-menu-separator-item .popup-separator-menu-item-separator {
background-color: transparent;
}
.draw-on-your-screen-menu-thin-separator-item .popup-separator-menu-item-separator {
margin-top: 0;
}
/* system-menu-action: from GS 3.34- */
.draw-on-your-screen-menu .system-menu-action {
min-width: 0;
border: none;
border-radius: 32px;
padding: 12px;
margin: 0;
}
.draw-on-your-screen-menu .system-menu-action:hover,
.draw-on-your-screen-menu .system-menu-action:focus {
border: none;
padding: 12px;
}
.draw-on-your-screen-menu .system-menu-action > StIcon {
icon-size: 16px;
}
.draw-on-your-screen-menu-slider-label {
min-width: 3em;
text-align: right;