diff --git a/menu.js b/menu.js new file mode 100644 index 0000000..a283e8d --- /dev/null +++ b/menu.js @@ -0,0 +1,627 @@ +/* jslint esversion: 6 */ + +/* + * Copyright 2019 Abakkk + * + * This file is part of DrawOnYourScreen, a drawing extension for GNOME Shell. + * https://framagit.org/abakkk/DrawOnYourScreen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +const ByteArray = imports.byteArray; +const Cairo = imports.cairo; +const Clutter = imports.gi.Clutter; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; +const Pango = imports.gi.Pango; +const PangoCairo = imports.gi.PangoCairo; +const St = imports.gi.St; + +const BoxPointer = imports.ui.boxpointer; +const Config = imports.misc.config; +const Main = imports.ui.main; +const PopupMenu = imports.ui.popupMenu; +const Slider = imports.ui.slider; +const Screenshot = imports.ui.screenshot; +const Tweener = imports.ui.tweener; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Convenience = ExtensionUtils.getSettings ? ExtensionUtils : Me.imports.convenience; +const Extension = Me.imports.extension; +const Prefs = Me.imports.prefs; +const _ = imports.gettext.domain(Me.metadata['gettext-domain']).gettext; + +const GS_VERSION = Config.PACKAGE_VERSION; +const CAIRO_DEBUG_EXTENDS = false; +const SVG_DEBUG_EXTENDS = false; +const SVG_DEBUG_SUPERPOSES_CAIRO = false; +const TEXT_CURSOR_TIME = 600; // ms + +const ICON_DIR = Me.dir.get_child('data').get_child('icons'); +const 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(); + +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; +}; + +const Shapes = { NONE: 0, LINE: 1, ELLIPSE: 2, RECTANGLE: 3, TEXT: 4, POLYGON: 5, POLYLINE: 6 }; +const Manipulations = { MOVE: 100, RESIZE: 101, MIRROR: 102 }; +var Tools = Object.assign({}, Shapes, Manipulations); +const Transformations = { TRANSLATION: 0, ROTATION: 1, SCALE_PRESERVE: 2, STRETCH: 3, REFLECTION: 4, INVERSION: 5 }; +const ToolNames = { 0: "Free drawing", 1: "Line", 2: "Ellipse", 3: "Rectangle", 4: "Text", 5: "Polygon", 6: "Polyline", 100: "Move", 101: "Resize", 102: "Mirror" }; +const LineCapNames = Object.assign(reverseEnumeration(Cairo.LineCap), { 2: 'Square' }); +const LineJoinNames = reverseEnumeration(Cairo.LineJoin); +const FillRuleNames = { 0: 'Nonzero', 1: 'Evenodd' }; +const FontGenericNames = { 0: 'Theme', 1: 'Sans-Serif', 2: 'Serif', 3: 'Monospace', 4: 'Cursive', 5: 'Fantasy' }; +const FontWeightNames = Object.assign(reverseEnumeration(Pango.Weight), { 200: "Ultra-light", 350: "Semi-light", 600: "Semi-bold", 800: "Ultra-bold" }); +delete FontWeightNames[Pango.Weight.ULTRAHEAVY]; +const FontStyleNames = reverseEnumeration(Pango.Style); +const FontStretchNames = reverseEnumeration(Pango.Stretch); +const FontVariantNames = reverseEnumeration(Pango.Variant); + +const getDateString = function() { + let date = GLib.DateTime.new_now_local(); + return `${date.format("%F")} ${date.format("%X")}`; +}; + +const getJsonFiles = function() { + let directory = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir']])); + + let enumerator; + try { + enumerator = directory.enumerate_children('standard::name,standard::display-name,standard::content-type,time::modified', Gio.FileQueryInfoFlags.NONE, null); + } catch(e) { + return []; + } + + let jsonFiles = []; + let fileInfo = enumerator.next_file(null); + while (fileInfo) { + if (fileInfo.get_content_type().indexOf('json') != -1 && fileInfo.get_name() != `${Me.metadata['persistent-file-name']}.json`) { + let file = enumerator.get_child(fileInfo); + jsonFiles.push({ name: fileInfo.get_name().slice(0, -5), + displayName: fileInfo.get_display_name().slice(0, -5), + // fileInfo.get_modification_date_time: Gio 2.62+ + modificationUnixTime: fileInfo.get_attribute_uint64('time::modified'), + delete: () => file.delete(null) }); + } + fileInfo = enumerator.next_file(null); + } + enumerator.close(null); + + jsonFiles.sort((a, b) => { + return b.modificationUnixTime - a.modificationUnixTime; + }); + + return jsonFiles; +}; + +const getActor = function(object) { + return GS_VERSION < '3.33.0' ? object.actor : object; +}; + +const 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.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.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, 'document-edit-symbolic', ToolNames, this.area, 'currentTool', this._updateSectionVisibility.bind(this)); + 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, LineJoinNames, this.area, 'currentLineJoin'); + this._addSubMenuItem(lineSection, this.linecapIcon, 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(); + let FontGenericNamesCopy = Object.create(FontGenericNames); + FontGenericNamesCopy[0] = this.area.currentThemeFontFamily; + this._addSubMenuItem(fontSection, 'font-x-generic-symbolic', FontGenericNamesCopy, this.area, 'currentFontGeneric'); + this._addSubMenuItem(fontSection, 'format-text-bold-symbolic', FontWeightNames, this.area, 'currentFontWeight'); + this._addSubMenuItem(fontSection, 'format-text-italic-symbolic', 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 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._updateSectionVisibility(); + }, + + _updateSectionVisibility: function() { + if (this.area.currentTool != Shapes.TEXT) { + this.lineSection.actor.show(); + this.fontSection.actor.hide(); + this.fillItem.setSensitive(true); + this.fillSection.setSensitive(true); + } else { + this.lineSection.actor.hide(); + this.fontSection.actor.show(); + this.fillItem.setSensitive(false); + this.fillSection.setSensitive(false); + } + + 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) { + let item = new PopupMenu.PopupSubMenuMenuItem(_(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 == 'currentFontGeneric') + text = `${_(obj[i])}`; + else if (targetProperty == 'currentFontWeight') + text = `${_(obj[i])}`; + else if (targetProperty == 'currentFontStyle') + text = `${_(obj[i])}`; + else + text = _(obj[i]); + + let iCaptured = Number(i); + let subItem = item.menu.addAction(text, () => { + item.label.set_text(_(obj[iCaptured])); + target[targetProperty] = iCaptured; + if (callback) + callback(); + }); + + subItem.label.get_clutter_text().set_use_markup(true); + + // change the display order of tools + if (obj == ToolNames && i == Shapes.POLYGON) + item.menu.moveMenuItem(subItem, 4); + else if (obj == ToolNames && i == Shapes.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)};`); + }); + colorItem.label.get_clutter_text().set_use_markup(true); + // 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)};`); + } + return GLib.SOURCE_REMOVE; + }); + 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(`${prefix}${this.area.jsonName}`); + 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.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 jsonFiles = getJsonFiles(); + jsonFiles.forEach(file => { + let item = this.openDrawingSubMenu.addAction(`${file.displayName}`, () => { + this.area.loadJson(file.name); + this._updateDrawingNameMenuItem(); + this._updateSaveDrawingSubMenuItemSensitivity(); + }); + item.label.get_clutter_text().set_use_markup(true); + + let expander = new St.Bin({ + style_class: 'popup-menu-item-expander', + x_expand: true, + }); + getActor(item).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(item).add_child(deleteButton); + + deleteButton.connect('clicked', () => { + file.delete(); + item.destroy(); + }); + }); + + 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); + }, + + _populateSaveDrawingSubMenu: function() { + this.saveDrawingSubMenu.removeAll(); + let saveEntry = new DrawingMenuEntry({ initialTextGetter: getDateString, + entryActivateCallback: (text) => { + this.area.saveAsJsonWithName(text); + this.saveDrawingSubMenu.toggle(); + this._updateDrawingNameMenuItem(); + }, + invalidStrings: [Me.metadata['persistent-file-name'], '/'], + primaryIconName: 'insert-text' }); + this.saveDrawingSubMenu.addMenuItem(saveEntry.item); + }, + + _addSeparator: function(menu) { + if (this.hasSeparators) { + let separatorItem = new PopupMenu.PopupSeparatorMenuItem(' '); + getActor(separatorItem).add_style_class_name('draw-on-your-screen-menu-separator-item'); + menu.addMenuItem(separatorItem); + } + } +}); + +// 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; + } +}); + +