diff --git a/data/theme/gnome-shell-sass/_common.scss b/data/theme/gnome-shell-sass/_common.scss index c8c53d557..50bfc6650 100644 --- a/data/theme/gnome-shell-sass/_common.scss +++ b/data/theme/gnome-shell-sass/_common.scss @@ -1195,6 +1195,15 @@ StScrollBar { } } +// Pointer accessibility notifications +.pie-timer { + width: 60px; + height: 60px; + -pie-border-width: 3px; + -pie-border-color: $selected_bg_color; + -pie-background-color: lighten(transparentize($selected_bg_color, 0.7), 40%); +} + /* NETWORK DIALOGS */ .nm-dialog { diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml index 7807cce9d..b5348ddcb 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -82,6 +82,7 @@ ui/pageIndicators.js ui/panel.js ui/panelMenu.js + ui/pointerA11yTimeout.js ui/pointerWatcher.js ui/popupMenu.js ui/remoteSearch.js @@ -122,6 +123,7 @@ ui/status/accessibility.js ui/status/brightness.js + ui/status/dwellClick.js ui/status/location.js ui/status/keyboard.js ui/status/nightLight.js diff --git a/js/ui/main.js b/js/ui/main.js index b3cd69283..c01be9882 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -38,6 +38,7 @@ const Magnifier = imports.ui.magnifier; const XdndHandler = imports.ui.xdndHandler; const KbdA11yDialog = imports.ui.kbdA11yDialog; const LocatePointer = imports.ui.locatePointer; +const PointerA11yTimeout = imports.ui.pointerA11yTimeout; const A11Y_SCHEMA = 'org.gnome.desktop.a11y.keyboard'; const STICKY_KEYS_ENABLE = 'stickykeys-enable'; @@ -82,6 +83,7 @@ let _cssStylesheet = null; let _a11ySettings = null; let _themeResource = null; let _oskResource = null; +let pointerA11yTimeout = null; function _sessionUpdated() { if (sessionMode.isPrimary) @@ -189,6 +191,8 @@ function _initializeUI() { layoutManager.init(); overview.init(); + pointerA11yTimeout = new PointerA11yTimeout.PointerA11yTimeout(); + _a11ySettings = new Gio.Settings({ schema_id: A11Y_SCHEMA }); global.display.connect('overlay-key', () => { diff --git a/js/ui/panel.js b/js/ui/panel.js index 6522e9099..ef08b1aad 100644 --- a/js/ui/panel.js +++ b/js/ui/panel.js @@ -817,6 +817,7 @@ const PANEL_ITEM_IMPLEMENTATIONS = { 'dateMenu': imports.ui.dateMenu.DateMenuButton, 'a11y': imports.ui.status.accessibility.ATIndicator, 'keyboard': imports.ui.status.keyboard.InputSourceIndicator, + 'dwellClick': imports.ui.status.dwellClick.DwellClickIndicator, }; var Panel = GObject.registerClass( diff --git a/js/ui/pointerA11yTimeout.js b/js/ui/pointerA11yTimeout.js new file mode 100644 index 000000000..d9309415b --- /dev/null +++ b/js/ui/pointerA11yTimeout.js @@ -0,0 +1,104 @@ +const { Clutter, GLib, GObject, Meta, St } = imports.gi; +const Tweener = imports.ui.tweener; +const Main = imports.ui.main; +const Cairo = imports.cairo; + +const ANIMATION_STEPS = 36.; + +var PieTimer = GObject.registerClass( +class PieTimer extends St.DrawingArea { + _init() { + this._x = 0; + this._y = 0; + this._startTime = 0; + this._duration = 0; + super._init( { style_class: 'pie-timer', + visible: false, + can_focus: false, + reactive: false }); + } + + vfunc_repaint() { + let node = this.get_theme_node(); + let backgroundColor = node.get_color('-pie-background-color'); + let borderColor = node.get_color('-pie-border-color'); + let borderWidth = node.get_length('-pie-border-width'); + let [width, height] = this.get_surface_size(); + let radius = Math.min(width / 2, height / 2); + + let currentTime = GLib.get_monotonic_time() / 1000.0; + let ellapsed = currentTime - this._startTime; + let angle = (ellapsed / this._duration) * 2 * Math.PI; + let startAngle = 3 * Math.PI / 2; + let endAngle = startAngle + angle; + + let cr = this.get_context(); + cr.setLineCap(Cairo.LineCap.ROUND); + cr.setLineJoin(Cairo.LineJoin.ROUND); + cr.translate(width / 2, height / 2); + + cr.moveTo(0, 0); + cr.arc(0, 0, radius - borderWidth, startAngle, endAngle); + cr.lineTo(0, 0); + cr.closePath(); + + cr.setLineWidth(0); + Clutter.cairo_set_source_color(cr, backgroundColor); + cr.fillPreserve(); + + cr.setLineWidth(borderWidth); + Clutter.cairo_set_source_color(cr, borderColor); + cr.stroke(); + + cr.$dispose(); + } + + start(x, y, duration) { + Tweener.removeTweens(this); + + this.x = x - this.width / 2; + this.y = y - this.height / 2; + this.show(); + Main.uiGroup.set_child_above_sibling(this, null); + + this._startTime = GLib.get_monotonic_time() / 1000.0; + this._duration = duration; + + Tweener.addTween(this, + { opacity: 255, + time: duration / 1000, + transition: 'easeOutQuad', + onUpdateScope: this, + onUpdate() { this.queue_repaint() }, + onCompleteScope: this, + onComplete() { this.stop(); } + }); + } + + stop() { + Tweener.removeTweens(this); + this.hide(); + } +}); + +var PointerA11yTimeout = class PointerA11yTimeout { + constructor() { + let manager = Clutter.DeviceManager.get_default(); + let pieTimer = new PieTimer(); + + Main.uiGroup.add_actor(pieTimer); + + manager.connect('ptr-a11y-timeout-started', (manager, device, type, timeout) => { + let [x, y, mods] = global.get_pointer(); + pieTimer.start(x, y, timeout); + if (type == Clutter.PointerA11yTimeoutType.GESTURE) + global.display.set_cursor(Meta.Cursor.CROSSHAIR); + }); + + manager.connect('ptr-a11y-timeout-stopped', (manager, device, type) => { + pieTimer.stop(); + if (type == Clutter.PointerA11yTimeoutType.GESTURE) + global.display.set_cursor(Meta.Cursor.DEFAULT); + }); + } +}; diff --git a/js/ui/sessionMode.js b/js/ui/sessionMode.js index 1f3e23b7a..ae6c64dd3 100644 --- a/js/ui/sessionMode.js +++ b/js/ui/sessionMode.js @@ -48,7 +48,7 @@ const _modes = { panel: { left: [], center: ['dateMenu'], - right: ['a11y', 'keyboard', 'aggregateMenu'] + right: ['dwellClick', 'a11y', 'keyboard', 'aggregateMenu'] }, panelStyle: 'login-screen' }, @@ -73,7 +73,7 @@ const _modes = { panel: { left: [], center: [], - right: ['a11y', 'keyboard', 'aggregateMenu'] + right: ['dwellClick', 'a11y', 'keyboard', 'aggregateMenu'] }, panelStyle: 'unlock-screen' }, @@ -101,7 +101,7 @@ const _modes = { panel: { left: ['activities', 'appMenu'], center: ['dateMenu'], - right: ['a11y', 'keyboard', 'aggregateMenu'] + right: ['dwellClick', 'a11y', 'keyboard', 'aggregateMenu'] } } }; diff --git a/js/ui/status/dwellClick.js b/js/ui/status/dwellClick.js new file mode 100644 index 000000000..cb9f331f3 --- /dev/null +++ b/js/ui/status/dwellClick.js @@ -0,0 +1,86 @@ +const { Clutter, Gio, GLib, GObject, St } = imports.gi; +const Mainloop = imports.mainloop; + +const PanelMenu = imports.ui.panelMenu; +const PopupMenu = imports.ui.popupMenu; + +const MOUSE_A11Y_SCHEMA = 'org.gnome.desktop.a11y.mouse'; +const KEY_DWELL_CLICK_ENABLED = 'dwell-click-enabled'; +const KEY_DWELL_MODE = 'dwell-mode'; +const DWELL_MODE_WINDOW = 'window'; +const DWELL_CLICK_MODES = { + primary: { + name: _("Single Click"), + icon: 'pointer-primary-click-symbolic', + type: Clutter.PointerA11yDwellClickType.PRIMARY + }, + double: { + name: _("Double Click"), + icon: 'pointer-double-click-symbolic', + type: Clutter.PointerA11yDwellClickType.DOUBLE + }, + drag: { + name: _("Drag"), + icon: 'pointer-drag-symbolic', + type: Clutter.PointerA11yDwellClickType.DRAG + }, + secondary: { + name: _("Secondary Click"), + icon: 'pointer-secondary-click-symbolic', + type: Clutter.PointerA11yDwellClickType.SECONDARY + }, +}; + +var DwellClickIndicator = GObject.registerClass( +class DwellClickIndicator extends PanelMenu.Button { + _init() { + super._init(0.0, _("Dwell Click")); + + this._hbox = new St.BoxLayout({ style_class: 'panel-status-menu-box' }); + this._icon = new St.Icon({ style_class: 'system-status-icon', + icon_name: 'pointer-primary-click-symbolic' }); + this._hbox.add_child(this._icon); + this._hbox.add_child(PopupMenu.arrowIcon(St.Side.BOTTOM)); + + this.add_child(this._hbox); + + this._a11ySettings = new Gio.Settings({ schema_id: MOUSE_A11Y_SCHEMA }); + this._a11ySettings.connect('changed::' + KEY_DWELL_CLICK_ENABLED, this._syncMenuVisibility.bind(this)); + this._a11ySettings.connect('changed::' + KEY_DWELL_MODE, this._syncMenuVisibility.bind(this)); + + this._deviceManager = Clutter.DeviceManager.get_default(); + this._deviceManager.connect('ptr-a11y-dwell-click-type-changed', this._updateClickType.bind(this)); + + this._addDwellAction(DWELL_CLICK_MODES.primary); + this._addDwellAction(DWELL_CLICK_MODES.double); + this._addDwellAction(DWELL_CLICK_MODES.drag); + this._addDwellAction(DWELL_CLICK_MODES.secondary); + + this._setClickType(DWELL_CLICK_MODES.primary); + this._syncMenuVisibility(); + } + + _syncMenuVisibility() { + this.visible = + (this._a11ySettings.get_boolean(KEY_DWELL_CLICK_ENABLED) && + this._a11ySettings.get_string(KEY_DWELL_MODE) == DWELL_MODE_WINDOW); + + return GLib.SOURCE_REMOVE; + } + + _addDwellAction(mode) { + this.menu.addAction(mode.name, this._setClickType.bind(this, mode), mode.icon); + } + + _updateClickType(manager, click_type) { + for (let mode in DWELL_CLICK_MODES) { + if (DWELL_CLICK_MODES[mode].type == click_type) + this._icon.icon_name = DWELL_CLICK_MODES[mode].icon; + } + } + + _setClickType(mode) { + this._deviceManager.set_pointer_a11y_dwell_click_type(mode.type); + this._icon.icon_name = mode.icon; + } +}); diff --git a/po/POTFILES.in b/po/POTFILES.in index 43ea408ac..a7b216710 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -54,6 +54,7 @@ js/ui/shellMountOperation.js js/ui/status/accessibility.js js/ui/status/bluetooth.js js/ui/status/brightness.js +js/ui/status/dwellClick.js js/ui/status/keyboard.js js/ui/status/location.js js/ui/status/network.js