summaryrefslogtreecommitdiffstats
path: root/js/ui/magnifier.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 15:07:22 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 15:07:22 +0000
commitf9d480cfe50ca1d7a0f0b5a2b8bb9932962bfbe7 (patch)
treece9e8db2d4e8799780fa72ae8f1953039373e2ee /js/ui/magnifier.js
parentInitial commit. (diff)
downloadgnome-shell-upstream/3.38.6.tar.xz
gnome-shell-upstream/3.38.6.zip
Adding upstream version 3.38.6.upstream/3.38.6upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/ui/magnifier.js')
-rw-r--r--js/ui/magnifier.js1989
1 files changed, 1989 insertions, 0 deletions
diff --git a/js/ui/magnifier.js b/js/ui/magnifier.js
new file mode 100644
index 0000000..3a778b6
--- /dev/null
+++ b/js/ui/magnifier.js
@@ -0,0 +1,1989 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const { Atspi, Clutter, GDesktopEnums,
+ Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
+const Signals = imports.signals;
+
+const Background = imports.ui.background;
+const FocusCaretTracker = imports.ui.focusCaretTracker;
+const Main = imports.ui.main;
+const MagnifierDBus = imports.ui.magnifierDBus;
+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 {
+ constructor() {
+ // 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();
+
+ // 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._mouseSprite);
+ 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);
+ });
+
+ // Export to dbus.
+ new MagnifierDBus.ShellMagnifier();
+ 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();
+ 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();
+ this._cursorTracker.set_pointer_visible(false);
+ }
+
+ /**
+ * setActive:
+ * Show/hide all the zoom regions.
+ * @param {bool} 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._cursorSpriteChangedId =
+ this._cursorTracker.connect('cursor-changed',
+ this._updateMouseSprite.bind(this));
+ Meta.disable_unredirect_for_display(global.display);
+ this.startTrackingMouse();
+ } else {
+ this._cursorTracker.disconnect(this._cursorSpriteChangedId);
+ 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 {bool} 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 / Clutter.get_default_frame_rate();
+ this._pointerWatch = PointerWatcher.getPointerWatcher().addWatch(interval, this.scrollToMousePos.bind(this));
+ }
+ }
+
+ /**
+ * stopTrackingMouse:
+ * Turn off mouse tracking, if not already doing so.
+ */
+ stopTrackingMouse() {
+ if (this._pointerWatch)
+ this._pointerWatch.remove();
+
+ this._pointerWatch = null;
+ }
+
+ /**
+ * isTrackingMouse:
+ * @returns {bool} 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.
+ * @returns {bool} true.
+ */
+ scrollToMousePos() {
+ let [xMouse, yMouse] = global.get_pointer();
+
+ if (xMouse != this.xMouse || yMouse != this.yMouse) {
+ 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();
+ }
+ return true;
+ }
+
+ /**
+ * 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._mouseSprite);
+ 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 {number[]} 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 {bool} 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 {bool} 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 {bool} 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);
+ }
+ }
+};
+Signals.addSignalMethods(Magnifier.prototype);
+
+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 = Meta.IdleMonitor.get_core();
+ 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);
+ }
+ }
+
+ _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.SCREEN);
+ } catch (e) {
+ log(`Failed to read extents of focused component: ${e.message}`);
+ return;
+ }
+
+ let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
+ let [xFocus, yFocus] = [(extents.x + (extents.width / 2)) * scaleFactor,
+ (extents.y + (extents.height / 2)) * scaleFactor];
+
+ 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(), 0);
+ } catch (e) {
+ log(`Failed to read extents of text caret: ${e.message}`);
+ return;
+ }
+
+ let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
+ let [xCaret, yCaret] = [extents.x * scaleFactor, extents.y * scaleFactor];
+
+ // 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 {bool} 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 {
+ this._disconnectSignals();
+ this._destroyActors();
+ }
+
+ this._syncCaretTracking();
+ this._syncFocusTracking();
+ }
+
+ /**
+ * isActive:
+ * @returns {bool} 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 {bool} 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 {bool} 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 {bool} 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 {bool}: 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 {bool} 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 {bool} 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();
+ }
+
+ _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:
+ * @param {Clutter.Actor} childActor: the actor returned from
+ * addToZoomRegion
+ * Remove the crosshairs actor from its parent container, or destroy the
+ * child actor if it was just a clone of the crosshairs actor.
+ */
+ 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[]} 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[]=} 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);
+ }
+});
+
+var MagShaderEffects = 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._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 {bool} invertFlag: Enabled flag.
+ */
+ setInvertLightness(invertFlag) {
+ this._inverse.set_enabled(invertFlag);
+ }
+
+ setColorSaturation(factor) {
+ this._colorDesaturation.set_factor(1.0 - factor);
+ }
+
+ /**
+ * 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);
+ }
+};