Magnifier: Implement focus and caret tracking

A11y users who use the magnifier may have trouble
focusing when they're typing or trying to keynav.
Implement a new system so that they can have the
magnifier track the caret and focus instead instead
of just the mouse.

Bug https://bugzilla.gnome.org/show_bug.cgi?id=647074
This commit is contained in:
Magdalen Berns 2013-09-04 18:53:36 +01:00 committed by Jasper St. Pierre
parent 420db828e9
commit 9d8f30f955
3 changed files with 204 additions and 20 deletions

View File

@ -55,6 +55,7 @@ nobase_dist_js_DATA = \
ui/extensionSystem.js \
ui/extensionDownloader.js \
ui/environment.js \
ui/focusCaretTracker.js\
ui/ibusCandidatePopup.js\
ui/grabHelper.js \
ui/iconGrid.js \

View File

@ -0,0 +1,68 @@
/** -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
* Copyright 2012 Inclusive Design Research Centre, OCAD University.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
* Author:
* Joseph Scheuhammer <clown@alum.mit.edu>
* Contributor:
* Magdalen Berns <m.berns@sms.ed.ac.uk>
*/
const Atspi = imports.gi.Atspi;
const Lang = imports.lang;
const Signals = imports.signals;
const CARETMOVED = 'object:text-caret-moved';
const STATECHANGED = 'object:state-changed';
const FocusCaretTracker = new Lang.Class({
Name: 'FocusCaretTracker',
_init: function() {
Atspi.init();
this._atspiListener = Atspi.EventListener.new(Lang.bind(this, this._onChanged));
},
_onChanged: function(event) {
let update = null;
if (event.type.indexOf(STATECHANGED) == 0)
update = 'focus-changed';
else if (event.type == CARETMOVED)
update = 'caret-moved';
if(update != null)
this.emit(update, event);
},
registerFocusListener: function() {
return this._atspiListener.register(STATECHANGED + ':focused') &&
this._atspiListener.register(STATECHANGED + ':selected');
},
registerCaretListener: function() {
return this._atspiListener.register(CARETMOVED);
},
deregisterFocusListener: function() {
return this._atspiListener.deregister(STATECHANGED + ':focused') &&
this._atspiListener.deregister(STATECHANGED + ':selected');
},
deregisterCaretListener: function() {
return this._atspiListener.deregister(CARETMOVED);
}
});
Signals.addSignalMethods(FocusCaretTracker.prototype);

View File

@ -1,5 +1,6 @@
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
const Atspi = imports.gi.Atspi;
const Clutter = imports.gi.Clutter;
const GDesktopEnums = imports.gi.GDesktopEnums;
const Gio = imports.gi.Gio;
@ -10,6 +11,7 @@ const Mainloop = imports.mainloop;
const Meta = imports.gi.Meta;
const Signals = imports.signals;
const FocusCaretTracker = imports.ui.focusCaretTracker;
const Main = imports.ui.main;
const MagnifierDBus = imports.ui.magnifierDBus;
const Params = imports.misc.params;
@ -37,6 +39,8 @@ const CONTRAST_BLUE_KEY = 'contrast-blue';
const LENS_MODE_KEY = 'lens-mode';
const CLAMP_MODE_KEY = 'scroll-at-edges';
const MOUSE_TRACKING_KEY = 'mouse-tracking';
const FOCUS_TRACKING_KEY = 'focus-tracking';
const CARET_TRACKING_KEY = 'caret-tracking';
const SHOW_CROSS_HAIRS_KEY = 'show-cross-hairs';
const CROSS_HAIRS_THICKNESS_KEY = 'cross-hairs-thickness';
const CROSS_HAIRS_COLOR_KEY = 'cross-hairs-color';
@ -449,6 +453,14 @@ const Magnifier = new Lang.Class({
if (aPref)
zoomRegion.setMouseTrackingMode(aPref);
aPref = this._settings.get_enum(FOCUS_TRACKING_KEY);
if (aPref)
zoomRegion.setFocusTrackingMode(aPref);
aPref = this._settings.get_enum(CARET_TRACKING_KEY);
if (aPref)
zoomRegion.setCaretTrackingMode(aPref);
aPref = this._settings.get_boolean(INVERT_LIGHTNESS_KEY);
if (aPref)
zoomRegion.setInvertLightness(aPref);
@ -488,6 +500,10 @@ const Magnifier = new Lang.Class({
Lang.bind(this, this._updateClampMode));
this._settings.connect('changed::' + MOUSE_TRACKING_KEY,
Lang.bind(this, this._updateMouseTrackingMode));
this._settings.connect('changed::' + FOCUS_TRACKING_KEY,
Lang.bind(this, this._updateFocusTrackingMode));
this._settings.connect('changed::' + CARET_TRACKING_KEY,
Lang.bind(this, this._updateCaretTrackingMode));
this._settings.connect('changed::' + INVERT_LIGHTNESS_KEY,
Lang.bind(this, this._updateInvertLightness));
@ -585,6 +601,24 @@ const Magnifier = new Lang.Class({
}
},
_updateFocusTrackingMode: function() {
// Applies only to the first zoom region.
if (this._zoomRegions.length) {
this._zoomRegions[0].setFocusTrackingMode(
this._settings.get_enum(FOCUS_TRACKING_KEY)
);
}
},
_updateCaretTrackingMode: function() {
// Applies only to the first zoom region.
if (this._zoomRegions.length) {
this._zoomRegions[0].setCaretTrackingMode(
this._settings.get_enum(CARET_TRACKING_KEY)
);
}
},
_updateInvertLightness: function() {
// Applies only to the first zoom region.
if (this._zoomRegions.length) {
@ -623,7 +657,7 @@ const Magnifier = new Lang.Class({
contrast.b = this._settings.get_double(CONTRAST_BLUE_KEY);
this._zoomRegions[0].setContrast(contrast);
}
},
}
});
Signals.addSignalMethods(Magnifier.prototype);
@ -632,8 +666,11 @@ const ZoomRegion = new Lang.Class({
_init: function(magnifier, mouseSourceActor) {
this._magnifier = magnifier;
this._focusCaretTracker = new FocusCaretTracker.FocusCaretTracker();
this._mouseTrackingMode = GDesktopEnums.MagnifierMouseTrackingMode.NONE;
this._focusTrackingMode = GDesktopEnums.MagnifierFocusTrackingMode.NONE;
this._caretTrackingMode = GDesktopEnums.MagnifierCaretTrackingMode.NONE;
this._clampScrollingAtEdges = false;
this._lensMode = false;
this._screenPosition = GDesktopEnums.MagnifierScreenPosition.FULL_SCREEN;
@ -659,9 +696,35 @@ const ZoomRegion = new Lang.Class({
this._xMagFactor = 1;
this._yMagFactor = 1;
this._followingCursor = false;
this._xFocus = 0;
this._yFocus = 0;
this._xCaret = 0;
this._yCaret = 0;
Main.layoutManager.connect('monitors-changed',
Lang.bind(this, this._monitorsChanged));
this._focusCaretTracker.connect('caret-moved',
Lang.bind(this, this._updateCaret));
this._focusCaretTracker.connect('focus-changed',
Lang.bind(this, this._updateFocus));
},
_updateFocus: function(caller, event) {
let component = event.source.get_component_iface();
if (!component || event.detail1 != 1)
return;
let extents = component.get_extents(Atspi.CoordType.SCREEN);
[this._xFocus, this._yFocus] = [extents.x, extents.y]
this._centerFromFocusPosition();
},
_updateCaret: function(caller, event) {
let text = event.source.get_text_iface();
if (!text)
return;
let extents = text.get_character_extents(text.get_caret_offset(), 0);
[this._xCaret, this._yCaret] = [extents.x, extents.y];
this._centerFromCaretPosition();
},
/**
@ -732,6 +795,30 @@ const ZoomRegion = new Lang.Class({
return this._mouseTrackingMode;
},
/**
* setFocusTrackingMode
* @mode: One of the enum FocusTrackingMode values.
*/
setFocusTrackingMode: function(mode) {
this._focusTrackingMode = mode;
if (this._focusTrackingMode == GDesktopEnums.MagnifierFocusTrackingMode.NONE)
this._focusCaretTracker.deregisterFocusListener();
else
this._focusCaretTracker.registerFocusListener();
},
/**
* setCaretTrackingMode
* @mode: One of the enum CaretTrackingMode values.
*/
setCaretTrackingMode: function(mode) {
this._caretTrackingMode = mode;
if (this._caretTrackingMode == GDesktopEnums.MagnifierCaretTrackingMode.NONE)
this._focusCaretTracker.deregisterCaretListener();
else
this._focusCaretTracker.registerCaretListener();
},
/**
* setViewPort
* Sets the position and size of the ZoomRegion on screen.
@ -1243,19 +1330,47 @@ const ZoomRegion = new Lang.Class({
let yMouse = this._magnifier.yMouse;
if (this._mouseTrackingMode == GDesktopEnums.MagnifierMouseTrackingMode.PROPORTIONAL) {
return this._centerFromMouseProportional(xMouse, yMouse);
return this._centerFromPointProportional(xMouse, yMouse);
}
else if (this._mouseTrackingMode == GDesktopEnums.MagnifierMouseTrackingMode.PUSH) {
return this._centerFromMousePush(xMouse, yMouse);
return this._centerFromPointPush(xMouse, yMouse);
}
else if (this._mouseTrackingMode == GDesktopEnums.MagnifierMouseTrackingMode.CENTERED) {
return this._centerFromMouseCentered(xMouse, yMouse);
return this._centerFromPointCentered(xMouse, yMouse);
}
return null; // Should never be hit
},
_centerFromMousePush: function(xMouse, yMouse) {
_centerFromCaretPosition: function() {
let xCaret = this._xCaret;
let yCaret = this._yCaret;
if (this._caretTrackingMode == GDesktopEnums.MagnifierCaretTrackingMode.PROPORTIONAL)
[xCaret, yCaret] = this._centerFromPointProportional(xCaret, yCaret);
else if (this._caretTrackingMode == GDesktopEnums.MagnifierCaretTrackingMode.PUSH)
[xCaret, yCaret] = this._centerFromPointPush(xCaret, yCaret);
else if (this._caretTrackingMode == GDesktopEnums.MagnifierCaretTrackingMode.CENTERED)
[xCaret, yCaret] = this._centerFromPointCentered(xCaret, yCaret);
this.scrollContentsTo(xCaret, yCaret);
},
_centerFromFocusPosition: function() {
let xFocus = this._xFocus;
let yFocus = this._yFocus;
if (this._focusTrackingMode == GDesktopEnums.MagnifierFocusTrackingMode.PROPORTIONAL)
[xFocus, yFocus] = this._centerFromPointProportional(xFocus, yFocus);
else if (this._focusTrackingMode == GDesktopEnums.MagnifierFocusTrackingMode.PUSH)
[xFocus, yFocus] = this._centerFromPointPush(xFocus, yFocus);
else if (this._focusTrackingMode == GDesktopEnums.MagnifierFocusTrackingMode.CENTERED)
[xFocus, yFocus] = this._centerFromPointCentered(xFocus, yFocus);
this.scrollContentsTo(xFocus, yFocus);
},
_centerFromPointPush: function(xPoint, yPoint) {
let [xRoi, yRoi, widthRoi, heightRoi] = this.getROI();
let [cursorWidth, cursorHeight] = this._mouseSourceActor.get_size();
let xPos = xRoi + widthRoi / 2;
@ -1263,20 +1378,20 @@ const ZoomRegion = new Lang.Class({
let xRoiRight = xRoi + widthRoi - cursorWidth;
let yRoiBottom = yRoi + heightRoi - cursorHeight;
if (xMouse < xRoi)
xPos -= (xRoi - xMouse);
else if (xMouse > xRoiRight)
xPos += (xMouse - xRoiRight);
if (xPoint < xRoi)
xPos -= (xRoi - xPoint);
else if (xPoint > xRoiRight)
xPos += (xPoint - xRoiRight);
if (yMouse < yRoi)
yPos -= (yRoi - yMouse);
else if (yMouse > yRoiBottom)
yPos += (yMouse - yRoiBottom);
if (yPoint < yRoi)
yPos -= (yRoi - yPoint);
else if (yPoint > yRoiBottom)
yPos += (yPoint - yRoiBottom);
return [xPos, yPos];
},
_centerFromMouseProportional: function(xMouse, yMouse) {
_centerFromPointProportional: function(xPoint, yPoint) {
let [xRoi, yRoi, widthRoi, heightRoi] = this.getROI();
let halfScreenWidth = global.screen_width / 2;
let halfScreenHeight = global.screen_height / 2;
@ -1285,16 +1400,16 @@ const ZoomRegion = new Lang.Class({
let unscaledPadding = Math.min(this._viewPortWidth, this._viewPortHeight) / 5;
let xPadding = unscaledPadding / this._xMagFactor;
let yPadding = unscaledPadding / this._yMagFactor;
let xProportion = (xMouse - halfScreenWidth) / halfScreenWidth; // -1 ... 1
let yProportion = (yMouse - halfScreenHeight) / halfScreenHeight; // -1 ... 1
let xPos = xMouse - xProportion * (widthRoi / 2 - xPadding);
let yPos = yMouse - yProportion * (heightRoi /2 - yPadding);
let xProportion = (xPoint - halfScreenWidth) / halfScreenWidth; // -1 ... 1
let yProportion = (yPoint - halfScreenHeight) / halfScreenHeight; // -1 ... 1
let xPos = xPoint - xProportion * (widthRoi / 2 - xPadding);
let yPos = yPoint - yProportion * (heightRoi /2 - yPadding);
return [xPos, yPos];
},
_centerFromMouseCentered: function(xMouse, yMouse) {
return [xMouse, yMouse];
_centerFromPointCentered: function(xPoint, yPoint) {
return [xPoint, yPoint];
},
_screenToViewPort: function(screenX, screenY) {