2168 lines
73 KiB
JavaScript
2168 lines
73 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
/* exported Magnifier */
|
|
|
|
const Atspi = imports.gi.Atspi;
|
|
const Clutter = imports.gi.Clutter;
|
|
const GDesktopEnums = imports.gi.GDesktopEnums;
|
|
const Gio = imports.gi.Gio;
|
|
const GLib = imports.gi.GLib;
|
|
const GObject = imports.gi.GObject;
|
|
const Meta = imports.gi.Meta;
|
|
const Shell = imports.gi.Shell;
|
|
const St = imports.gi.St;
|
|
const Signals = imports.misc.signals;
|
|
|
|
const Background = imports.ui.background;
|
|
const FocusCaretTracker = imports.ui.focusCaretTracker;
|
|
const Main = imports.ui.main;
|
|
const Params = imports.misc.params;
|
|
const PointerWatcher = imports.ui.pointerWatcher;
|
|
|
|
var CROSSHAIRS_CLIP_SIZE = [100, 100];
|
|
var NO_CHANGE = 0.0;
|
|
|
|
var POINTER_REST_TIME = 1000; // milliseconds
|
|
|
|
// Settings
|
|
const MAGNIFIER_SCHEMA = 'org.gnome.desktop.a11y.magnifier';
|
|
const SCREEN_POSITION_KEY = 'screen-position';
|
|
const MAG_FACTOR_KEY = 'mag-factor';
|
|
const INVERT_LIGHTNESS_KEY = 'invert-lightness';
|
|
const COLOR_SATURATION_KEY = 'color-saturation';
|
|
const BRIGHT_RED_KEY = 'brightness-red';
|
|
const BRIGHT_GREEN_KEY = 'brightness-green';
|
|
const BRIGHT_BLUE_KEY = 'brightness-blue';
|
|
const CONTRAST_RED_KEY = 'contrast-red';
|
|
const CONTRAST_GREEN_KEY = 'contrast-green';
|
|
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';
|
|
const CROSS_HAIRS_OPACITY_KEY = 'cross-hairs-opacity';
|
|
const CROSS_HAIRS_LENGTH_KEY = 'cross-hairs-length';
|
|
const CROSS_HAIRS_CLIP_KEY = 'cross-hairs-clip';
|
|
|
|
var MouseSpriteContent = GObject.registerClass({
|
|
Implements: [Clutter.Content],
|
|
}, class MouseSpriteContent extends GObject.Object {
|
|
_init() {
|
|
super._init();
|
|
this._texture = null;
|
|
}
|
|
|
|
vfunc_get_preferred_size() {
|
|
if (!this._texture)
|
|
return [false, 0, 0];
|
|
|
|
return [true, this._texture.get_width(), this._texture.get_height()];
|
|
}
|
|
|
|
vfunc_paint_content(actor, node, _paintContext) {
|
|
if (!this._texture)
|
|
return;
|
|
|
|
let color = Clutter.Color.get_static(Clutter.StaticColor.WHITE);
|
|
let [minFilter, magFilter] = actor.get_content_scaling_filters();
|
|
let textureNode = new Clutter.TextureNode(this._texture,
|
|
color, minFilter, magFilter);
|
|
textureNode.set_name('MouseSpriteContent');
|
|
node.add_child(textureNode);
|
|
|
|
textureNode.add_rectangle(actor.get_content_box());
|
|
}
|
|
|
|
get texture() {
|
|
return this._texture;
|
|
}
|
|
|
|
set texture(coglTexture) {
|
|
if (this._texture == coglTexture)
|
|
return;
|
|
|
|
let oldTexture = this._texture;
|
|
this._texture = coglTexture;
|
|
this.invalidate();
|
|
|
|
if (!oldTexture || !coglTexture ||
|
|
oldTexture.get_width() != coglTexture.get_width() ||
|
|
oldTexture.get_height() != coglTexture.get_height())
|
|
this.invalidate_size();
|
|
}
|
|
});
|
|
|
|
var Magnifier = class Magnifier extends Signals.EventEmitter {
|
|
constructor() {
|
|
super();
|
|
|
|
// Magnifier is a manager of ZoomRegions.
|
|
this._zoomRegions = [];
|
|
|
|
// Create small clutter tree for the magnified mouse.
|
|
let cursorTracker = Meta.CursorTracker.get_for_display(global.display);
|
|
this._cursorTracker = cursorTracker;
|
|
|
|
this._mouseSprite = new Clutter.Actor({ request_mode: Clutter.RequestMode.CONTENT_SIZE });
|
|
this._mouseSprite.content = new MouseSpriteContent();
|
|
|
|
this._cursorRoot = new Clutter.Actor();
|
|
this._cursorRoot.add_actor(this._mouseSprite);
|
|
|
|
// Create the first ZoomRegion and initialize it according to the
|
|
// magnification settings.
|
|
|
|
[this.xMouse, this.yMouse] = global.get_pointer();
|
|
|
|
let aZoomRegion = new ZoomRegion(this, this._cursorRoot);
|
|
this._zoomRegions.push(aZoomRegion);
|
|
this._settingsInit(aZoomRegion);
|
|
aZoomRegion.scrollContentsTo(this.xMouse, this.yMouse);
|
|
|
|
St.Settings.get().connect('notify::magnifier-active', () => {
|
|
this.setActive(St.Settings.get().magnifier_active);
|
|
});
|
|
|
|
this.setActive(St.Settings.get().magnifier_active);
|
|
}
|
|
|
|
/**
|
|
* showSystemCursor:
|
|
* Show the system mouse pointer.
|
|
*/
|
|
showSystemCursor() {
|
|
const seat = Clutter.get_default_backend().get_default_seat();
|
|
|
|
if (seat.is_unfocus_inhibited())
|
|
seat.uninhibit_unfocus();
|
|
|
|
if (this._cursorVisibilityChangedId) {
|
|
this._cursorTracker.disconnect(this._cursorVisibilityChangedId);
|
|
delete this._cursorVisibilityChangedId;
|
|
|
|
this._cursorTracker.set_pointer_visible(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* hideSystemCursor:
|
|
* Hide the system mouse pointer.
|
|
*/
|
|
hideSystemCursor() {
|
|
const seat = Clutter.get_default_backend().get_default_seat();
|
|
|
|
if (!seat.is_unfocus_inhibited())
|
|
seat.inhibit_unfocus();
|
|
|
|
if (!this._cursorVisibilityChangedId) {
|
|
this._cursorTracker.set_pointer_visible(false);
|
|
this._cursorVisibilityChangedId = this._cursorTracker.connect('visibility-changed', () => {
|
|
if (this._cursorTracker.get_pointer_visible())
|
|
this._cursorTracker.set_pointer_visible(false);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* setActive:
|
|
* Show/hide all the zoom regions.
|
|
*
|
|
* @param {boolean} activate Boolean to activate or de-activate the magnifier.
|
|
*/
|
|
setActive(activate) {
|
|
let isActive = this.isActive();
|
|
|
|
this._zoomRegions.forEach(zoomRegion => {
|
|
zoomRegion.setActive(activate);
|
|
});
|
|
|
|
if (isActive === activate)
|
|
return;
|
|
|
|
if (activate) {
|
|
this._updateMouseSprite();
|
|
this._cursorTracker.connectObject(
|
|
'cursor-changed', this._updateMouseSprite.bind(this), this);
|
|
Meta.disable_unredirect_for_display(global.display);
|
|
this.startTrackingMouse();
|
|
} else {
|
|
this._cursorTracker.disconnectObject(this);
|
|
this._mouseSprite.content.texture = null;
|
|
Meta.enable_unredirect_for_display(global.display);
|
|
this.stopTrackingMouse();
|
|
}
|
|
|
|
if (this._crossHairs)
|
|
this._crossHairs.setEnabled(activate);
|
|
|
|
// Make sure system mouse pointer is shown when all zoom regions are
|
|
// invisible.
|
|
if (!activate)
|
|
this.showSystemCursor();
|
|
|
|
// Notify interested parties of this change
|
|
this.emit('active-changed', activate);
|
|
}
|
|
|
|
/**
|
|
* isActive:
|
|
*
|
|
* @returns {boolean} Whether the magnifier is active.
|
|
*/
|
|
isActive() {
|
|
// Sufficient to check one ZoomRegion since Magnifier's active
|
|
// state applies to all of them.
|
|
if (this._zoomRegions.length == 0)
|
|
return false;
|
|
else
|
|
return this._zoomRegions[0].isActive();
|
|
}
|
|
|
|
/**
|
|
* startTrackingMouse:
|
|
* Turn on mouse tracking, if not already doing so.
|
|
*/
|
|
startTrackingMouse() {
|
|
if (!this._pointerWatch) {
|
|
let interval = 1000 / 60;
|
|
this._pointerWatch = PointerWatcher.getPointerWatcher().addWatch(interval, this.scrollToMousePos.bind(this));
|
|
|
|
this.scrollToMousePos();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* stopTrackingMouse:
|
|
* Turn off mouse tracking, if not already doing so.
|
|
*/
|
|
stopTrackingMouse() {
|
|
if (this._pointerWatch)
|
|
this._pointerWatch.remove();
|
|
|
|
this._pointerWatch = null;
|
|
}
|
|
|
|
/**
|
|
* isTrackingMouse:
|
|
*
|
|
* @returns {boolean} whether the magnifier is currently tracking the mouse
|
|
*/
|
|
isTrackingMouse() {
|
|
return !!this._mouseTrackingId;
|
|
}
|
|
|
|
/**
|
|
* scrollToMousePos:
|
|
* Position all zoom regions' ROI relative to the current location of the
|
|
* system pointer.
|
|
*
|
|
* @param {[xMouse: number, yMouse: number] | []} args
|
|
*/
|
|
scrollToMousePos(...args) {
|
|
const [xMouse, yMouse] = args.length ? args : global.get_pointer();
|
|
|
|
if (xMouse === this.xMouse && yMouse === this.yMouse)
|
|
return;
|
|
|
|
this.xMouse = xMouse;
|
|
this.yMouse = yMouse;
|
|
|
|
let sysMouseOverAny = false;
|
|
this._zoomRegions.forEach(zoomRegion => {
|
|
if (zoomRegion.scrollToMousePos())
|
|
sysMouseOverAny = true;
|
|
});
|
|
if (sysMouseOverAny)
|
|
this.hideSystemCursor();
|
|
else
|
|
this.showSystemCursor();
|
|
}
|
|
|
|
/**
|
|
* createZoomRegion:
|
|
* Create a ZoomRegion instance with the given properties.
|
|
*
|
|
* @param {number} xMagFactor
|
|
* The power to set horizontal magnification of the ZoomRegion. A value
|
|
* of 1.0 means no magnification, a value of 2.0 doubles the size.
|
|
* @param {number} yMagFactor
|
|
* The power to set the vertical magnification of the ZoomRegion.
|
|
* @param {{x: number, y: number, width: number, height: number}} roi
|
|
* The reg Object that defines the region to magnify, given in
|
|
* unmagnified coordinates.
|
|
* @param {{x: number, y: number, width: number, height: number}} viewPort
|
|
* Object that defines the position of the ZoomRegion on screen.
|
|
* @returns {ZoomRegion} the newly created ZoomRegion.
|
|
*/
|
|
createZoomRegion(xMagFactor, yMagFactor, roi, viewPort) {
|
|
let zoomRegion = new ZoomRegion(this, this._cursorRoot);
|
|
zoomRegion.setViewPort(viewPort);
|
|
|
|
// We ignore the redundant width/height on the ROI
|
|
let fixedROI = Object.create(roi);
|
|
fixedROI.width = viewPort.width / xMagFactor;
|
|
fixedROI.height = viewPort.height / yMagFactor;
|
|
zoomRegion.setROI(fixedROI);
|
|
|
|
zoomRegion.addCrosshairs(this._crossHairs);
|
|
return zoomRegion;
|
|
}
|
|
|
|
/**
|
|
* addZoomRegion:
|
|
* Append the given ZoomRegion to the list of currently defined ZoomRegions
|
|
* for this Magnifier instance.
|
|
*
|
|
* @param {ZoomRegion} zoomRegion The zoomRegion to add.
|
|
*/
|
|
addZoomRegion(zoomRegion) {
|
|
if (zoomRegion) {
|
|
this._zoomRegions.push(zoomRegion);
|
|
if (!this.isTrackingMouse())
|
|
this.startTrackingMouse();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* getZoomRegions:
|
|
* Return a list of ZoomRegion's for this Magnifier.
|
|
*
|
|
* @returns {ZoomRegion[]} The Magnifier's zoom region list.
|
|
*/
|
|
getZoomRegions() {
|
|
return this._zoomRegions;
|
|
}
|
|
|
|
/**
|
|
* clearAllZoomRegions:
|
|
* Remove all the zoom regions from this Magnfier's ZoomRegion list.
|
|
*/
|
|
clearAllZoomRegions() {
|
|
for (let i = 0; i < this._zoomRegions.length; i++)
|
|
this._zoomRegions[i].setActive(false);
|
|
|
|
this._zoomRegions.length = 0;
|
|
this.stopTrackingMouse();
|
|
this.showSystemCursor();
|
|
}
|
|
|
|
/**
|
|
* addCrosshairs:
|
|
* Add and show a cross hair centered on the magnified mouse.
|
|
*/
|
|
addCrosshairs() {
|
|
if (!this._crossHairs)
|
|
this._crossHairs = new Crosshairs();
|
|
|
|
let thickness = this._settings.get_int(CROSS_HAIRS_THICKNESS_KEY);
|
|
let color = this._settings.get_string(CROSS_HAIRS_COLOR_KEY);
|
|
let opacity = this._settings.get_double(CROSS_HAIRS_OPACITY_KEY);
|
|
let length = this._settings.get_int(CROSS_HAIRS_LENGTH_KEY);
|
|
let clip = this._settings.get_boolean(CROSS_HAIRS_CLIP_KEY);
|
|
|
|
this.setCrosshairsThickness(thickness);
|
|
this.setCrosshairsColor(color);
|
|
this.setCrosshairsOpacity(opacity);
|
|
this.setCrosshairsLength(length);
|
|
this.setCrosshairsClip(clip);
|
|
|
|
let theCrossHairs = this._crossHairs;
|
|
this._zoomRegions.forEach(zoomRegion => {
|
|
zoomRegion.addCrosshairs(theCrossHairs);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* setCrosshairsVisible:
|
|
*
|
|
* Show or hide the cross hair
|
|
*
|
|
* @param {boolean} visible Flag that indicates show (true) or hide (false).
|
|
*/
|
|
setCrosshairsVisible(visible) {
|
|
if (visible) {
|
|
if (!this._crossHairs)
|
|
this.addCrosshairs();
|
|
this._crossHairs.show();
|
|
} else {
|
|
// eslint-disable-next-line no-lonely-if
|
|
if (this._crossHairs)
|
|
this._crossHairs.hide();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* setCrosshairsColor:
|
|
*
|
|
* Set the color of the crosshairs for all ZoomRegions.
|
|
*
|
|
* @param {string} color The color as a string, e.g. '#ff0000ff' or 'red'.
|
|
*/
|
|
setCrosshairsColor(color) {
|
|
if (this._crossHairs) {
|
|
let [res_, clutterColor] = Clutter.Color.from_string(color);
|
|
this._crossHairs.setColor(clutterColor);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* getCrosshairsColor:
|
|
* Get the color of the crosshairs.
|
|
*
|
|
* @returns {string} The color as a string, e.g. '#0000ffff' or 'blue'.
|
|
*/
|
|
getCrosshairsColor() {
|
|
if (this._crossHairs) {
|
|
let clutterColor = this._crossHairs.getColor();
|
|
return clutterColor.to_string();
|
|
} else {
|
|
return '#00000000';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* setCrosshairsThickness:
|
|
*
|
|
* Set the crosshairs thickness for all ZoomRegions.
|
|
*
|
|
* @param {number} thickness The width of the vertical and
|
|
* horizontal lines of the crosshairs.
|
|
*/
|
|
setCrosshairsThickness(thickness) {
|
|
if (this._crossHairs)
|
|
this._crossHairs.setThickness(thickness);
|
|
}
|
|
|
|
/**
|
|
* getCrosshairsThickness:
|
|
* Get the crosshairs thickness.
|
|
*
|
|
* @returns {number} The width of the vertical and horizontal
|
|
* lines of the crosshairs.
|
|
*/
|
|
getCrosshairsThickness() {
|
|
if (this._crossHairs)
|
|
return this._crossHairs.getThickness();
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* setCrosshairsOpacity:
|
|
*
|
|
* @param {number} opacity Value between 0.0 (transparent)
|
|
* and 1.0 (fully opaque).
|
|
*/
|
|
setCrosshairsOpacity(opacity) {
|
|
if (this._crossHairs)
|
|
this._crossHairs.setOpacity(opacity * 255);
|
|
}
|
|
|
|
/**
|
|
* getCrosshairsOpacity:
|
|
* @returns {number} Value between 0.0 (transparent) and 1.0 (fully opaque).
|
|
*/
|
|
getCrosshairsOpacity() {
|
|
if (this._crossHairs)
|
|
return this._crossHairs.getOpacity() / 255.0;
|
|
else
|
|
return 0.0;
|
|
}
|
|
|
|
/**
|
|
* setCrosshairsLength:
|
|
*
|
|
* Set the crosshairs length for all ZoomRegions.
|
|
*
|
|
* @param {number} length The length of the vertical and horizontal
|
|
* lines making up the crosshairs.
|
|
*/
|
|
setCrosshairsLength(length) {
|
|
if (this._crossHairs) {
|
|
let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
|
|
this._crossHairs.setLength(length / scaleFactor);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* getCrosshairsLength:
|
|
* Get the crosshairs length.
|
|
*
|
|
* @returns {number} The length of the vertical and horizontal
|
|
* lines making up the crosshairs.
|
|
*/
|
|
getCrosshairsLength() {
|
|
if (this._crossHairs)
|
|
return this._crossHairs.getLength();
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* setCrosshairsClip:
|
|
*
|
|
* Set whether the crosshairs are clipped at their intersection.
|
|
*
|
|
* @param {boolean} clip Flag to indicate whether to clip the crosshairs.
|
|
*/
|
|
setCrosshairsClip(clip) {
|
|
if (!this._crossHairs)
|
|
return;
|
|
|
|
// Setting no clipping on crosshairs means a zero sized clip rectangle.
|
|
this._crossHairs.setClip(clip ? CROSSHAIRS_CLIP_SIZE : [0, 0]);
|
|
}
|
|
|
|
/**
|
|
* getCrosshairsClip:
|
|
* Get whether the crosshairs are clipped by the mouse image.
|
|
*
|
|
* @returns {boolean} Whether the crosshairs are clipped.
|
|
*/
|
|
getCrosshairsClip() {
|
|
if (this._crossHairs) {
|
|
let [clipWidth, clipHeight] = this._crossHairs.getClip();
|
|
return clipWidth > 0 && clipHeight > 0;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Private methods //
|
|
|
|
_updateMouseSprite() {
|
|
this._updateSpriteTexture();
|
|
let [xHot, yHot] = this._cursorTracker.get_hot();
|
|
this._mouseSprite.set({
|
|
translation_x: -xHot,
|
|
translation_y: -yHot,
|
|
});
|
|
}
|
|
|
|
_updateSpriteTexture() {
|
|
let sprite = this._cursorTracker.get_sprite();
|
|
|
|
if (sprite) {
|
|
this._mouseSprite.content.texture = sprite;
|
|
this._mouseSprite.show();
|
|
} else {
|
|
this._mouseSprite.hide();
|
|
}
|
|
}
|
|
|
|
_settingsInit(zoomRegion) {
|
|
this._settings = new Gio.Settings({ schema_id: MAGNIFIER_SCHEMA });
|
|
|
|
this._settings.connect(`changed::${SCREEN_POSITION_KEY}`,
|
|
this._updateScreenPosition.bind(this));
|
|
this._settings.connect(`changed::${MAG_FACTOR_KEY}`,
|
|
this._updateMagFactor.bind(this));
|
|
this._settings.connect(`changed::${LENS_MODE_KEY}`,
|
|
this._updateLensMode.bind(this));
|
|
this._settings.connect(`changed::${CLAMP_MODE_KEY}`,
|
|
this._updateClampMode.bind(this));
|
|
this._settings.connect(`changed::${MOUSE_TRACKING_KEY}`,
|
|
this._updateMouseTrackingMode.bind(this));
|
|
this._settings.connect(`changed::${FOCUS_TRACKING_KEY}`,
|
|
this._updateFocusTrackingMode.bind(this));
|
|
this._settings.connect(`changed::${CARET_TRACKING_KEY}`,
|
|
this._updateCaretTrackingMode.bind(this));
|
|
|
|
this._settings.connect(`changed::${INVERT_LIGHTNESS_KEY}`,
|
|
this._updateInvertLightness.bind(this));
|
|
this._settings.connect(`changed::${COLOR_SATURATION_KEY}`,
|
|
this._updateColorSaturation.bind(this));
|
|
|
|
this._settings.connect(`changed::${BRIGHT_RED_KEY}`,
|
|
this._updateBrightness.bind(this));
|
|
this._settings.connect(`changed::${BRIGHT_GREEN_KEY}`,
|
|
this._updateBrightness.bind(this));
|
|
this._settings.connect(`changed::${BRIGHT_BLUE_KEY}`,
|
|
this._updateBrightness.bind(this));
|
|
|
|
this._settings.connect(`changed::${CONTRAST_RED_KEY}`,
|
|
this._updateContrast.bind(this));
|
|
this._settings.connect(`changed::${CONTRAST_GREEN_KEY}`,
|
|
this._updateContrast.bind(this));
|
|
this._settings.connect(`changed::${CONTRAST_BLUE_KEY}`,
|
|
this._updateContrast.bind(this));
|
|
|
|
this._settings.connect(`changed::${SHOW_CROSS_HAIRS_KEY}`, () => {
|
|
this.setCrosshairsVisible(this._settings.get_boolean(SHOW_CROSS_HAIRS_KEY));
|
|
});
|
|
|
|
this._settings.connect(`changed::${CROSS_HAIRS_THICKNESS_KEY}`, () => {
|
|
this.setCrosshairsThickness(this._settings.get_int(CROSS_HAIRS_THICKNESS_KEY));
|
|
});
|
|
|
|
this._settings.connect(`changed::${CROSS_HAIRS_COLOR_KEY}`, () => {
|
|
this.setCrosshairsColor(this._settings.get_string(CROSS_HAIRS_COLOR_KEY));
|
|
});
|
|
|
|
this._settings.connect(`changed::${CROSS_HAIRS_OPACITY_KEY}`, () => {
|
|
this.setCrosshairsOpacity(this._settings.get_double(CROSS_HAIRS_OPACITY_KEY));
|
|
});
|
|
|
|
this._settings.connect(`changed::${CROSS_HAIRS_LENGTH_KEY}`, () => {
|
|
this.setCrosshairsLength(this._settings.get_int(CROSS_HAIRS_LENGTH_KEY));
|
|
});
|
|
|
|
this._settings.connect(`changed::${CROSS_HAIRS_CLIP_KEY}`, () => {
|
|
this.setCrosshairsClip(this._settings.get_boolean(CROSS_HAIRS_CLIP_KEY));
|
|
});
|
|
|
|
if (zoomRegion) {
|
|
// Mag factor is accurate to two decimal places.
|
|
let aPref = parseFloat(this._settings.get_double(MAG_FACTOR_KEY).toFixed(2));
|
|
if (aPref != 0.0)
|
|
zoomRegion.setMagFactor(aPref, aPref);
|
|
|
|
aPref = this._settings.get_enum(SCREEN_POSITION_KEY);
|
|
if (aPref)
|
|
zoomRegion.setScreenPosition(aPref);
|
|
|
|
zoomRegion.setLensMode(this._settings.get_boolean(LENS_MODE_KEY));
|
|
zoomRegion.setClampScrollingAtEdges(!this._settings.get_boolean(CLAMP_MODE_KEY));
|
|
|
|
aPref = this._settings.get_enum(MOUSE_TRACKING_KEY);
|
|
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);
|
|
|
|
aPref = this._settings.get_double(COLOR_SATURATION_KEY);
|
|
if (aPref)
|
|
zoomRegion.setColorSaturation(aPref);
|
|
|
|
let bc = {};
|
|
bc.r = this._settings.get_double(BRIGHT_RED_KEY);
|
|
bc.g = this._settings.get_double(BRIGHT_GREEN_KEY);
|
|
bc.b = this._settings.get_double(BRIGHT_BLUE_KEY);
|
|
zoomRegion.setBrightness(bc);
|
|
|
|
bc.r = this._settings.get_double(CONTRAST_RED_KEY);
|
|
bc.g = this._settings.get_double(CONTRAST_GREEN_KEY);
|
|
bc.b = this._settings.get_double(CONTRAST_BLUE_KEY);
|
|
zoomRegion.setContrast(bc);
|
|
}
|
|
|
|
let showCrosshairs = this._settings.get_boolean(SHOW_CROSS_HAIRS_KEY);
|
|
this.addCrosshairs();
|
|
this.setCrosshairsVisible(showCrosshairs);
|
|
}
|
|
|
|
_updateScreenPosition() {
|
|
// Applies only to the first zoom region.
|
|
if (this._zoomRegions.length) {
|
|
let position = this._settings.get_enum(SCREEN_POSITION_KEY);
|
|
this._zoomRegions[0].setScreenPosition(position);
|
|
if (position != GDesktopEnums.MagnifierScreenPosition.FULL_SCREEN)
|
|
this._updateLensMode();
|
|
}
|
|
}
|
|
|
|
_updateMagFactor() {
|
|
// Applies only to the first zoom region.
|
|
if (this._zoomRegions.length) {
|
|
// Mag factor is accurate to two decimal places.
|
|
let magFactor = parseFloat(this._settings.get_double(MAG_FACTOR_KEY).toFixed(2));
|
|
this._zoomRegions[0].setMagFactor(magFactor, magFactor);
|
|
}
|
|
}
|
|
|
|
_updateLensMode() {
|
|
// Applies only to the first zoom region.
|
|
if (this._zoomRegions.length)
|
|
this._zoomRegions[0].setLensMode(this._settings.get_boolean(LENS_MODE_KEY));
|
|
}
|
|
|
|
_updateClampMode() {
|
|
// Applies only to the first zoom region.
|
|
if (this._zoomRegions.length) {
|
|
this._zoomRegions[0].setClampScrollingAtEdges(
|
|
!this._settings.get_boolean(CLAMP_MODE_KEY));
|
|
}
|
|
}
|
|
|
|
_updateMouseTrackingMode() {
|
|
// Applies only to the first zoom region.
|
|
if (this._zoomRegions.length) {
|
|
this._zoomRegions[0].setMouseTrackingMode(
|
|
this._settings.get_enum(MOUSE_TRACKING_KEY));
|
|
}
|
|
}
|
|
|
|
_updateFocusTrackingMode() {
|
|
// Applies only to the first zoom region.
|
|
if (this._zoomRegions.length) {
|
|
this._zoomRegions[0].setFocusTrackingMode(
|
|
this._settings.get_enum(FOCUS_TRACKING_KEY));
|
|
}
|
|
}
|
|
|
|
_updateCaretTrackingMode() {
|
|
// Applies only to the first zoom region.
|
|
if (this._zoomRegions.length) {
|
|
this._zoomRegions[0].setCaretTrackingMode(
|
|
this._settings.get_enum(CARET_TRACKING_KEY));
|
|
}
|
|
}
|
|
|
|
_updateInvertLightness() {
|
|
// Applies only to the first zoom region.
|
|
if (this._zoomRegions.length) {
|
|
this._zoomRegions[0].setInvertLightness(
|
|
this._settings.get_boolean(INVERT_LIGHTNESS_KEY));
|
|
}
|
|
}
|
|
|
|
_updateColorSaturation() {
|
|
// Applies only to the first zoom region.
|
|
if (this._zoomRegions.length) {
|
|
this._zoomRegions[0].setColorSaturation(
|
|
this._settings.get_double(COLOR_SATURATION_KEY));
|
|
}
|
|
}
|
|
|
|
_updateBrightness() {
|
|
// Applies only to the first zoom region.
|
|
if (this._zoomRegions.length) {
|
|
let brightness = {};
|
|
brightness.r = this._settings.get_double(BRIGHT_RED_KEY);
|
|
brightness.g = this._settings.get_double(BRIGHT_GREEN_KEY);
|
|
brightness.b = this._settings.get_double(BRIGHT_BLUE_KEY);
|
|
this._zoomRegions[0].setBrightness(brightness);
|
|
}
|
|
}
|
|
|
|
_updateContrast() {
|
|
// Applies only to the first zoom region.
|
|
if (this._zoomRegions.length) {
|
|
let contrast = {};
|
|
contrast.r = this._settings.get_double(CONTRAST_RED_KEY);
|
|
contrast.g = this._settings.get_double(CONTRAST_GREEN_KEY);
|
|
contrast.b = this._settings.get_double(CONTRAST_BLUE_KEY);
|
|
this._zoomRegions[0].setContrast(contrast);
|
|
}
|
|
}
|
|
};
|
|
|
|
var ZoomRegion = class ZoomRegion {
|
|
constructor(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;
|
|
this._invertLightness = false;
|
|
this._colorSaturation = 1.0;
|
|
this._brightness = { r: NO_CHANGE, g: NO_CHANGE, b: NO_CHANGE };
|
|
this._contrast = { r: NO_CHANGE, g: NO_CHANGE, b: NO_CHANGE };
|
|
|
|
this._magView = null;
|
|
this._background = null;
|
|
this._uiGroupClone = null;
|
|
this._mouseSourceActor = mouseSourceActor;
|
|
this._mouseActor = null;
|
|
this._crossHairs = null;
|
|
this._crossHairsActor = null;
|
|
|
|
this._viewPortX = 0;
|
|
this._viewPortY = 0;
|
|
this._viewPortWidth = global.screen_width;
|
|
this._viewPortHeight = global.screen_height;
|
|
this._xCenter = this._viewPortWidth / 2;
|
|
this._yCenter = this._viewPortHeight / 2;
|
|
this._xMagFactor = 1;
|
|
this._yMagFactor = 1;
|
|
this._followingCursor = false;
|
|
this._xFocus = 0;
|
|
this._yFocus = 0;
|
|
this._xCaret = 0;
|
|
this._yCaret = 0;
|
|
|
|
this._pointerIdleMonitor = global.backend.get_core_idle_monitor();
|
|
this._scrollContentsTimerId = 0;
|
|
}
|
|
|
|
_connectSignals() {
|
|
if (this._signalConnections)
|
|
return;
|
|
|
|
this._signalConnections = [];
|
|
let id = Main.layoutManager.connect('monitors-changed',
|
|
this._monitorsChanged.bind(this));
|
|
this._signalConnections.push([Main.layoutManager, id]);
|
|
|
|
id = this._focusCaretTracker.connect('caret-moved', this._updateCaret.bind(this));
|
|
this._signalConnections.push([this._focusCaretTracker, id]);
|
|
|
|
id = this._focusCaretTracker.connect('focus-changed', this._updateFocus.bind(this));
|
|
this._signalConnections.push([this._focusCaretTracker, id]);
|
|
}
|
|
|
|
_disconnectSignals() {
|
|
for (let [obj, id] of this._signalConnections)
|
|
obj.disconnect(id);
|
|
|
|
delete this._signalConnections;
|
|
}
|
|
|
|
_updateScreenPosition() {
|
|
if (this._screenPosition == GDesktopEnums.MagnifierScreenPosition.NONE) {
|
|
this._setViewPort({
|
|
x: this._viewPortX,
|
|
y: this._viewPortY,
|
|
width: this._viewPortWidth,
|
|
height: this._viewPortHeight,
|
|
});
|
|
} else {
|
|
this.setScreenPosition(this._screenPosition);
|
|
}
|
|
}
|
|
|
|
_convertExtentsToScreenSpace(accessible, extents) {
|
|
const toplevelWindowTypes = new Set([
|
|
Atspi.Role.FRAME,
|
|
Atspi.Role.DIALOG,
|
|
Atspi.Role.WINDOW,
|
|
]);
|
|
|
|
try {
|
|
let app = null;
|
|
let parentWindow = null;
|
|
let iter = accessible;
|
|
while (iter) {
|
|
if (iter.get_role() === Atspi.Role.APPLICATION) {
|
|
app = iter;
|
|
/* This is the last Accessible we are interested in */
|
|
break;
|
|
} else if (toplevelWindowTypes.has(iter.get_role())) {
|
|
parentWindow = iter;
|
|
}
|
|
iter = iter.get_parent();
|
|
}
|
|
|
|
/* We don't want to translate our own events to the focus window.
|
|
* They are also already scaled by clutter before being sent, so
|
|
* we don't need to do that here either. */
|
|
if (app && app.get_name() === 'gnome-shell')
|
|
return extents;
|
|
|
|
/* Only events from the focused widget of the focused window. Some
|
|
* widgets seem to claim to have focus when the window does not so
|
|
* check both. */
|
|
const windowActive = parentWindow &&
|
|
parentWindow.get_state_set().contains(Atspi.StateType.ACTIVE);
|
|
const accessibleFocused =
|
|
accessible.get_state_set().contains(Atspi.StateType.FOCUSED);
|
|
if (!windowActive || !accessibleFocused)
|
|
return null;
|
|
} catch (e) {
|
|
throw new Error(`Failed to validate parent window: ${e}`);
|
|
}
|
|
|
|
const { focusWindow } = global.display;
|
|
if (!focusWindow)
|
|
return null;
|
|
|
|
let windowRect = focusWindow.get_frame_rect();
|
|
if (!focusWindow.is_client_decorated())
|
|
windowRect = focusWindow.frame_rect_to_client_rect(windowRect);
|
|
|
|
const scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
|
|
const screenSpaceExtents = new Atspi.Rect({
|
|
x: windowRect.x + (scaleFactor * extents.x),
|
|
y: windowRect.y + (scaleFactor * extents.y),
|
|
width: scaleFactor * extents.width,
|
|
height: scaleFactor * extents.height,
|
|
});
|
|
|
|
return screenSpaceExtents;
|
|
}
|
|
|
|
_updateFocus(caller, event) {
|
|
let component = event.source.get_component_iface();
|
|
if (!component || event.detail1 != 1)
|
|
return;
|
|
let extents;
|
|
try {
|
|
extents = component.get_extents(Atspi.CoordType.WINDOW);
|
|
extents = this._convertExtentsToScreenSpace(event.source, extents);
|
|
if (!extents)
|
|
return;
|
|
} catch (e) {
|
|
log(`Failed to read extents of focused component: ${e.message}`);
|
|
return;
|
|
}
|
|
|
|
const [xFocus, yFocus] = [
|
|
extents.x + (extents.width / 2),
|
|
extents.y + (extents.height / 2),
|
|
];
|
|
|
|
if (this._xFocus !== xFocus || this._yFocus !== yFocus) {
|
|
[this._xFocus, this._yFocus] = [xFocus, yFocus];
|
|
this._centerFromFocusPosition();
|
|
}
|
|
}
|
|
|
|
_updateCaret(caller, event) {
|
|
let text = event.source.get_text_iface();
|
|
if (!text)
|
|
return;
|
|
let extents;
|
|
try {
|
|
extents = text.get_character_extents(text.get_caret_offset(),
|
|
Atspi.CoordType.WINDOW);
|
|
extents = this._convertExtentsToScreenSpace(text, extents);
|
|
if (!extents)
|
|
return;
|
|
} catch (e) {
|
|
log(`Failed to read extents of text caret: ${e.message}`);
|
|
return;
|
|
}
|
|
|
|
const [xCaret, yCaret] = [extents.x, extents.y];
|
|
|
|
// Ignore event(s) if the caret size is none (0x0). This happens a lot if
|
|
// the cursor offset can't be translated into a location. This is a work
|
|
// around.
|
|
if (extents.width === 0 && extents.height === 0)
|
|
return;
|
|
|
|
if (this._xCaret !== xCaret || this._yCaret !== yCaret) {
|
|
[this._xCaret, this._yCaret] = [xCaret, yCaret];
|
|
this._centerFromCaretPosition();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* setActive:
|
|
*
|
|
* @param {boolean} activate Boolean to show/hide the ZoomRegion.
|
|
*/
|
|
setActive(activate) {
|
|
if (activate == this.isActive())
|
|
return;
|
|
|
|
if (activate) {
|
|
this._createActors();
|
|
if (this._isMouseOverRegion())
|
|
this._magnifier.hideSystemCursor();
|
|
this._updateScreenPosition();
|
|
this._updateMagViewGeometry();
|
|
this._updateCloneGeometry();
|
|
this._updateMousePosition();
|
|
this._connectSignals();
|
|
} else {
|
|
Main.uiGroup.set_opacity(255);
|
|
this._disconnectSignals();
|
|
this._destroyActors();
|
|
}
|
|
|
|
this._syncCaretTracking();
|
|
this._syncFocusTracking();
|
|
}
|
|
|
|
/**
|
|
* isActive:
|
|
*
|
|
* @returns {boolean} Whether this ZoomRegion is active
|
|
*/
|
|
isActive() {
|
|
return this._magView != null;
|
|
}
|
|
|
|
/**
|
|
* setMagFactor:
|
|
*
|
|
* @param {number} xMagFactor The power to set the horizontal
|
|
* magnification factor to of the magnified view. A value of 1.0
|
|
* means no magnification. A value of 2.0 doubles the size.
|
|
* @param {number} yMagFactor The power to set the vertical
|
|
* magnification factor to of the magnified view.
|
|
*/
|
|
setMagFactor(xMagFactor, yMagFactor) {
|
|
this._changeROI({
|
|
xMagFactor,
|
|
yMagFactor,
|
|
redoCursorTracking: this._followingCursor,
|
|
animate: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* getMagFactor:
|
|
*
|
|
* @returns {number[]} an array, [xMagFactor, yMagFactor], containing
|
|
* the horizontal and vertical magnification powers. A value of
|
|
* 1.0 means no magnification. A value of 2.0 means the contents
|
|
* are doubled in size, and so on.
|
|
*/
|
|
getMagFactor() {
|
|
return [this._xMagFactor, this._yMagFactor];
|
|
}
|
|
|
|
/**
|
|
* setMouseTrackingMode
|
|
*
|
|
* @param {GDesktopEnums.MagnifierMouseTrackingMode} mode the new mode
|
|
*/
|
|
setMouseTrackingMode(mode) {
|
|
if (mode >= GDesktopEnums.MagnifierMouseTrackingMode.NONE &&
|
|
mode <= GDesktopEnums.MagnifierMouseTrackingMode.PUSH)
|
|
this._mouseTrackingMode = mode;
|
|
}
|
|
|
|
/**
|
|
* getMouseTrackingMode:
|
|
*
|
|
* @returns {GDesktopEnums.MagnifierMouseTrackingMode} the current mode
|
|
*/
|
|
getMouseTrackingMode() {
|
|
return this._mouseTrackingMode;
|
|
}
|
|
|
|
/**
|
|
* setFocusTrackingMode
|
|
*
|
|
* @param {GDesktopEnums.MagnifierFocusTrackingMode} mode the new mode
|
|
*/
|
|
setFocusTrackingMode(mode) {
|
|
this._focusTrackingMode = mode;
|
|
this._syncFocusTracking();
|
|
}
|
|
|
|
/**
|
|
* setCaretTrackingMode
|
|
*
|
|
* @param {GDesktopEnums.MagnifierCaretTrackingMode} mode the new mode
|
|
*/
|
|
setCaretTrackingMode(mode) {
|
|
this._caretTrackingMode = mode;
|
|
this._syncCaretTracking();
|
|
}
|
|
|
|
_syncFocusTracking() {
|
|
let enabled = this._focusTrackingMode != GDesktopEnums.MagnifierFocusTrackingMode.NONE &&
|
|
this.isActive();
|
|
|
|
if (enabled)
|
|
this._focusCaretTracker.registerFocusListener();
|
|
else
|
|
this._focusCaretTracker.deregisterFocusListener();
|
|
}
|
|
|
|
_syncCaretTracking() {
|
|
let enabled = this._caretTrackingMode != GDesktopEnums.MagnifierCaretTrackingMode.NONE &&
|
|
this.isActive();
|
|
|
|
if (enabled)
|
|
this._focusCaretTracker.registerCaretListener();
|
|
else
|
|
this._focusCaretTracker.deregisterCaretListener();
|
|
}
|
|
|
|
/**
|
|
* setViewPort
|
|
* Sets the position and size of the ZoomRegion on screen.
|
|
*
|
|
* @param {{x: number, y: number, width: number, height: number}} viewPort
|
|
* Object defining the position and size of the view port.
|
|
* The values are in stage coordinate space.
|
|
*/
|
|
setViewPort(viewPort) {
|
|
this._setViewPort(viewPort);
|
|
this._screenPosition = GDesktopEnums.MagnifierScreenPosition.NONE;
|
|
}
|
|
|
|
/**
|
|
* setROI
|
|
* Sets the "region of interest" that the ZoomRegion is magnifying.
|
|
*
|
|
* @param {{x: number, y: number, width: number, height: number}} roi
|
|
* Object that defines the region of the screen to magnify.
|
|
* The values are in screen (unmagnified) coordinate space.
|
|
*/
|
|
setROI(roi) {
|
|
if (roi.width <= 0 || roi.height <= 0)
|
|
return;
|
|
|
|
this._followingCursor = false;
|
|
this._changeROI({
|
|
xMagFactor: this._viewPortWidth / roi.width,
|
|
yMagFactor: this._viewPortHeight / roi.height,
|
|
xCenter: roi.x + roi.width / 2,
|
|
yCenter: roi.y + roi.height / 2,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* getROI:
|
|
* Retrieves the "region of interest" -- the rectangular bounds of that part
|
|
* of the desktop that the magnified view is showing (x, y, width, height).
|
|
* The bounds are given in non-magnified coordinates.
|
|
*
|
|
* @returns {number[]} an array, [x, y, width, height], representing
|
|
* the bounding rectangle of what is shown in the magnified view.
|
|
*/
|
|
getROI() {
|
|
let roiWidth = this._viewPortWidth / this._xMagFactor;
|
|
let roiHeight = this._viewPortHeight / this._yMagFactor;
|
|
|
|
return [
|
|
this._xCenter - roiWidth / 2,
|
|
this._yCenter - roiHeight / 2,
|
|
roiWidth, roiHeight,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* setLensMode:
|
|
*
|
|
* Turn lens mode on/off. In full screen mode, lens mode does nothing since
|
|
* a lens the size of the screen is pointless.
|
|
*
|
|
* @param {boolean} lensMode Whether lensMode should be active
|
|
*/
|
|
setLensMode(lensMode) {
|
|
this._lensMode = lensMode;
|
|
if (!this._lensMode)
|
|
this.setScreenPosition(this._screenPosition);
|
|
}
|
|
|
|
/**
|
|
* isLensMode:
|
|
* Is lens mode on or off?
|
|
*
|
|
* @returns {boolean} The lens mode state.
|
|
*/
|
|
isLensMode() {
|
|
return this._lensMode;
|
|
}
|
|
|
|
/**
|
|
* setClampScrollingAtEdges:
|
|
* Stop vs. allow scrolling of the magnified contents when it scroll beyond
|
|
* the edges of the screen.
|
|
*
|
|
* @param {boolean} clamp Boolean to turn on/off clamping.
|
|
*/
|
|
setClampScrollingAtEdges(clamp) {
|
|
this._clampScrollingAtEdges = clamp;
|
|
if (clamp)
|
|
this._changeROI();
|
|
}
|
|
|
|
/**
|
|
* setTopHalf:
|
|
* Magnifier view occupies the top half of the screen.
|
|
*/
|
|
setTopHalf() {
|
|
let viewPort = {};
|
|
viewPort.x = 0;
|
|
viewPort.y = 0;
|
|
viewPort.width = global.screen_width;
|
|
viewPort.height = global.screen_height / 2;
|
|
this._setViewPort(viewPort);
|
|
this._screenPosition = GDesktopEnums.MagnifierScreenPosition.TOP_HALF;
|
|
}
|
|
|
|
/**
|
|
* setBottomHalf:
|
|
* Magnifier view occupies the bottom half of the screen.
|
|
*/
|
|
setBottomHalf() {
|
|
let viewPort = {};
|
|
viewPort.x = 0;
|
|
viewPort.y = global.screen_height / 2;
|
|
viewPort.width = global.screen_width;
|
|
viewPort.height = global.screen_height / 2;
|
|
this._setViewPort(viewPort);
|
|
this._screenPosition = GDesktopEnums.MagnifierScreenPosition.BOTTOM_HALF;
|
|
}
|
|
|
|
/**
|
|
* setLeftHalf:
|
|
* Magnifier view occupies the left half of the screen.
|
|
*/
|
|
setLeftHalf() {
|
|
let viewPort = {};
|
|
viewPort.x = 0;
|
|
viewPort.y = 0;
|
|
viewPort.width = global.screen_width / 2;
|
|
viewPort.height = global.screen_height;
|
|
this._setViewPort(viewPort);
|
|
this._screenPosition = GDesktopEnums.MagnifierScreenPosition.LEFT_HALF;
|
|
}
|
|
|
|
/**
|
|
* setRightHalf:
|
|
* Magnifier view occupies the right half of the screen.
|
|
*/
|
|
setRightHalf() {
|
|
let viewPort = {};
|
|
viewPort.x = global.screen_width / 2;
|
|
viewPort.y = 0;
|
|
viewPort.width = global.screen_width / 2;
|
|
viewPort.height = global.screen_height;
|
|
this._setViewPort(viewPort);
|
|
this._screenPosition = GDesktopEnums.MagnifierScreenPosition.RIGHT_HALF;
|
|
}
|
|
|
|
/**
|
|
* setFullScreenMode:
|
|
* Set the ZoomRegion to full-screen mode.
|
|
* Note: disallows lens mode.
|
|
*/
|
|
setFullScreenMode() {
|
|
let viewPort = {};
|
|
viewPort.x = 0;
|
|
viewPort.y = 0;
|
|
viewPort.width = global.screen_width;
|
|
viewPort.height = global.screen_height;
|
|
this.setViewPort(viewPort);
|
|
|
|
this._screenPosition = GDesktopEnums.MagnifierScreenPosition.FULL_SCREEN;
|
|
}
|
|
|
|
/**
|
|
* setScreenPosition:
|
|
* Positions the zoom region to one of the enumerated positions on the
|
|
* screen.
|
|
*
|
|
* @param {GDesktopEnums.MagnifierScreenPosition} inPosition the position
|
|
*/
|
|
setScreenPosition(inPosition) {
|
|
switch (inPosition) {
|
|
case GDesktopEnums.MagnifierScreenPosition.FULL_SCREEN:
|
|
this.setFullScreenMode();
|
|
break;
|
|
case GDesktopEnums.MagnifierScreenPosition.TOP_HALF:
|
|
this.setTopHalf();
|
|
break;
|
|
case GDesktopEnums.MagnifierScreenPosition.BOTTOM_HALF:
|
|
this.setBottomHalf();
|
|
break;
|
|
case GDesktopEnums.MagnifierScreenPosition.LEFT_HALF:
|
|
this.setLeftHalf();
|
|
break;
|
|
case GDesktopEnums.MagnifierScreenPosition.RIGHT_HALF:
|
|
this.setRightHalf();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* getScreenPosition:
|
|
* Tell the outside world what the current mode is -- magnifiying the
|
|
* top half, bottom half, etc.
|
|
*
|
|
* @returns {GDesktopEnums.MagnifierScreenPosition}: the current position.
|
|
*/
|
|
getScreenPosition() {
|
|
return this._screenPosition;
|
|
}
|
|
|
|
_clearScrollContentsTimer() {
|
|
if (this._scrollContentsTimerId !== 0) {
|
|
GLib.source_remove(this._scrollContentsTimerId);
|
|
this._scrollContentsTimerId = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* scrollToMousePos:
|
|
* Set the region of interest based on the position of the system pointer.
|
|
*
|
|
* @returns {boolean}: Whether the system mouse pointer is over the
|
|
* magnified view.
|
|
*/
|
|
scrollToMousePos() {
|
|
this._followingCursor = true;
|
|
if (this._mouseTrackingMode != GDesktopEnums.MagnifierMouseTrackingMode.NONE)
|
|
this._changeROI({ redoCursorTracking: true });
|
|
else
|
|
this._updateMousePosition();
|
|
|
|
this._clearScrollContentsTimer();
|
|
this._scrollContentsTimerId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, POINTER_REST_TIME, () => {
|
|
this._followingCursor = false;
|
|
if (this._xDelayed !== null && this._yDelayed !== null) {
|
|
this._scrollContentsToDelayed(this._xDelayed, this._yDelayed);
|
|
this._xDelayed = null;
|
|
this._yDelayed = null;
|
|
}
|
|
|
|
this._scrollContentsTimerId = 0;
|
|
|
|
return GLib.SOURCE_REMOVE;
|
|
});
|
|
|
|
// Determine whether the system mouse pointer is over this zoom region.
|
|
return this._isMouseOverRegion();
|
|
}
|
|
|
|
_scrollContentsToDelayed(x, y) {
|
|
if (this._followingCursor) {
|
|
this._xDelayed = x;
|
|
this._yDelayed = y;
|
|
} else {
|
|
this.scrollContentsTo(x, y);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* scrollContentsTo:
|
|
* Shift the contents of the magnified view such it is centered on the given
|
|
* coordinate.
|
|
*
|
|
* @param {number} x The x-coord of the point to center on.
|
|
* @param {number} y The y-coord of the point to center on.
|
|
*/
|
|
scrollContentsTo(x, y) {
|
|
if (x < 0 || x > global.screen_width ||
|
|
y < 0 || y > global.screen_height)
|
|
return;
|
|
|
|
this._clearScrollContentsTimer();
|
|
|
|
this._followingCursor = false;
|
|
this._changeROI({
|
|
xCenter: x,
|
|
yCenter: y,
|
|
animate: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* addCrosshairs:
|
|
* Add crosshairs centered on the magnified mouse.
|
|
*
|
|
* @param {Crosshairs} crossHairs Crosshairs instance
|
|
*/
|
|
addCrosshairs(crossHairs) {
|
|
this._crossHairs = crossHairs;
|
|
|
|
// If the crossHairs is not already within a larger container, add it
|
|
// to this zoom region. Otherwise, add a clone.
|
|
if (crossHairs && this.isActive())
|
|
this._crossHairsActor = crossHairs.addToZoomRegion(this, this._mouseActor);
|
|
}
|
|
|
|
/**
|
|
* setInvertLightness:
|
|
* Set whether to invert the lightness of the magnified view.
|
|
*
|
|
* @param {boolean} flag whether brightness should be inverted
|
|
*/
|
|
setInvertLightness(flag) {
|
|
this._invertLightness = flag;
|
|
if (this._magShaderEffects)
|
|
this._magShaderEffects.setInvertLightness(this._invertLightness);
|
|
}
|
|
|
|
/**
|
|
* getInvertLightness:
|
|
*
|
|
* Retrieve whether the lightness is inverted.
|
|
*
|
|
* @returns {boolean} whether brightness should be inverted
|
|
*/
|
|
getInvertLightness() {
|
|
return this._invertLightness;
|
|
}
|
|
|
|
/**
|
|
* setColorSaturation:
|
|
*
|
|
* Set the color saturation of the magnified view.
|
|
*
|
|
* @param {number} saturation A value from 0.0 to 1.0 that defines
|
|
* the color saturation, with 0.0 defining no color (grayscale),
|
|
* and 1.0 defining full color.
|
|
*/
|
|
setColorSaturation(saturation) {
|
|
this._colorSaturation = saturation;
|
|
if (this._magShaderEffects)
|
|
this._magShaderEffects.setColorSaturation(this._colorSaturation);
|
|
}
|
|
|
|
/**
|
|
* getColorSaturation:
|
|
* Retrieve the color saturation of the magnified view.
|
|
*
|
|
* @returns {number} the color saturation
|
|
*/
|
|
getColorSaturation() {
|
|
return this._colorSaturation;
|
|
}
|
|
|
|
/**
|
|
* setBrightness:
|
|
* Alter the brightness of the magnified view.
|
|
*
|
|
* @param {object} brightness Object containing the contrast for the
|
|
* red, green, and blue channels. Values of 0.0 represent "standard"
|
|
* brightness (no change), whereas values less or greater than
|
|
* 0.0 indicate decreased or incresaed brightness, respectively.
|
|
*
|
|
* {number} brightness.r - the red component
|
|
* {number} brightness.g - the green component
|
|
* {number} brightness.b - the blue component
|
|
*/
|
|
setBrightness(brightness) {
|
|
this._brightness.r = brightness.r;
|
|
this._brightness.g = brightness.g;
|
|
this._brightness.b = brightness.b;
|
|
if (this._magShaderEffects)
|
|
this._magShaderEffects.setBrightness(this._brightness);
|
|
}
|
|
|
|
/**
|
|
* setContrast:
|
|
* Alter the contrast of the magnified view.
|
|
*
|
|
* @param {object} contrast Object containing the contrast for the
|
|
* red, green, and blue channels. Values of 0.0 represent "standard"
|
|
* contrast (no change), whereas values less or greater than
|
|
* 0.0 indicate decreased or incresaed contrast, respectively.
|
|
*
|
|
* {number} contrast.r - the red component
|
|
* {number} contrast.g - the green component
|
|
* {number} contrast.b - the blue component
|
|
*/
|
|
setContrast(contrast) {
|
|
this._contrast.r = contrast.r;
|
|
this._contrast.g = contrast.g;
|
|
this._contrast.b = contrast.b;
|
|
if (this._magShaderEffects)
|
|
this._magShaderEffects.setContrast(this._contrast);
|
|
}
|
|
|
|
/**
|
|
* getContrast:
|
|
* Retrieve the contrast of the magnified view.
|
|
*
|
|
* @returns {{r: number, g: number, b: number}}: Object containing
|
|
* the contrast for the red, green, and blue channels.
|
|
*/
|
|
getContrast() {
|
|
let contrast = {};
|
|
contrast.r = this._contrast.r;
|
|
contrast.g = this._contrast.g;
|
|
contrast.b = this._contrast.b;
|
|
return contrast;
|
|
}
|
|
|
|
// Private methods //
|
|
|
|
_createActors() {
|
|
// The root actor for the zoom region
|
|
this._magView = new St.Bin({ style_class: 'magnifier-zoom-region' });
|
|
global.stage.add_actor(this._magView);
|
|
|
|
// hide the magnified region from CLUTTER_PICK_ALL
|
|
Shell.util_set_hidden_from_pick(this._magView, true);
|
|
|
|
// Add a group to clip the contents of the magnified view.
|
|
let mainGroup = new Clutter.Actor({ clip_to_allocation: true });
|
|
this._magView.set_child(mainGroup);
|
|
|
|
// Add a background for when the magnified uiGroup is scrolled
|
|
// out of view (don't want to see desktop showing through).
|
|
this._background = new Background.SystemBackground();
|
|
mainGroup.add_actor(this._background);
|
|
|
|
// Clone the group that contains all of UI on the screen. This is the
|
|
// chrome, the windows, etc.
|
|
this._uiGroupClone = new Clutter.Clone({
|
|
source: Main.uiGroup,
|
|
clip_to_allocation: true,
|
|
});
|
|
mainGroup.add_actor(this._uiGroupClone);
|
|
|
|
// Add either the given mouseSourceActor to the ZoomRegion, or a clone of
|
|
// it.
|
|
if (this._mouseSourceActor.get_parent() != null)
|
|
this._mouseActor = new Clutter.Clone({ source: this._mouseSourceActor });
|
|
else
|
|
this._mouseActor = this._mouseSourceActor;
|
|
mainGroup.add_actor(this._mouseActor);
|
|
|
|
if (this._crossHairs)
|
|
this._crossHairsActor = this._crossHairs.addToZoomRegion(this, this._mouseActor);
|
|
else
|
|
this._crossHairsActor = null;
|
|
|
|
// Contrast and brightness effects.
|
|
this._magShaderEffects = new MagShaderEffects(mainGroup);
|
|
this._magShaderEffects.setColorSaturation(this._colorSaturation);
|
|
this._magShaderEffects.setInvertLightness(this._invertLightness);
|
|
this._magShaderEffects.setBrightness(this._brightness);
|
|
this._magShaderEffects.setContrast(this._contrast);
|
|
}
|
|
|
|
_destroyActors() {
|
|
if (this._mouseActor == this._mouseSourceActor)
|
|
this._mouseActor.get_parent().remove_actor(this._mouseActor);
|
|
if (this._crossHairs)
|
|
this._crossHairs.removeFromParent(this._crossHairsActor);
|
|
|
|
this._magShaderEffects.destroyEffects();
|
|
this._magShaderEffects = null;
|
|
this._magView.destroy();
|
|
this._magView = null;
|
|
this._background = null;
|
|
this._uiGroupClone = null;
|
|
this._mouseActor = null;
|
|
this._crossHairsActor = null;
|
|
}
|
|
|
|
_setViewPort(viewPort, fromROIUpdate) {
|
|
// Sets the position of the zoom region on the screen
|
|
|
|
let width = Math.round(Math.min(viewPort.width, global.screen_width));
|
|
let height = Math.round(Math.min(viewPort.height, global.screen_height));
|
|
let x = Math.max(viewPort.x, 0);
|
|
let y = Math.max(viewPort.y, 0);
|
|
|
|
x = Math.round(Math.min(x, global.screen_width - width));
|
|
y = Math.round(Math.min(y, global.screen_height - height));
|
|
|
|
this._viewPortX = x;
|
|
this._viewPortY = y;
|
|
this._viewPortWidth = width;
|
|
this._viewPortHeight = height;
|
|
|
|
this._updateMagViewGeometry();
|
|
|
|
if (!fromROIUpdate)
|
|
this._changeROI({ redoCursorTracking: this._followingCursor }); // will update mouse
|
|
|
|
if (this.isActive() && this._isMouseOverRegion())
|
|
this._magnifier.hideSystemCursor();
|
|
|
|
const uiGroupIsOccluded = this.isActive() && this._isFullScreen();
|
|
Main.uiGroup.set_opacity(uiGroupIsOccluded ? 0 : 255);
|
|
}
|
|
|
|
_changeROI(params) {
|
|
// Updates the area we are viewing; the magnification factors
|
|
// and center can be set explicitly, or we can recompute
|
|
// the position based on the mouse cursor position
|
|
|
|
params = Params.parse(params, {
|
|
xMagFactor: this._xMagFactor,
|
|
yMagFactor: this._yMagFactor,
|
|
xCenter: this._xCenter,
|
|
yCenter: this._yCenter,
|
|
redoCursorTracking: false,
|
|
animate: false,
|
|
});
|
|
|
|
if (params.xMagFactor <= 0)
|
|
params.xMagFactor = this._xMagFactor;
|
|
if (params.yMagFactor <= 0)
|
|
params.yMagFactor = this._yMagFactor;
|
|
|
|
this._xMagFactor = params.xMagFactor;
|
|
this._yMagFactor = params.yMagFactor;
|
|
|
|
if (params.redoCursorTracking &&
|
|
this._mouseTrackingMode != GDesktopEnums.MagnifierMouseTrackingMode.NONE) {
|
|
// This depends on this.xMagFactor/yMagFactor already being updated
|
|
[params.xCenter, params.yCenter] = this._centerFromMousePosition();
|
|
}
|
|
|
|
if (this._clampScrollingAtEdges) {
|
|
let roiWidth = this._viewPortWidth / this._xMagFactor;
|
|
let roiHeight = this._viewPortHeight / this._yMagFactor;
|
|
|
|
params.xCenter = Math.min(params.xCenter, global.screen_width - roiWidth / 2);
|
|
params.xCenter = Math.max(params.xCenter, roiWidth / 2);
|
|
params.yCenter = Math.min(params.yCenter, global.screen_height - roiHeight / 2);
|
|
params.yCenter = Math.max(params.yCenter, roiHeight / 2);
|
|
}
|
|
|
|
this._xCenter = params.xCenter;
|
|
this._yCenter = params.yCenter;
|
|
|
|
// If in lens mode, move the magnified view such that it is centered
|
|
// over the actual mouse. However, in full screen mode, the "lens" is
|
|
// the size of the screen -- pointless to move such a large lens around.
|
|
if (this._lensMode && !this._isFullScreen()) {
|
|
this._setViewPort({
|
|
x: this._xCenter - this._viewPortWidth / 2,
|
|
y: this._yCenter - this._viewPortHeight / 2,
|
|
width: this._viewPortWidth,
|
|
height: this._viewPortHeight,
|
|
}, true);
|
|
}
|
|
|
|
this._updateCloneGeometry(params.animate);
|
|
}
|
|
|
|
_isMouseOverRegion() {
|
|
// Return whether the system mouse sprite is over this ZoomRegion. If the
|
|
// mouse's position is not given, then it is fetched.
|
|
let mouseIsOver = false;
|
|
if (this.isActive()) {
|
|
let xMouse = this._magnifier.xMouse;
|
|
let yMouse = this._magnifier.yMouse;
|
|
|
|
mouseIsOver =
|
|
xMouse >= this._viewPortX && xMouse < (this._viewPortX + this._viewPortWidth) &&
|
|
yMouse >= this._viewPortY && yMouse < (this._viewPortY + this._viewPortHeight);
|
|
}
|
|
return mouseIsOver;
|
|
}
|
|
|
|
_isFullScreen() {
|
|
// Does the magnified view occupy the whole screen? Note that this
|
|
// doesn't necessarily imply
|
|
// this._screenPosition = GDesktopEnums.MagnifierScreenPosition.FULL_SCREEN;
|
|
|
|
if (this._viewPortX != 0 || this._viewPortY != 0)
|
|
return false;
|
|
if (this._viewPortWidth != global.screen_width ||
|
|
this._viewPortHeight != global.screen_height)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
_centerFromMousePosition() {
|
|
// Determines where the center should be given the current cursor
|
|
// position and mouse tracking mode
|
|
|
|
let xMouse = this._magnifier.xMouse;
|
|
let yMouse = this._magnifier.yMouse;
|
|
|
|
if (this._mouseTrackingMode == GDesktopEnums.MagnifierMouseTrackingMode.PROPORTIONAL)
|
|
return this._centerFromPointProportional(xMouse, yMouse);
|
|
else if (this._mouseTrackingMode == GDesktopEnums.MagnifierMouseTrackingMode.PUSH)
|
|
return this._centerFromPointPush(xMouse, yMouse);
|
|
else if (this._mouseTrackingMode == GDesktopEnums.MagnifierMouseTrackingMode.CENTERED)
|
|
return this._centerFromPointCentered(xMouse, yMouse);
|
|
|
|
return null; // Should never be hit
|
|
}
|
|
|
|
_centerFromCaretPosition() {
|
|
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._scrollContentsToDelayed(xCaret, yCaret);
|
|
}
|
|
|
|
_centerFromFocusPosition() {
|
|
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._scrollContentsToDelayed(xFocus, yFocus);
|
|
}
|
|
|
|
_centerFromPointPush(xPoint, yPoint) {
|
|
let [xRoi, yRoi, widthRoi, heightRoi] = this.getROI();
|
|
let [cursorWidth, cursorHeight] = this._mouseSourceActor.get_size();
|
|
let xPos = xRoi + widthRoi / 2;
|
|
let yPos = yRoi + heightRoi / 2;
|
|
let xRoiRight = xRoi + widthRoi - cursorWidth;
|
|
let yRoiBottom = yRoi + heightRoi - cursorHeight;
|
|
|
|
if (xPoint < xRoi)
|
|
xPos -= xRoi - xPoint;
|
|
else if (xPoint > xRoiRight)
|
|
xPos += xPoint - xRoiRight;
|
|
|
|
if (yPoint < yRoi)
|
|
yPos -= yRoi - yPoint;
|
|
else if (yPoint > yRoiBottom)
|
|
yPos += yPoint - yRoiBottom;
|
|
|
|
return [xPos, yPos];
|
|
}
|
|
|
|
_centerFromPointProportional(xPoint, yPoint) {
|
|
let [xRoi_, yRoi_, widthRoi, heightRoi] = this.getROI();
|
|
let halfScreenWidth = global.screen_width / 2;
|
|
let halfScreenHeight = global.screen_height / 2;
|
|
// We want to pad with a constant distance after zooming, so divide
|
|
// by the magnification factor.
|
|
let unscaledPadding = Math.min(this._viewPortWidth, this._viewPortHeight) / 5;
|
|
let xPadding = unscaledPadding / this._xMagFactor;
|
|
let yPadding = unscaledPadding / this._yMagFactor;
|
|
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];
|
|
}
|
|
|
|
_centerFromPointCentered(xPoint, yPoint) {
|
|
return [xPoint, yPoint];
|
|
}
|
|
|
|
_screenToViewPort(screenX, screenY) {
|
|
// Converts coordinates relative to the (unmagnified) screen to coordinates
|
|
// relative to the origin of this._magView
|
|
return [
|
|
this._viewPortWidth / 2 + (screenX - this._xCenter) * this._xMagFactor,
|
|
this._viewPortHeight / 2 + (screenY - this._yCenter) * this._yMagFactor,
|
|
];
|
|
}
|
|
|
|
_updateMagViewGeometry() {
|
|
if (!this.isActive())
|
|
return;
|
|
|
|
if (this._isFullScreen())
|
|
this._magView.add_style_class_name('full-screen');
|
|
else
|
|
this._magView.remove_style_class_name('full-screen');
|
|
|
|
this._magView.set_size(this._viewPortWidth, this._viewPortHeight);
|
|
this._magView.set_position(this._viewPortX, this._viewPortY);
|
|
}
|
|
|
|
_updateCloneGeometry(animate = false) {
|
|
if (!this.isActive())
|
|
return;
|
|
|
|
let [x, y] = this._screenToViewPort(0, 0);
|
|
this._uiGroupClone.ease({
|
|
x: Math.round(x),
|
|
y: Math.round(y),
|
|
scale_x: this._xMagFactor,
|
|
scale_y: this._yMagFactor,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
duration: animate ? 100 : 0,
|
|
});
|
|
|
|
let [mouseX, mouseY] = this._getMousePosition();
|
|
this._mouseActor.ease({
|
|
x: mouseX,
|
|
y: mouseY,
|
|
scale_x: this._xMagFactor,
|
|
scale_y: this._yMagFactor,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
duration: animate ? 100 : 0,
|
|
});
|
|
|
|
if (this._crossHairsActor) {
|
|
let [crossX, crossY] = this._getCrossHairsPosition();
|
|
this._crossHairsActor.ease({
|
|
x: crossX,
|
|
y: crossY,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
duration: animate ? 100 : 0,
|
|
});
|
|
}
|
|
}
|
|
|
|
_updateMousePosition() {
|
|
let [xMagMouse, yMagMouse] = this._getMousePosition();
|
|
this._mouseActor.set_position(xMagMouse, yMagMouse);
|
|
|
|
if (this._crossHairsActor) {
|
|
let [crossX, crossY] = this._getCrossHairsPosition();
|
|
this._crossHairsActor.set_position(crossX, crossY);
|
|
}
|
|
}
|
|
|
|
_getMousePosition() {
|
|
let [xMagMouse, yMagMouse] = this._screenToViewPort(
|
|
this._magnifier.xMouse, this._magnifier.yMouse);
|
|
return [Math.round(xMagMouse), Math.round(yMagMouse)];
|
|
}
|
|
|
|
_getCrossHairsPosition() {
|
|
let [xMagMouse, yMagMouse] = this._getMousePosition();
|
|
let [groupWidth, groupHeight] = this._crossHairsActor.get_size();
|
|
|
|
return [xMagMouse - groupWidth / 2, yMagMouse - groupHeight / 2];
|
|
}
|
|
|
|
_monitorsChanged() {
|
|
this._background.set_size(global.screen_width, global.screen_height);
|
|
this._updateScreenPosition();
|
|
}
|
|
}
|
|
|
|
var Crosshairs = GObject.registerClass(
|
|
class Crosshairs extends Clutter.Actor {
|
|
_init() {
|
|
// Set the group containing the crosshairs to three times the desktop
|
|
// size in case the crosshairs need to appear to be infinite in
|
|
// length (i.e., extend beyond the edges of the view they appear in).
|
|
let groupWidth = global.screen_width * 3;
|
|
let groupHeight = global.screen_height * 3;
|
|
|
|
super._init({
|
|
clip_to_allocation: false,
|
|
width: groupWidth,
|
|
height: groupHeight,
|
|
});
|
|
this._horizLeftHair = new Clutter.Actor();
|
|
this._horizRightHair = new Clutter.Actor();
|
|
this._vertTopHair = new Clutter.Actor();
|
|
this._vertBottomHair = new Clutter.Actor();
|
|
this.add_actor(this._horizLeftHair);
|
|
this.add_actor(this._horizRightHair);
|
|
this.add_actor(this._vertTopHair);
|
|
this.add_actor(this._vertBottomHair);
|
|
this._clipSize = [0, 0];
|
|
this._clones = [];
|
|
this.reCenter();
|
|
this._monitorsChangedId = 0;
|
|
}
|
|
|
|
_monitorsChanged() {
|
|
this.set_size(global.screen_width * 3, global.screen_height * 3);
|
|
this.reCenter();
|
|
}
|
|
|
|
setEnabled(enabled) {
|
|
if (enabled && this._monitorsChangedId === 0) {
|
|
this._monitorsChangedId = Main.layoutManager.connect(
|
|
'monitors-changed', this._monitorsChanged.bind(this));
|
|
} else if (!enabled && this._monitorsChangedId !== 0) {
|
|
Main.layoutManager.disconnect(this._monitorsChangedId);
|
|
this._monitorsChangedId = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* addToZoomRegion
|
|
* Either add the crosshairs actor to the given ZoomRegion, or, if it is
|
|
* already part of some other ZoomRegion, create a clone of the crosshairs
|
|
* actor, and add the clone instead. Returns either the original or the
|
|
* clone.
|
|
*
|
|
* @param {ZoomRegion} zoomRegion The container to add the crosshairs
|
|
* group to.
|
|
* @param {Clutter.Actor} magnifiedMouse The mouse actor for the
|
|
* zoom region -- used to position the crosshairs and properly
|
|
* layer them below the mouse.
|
|
* @returns {Clutter.Actor} The crosshairs actor, or its clone.
|
|
*/
|
|
addToZoomRegion(zoomRegion, magnifiedMouse) {
|
|
let crosshairsActor = null;
|
|
if (zoomRegion && magnifiedMouse) {
|
|
let container = magnifiedMouse.get_parent();
|
|
if (container) {
|
|
crosshairsActor = this;
|
|
if (this.get_parent() != null) {
|
|
crosshairsActor = new Clutter.Clone({ source: this });
|
|
this._clones.push(crosshairsActor);
|
|
|
|
// Clones don't share visibility.
|
|
this.bind_property('visible', crosshairsActor, 'visible',
|
|
GObject.BindingFlags.SYNC_CREATE);
|
|
}
|
|
|
|
container.add_actor(crosshairsActor);
|
|
container.set_child_above_sibling(magnifiedMouse, crosshairsActor);
|
|
let [xMouse, yMouse] = magnifiedMouse.get_position();
|
|
let [crosshairsWidth, crosshairsHeight] = crosshairsActor.get_size();
|
|
crosshairsActor.set_position(xMouse - crosshairsWidth / 2, yMouse - crosshairsHeight / 2);
|
|
}
|
|
}
|
|
return crosshairsActor;
|
|
}
|
|
|
|
/**
|
|
* removeFromParent:
|
|
*
|
|
* Remove the crosshairs actor from its parent container, or destroy the
|
|
* child actor if it was just a clone of the crosshairs actor.
|
|
*
|
|
* @param {Clutter.Actor} childActor the actor returned from
|
|
* addToZoomRegion
|
|
*/
|
|
removeFromParent(childActor) {
|
|
if (childActor == this)
|
|
childActor.get_parent().remove_actor(childActor);
|
|
else
|
|
childActor.destroy();
|
|
}
|
|
|
|
/**
|
|
* setColor:
|
|
* Set the color of the crosshairs.
|
|
*
|
|
* @param {Clutter.Color} clutterColor The color
|
|
*/
|
|
setColor(clutterColor) {
|
|
this._horizLeftHair.background_color = clutterColor;
|
|
this._horizRightHair.background_color = clutterColor;
|
|
this._vertTopHair.background_color = clutterColor;
|
|
this._vertBottomHair.background_color = clutterColor;
|
|
}
|
|
|
|
/**
|
|
* getColor:
|
|
* Get the color of the crosshairs.
|
|
*
|
|
* @returns {ClutterColor} the crosshairs color
|
|
*/
|
|
getColor() {
|
|
return this._horizLeftHair.get_color();
|
|
}
|
|
|
|
/**
|
|
* setThickness:
|
|
*
|
|
* Set the width of the vertical and horizontal lines of the crosshairs.
|
|
*
|
|
* @param {number} thickness the new thickness value
|
|
*/
|
|
setThickness(thickness) {
|
|
this._horizLeftHair.set_height(thickness);
|
|
this._horizRightHair.set_height(thickness);
|
|
this._vertTopHair.set_width(thickness);
|
|
this._vertBottomHair.set_width(thickness);
|
|
this.reCenter();
|
|
}
|
|
|
|
/**
|
|
* getThickness:
|
|
* Get the width of the vertical and horizontal lines of the crosshairs.
|
|
*
|
|
* @returns {number} The thickness of the crosshairs.
|
|
*/
|
|
getThickness() {
|
|
return this._horizLeftHair.get_height();
|
|
}
|
|
|
|
/**
|
|
* setOpacity:
|
|
* Set how opaque the crosshairs are.
|
|
*
|
|
* @param {number} opacity Value between 0 (fully transparent)
|
|
* and 255 (full opaque).
|
|
*/
|
|
setOpacity(opacity) {
|
|
// set_opacity() throws an exception for values outside the range
|
|
// [0, 255].
|
|
if (opacity < 0)
|
|
opacity = 0;
|
|
else if (opacity > 255)
|
|
opacity = 255;
|
|
|
|
this._horizLeftHair.set_opacity(opacity);
|
|
this._horizRightHair.set_opacity(opacity);
|
|
this._vertTopHair.set_opacity(opacity);
|
|
this._vertBottomHair.set_opacity(opacity);
|
|
}
|
|
|
|
/**
|
|
* setLength:
|
|
* Set the length of the vertical and horizontal lines in the crosshairs.
|
|
*
|
|
* @param {number} length The length of the crosshairs.
|
|
*/
|
|
setLength(length) {
|
|
this._horizLeftHair.set_width(length);
|
|
this._horizRightHair.set_width(length);
|
|
this._vertTopHair.set_height(length);
|
|
this._vertBottomHair.set_height(length);
|
|
this.reCenter();
|
|
}
|
|
|
|
/**
|
|
* getLength:
|
|
* Get the length of the vertical and horizontal lines in the crosshairs.
|
|
*
|
|
* @returns {number} The length of the crosshairs.
|
|
*/
|
|
getLength() {
|
|
return this._horizLeftHair.get_width();
|
|
}
|
|
|
|
/**
|
|
* setClip:
|
|
* Set the width and height of the rectangle that clips the crosshairs at
|
|
* their intersection
|
|
*
|
|
* @param {[number, number]} size Array of [width, height] defining the size
|
|
* of the clip rectangle.
|
|
*/
|
|
setClip(size) {
|
|
if (size) {
|
|
// Take a chunk out of the crosshairs where it intersects the
|
|
// mouse.
|
|
this._clipSize = size;
|
|
this.reCenter();
|
|
} else {
|
|
// Restore the missing chunk.
|
|
this._clipSize = [0, 0];
|
|
this.reCenter();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* reCenter:
|
|
* Reposition the horizontal and vertical hairs such that they cross at
|
|
* the center of crosshairs group. If called with the dimensions of
|
|
* the clip rectangle, these are used to update the size of the clip.
|
|
*
|
|
* @param {[number, number]} [clipSize] If present, the clip's [width, height].
|
|
*/
|
|
reCenter(clipSize) {
|
|
let [groupWidth, groupHeight] = this.get_size();
|
|
let leftLength = this._horizLeftHair.get_width();
|
|
let topLength = this._vertTopHair.get_height();
|
|
let thickness = this._horizLeftHair.get_height();
|
|
|
|
// Deal with clip rectangle.
|
|
if (clipSize)
|
|
this._clipSize = clipSize;
|
|
let clipWidth = this._clipSize[0];
|
|
let clipHeight = this._clipSize[1];
|
|
|
|
let left = groupWidth / 2 - clipWidth / 2 - leftLength - thickness / 2;
|
|
let right = groupWidth / 2 + clipWidth / 2 + thickness / 2;
|
|
let top = groupHeight / 2 - clipHeight / 2 - topLength - thickness / 2;
|
|
let bottom = groupHeight / 2 + clipHeight / 2 + thickness / 2;
|
|
this._horizLeftHair.set_position(left, (groupHeight - thickness) / 2);
|
|
this._horizRightHair.set_position(right, (groupHeight - thickness) / 2);
|
|
this._vertTopHair.set_position((groupWidth - thickness) / 2, top);
|
|
this._vertBottomHair.set_position((groupWidth - thickness) / 2, bottom);
|
|
}
|
|
});
|
|
|
|
class MagShaderEffects {
|
|
constructor(uiGroupClone) {
|
|
this._inverse = new Shell.InvertLightnessEffect();
|
|
this._brightnessContrast = new Clutter.BrightnessContrastEffect();
|
|
this._colorDesaturation = new Clutter.DesaturateEffect();
|
|
this._inverse.set_enabled(false);
|
|
this._brightnessContrast.set_enabled(false);
|
|
this._colorDesaturation.set_enabled(false);
|
|
|
|
this._magView = uiGroupClone;
|
|
this._magView.add_effect(this._inverse);
|
|
this._magView.add_effect(this._brightnessContrast);
|
|
this._magView.add_effect(this._colorDesaturation);
|
|
}
|
|
|
|
/**
|
|
* destroyEffects:
|
|
* Remove contrast and brightness effects from the magnified view, and
|
|
* lose the reference to the actor they were applied to. Don't use this
|
|
* object after calling this.
|
|
*/
|
|
destroyEffects() {
|
|
this._magView.clear_effects();
|
|
this._colorDesaturation = null;
|
|
this._brightnessContrast = null;
|
|
this._inverse = null;
|
|
this._magView = null;
|
|
}
|
|
|
|
/**
|
|
* setInvertLightness:
|
|
* Enable/disable invert lightness effect.
|
|
*
|
|
* @param {boolean} invertFlag Enabled flag.
|
|
*/
|
|
setInvertLightness(invertFlag) {
|
|
this._inverse.set_enabled(invertFlag);
|
|
}
|
|
|
|
setColorSaturation(factor) {
|
|
this._colorDesaturation.set_factor(1.0 - factor);
|
|
this._colorDesaturation.set_enabled(factor !== 1.0);
|
|
}
|
|
|
|
/**
|
|
* setBrightness:
|
|
* Set the brightness of the magnified view.
|
|
*
|
|
* @param {object} brightness Object containing the contrast for the
|
|
* red, green, and blue channels. Values of 0.0 represent "standard"
|
|
* brightness (no change), whereas values less or greater than
|
|
* 0.0 indicate decreased or incresaed brightness, respectively.
|
|
*
|
|
* {number} brightness.r - the red component
|
|
* {number} brightness.g - the green component
|
|
* {number} brightness.b - the blue component
|
|
*/
|
|
setBrightness(brightness) {
|
|
let bRed = brightness.r;
|
|
let bGreen = brightness.g;
|
|
let bBlue = brightness.b;
|
|
this._brightnessContrast.set_brightness_full(bRed, bGreen, bBlue);
|
|
|
|
// Enable the effect if the brightness OR contrast change are such that
|
|
// it modifies the brightness and/or contrast.
|
|
let [cRed, cGreen, cBlue] = this._brightnessContrast.get_contrast();
|
|
this._brightnessContrast.set_enabled(
|
|
bRed !== NO_CHANGE || bGreen !== NO_CHANGE || bBlue !== NO_CHANGE ||
|
|
cRed !== NO_CHANGE || cGreen !== NO_CHANGE || cBlue !== NO_CHANGE);
|
|
}
|
|
|
|
/**
|
|
* Set the contrast of the magnified view.
|
|
*
|
|
* @param {object} contrast Object containing the contrast for the
|
|
* red, green, and blue channels. Values of 0.0 represent "standard"
|
|
* contrast (no change), whereas values less or greater than
|
|
* 0.0 indicate decreased or incresaed contrast, respectively.
|
|
*
|
|
* {number} contrast.r - the red component
|
|
* {number} contrast.g - the green component
|
|
* {number} contrast.b - the blue component
|
|
*/
|
|
setContrast(contrast) {
|
|
let cRed = contrast.r;
|
|
let cGreen = contrast.g;
|
|
let cBlue = contrast.b;
|
|
|
|
this._brightnessContrast.set_contrast_full(cRed, cGreen, cBlue);
|
|
|
|
// Enable the effect if the contrast OR brightness change are such that
|
|
// it modifies the brightness and/or contrast.
|
|
// should be able to use Clutter.color_equal(), but that complains of
|
|
// a null first argument.
|
|
let [bRed, bGreen, bBlue] = this._brightnessContrast.get_brightness();
|
|
this._brightnessContrast.set_enabled(
|
|
cRed !== NO_CHANGE || cGreen !== NO_CHANGE || cBlue !== NO_CHANGE ||
|
|
bRed !== NO_CHANGE || bGreen !== NO_CHANGE || bBlue !== NO_CHANGE);
|
|
}
|
|
}
|