diff --git a/draw.js b/draw.js index af90830..4ebcaad 100644 --- a/draw.js +++ b/draw.js @@ -22,21 +22,35 @@ const Cairo = imports.cairo; const Clutter = imports.gi.Clutter; +const GdkPixbuf = imports.gi.GdkPixbuf; const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; const Gtk = imports.gi.Gtk; const Lang = imports.lang; const Mainloop = imports.mainloop; +const Shell = imports.gi.Shell; const Signals = imports.signals; const St = imports.gi.St; + +const BoxPointer = imports.ui.boxpointer; +const Main = imports.ui.main; +const PopupMenu = imports.ui.popupMenu; +const Slider = imports.ui.slider; const Screenshot = imports.ui.screenshot; const Tweener = imports.ui.tweener; const ExtensionUtils = imports.misc.extensionUtils; const Extension = ExtensionUtils.getCurrentExtension(); const Convenience = Extension.imports.convenience; +const ExtensionJs = Extension.imports.extension; const Prefs = Extension.imports.prefs; const _ = imports.gettext.domain(Extension.metadata["gettext-domain"]).gettext; +const FILL_ICON_PATH = Extension.dir.get_child('icons').get_child('fill-symbolic.svg').get_path(); +const LINEJOIN_ICON_PATH = Extension.dir.get_child('icons').get_child('linejoin-symbolic.svg').get_path(); +const LINECAP_ICON_PATH = Extension.dir.get_child('icons').get_child('linecap-symbolic.svg').get_path(); +const DASHED_LINE_ICON_PATH = Extension.dir.get_child('icons').get_child('dashed-line-symbolic.svg').get_path(); + var Shapes = { NONE: 0, LINE: 1, ELLIPSE: 2, RECTANGLE: 3, TEXT: 4 }; var ShapeNames = { 0: "Free drawing", 1: "Line", 2: "Ellipse", 3: "Rectangle", 4: "Text" }; var LineCapNames = { 0: 'Butt', 1: 'Round', 2: 'Square' }; @@ -60,6 +74,7 @@ var DrawingArea = new Lang.Class({ this.settings = Convenience.getSettings(); this.emitter = new DrawingAreaEmitter(); this.monitor = monitor; + this.menu = new DrawingMenu(this); this.helper = helper; this.elements = []; @@ -70,6 +85,7 @@ var DrawingArea = new Lang.Class({ this.hasBackground = false; this.textHasCursor = false; this.dashedLine = false; + this.fill = false; this.colors = [Clutter.Color.new(0, 0, 0, 255)]; if (loadJson) @@ -154,18 +170,27 @@ var DrawingArea = new Lang.Class({ } if (button == 1) { - this._startDrawing(x, y, false, shiftPressed); + this._startDrawing(x, y, shiftPressed); return Clutter.EVENT_STOP; } else if (button == 2) { this.toggleShape(); } else if (button == 3) { - this._startDrawing(x, y, true, shiftPressed); + this._stopDrawing(); + this.menu.open(x, y); return Clutter.EVENT_STOP; } return Clutter.EVENT_PROPAGATE; }, + _onKeyboardPopupMenu: function() { + this._stopDrawing(); + if (this.helper.visible) + this.helper.hideHelp(); + this.menu.popup(); + return Clutter.EVENT_STOP; + }, + _onKeyPressed: function(actor, event) { if (event.get_key_symbol() == Clutter.Escape) { this.emitter.emit('stop-drawing'); @@ -213,7 +238,7 @@ var DrawingArea = new Lang.Class({ return Clutter.EVENT_STOP; }, - _startDrawing: function(stageX, stageY, fill, eraser) { + _startDrawing: function(stageX, stageY, eraser) { let [success, startX, startY] = this.transform_stage_point(stageX, stageY); if (!success) @@ -230,7 +255,7 @@ var DrawingArea = new Lang.Class({ color: this.currentColor.to_string(), line: { lineWidth: this.currentLineWidth, lineJoin: this.currentLineJoin, lineCap: this.currentLineCap }, dash: { array: this.dashedLine ? this.dashArray : [0, 0] , offset: this.dashedLine ? this.dashOffset : 0 }, - fill: fill, + fill: this.fill, eraser: eraser, transform: { active: false, center: [0, 0], angle: 0, startAngle: 0, ratio: 1 }, text: '', @@ -406,6 +431,11 @@ var DrawingArea = new Lang.Class({ this.selectShape((this.currentShape == Object.keys(Shapes).length - 1) ? 0 : this.currentShape + 1); }, + toggleFill: function() { + this.fill = !this.fill; + this.emitter.emit('show-osd', this.fill ? _("Fill") : _("Stroke"), null); + }, + toggleDash: function() { this.dashedLine = !this.dashedLine; this.emitter.emit('show-osd', this.dashedLine ? _("Dashed line") : _("Full line"), null); @@ -464,6 +494,7 @@ var DrawingArea = new Lang.Class({ enterDrawingMode: function() { this.keyPressedHandler = this.connect('key-press-event', this._onKeyPressed.bind(this)); this.buttonPressedHandler = this.connect('button-press-event', this._onButtonPressed.bind(this)); + this._onKeyboardPopupMenuHandler = this.connect('popup-menu', this._onKeyboardPopupMenu.bind(this)); this.scrollHandler = this.connect('scroll-event', this._onScroll.bind(this)); this.selectShape(Shapes.NONE); this.get_parent().set_background_color(this.hasBackground ? this.activeBackgroundColor : null); @@ -479,6 +510,10 @@ var DrawingArea = new Lang.Class({ this.disconnect(this.buttonPressedHandler); this.buttonPressedHandler = null; } + if (this._onKeyboardPopupMenuHandler) { + this.disconnect(this._onKeyboardPopupMenuHandler); + this._onKeyboardPopupMenuHandler = null; + } if (this.motionHandler) { this.disconnect(this.motionHandler); this.motionHandler = null; @@ -498,7 +533,9 @@ var DrawingArea = new Lang.Class({ this.currentElement = null; this._stopCursorTimeout(); this.dashedLine = false; + this.fill = false; this._redisplay(); + this.menu.close(); this.get_parent().set_background_color(null); if (save) this.saveAsJson(); @@ -584,6 +621,7 @@ var DrawingArea = new Lang.Class({ disable: function() { this.erase(); + this.menu.disable(); } }); @@ -941,3 +979,221 @@ var DrawingHelper = new Lang.Class({ }, }); + +var DrawingMenu = new Lang.Class({ + Name: 'DrawOnYourScreenDrawingMenu', + + _init: function(area) { + 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({ actor: 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.hide(); + + // do not close the menu on item activated + this.menu.itemActivated = () => {}; + this.menu.connect('open-state-changed', this._onMenuOpenStateChanged.bind(this)); + }, + + disable: function() { + this.menuManager.removeMenu(this.menu); + Main.layoutManager.uiGroup.remove_actor(this.menu.actor); + this.menu.actor.destroy(); + }, + + _onMenuOpenStateChanged: function(menu, open) { + if (!open) + // actionMode has changed, set previous actionMode in order to keep internal shortcuts working + Main.actionMode = ExtensionJs.DRAWING_ACTION_MODE | Shell.ActionMode.NORMAL; + }, + + 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.menu.addAction(_("Undo"), this.area.undo.bind(this.area), 'edit-undo-symbolic'); + this.menu.addAction(_("Redo"), this.area.redo.bind(this.area), 'edit-redo-symbolic'); + this.menu.addAction(_("Erase"), this.area.deleteLastElement.bind(this.area), 'edit-clear-all-symbolic'); + this.menu.addAction(_("Smooth"), this.area.smoothLastElement.bind(this.area), 'format-text-strikethrough-symbolic'); + this._addSeparator(this.menu); + + this._addSubMenuItem(this.menu, null, ShapeNames, this.area, 'currentShape', this.updateSectionVisibility.bind(this)); + this._addColorSubMenuItem(this.menu); + let fillIcon = GdkPixbuf.Pixbuf.new_from_file_at_size(FILL_ICON_PATH, 24, 24); + this._addSwitchItem(this.menu, _("Fill"), fillIcon, this.area, 'fill', this.updateSectionVisibility.bind(this)); + this._addSeparator(this.menu); + + let lineSection = new PopupMenu.PopupMenuSection(); + this._addSliderItem(lineSection, this.area, 'currentLineWidth'); + let linejoinIcon = GdkPixbuf.Pixbuf.new_from_file_at_size(LINEJOIN_ICON_PATH, 24, 24); + this._addSubMenuItem(lineSection, linejoinIcon, LineJoinNames, this.area, 'currentLineJoin'); + let linecapIcon = GdkPixbuf.Pixbuf.new_from_file_at_size(LINECAP_ICON_PATH, 24, 24); + this._addSubMenuItem(lineSection, linecapIcon, LineCapNames, this.area, 'currentLineCap'); + let dashedLineIcon = GdkPixbuf.Pixbuf.new_from_file_at_size(DASHED_LINE_ICON_PATH, 24, 24); + this._addSwitchItem(lineSection, _("Dashed"), dashedLineIcon, this.area, 'dashedLine'); + this._addSeparator(lineSection); + this.menu.addMenuItem(lineSection); + this.lineSection = lineSection; + + let fontSection = new PopupMenu.PopupMenuSection(); + let FontFamilyNamesCopy = Object.create(FontFamilyNames); + FontFamilyNamesCopy[0] = this.area.fontFamily; + this._addSubMenuItem(fontSection, 'font-x-generic-symbolic', FontFamilyNamesCopy, this.area, 'currentFontFamilyId'); + this._addSubMenuItem(fontSection, 'format-text-bold-symbolic', FontWeightNames, this.area, 'currentFontWeight'); + this._addSubMenuItem(fontSection, 'format-text-italic-symbolic', FontStyleNames, this.area, 'currentFontStyle'); + this._addSeparator(fontSection); + this.menu.addMenuItem(fontSection); + this.fontSection = fontSection; + + let manager = ExtensionJs.manager; + this._addSwitchItemWithCallback(this.menu, _("Hide panel and dock"), manager.hiddenList ? true : false, manager.togglePanelAndDockOpacity.bind(manager)); + this._addSwitchItemWithCallback(this.menu, _("Add a drawing background"), this.area.hasBackground, this.area.toggleBackground.bind(this.area)); + this._addSwitchItemWithCallback(this.menu, _("Square drawing area"), this.area.isSquareArea, this.area.toggleSquareArea.bind(this.area)); + this._addSeparator(this.menu); + + this.menu.addAction(_("Save drawing as a SVG file"), this.area.saveAsSvg.bind(this.area), 'document-save-symbolic'); + this.menu.addAction(_("Open stylesheet.css"), manager.openStylesheetFile.bind(manager), 'document-open-symbolic'); + this.menu.addAction(_("Show help"), this.area.toggleHelp.bind(this.area), 'preferences-desktop-keyboard-shortcuts-symbolic'); + + this.updateSectionVisibility(); + }, + + updateSectionVisibility: function() { + if (this.area.fill || this.area.currentShape == Shapes.TEXT) + this.lineSection.actor.hide(); + else + this.lineSection.actor.show(); + + if (this.area.currentShape != Shapes.TEXT) + this.fontSection.actor.hide(); + else + this.fontSection.actor.show(); + }, + + _addSwitchItem: function(menu, label, icon, target, targetProperty, callback) { + let item = new PopupMenu.PopupSwitchMenuItem(label, target[targetProperty]); + + if (icon) { + item.icon = new St.Icon({ style_class: 'popup-menu-icon' }); + item.actor.insert_child_at_index(item.icon, 1); + if (icon instanceof GObject.Object && GObject.type_is_a(icon, GdkPixbuf.Pixbuf)) + item.icon.set_gicon(icon); + else + item.icon.set_icon_name(icon); + } + + item.connect('toggled', (item, state) => { + target[targetProperty] = state; + if (callback) + callback(); + }); + menu.addMenuItem(item); + }, + + _addSwitchItemWithCallback: 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: target[targetProperty] + " px", style_class: 'draw-on-your-screen-menu-slider-label' }); + let slider = new Slider.Slider(target[targetProperty] / 50); + + slider.connect('value-changed', (slider, value, property) => { + target[targetProperty] = Math.max(Math.round(value * 50), 1); + label.set_text(target[targetProperty] + " px"); + }); + + item.actor.add(slider.actor, { expand: true }); + item.actor.add(label); + item.actor.connect('key-press-event', slider.onKeyPressEvent.bind(slider)); + menu.addMenuItem(item); + }, + + _addSubMenuItem: function(menu, icon, obj, target, targetProperty, callback) { + let item = new PopupMenu.PopupSubMenuMenuItem(_(obj[target[targetProperty]]), icon ? true : false); + if (icon && icon instanceof GObject.Object && GObject.type_is_a(icon, GdkPixbuf.Pixbuf)) + item.icon.set_gicon(icon); + else if (icon) + item.icon.set_icon_name(icon); + + item.menu.itemActivated = () => { + item.menu.close(); + }; + + Mainloop.timeout_add(0, () => { + for (let i in obj) { + item.menu.addAction(_(obj[i]), () => { + item.label.set_text(_(obj[i])); + target[targetProperty] = i; + if (callback) + callback(); + }); + } + return GLib.SOURCE_REMOVE; + }); + menu.addMenuItem(item); + }, + + _addColorSubMenuItem: function(menu) { + let item = new PopupMenu.PopupSubMenuMenuItem(_("Color"), true); + item.icon.set_icon_name('document-edit-symbolic'); + item.icon.set_style(`color:${this.area.currentColor.to_string().slice(0, 7)};`); + + item.menu.itemActivated = () => { + item.menu.close(); + }; + + Mainloop.timeout_add(0, () => { + for (let i = 1; i < this.area.colors.length; i++) { + let text = `${this.area.colors[i].to_string()}`; + let colorItem = item.menu.addAction(text, () => { + this.area.currentColor = this.area.colors[i]; + item.icon.set_style(`color:${this.area.currentColor.to_string().slice(0, 7)};`); + }); + colorItem.label.get_clutter_text().set_use_markup(true); + } + return GLib.SOURCE_REMOVE; + }); + menu.addMenuItem(item); + }, + + _addSeparator: function(menu) { + let separator = new PopupMenu.PopupSeparatorMenuItem(' '); + separator.actor.add_style_class_name('draw-on-your-screen-menu-separator'); + menu.addMenuItem(separator); + } +}); + diff --git a/extension.js b/extension.js index 40c8273..176c37d 100644 --- a/extension.js +++ b/extension.js @@ -32,6 +32,9 @@ const Convenience = Extension.imports.convenience; const Draw = Extension.imports.draw; const _ = imports.gettext.domain(Extension.metadata["gettext-domain"]).gettext; +// DRAWING_ACTION_MODE is a custom Shell.ActionMode +var DRAWING_ACTION_MODE = Math.pow(2,14); + let manager; function init() { @@ -147,6 +150,7 @@ var AreaManager = new Lang.Class({ 'toggle-linejoin': this.activeArea.toggleLineJoin.bind(this.activeArea), 'toggle-linecap': this.activeArea.toggleLineCap.bind(this.activeArea), 'toggle-dash' : this.activeArea.toggleDash.bind(this.activeArea), + 'toggle-fill' : this.activeArea.toggleFill.bind(this.activeArea), 'select-none-shape': () => this.activeArea.selectShape(Draw.Shapes.NONE), 'select-line-shape': () => this.activeArea.selectShape(Draw.Shapes.LINE), 'select-ellipse-shape': () => this.activeArea.selectShape(Draw.Shapes.ELLIPSE), @@ -164,7 +168,7 @@ var AreaManager = new Lang.Class({ Main.wm.addKeybinding(key, this.settings, Meta.KeyBindingFlags.NONE, - 256, + DRAWING_ACTION_MODE, this.internalKeybindings[key]); } @@ -172,7 +176,7 @@ var AreaManager = new Lang.Class({ Main.wm.addKeybinding('select-color' + i, this.settings, Meta.KeyBindingFlags.NONE, - 256, + DRAWING_ACTION_MODE, () => this.activeArea.selectColor(i)); } }, @@ -270,8 +274,8 @@ var AreaManager = new Lang.Class({ activeContainer.get_parent().remove_actor(activeContainer); Main.uiGroup.add_child(activeContainer); - // 256 is a custom Shell.ActionMode - if (!Main.pushModal(this.areas[currentIndex], { actionMode: 256 | 1 })) + // add Shell.ActionMode.NORMAL to keep system keybindings enabled (e.g. Alt + F2 ...) + if (!Main.pushModal(this.areas[currentIndex], { actionMode: DRAWING_ACTION_MODE | Shell.ActionMode.NORMAL })) return; this.activeArea = this.areas[currentIndex]; this.addInternalKeybindings(); diff --git a/icons/dashed-line-symbolic.svg b/icons/dashed-line-symbolic.svg new file mode 100644 index 0000000..9e019e9 --- /dev/null +++ b/icons/dashed-line-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/fill-symbolic.svg b/icons/fill-symbolic.svg new file mode 100644 index 0000000..7f80c03 --- /dev/null +++ b/icons/fill-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/linecap-symbolic.svg b/icons/linecap-symbolic.svg new file mode 100644 index 0000000..7ef7d69 --- /dev/null +++ b/icons/linecap-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/linejoin-symbolic.svg b/icons/linejoin-symbolic.svg new file mode 100644 index 0000000..f4ab5f4 --- /dev/null +++ b/icons/linejoin-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/locale/draw-on-your-screen.pot b/locale/draw-on-your-screen.pot index a1ab2ec..ef0631d 100644 --- a/locale/draw-on-your-screen.pot +++ b/locale/draw-on-your-screen.pot @@ -43,6 +43,12 @@ msgstr "" msgid "Text" msgstr "" +msgid "Fill" +msgstr "" + +msgid "Stroke" +msgstr "" + msgid "Dashed line" msgstr "" @@ -69,6 +75,21 @@ msgstr "" msgid "System" msgstr "" +msgid "Undo" +msgstr "" + +msgid "Redo" +msgstr "" + +msgid "Erase" +msgstr "" + +msgid "Smooth" +msgstr "" + +msgid "Dashed" +msgstr "" + #: prefs.js # GLOBAL_KEYBINDINGS @@ -93,6 +114,24 @@ msgstr "" msgid "Smooth last brushstroke" msgstr "" +msgid "Select line" +msgstr "" + +msgid "Select ellipse" +msgstr "" + +msgid "Select rectangle" +msgstr "" + +msgid "Select text" +msgstr "" + +msgid "Unselect shape (free drawing)" +msgstr "" + +msgid "Select fill/stroke" +msgstr "" + msgid "Increment line width" msgstr "" @@ -115,21 +154,6 @@ msgstr "" #msgid "Dashed line" #msgstr "" -msgid "Select line" -msgstr "" - -msgid "Select ellipse" -msgstr "" - -msgid "Select rectangle" -msgstr "" - -msgid "Select text" -msgstr "" - -msgid "Unselect shape (free drawing)" -msgstr "" - msgid "Change font family (generic name)" msgstr "" diff --git a/prefs.js b/prefs.js index adca46c..922437d 100644 --- a/prefs.js +++ b/prefs.js @@ -43,6 +43,13 @@ var INTERNAL_KEYBINDINGS = { 'delete-last-element' : "Erase last brushstroke", 'smooth-last-element': "Smooth last brushstroke", '-separator-1': '', + 'select-line-shape': "Select line", + 'select-ellipse-shape': "Select ellipse", + 'select-rectangle-shape': "Select rectangle", + 'select-text-shape': "Select text", + 'select-none-shape': "Unselect shape (free drawing)", + 'toggle-fill': "Select fill/stroke", + '-separator-2': '', 'increment-line-width': "Increment line width", 'decrement-line-width': "Decrement line width", 'increment-line-width-more': "Increment line width even more", @@ -50,12 +57,6 @@ var INTERNAL_KEYBINDINGS = { 'toggle-linejoin': "Change linejoin", 'toggle-linecap': "Change linecap", 'toggle-dash': "Dashed line", - '-separator-2': '', - 'select-line-shape': "Select line", - 'select-ellipse-shape': "Select ellipse", - 'select-rectangle-shape': "Select rectangle", - 'select-text-shape': "Select text", - 'select-none-shape': "Unselect shape (free drawing)", '-separator-3': '', 'toggle-font-family': "Change font family (generic name)", 'toggle-font-weight': "Change font weight", diff --git a/schemas/gschemas.compiled b/schemas/gschemas.compiled index dfce124..6645b28 100644 Binary files a/schemas/gschemas.compiled and b/schemas/gschemas.compiled differ diff --git a/schemas/org.gnome.shell.extensions.draw-on-your-screen.gschema.xml b/schemas/org.gnome.shell.extensions.draw-on-your-screen.gschema.xml index 57c0d1c..770e394 100644 --- a/schemas/org.gnome.shell.extensions.draw-on-your-screen.gschema.xml +++ b/schemas/org.gnome.shell.extensions.draw-on-your-screen.gschema.xml @@ -117,10 +117,15 @@ toggle linecap - ["<Primary>a"] + ["<Primary>period"] toggle dash toggle dash + + ["<Primary>a"] + toggle fill + toggle fill + KP_1','1']]]> select color1 diff --git a/stylesheet.css b/stylesheet.css index 13db721..54e1592 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -77,4 +77,24 @@ min-height: 0.6em; } + /* context menu */ + + .draw-on-your-screen-menu { + font-size: 0.98em; /* default: 1em */ + } + + .draw-on-your-screen-menu .popup-menu-item { + padding-top: .37em; /* default: .4em */ + padding-bottom: .37em; +} + + .draw-on-your-screen-menu-separator StLabel { + font-size: 0; + } + + .draw-on-your-screen-menu-slider-label { + min-width: 3em; + text-align: right; + } +