accessibility: Add pointer accessibility support

Adds the UI part for the pointer accessibility features.

The various timeouts running are notified using a pie-timer showing
under the pointer.

For dwell-click type selection, we use a drop-down menu. Users can
use the dwell-click to select the next type of dwell click to be
emitted.

https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/474
This commit is contained in:
Olivier Fourdan 2019-03-20 17:46:12 +01:00
parent 14d9839ed3
commit 5ace4682bf
8 changed files with 210 additions and 3 deletions

View File

@ -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 */ /* NETWORK DIALOGS */
.nm-dialog { .nm-dialog {

View File

@ -82,6 +82,7 @@
<file>ui/pageIndicators.js</file> <file>ui/pageIndicators.js</file>
<file>ui/panel.js</file> <file>ui/panel.js</file>
<file>ui/panelMenu.js</file> <file>ui/panelMenu.js</file>
<file>ui/pointerA11yTimeout.js</file>
<file>ui/pointerWatcher.js</file> <file>ui/pointerWatcher.js</file>
<file>ui/popupMenu.js</file> <file>ui/popupMenu.js</file>
<file>ui/remoteSearch.js</file> <file>ui/remoteSearch.js</file>
@ -122,6 +123,7 @@
<file>ui/status/accessibility.js</file> <file>ui/status/accessibility.js</file>
<file>ui/status/brightness.js</file> <file>ui/status/brightness.js</file>
<file>ui/status/dwellClick.js</file>
<file>ui/status/location.js</file> <file>ui/status/location.js</file>
<file>ui/status/keyboard.js</file> <file>ui/status/keyboard.js</file>
<file>ui/status/nightLight.js</file> <file>ui/status/nightLight.js</file>

View File

@ -38,6 +38,7 @@ const Magnifier = imports.ui.magnifier;
const XdndHandler = imports.ui.xdndHandler; const XdndHandler = imports.ui.xdndHandler;
const KbdA11yDialog = imports.ui.kbdA11yDialog; const KbdA11yDialog = imports.ui.kbdA11yDialog;
const LocatePointer = imports.ui.locatePointer; const LocatePointer = imports.ui.locatePointer;
const PointerA11yTimeout = imports.ui.pointerA11yTimeout;
const A11Y_SCHEMA = 'org.gnome.desktop.a11y.keyboard'; const A11Y_SCHEMA = 'org.gnome.desktop.a11y.keyboard';
const STICKY_KEYS_ENABLE = 'stickykeys-enable'; const STICKY_KEYS_ENABLE = 'stickykeys-enable';
@ -82,6 +83,7 @@ let _cssStylesheet = null;
let _a11ySettings = null; let _a11ySettings = null;
let _themeResource = null; let _themeResource = null;
let _oskResource = null; let _oskResource = null;
let pointerA11yTimeout = null;
function _sessionUpdated() { function _sessionUpdated() {
if (sessionMode.isPrimary) if (sessionMode.isPrimary)
@ -189,6 +191,8 @@ function _initializeUI() {
layoutManager.init(); layoutManager.init();
overview.init(); overview.init();
pointerA11yTimeout = new PointerA11yTimeout.PointerA11yTimeout();
_a11ySettings = new Gio.Settings({ schema_id: A11Y_SCHEMA }); _a11ySettings = new Gio.Settings({ schema_id: A11Y_SCHEMA });
global.display.connect('overlay-key', () => { global.display.connect('overlay-key', () => {

View File

@ -817,6 +817,7 @@ const PANEL_ITEM_IMPLEMENTATIONS = {
'dateMenu': imports.ui.dateMenu.DateMenuButton, 'dateMenu': imports.ui.dateMenu.DateMenuButton,
'a11y': imports.ui.status.accessibility.ATIndicator, 'a11y': imports.ui.status.accessibility.ATIndicator,
'keyboard': imports.ui.status.keyboard.InputSourceIndicator, 'keyboard': imports.ui.status.keyboard.InputSourceIndicator,
'dwellClick': imports.ui.status.dwellClick.DwellClickIndicator,
}; };
var Panel = GObject.registerClass( var Panel = GObject.registerClass(

104
js/ui/pointerA11yTimeout.js Normal file
View File

@ -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);
});
}
};

View File

@ -48,7 +48,7 @@ const _modes = {
panel: { panel: {
left: [], left: [],
center: ['dateMenu'], center: ['dateMenu'],
right: ['a11y', 'keyboard', 'aggregateMenu'] right: ['dwellClick', 'a11y', 'keyboard', 'aggregateMenu']
}, },
panelStyle: 'login-screen' panelStyle: 'login-screen'
}, },
@ -73,7 +73,7 @@ const _modes = {
panel: { panel: {
left: [], left: [],
center: [], center: [],
right: ['a11y', 'keyboard', 'aggregateMenu'] right: ['dwellClick', 'a11y', 'keyboard', 'aggregateMenu']
}, },
panelStyle: 'unlock-screen' panelStyle: 'unlock-screen'
}, },
@ -101,7 +101,7 @@ const _modes = {
panel: { panel: {
left: ['activities', 'appMenu'], left: ['activities', 'appMenu'],
center: ['dateMenu'], center: ['dateMenu'],
right: ['a11y', 'keyboard', 'aggregateMenu'] right: ['dwellClick', 'a11y', 'keyboard', 'aggregateMenu']
} }
} }
}; };

View File

@ -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;
}
});

View File

@ -54,6 +54,7 @@ js/ui/shellMountOperation.js
js/ui/status/accessibility.js js/ui/status/accessibility.js
js/ui/status/bluetooth.js js/ui/status/bluetooth.js
js/ui/status/brightness.js js/ui/status/brightness.js
js/ui/status/dwellClick.js
js/ui/status/keyboard.js js/ui/status/keyboard.js
js/ui/status/location.js js/ui/status/location.js
js/ui/status/network.js js/ui/status/network.js