/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ const DBus = imports.dbus; const Gtk = imports.gi.Gtk; const Gdk = imports.gi.Gdk; const Clutter = imports.gi.Clutter; const Shell = imports.gi.Shell; const St = imports.gi.St; const Lang = imports.lang; const Mainloop = imports.mainloop; const Main = imports.ui.main; const MagnifierDBus = imports.ui.magnifierDBus; const MouseTrackingMode = { NONE: 0, CENTERED: 1, PUSH: 2, PROPORTIONAL: 3 }; const ScreenPosition = { NONE: 0, FULL_SCREEN: 1, TOP_HALF: 2, BOTTOM_HALF: 3, LEFT_HALF: 4, RIGHT_HALF: 5 }; // Default settings const DEFAULT_X_MAGFACTOR = 2; const DEFAULT_Y_MAGFACTOR = 2; const DEFAULT_MOUSE_POLL_FREQUENCY = 50; const DEFAULT_LENS_MODE = false; const DEFAULT_SCREEN_POSITION = ScreenPosition.BOTTOM_HALF; const DEFAULT_MOUSE_TRACKING_MODE = MouseTrackingMode.CENTERED; const DEFAULT_CLAMP_SCROLLING_AT_EDGES = true; const DEFAULT_SHOW_CROSSHAIRS = false; const DEFAULT_CROSSHAIRS_THICKNESS = 8; const DEFAULT_CROSSHAIRS_OPACITY = 169; // 66% const DEFAULT_CROSSHAIRS_LENGTH = 4096; const DEFAULT_CROSSHAIRS_CLIP = false; const DEFAULT_CROSSHAIRS_CLIP_SIZE = [100, 100]; const DEFAULT_CROSSHAIRS_COLOR = new Clutter.Color(); DEFAULT_CROSSHAIRS_COLOR.from_string('Red'); // GConf settings const A11Y_MAG_PREFS_DIR = '/desktop/gnome/accessibility/magnifier'; const SHOW_KEY = A11Y_MAG_PREFS_DIR + '/show_magnifier'; const SCREEN_POSITION_KEY = A11Y_MAG_PREFS_DIR + '/screen_position'; const MAG_FACTOR_KEY = A11Y_MAG_PREFS_DIR + '/mag_factor'; const LENS_MODE_KEY = A11Y_MAG_PREFS_DIR + '/lens_mode'; const CLAMP_MODE_KEY = A11Y_MAG_PREFS_DIR + '/scroll_at_edges'; const MOUSE_TRACKING_KEY = A11Y_MAG_PREFS_DIR + '/mouse_tracking'; const SHOW_CROSS_HAIRS_KEY = A11Y_MAG_PREFS_DIR + '/show_cross_hairs'; const CROSS_HAIRS_THICKNESS_KEY = A11Y_MAG_PREFS_DIR + '/cross_hairs_thickness'; const CROSS_HAIRS_COLOR_KEY = A11Y_MAG_PREFS_DIR + '/cross_hairs_color'; const CROSS_HAIRS_OPACITY_KEY = A11Y_MAG_PREFS_DIR + '/cross_hairs_opacity'; const CROSS_HAIRS_LENGTH_KEY = A11Y_MAG_PREFS_DIR + '/cross_hairs_length'; const CROSS_HAIRS_CLIP_KEY = A11Y_MAG_PREFS_DIR + '/cross_hairs_clip'; let magDBusService = null; function Magnifier() { this._init(); } Magnifier.prototype = { _init: function() { // Magnifier is a manager of ZoomRegions. this._zoomRegions = []; // Create small clutter tree for the magnified mouse. let xfixesCursor = Shell.XFixesCursor.get_default(); this._mouseSprite = new Clutter.Texture(); xfixesCursor.update_texture_image(this._mouseSprite); this._cursorRoot = new Clutter.Group(); this._cursorRoot.add_actor(this._mouseSprite); // Create the first ZoomRegion and initialize it according to the // magnification GConf settings. let [objUnder, xMouse, yMouse, mask] = Gdk.Screen.get_default().get_root_window().get_pointer(); let aZoomRegion = new ZoomRegion(this, this._cursorRoot); this._zoomRegions.push(aZoomRegion); let showAtLaunch = this._gConfInit(aZoomRegion); aZoomRegion.scrollContentsTo(xMouse, yMouse); xfixesCursor.connect('cursor-change', Lang.bind(this, this._updateMouseSprite)); this._xfixesCursor = xfixesCursor; // Export to dbus. magDBusService = new MagnifierDBus.ShellMagnifier(); this.setActive(showAtLaunch); }, /** * showSystemCursor: * Show the system mouse pointer. */ showSystemCursor: function() { this._xfixesCursor.show(); }, /** * hideSystemCursor: * Hide the system mouse pointer. */ hideSystemCursor: function() { this._xfixesCursor.hide(); }, /** * setActive: * Show/hide all the zoom regions. * @activate: Boolean to activate or de-activate the magnifier. */ setActive: function(activate) { this._zoomRegions.forEach (function(zoomRegion, index, array) { zoomRegion.setActive(activate); }); if (activate) this.startTrackingMouse(); else this.stopTrackingMouse(); // Make sure system mouse pointer is shown when all zoom regions are // invisible. if (!activate) this._xfixesCursor.show(); }, /** * isActive: * @return Whether the magnifier is active (boolean). */ isActive: function() { // 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: function() { // initialize previous mouse coord to undefined. let prevCoord = { x: NaN, y: NaN }; if (!this._mouseTrackingId) this._mouseTrackingId = Mainloop.timeout_add( DEFAULT_MOUSE_POLL_FREQUENCY, Lang.bind(this, this.scrollToMousePos, prevCoord) ); }, /** * stopTrackingMouse: * Turn off mouse tracking, if not already doing so. */ stopTrackingMouse: function() { if (this._mouseTrackingId) Mainloop.source_remove(this._mouseTrackingId); this._mouseTrackingId = null; }, /** * isTrackingMouse: * Is the magnifier tracking the mouse currently? */ isTrackingMouse: function() { return !!this._mouseTrackingId; }, /** * scrollToMousePos: * Position all zoom regions' ROI relative to the current location of the * system pointer. * @prevCoord: The previous mouse coordinates. Used to stop scrolling if * the new position is the same as the last one (optional). * @return true. */ scrollToMousePos: function(prevCoord) { let [objUnder, xMouse, yMouse, mask] = Gdk.Screen.get_default().get_root_window().get_pointer(); if (!prevCoord || prevCoord.x != xMouse || prevCoord.y != yMouse) { let sysMouseOverAny = false; this._zoomRegions.forEach(function(zoomRegion, index, array) { if (zoomRegion.scrollToMousePos()) sysMouseOverAny = true; }); if (sysMouseOverAny) this.hideSystemCursor(); else this.showSystemCursor(); if (prevCoord) { prevCoord.x = xMouse; prevCoord.y = yMouse; } } return true; }, /** * createZoomRegion: * Create a ZoomRegion instance with the given properties. * @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. * @yMagFactor: The power to set the vertical magnification of the * ZoomRegion. * @roi Object in the form { x, y, width, height } that * defines the region to magnify. Given in unmagnified * coordinates. * @viewPort Object in the form { x, y, width, height } that defines * the position of the ZoomRegion on screen. * @return The newly created ZoomRegion. */ createZoomRegion: function(xMagFactor, yMagFactor, roi, viewPort) { let zoomRegion = new ZoomRegion(this, this._cursorRoot); zoomRegion.setMagFactor(xMagFactor, yMagFactor); zoomRegion.setViewPort(viewPort); zoomRegion.setROI(roi); zoomRegion.addCrosshairs(this._crossHairs); return zoomRegion; }, /** * addZoomRegion: * Append the given ZoomRegion to the list of currently defined ZoomRegions * for this Magnifier instance. * @zoomRegion: The zoomRegion to add. */ addZoomRegion: function(zoomRegion) { if(zoomRegion) { this._zoomRegions.push(zoomRegion); if (!this.isTrackingMouse()) this.startTrackingMouse(); } }, /** * getZoomRegions: * Return a list of ZoomRegion's for this Magnifier. * @return: The Magnifier's zoom region list (array). */ getZoomRegions: function() { return this._zoomRegions; }, /** * clearAllZoomRegions: * Remove all the zoom regions from this Magnfier's ZoomRegion list. */ clearAllZoomRegions: function() { // First ZoomRegion is special since its magnified mouse and crosshairs // are the original -- all the others are Clutter.Clone's. Deal with // all but first zoom region. for (let i = 1; i < this._zoomRegions.length; i++) { this._zoomRegions[i].setActive(false); this._zoomRegions[i].removeFromStage(); } this._zoomRegions[0].setActive(false); // Detach the (original) magnified mouse and cross hair for later reuse // before removing ZoomRegion from the stage. this._cursorRoot.get_parent().remove_actor(this._cursorRoot); if (this._crossHairs) this._crossHairs.removeFromParent(); this._zoomRegions[0].removeFromStage(); this._zoomRegions.length = 0; this.stopTrackingMouse(); this.showSystemCursor(); }, /** * addCrosshairs: * Add and show a cross hair centered on the magnified mouse. * @thickness: The thickness of the vertical and horizontal lines of the * crosshair. * @color: The color of the crosshairs * @opacity: The opacity. * @length: The length of each hair. * @clip: Whether the crosshairs intersection is clipped by the * magnified mouse image. */ addCrosshairs: function(thickness, color, opacity, length, clip) { if (!this._crossHairs) this._crossHairs = new Crosshairs(); this.setCrosshairsThickness(thickness); this.setCrosshairsColor(color); this.setCrosshairsOpacity(opacity); this.setCrosshairsLength(length); this.setCrosshairsClip(clip); let theCrossHairs = this._crossHairs; this._zoomRegions.forEach (function(zoomRegion, index, array) { zoomRegion.addCrosshairs(theCrossHairs); }); }, /** * setCrosshairsVisible: * Show or hide the cross hair. * @visible Flag that indicates show (true) or hide (false). */ setCrosshairsVisible: function(visible) { if (visible) { if (!this._crossHairs) this.addCrosshairs(DEFAULT_CROSSHAIRS_THICKNESS, DEFAULT_CROSSHAIRS_COLOR, DEFAULT_CROSSHAIRS_OPACITY, DEFAULT_CROSSHAIRS_CLIP); this._crossHairs.show(); } else { if (this._crossHairs) this._crossHairs.hide(); } }, /** * setCrosshairsColor: * Set the color of the crosshairs for all ZoomRegions. * @color: The color as a string, e.g. '#ff0000ff' or 'red'. */ setCrosshairsColor: function(color) { if (this._crossHairs) { let clutterColor = new Clutter.Color(); clutterColor.from_string(color); this._crossHairs.setColor(clutterColor); } }, /** * getCrosshairsColor: * Get the color of the crosshairs. * @return: The color as a string, e.g. '#0000ffff' or 'blue'. */ getCrosshairsColor: function() { if (this._crossHairs) { let clutterColor = this._crossHairs.getColor(); return clutterColor.to_string(); } else return '#00000000'; }, /** * setCrosshairsThickness: * Set the crosshairs thickness for all ZoomRegions. * @thickness: The width of the vertical and horizontal lines of the * crosshairs. */ setCrosshairsThickness: function(thickness) { if (this._crossHairs) this._crossHairs.setThickness(thickness); }, /** * getCrosshairsThickness: * Get the crosshairs thickness. * @return: The width of the vertical and horizontal lines of the * crosshairs. */ getCrosshairsThickness: function() { if (this._crossHairs) return this._crossHairs.getThickness(); else return 0; }, /** * setCrosshairsOpacity: * @opacity: Value between 0 (transparent) and 255 (fully opaque). */ setCrosshairsOpacity: function(opacity) { if (this._crossHairs) this._crossHairs.setOpacity(opacity); }, /** * getCrosshairsOpacity: * @return: Value between 0 (transparent) and 255 (fully opaque). */ getCrosshairsOpacity: function() { if (this._crossHairs) return this._crossHairs.getOpacity(); else return 0; }, /** * setCrosshairsLength: * Set the crosshairs length for all ZoomRegions. * @length: The length of the vertical and horizontal lines making up the * crosshairs. */ setCrosshairsLength: function(length) { if (this._crossHairs) this._crossHairs.setLength(length); }, /** * getCrosshairsLength: * Get the crosshairs length. * @return: The length of the vertical and horizontal lines making up the * crosshairs. */ getCrosshairsLength: function() { if (this._crossHairs) return this._crossHairs.getLength(); else return 0; }, /** * setCrosshairsClip: * Set whether the crosshairs are clipped at their intersection. * @clip: Flag to indicate whether to clip the crosshairs. */ setCrosshairsClip: function(clip) { if (clip) { if (this._crossHairs) this._crossHairs.setClip(DEFAULT_CROSSHAIRS_CLIP_SIZE); } else { // Setting no clipping on crosshairs means a zero sized clip // rectangle. if (this._crossHairs) this._crossHairs.setClip([0, 0]); } }, /** * getCrosshairsClip: * Get whether the crosshairs are clipped by the mouse image. * @return: Whether the crosshairs are clipped. */ getCrosshairsClip: function() { if (this._crossHairs) { let [clipWidth, clipHeight] = this._crossHairs.getClip(); return (clipWidth > 0 && clipHeight > 0); } else return false; }, //// Private methods //// _updateMouseSprite: function() { this._xfixesCursor.update_texture_image(this._mouseSprite); let xHot = this._xfixesCursor.get_hot_x(); let yHot = this._xfixesCursor.get_hot_y(); this._mouseSprite.set_anchor_point(xHot, yHot); }, _gConfInit: function(zoomRegion) { let gConf = Shell.GConf.get_default(); if (zoomRegion) { // Mag factor is accurate to two decimal places. let aPref = parseFloat(gConf.get_float(MAG_FACTOR_KEY).toFixed(2)); if (aPref != 0.0) zoomRegion.setMagFactor(aPref, aPref); aPref = gConf.get_int(SCREEN_POSITION_KEY); if (aPref) zoomRegion.setScreenPosition(aPref); zoomRegion.setLensMode(gConf.get_boolean(LENS_MODE_KEY)); zoomRegion.setClampScrollingAtEdges(!gConf.get_boolean(CLAMP_MODE_KEY)); aPref = gConf.get_int(MOUSE_TRACKING_KEY); if (aPref) zoomRegion.setMouseTrackingMode(aPref); } let showCrosshairs = gConf.get_boolean(SHOW_CROSS_HAIRS_KEY); let thickness = gConf.get_int(CROSS_HAIRS_THICKNESS_KEY); let color = gConf.get_string(CROSS_HAIRS_COLOR_KEY); let opacity = gConf.get_int(CROSS_HAIRS_OPACITY_KEY); let length = gConf.get_int(CROSS_HAIRS_LENGTH_KEY); let clip = gConf.get_boolean(CROSS_HAIRS_CLIP_KEY); this.addCrosshairs(thickness, color, opacity, length, clip); this.setCrosshairsVisible(showCrosshairs); gConf.watch_directory(A11Y_MAG_PREFS_DIR); gConf.connect('changed::' + SHOW_KEY, Lang.bind(this, this._updateShowHide)); gConf.connect('changed::' + SCREEN_POSITION_KEY, Lang.bind(this, this._updateScreenPosition)); gConf.connect('changed::' + MAG_FACTOR_KEY, Lang.bind(this, this._updateMagFactor)); gConf.connect('changed::' + LENS_MODE_KEY, Lang.bind(this, this._updateLensMode)); gConf.connect('changed::' + CLAMP_MODE_KEY, Lang.bind(this, this._updateClampMode)); gConf.connect('changed::' + MOUSE_TRACKING_KEY, Lang.bind(this, this._updateMouseTrackingMode)); gConf.connect('changed::' + SHOW_CROSS_HAIRS_KEY, Lang.bind(this, this._updateShowCrosshairs)); gConf.connect('changed::' + CROSS_HAIRS_THICKNESS_KEY, Lang.bind(this, this._updateCrosshairsThickness)); gConf.connect('changed::' + CROSS_HAIRS_COLOR_KEY, Lang.bind(this, this._updateCrosshairsColor)); gConf.connect('changed::' + CROSS_HAIRS_OPACITY_KEY, Lang.bind(this, this._updateCrosshairsOpacity)); gConf.connect('changed::' + CROSS_HAIRS_LENGTH_KEY, Lang.bind(this, this._updateCrosshairsLength)); gConf.connect('changed::' + CROSS_HAIRS_CLIP_KEY, Lang.bind(this, this._updateCrosshairsClip)); return gConf.get_boolean(SHOW_KEY); }, _updateShowHide: function() { let gConf = Shell.GConf.get_default(); this.setActive(gConf.get_boolean(SHOW_KEY)); }, _updateScreenPosition: function() { // Applies only to the first zoom region. if (this._zoomRegions.length) { let gConf = Shell.GConf.get_default(); let position = gConf.get_int(SCREEN_POSITION_KEY); this._zoomRegions[0].setScreenPosition(position); if (position != ScreenPosition.FULL_SCREEN) this._updateLensMode(); } }, _updateMagFactor: function() { // Applies only to the first zoom region. if (this._zoomRegions.length) { let gConf = Shell.GConf.get_default(); // Mag factor is accurate to two decimal places. let magFactor = parseFloat(gConf.get_float(MAG_FACTOR_KEY).toFixed(2)); this._zoomRegions[0].setMagFactor(magFactor, magFactor); } }, _updateLensMode: function() { // Applies only to the first zoom region. if (this._zoomRegions.length) { let gConf = Shell.GConf.get_default(); this._zoomRegions[0].setLensMode(gConf.get_boolean(LENS_MODE_KEY)); } }, _updateClampMode: function() { // Applies only to the first zoom region. if (this._zoomRegions.length) { let gConf = Shell.GConf.get_default(); this._zoomRegions[0].setClampScrollingAtEdges( !gConf.get_boolean(CLAMP_MODE_KEY) ); } }, _updateMouseTrackingMode: function() { // Applies only to the first zoom region. if (this._zoomRegions.length) { let gConf = Shell.GConf.get_default(); this._zoomRegions[0].setMouseTrackingMode( gConf.get_int(MOUSE_TRACKING_KEY) ); } }, _updateShowCrosshairs: function() { let gConf = Shell.GConf.get_default(); this.setCrosshairsVisible(gConf.get_boolean(SHOW_CROSS_HAIRS_KEY)); }, _updateCrosshairsThickness: function() { let gConf = Shell.GConf.get_default(); this.setCrosshairsThickness(gConf.get_int(CROSS_HAIRS_THICKNESS_KEY)); }, _updateCrosshairsColor: function() { let gConf = Shell.GConf.get_default(); this.setCrosshairsColor(gConf.get_string(CROSS_HAIRS_COLOR_KEY)); }, _updateCrosshairsOpacity: function() { let gConf = Shell.GConf.get_default(); this.setCrosshairsOpacity(gConf.get_int(CROSS_HAIRS_OPACITY_KEY)); }, _updateCrosshairsLength: function() { let gConf = Shell.GConf.get_default(); this.setCrosshairsLength(gConf.get_int(CROSS_HAIRS_LENGTH_KEY)); }, _updateCrosshairsClip: function() { let gConf = Shell.GConf.get_default(); this.setCrosshairsClip(gConf.get_boolean(CROSS_HAIRS_CLIP_KEY)); } } function ZoomRegion(magnifier, mouseRoot) { this._init(magnifier, mouseRoot); } ZoomRegion.prototype = { _init: function(magnifier, mouseRoot) { this._magnifier = magnifier; // The root actor for the zoom region this._magView = new St.Bin({ style_class: 'magnifier-zoom-region', x_fill: true, y_fill: true }); global.stage.add_actor(this._magView); this._magView.hide(); // Append a Clutter.Group to clip the contents of the magnified view. this._mainGroup = new Clutter.Group({ clip_to_allocation: true }); this._magView.set_child(this._mainGroup); // Add a background for when the magnified uiGroup is scrolled // out of view (don't want to see desktop showing through). let background = new Clutter.Rectangle({ color: Main.DEFAULT_BACKGROUND_COLOR }); this._mainGroup.add_actor(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 }); this._mainGroup.add_actor(this._uiGroupClone); Main.uiGroup.set_size(global.screen_width, global.screen_height); background.set_size(global.screen_width, global.screen_height); this._uiGroupClone.set_size(global.screen_width, global.screen_height); // Add either the given mouseRoot to the ZoomRegion, or a clone of // it. if (mouseRoot.get_parent() != null) this._mouseRoot = new Clutter.Clone({ source: mouseRoot }); else this._mouseRoot = mouseRoot; this._mainGroup.add_actor(this._mouseRoot); this._crossHairs = null; this.setMagFactor(DEFAULT_X_MAGFACTOR, DEFAULT_Y_MAGFACTOR); this.setScreenPosition(DEFAULT_SCREEN_POSITION); this.setLensMode(DEFAULT_LENS_MODE); this.setClampScrollingAtEdges(DEFAULT_CLAMP_SCROLLING_AT_EDGES); this.setMouseTrackingMode(DEFAULT_MOUSE_TRACKING_MODE); }, /** * setActive: * @activate: Boolean to show/hide the ZoomRegion. */ setActive: function(activate) { if (activate) { this._magView.show(); if (this.isMouseOverRegion()) this._magnifier.hideSystemCursor(); this._updateMousePosition(false /* mouse didn't move */); } else this._magView.hide(); }, /** * isActive: * @return Whether this ZoomRegion is active (boolean). */ isActive: function() { return this._magView.visible; }, /** * removeFromStage: * Remove the magnified view from the stage. */ removeFromStage: function() { global.stage.remove_actor(this._magView); this._mouseRoot = null; this._uiGroupClone = null; this._magView = null; }, /** * setMagFactor: * @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. * @yMagFactor: The power to set the vertical magnification factor to * of the magnified view. */ setMagFactor: function(xMagFactor, yMagFactor) { if (xMagFactor > 0 && yMagFactor > 0) { // Changing the mag factor moves the pixels along the axes of // magnification. Set the view back to the point that was at the centre // of the region of interest. let [x, y, width, height] = this.getROI(); let xCentre = x + width / 2; let yCentre = y + height / 2; this._uiGroupClone.set_scale(xMagFactor, yMagFactor); this._mouseRoot.set_scale(xMagFactor, yMagFactor); this._calcRightBottomStops(); this._scrollToPosition(xCentre, yCentre); this._updateMousePosition(false /* mouse didn't move */); } }, /** * getMagFactor: * @return 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: function() { return this._uiGroupClone.get_scale(); }, /** * setMouseTrackingMode * @mode: One of the enum MouseTrackingMode values. */ setMouseTrackingMode: function(mode) { if (mode >= MouseTrackingMode.NONE && mode <= MouseTrackingMode.PROPORTIONAL) this._mouseTrackingMode = mode; }, /** * getMouseTrackingMode * @return: One of the enum MouseTrackingMode values. */ getMouseTrackingMode: function() { return this._mouseTrackingMode; }, /** * setViewPort * Sets the position and size of the ZoomRegion on screen. * @viewPort: Object defining the position and size of the view port. It * has the form { x, y, width, height }. The values are in * stage coordinate space. */ setViewPort: function(viewPort) { let [xRoi, yRoi, wRoi, hRoi] = this.getROI(); // Remove border if the view port is the entire screen. Otherwise, // ensure that the border is there. if (viewPort.x == 0 && viewPort.y == 0 && viewPort.width == global.screen_width && viewPort.height == global.screen_height) this._magView.add_style_class_name('full-screen'); else this._magView.remove_style_class_name('full-screen'); this.setSize(viewPort.width, viewPort.height); this.setPosition(viewPort.x, viewPort.y); if (this._crossHairs) this._crossHairs.reCenter(); this.scrollContentsTo(xRoi + wRoi / 2, yRoi + hRoi / 2); if (this.isMouseOverRegion()) this._magnifier.hideSystemCursor(); this._screenPosition = ScreenPosition.NONE; }, /** * setROI * Sets the "region of interest" that the ZoomRegion is magnifying. * @roi: Object that defines the region of the screen to magnify. It * has the form { x, y, width, height }. The values are in * screen (unmagnified) coordinate space. */ setROI: function(roi) { let xRoiCenter = roi.x + roi.width / 2; let yRoiCenter = roi.y + roi.height / 2; this.scrollContentsTo(xRoiCenter, yRoiCenter); }, /** * setSize: * @width: The width to set the magnified view to. * @height: The height to set the magnified view to. */ setSize: function(width, height) { this._magView.set_size(width, height); this._calcRightBottomStops(); }, /** * getSize: * @return an array, [width, height], that specifies the size of the * magnified view. */ getSize: function() { return this._magView.get_size(); }, /** * setPosition: * Position the magnified view at the given coordinates. * @x: The x-coord of the new position. * @y: The y-coord of the new position. */ setPosition: function(x, y) { let [width, height] = this._magView.get_size(); if (this._clampScrollingAtEdges) { // Restrict positioning so view doesn't go beyond any edge of the // screen. if (x < 0) x = 0; if (x + width > global.screen_width) x = global.screen_width - width; if (y < 0) y = 0; if (y + height > global.screen_height) y = global.screen_height - height; } this._magView.set_position(x, y); }, /** * getPosition: * @return an array, [x, y], that gives the position of the * magnified view on screen. */ getPosition: function() { return this._magView.get_position(); }, /** * getCenter: * @return an array, [x, y], that is half the width and height of the * magnified view (the center of the magnified view). */ getCenter: function() { let [width, height] = this._magView.get_size(); return [width / 2, height / 2]; }, /** * isFullScreenMode: * Does the magnified view occupy the whole screen? */ isFullScreenMode: function() { let [x, y] = this._magView.get_position(); if (x != 0 || y != 0) return false; [width, height] = this._magView.get_size(); if (width != global.screen_width || height != global.screen_height) return false; return true; }, /** * 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. * @return an array, [x, y, width, height], representing the bounding * rectangle of what is shown in the magnified view. */ getROI: function() { let [xMagnified, yMagnified] = this._uiGroupClone.get_position(); let [xMagFactor, yMagFactor] = this.getMagFactor(); let [width, height] = this.getSize(); let x = (0 - xMagnified) / xMagFactor; let y = (0 - yMagnified) / yMagFactor; return [x, y, width / xMagFactor, height / yMagFactor]; }, /** * setLensMode: * Turn lens mode on/off. In full screen mode, lens mode is alway off since * a lens the size of the screen is pointless. * @lensMode: A boolean to set the sense of lens mode. */ setLensMode: function(lensMode) { let fullScreen = this.isFullScreenMode(); this._lensMode = (lensMode && !fullScreen); if (!this._lensMode && !fullScreen) this.setScreenPosition (this._screenPosition); }, /** * isLensMode: * Is lens mode on or off? * @return The lens mode state as a boolean. */ isLensMode: function() { return this._lensMode; }, /** * setClampScrollingAtEdges: * Stop vs. allow scrolling of the magnified contents when it scroll beyond * the edges of the screen. * @clamp: Boolean to turn on/off clamping. */ setClampScrollingAtEdges: function(clamp) { this._clampScrollingAtEdges = clamp; }, /** * setTopHalf: * Magnifier view occupies the top half of the screen. */ setTopHalf: function() { let viewPort = {}; viewPort.x = 0; viewPort.y = 0; viewPort.width = global.screen_width; viewPort.height = global.screen_height/2; this.setViewPort(viewPort); this._screenPosition = ScreenPosition.TOP_HALF; }, /** * setBottomHalf: * Magnifier view occupies the bottom half of the screen. */ setBottomHalf: function() { 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 = ScreenPosition.BOTTOM_HALF; }, /** * setLeftHalf: * Magnifier view occupies the left half of the screen. */ setLeftHalf: function() { let viewPort = {}; viewPort.x = 0; viewPort.y = 0; viewPort.width = global.screen_width/2; viewPort.height = global.screen_height; this.setViewPort(viewPort); this._screenPosition = ScreenPosition.LEFT_HALF; }, /** * setRightHalf: * Magnifier view occupies the right half of the screen. */ setRightHalf: function() { 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 = ScreenPosition.RIGHT_HALF; }, /** * getScreenPosition: * Tell the outside world what the current mode is -- magnifiying the * top half, bottom half, etc. * @return: the current mode. */ getScreenPosition: function() { return this._screenPosition; }, /** * scrollToMousePos: * Set the region of interest based on the position of the system pointer. * @return: Whether the system mouse pointer is over the magnified view. */ scrollToMousePos: function() { let [objUnder, xMouse, yMouse, mask] = Gdk.Screen.get_default().get_root_window().get_pointer(); if (this._mouseTrackingMode == MouseTrackingMode.PROPORTIONAL) { this._setROIProportional(xMouse, yMouse); } else if (this._mouseTrackingMode == MouseTrackingMode.PUSH) { this._setROIPush(xMouse, yMouse); } else if (this._mouseTrackingMode == MouseTrackingMode.CENTERED) { this._setROICentered(xMouse, yMouse); } this._updateMousePosition(true); // Determine whether the system mouse pointer is over this zoom region. return this.isMouseOverRegion(xMouse, yMouse); }, /** * setFullScreenMode: * Set the ZoomRegion to full-screen mode. * Note: disallows lens mode. */ setFullScreenMode: function() { if (!this.isFullScreenMode()) { let viewPort = {}; viewPort.x = 0; viewPort.y = 0; viewPort.width = global.screen_width; viewPort.height = global.screen_height; this.setViewPort(viewPort); this.setLensMode(false); if (this.isActive()) this._magnifier.hideSystemCursor(); this._screenPosition = ScreenPosition.FULL_SCREEN; } }, /** * setScreenPosition: * Positions the zoom region to one of the enumerated positions on the * screen. * @position: one of Magnifier.FULL_SCREEN, Magnifier.TOP_HALF, * Magnifier.BOTTOM_HALF,Magnifier.LEFT_HALF, or * Magnifier.RIGHT_HALF. */ setScreenPosition: function(inPosition) { switch (inPosition) { case ScreenPosition.FULL_SCREEN: this.setFullScreenMode(); break; case ScreenPosition.TOP_HALF: this.setTopHalf(); break; case ScreenPosition.BOTTOM_HALF: this.setBottomHalf(); break; case ScreenPosition.LEFT_HALF: this.setLeftHalf(); break; case ScreenPosition.RIGHT_HALF: this.setRightHalf(); break; } }, /** * scrollContentsTo: * Shift the contents of the magnified view such it is centered on the given * coordinate. Also, update the position of the magnified mouse image after * the shift. * @x: The x-coord of the point to center on. * @y: The y-coord of the point to center on. */ scrollContentsTo: function(x, y) { this._scrollToPosition(x, y); this._updateMousePosition(false /* mouse didn't move */); }, /** * isMouseOverRegion: * Return whether the system mouse sprite is over this ZoomRegion. If the * mouse's position is not given, then it is fetched. * @xMouse: The system mouse's x-coord. Optional. * @yMouse: The system mouse's y-coord. Optional. * @return: Boolean: true if the mouse is over the zoom region; false * otherwise. */ isMouseOverRegion: function(xMouse, yMouse) { let mouseIsOver = false; if (this.isActive()) { if (!xMouse || !yMouse) { let [objUnder, x, y, mask] = Gdk.Screen.get_default().get_root_window().get_pointer(); xMouse = x; yMouse = y; } let [x, y] = this.getPosition(); let [width, height] = this.getSize(); mouseIsOver = ( xMouse >= x && xMouse < (x + width) && yMouse >= y && yMouse < (y + height) ); } return mouseIsOver; }, /** * addCrosshairs: * Add crosshairs centered on the magnified mouse. * @crossHairs Clutter.Group that contains the actors for the crosshairs. */ addCrosshairs: function(crossHairs) { // If the crossHairs is not already within a larger container, add it // to this zoom region. Otherwise, add a clone. if (crossHairs) { this._crosshairsActor = crossHairs.addToZoomRegion(this, this._mouseRoot); this._crossHairs = crossHairs; } }, //// Private methods //// _scrollToPosition: function(x, y) { // Given the point (x, y) in non-magnified coordinates, scroll the // magnified contenst such that the point is at the centre of the // magnified view. let [xMagFactor, yMagFactor] = this.getMagFactor(); let xMagnified = x * xMagFactor; let yMagnified = y * yMagFactor; let [xCenterMagView, yCenterMagView] = this.getCenter(); let newX = xCenterMagView - xMagnified; let newY = yCenterMagView - yMagnified; if (this._clampScrollingAtEdges) { if (newX > 0) newX = 0; else if (newX < this._rightStop) newX = this._rightStop; if (newY > 0) newY = 0; else if (newY < this._bottomStop) newY = this._bottomStop; this._uiGroupClone.set_position(newX, newY); } else this._uiGroupClone.set_position(newX, newY); // 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.isFullScreenMode()) this.setPosition(x - xCenterMagView, y - yCenterMagView); }, _calcRightBottomStops: function() { // Calculate the location of the top-left corner of _uiGroupClone // when its right and bottom edges are coincident with the right and // bottom edges of the _magView. let [contentWidth, contentHeight] = this._uiGroupClone.get_size(); let [viewWidth, viewHeight] = this.getSize(); let [xMagFactor, yMagFactor] = this.getMagFactor(); let rightStop = viewWidth - (contentWidth * xMagFactor); let bottomStop = viewHeight - (contentHeight * yMagFactor); this._rightStop = parseInt(rightStop.toFixed(1)); this._bottomStop = parseInt(bottomStop.toFixed(1)); }, _setROIPush: function(xMouse, yMouse) { let [xRoi, yRoi, widthRoi, heightRoi] = this.getROI(); let [cursorWidth, cursorHeight] = this._mouseRoot.get_size(); let xPos = xRoi + widthRoi / 2; let yPos = yRoi + heightRoi / 2; let xRoiRight = xRoi + widthRoi - cursorWidth; let yRoiBottom = yRoi + heightRoi - cursorHeight; if (xMouse < xRoi) xPos -= (xRoi - xMouse); else if (xMouse > xRoiRight) xPos += (xMouse - xRoiRight); if (yMouse < yRoi) yPos -= (yRoi - yMouse); else if (yMouse > yRoiBottom) yPos += (yMouse - yRoiBottom); this._scrollToPosition(xPos, yPos); }, _setROIProportional: function(xMouse, yMouse) { let [xRoi, yRoi, widthRoi, heightRoi] = this.getROI(); let halfScreenWidth = global.screen_width / 2; let halfScreenHeight = global.screen_height / 2; let xProportion = (halfScreenWidth - xMouse) / halfScreenWidth; let yProportion = (halfScreenHeight - yMouse) / halfScreenHeight; let xPos = xMouse + xProportion * widthRoi / 2; let yPos = yMouse + yProportion * heightRoi / 2; this._scrollToPosition(xPos, yPos); }, _setROICentered: function(xMouse, yMouse) { this._scrollToPosition(xMouse, yMouse); }, _updateMousePosition: function(mouseMoved) { let [x, y] = this._uiGroupClone.get_position(); x = parseInt(x.toFixed(1)); y = parseInt(y.toFixed(1)); let [xCenterMagView, yCenterMagView] = this.getCenter(); let [objUnder, xMouse, yMouse, mask] = Gdk.Screen.get_default().get_root_window().get_pointer(); let [xMagFactor, yMagFactor] = this.getMagFactor(); let xMagMouse = xMouse * xMagFactor + x; let yMagMouse = yMouse * yMagFactor + y; if (mouseMoved) { if (x == 0) xMagMouse = xMouse * xMagFactor; else if (x == this._rightStop) xMagMouse = (xMouse * xMagFactor) + this._rightStop; else if (this._mouseTrackingMode == MouseTrackingMode.CENTERED) xMagMouse = xCenterMagView; if (y == 0) yMagMouse = yMouse * yMagFactor; else if (y == this._bottomStop) yMagMouse = (yMouse * yMagFactor) + this._bottomStop; else if (this._mouseTrackingMode == MouseTrackingMode.CENTERED) yMagMouse = yCenterMagView; } this._mouseRoot.set_position(xMagMouse, yMagMouse); this._updateCrosshairsPosition(xMagMouse, yMagMouse); }, _updateCrosshairsPosition: function(x, y) { if (this._crosshairsActor) { let [groupWidth, groupHeight] = this._crosshairsActor.get_size(); this._crosshairsActor.set_position(x - groupWidth / 2, y - groupHeight / 2); } } } function Crosshairs() { this._init(); } Crosshairs.prototype = { _init: function() { // 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; this._actor = new Clutter.Group({ clip_to_allocation: false, width: groupWidth, height: groupHeight }); this._horizLeftHair = new Clutter.Rectangle({ color: DEFAULT_CROSSHAIRS_COLOR, width: groupWidth / 2, height: DEFAULT_CROSSHAIRS_THICKNESS, opacity: DEFAULT_CROSSHAIRS_OPACITY }); this._horizRightHair = new Clutter.Rectangle({ color: DEFAULT_CROSSHAIRS_COLOR, width: groupWidth / 2, height: DEFAULT_CROSSHAIRS_THICKNESS, opacity: DEFAULT_CROSSHAIRS_OPACITY }); this._vertTopHair = new Clutter.Rectangle({ color: DEFAULT_CROSSHAIRS_COLOR, width: DEFAULT_CROSSHAIRS_THICKNESS, height: groupHeight / 2, opacity: DEFAULT_CROSSHAIRS_OPACITY }); this._vertBottomHair = new Clutter.Rectangle({ color: DEFAULT_CROSSHAIRS_COLOR, width: DEFAULT_CROSSHAIRS_THICKNESS, height: groupHeight / 2, opacity: DEFAULT_CROSSHAIRS_OPACITY }); this._actor.add_actor(this._horizLeftHair); this._actor.add_actor(this._horizRightHair); this._actor.add_actor(this._vertTopHair); this._actor.add_actor(this._vertBottomHair); this._clipSize = [0, 0]; this._clones = []; this.reCenter(); }, /** * 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. * @zoomRegion: The container to add the crosshairs group to. * @magnifiedMouse: The mouse actor for the zoom region -- used to * position the crosshairs and properly layer them below * the mouse. * @return The crosshairs actor, or its clone. */ addToZoomRegion: function(zoomRegion, magnifiedMouse) { let crosshairsActor = null; if (zoomRegion && magnifiedMouse) { let container = magnifiedMouse.get_parent(); if (container) { crosshairsActor = this._actor; if (this._actor.get_parent() != null) { crosshairsActor = new Clutter.Clone({ source: this._actor }); this._clones.push(crosshairsActor); } if (this._actor.visible) crosshairsActor.show(); else crosshairsActor.hide(); container.add_actor(crosshairsActor); container.raise_child(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. */ removeFromParent: function() { this._actor.get_parent().remove_actor(this._actor); }, /** * setColor: * Set the color of the crosshairs. * @clutterColor: The color as a Clutter.Color. */ setColor: function(clutterColor) { this._horizLeftHair.set_color(clutterColor); this._horizRightHair.set_color(clutterColor); this._vertTopHair.set_color(clutterColor); this._vertBottomHair.set_color(clutterColor); }, /** * getColor: * Get the color of the crosshairs. * @color: The color as a Clutter.Color. */ getColor: function() { let clutterColor = new Clutter.Color(); this._horizLeftHair.get_color(clutterColor); return clutterColor; }, /** * setThickness: * Set the width of the vertical and horizontal lines of the crosshairs. * @thickness */ setThickness: function(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. * @return: The thickness of the crosshairs. */ getThickness: function() { return this._horizLeftHair.get_height(); }, /** * setOpacity: * Set how opaque the crosshairs are. * @opacity: Value between 0 (fully transparent) and 255 (full opaque). */ setOpacity: function(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); }, /** * getOpacity: * Retriev how opaque the crosshairs are. * @return: A value between 0 (transparent) and 255 (opaque). */ getOpacity: function() { return this._horizLeftHair.get_opacity(); }, /** * setLength: * Set the length of the vertical and horizontal lines in the crosshairs. * @length: The length of the crosshairs. */ setLength: function(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. * @return: The length of the crosshairs. */ getLength: function() { return this._horizLeftHair.get_width(); }, /** * setClip: * Set the width and height of the rectangle that clips the crosshairs at * their intersection * @size: Array of [width, height] defining the size of the clip * rectangle. */ setClip: function(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(); } }, /** * getClip: * Get the dimensions of the clip rectangle. * @return: An array of the form [width, height]. */ getClip: function() { return this._clipSize; }, /** * show: * Show the crosshairs. */ show: function() { this._actor.show(); // Clones don't share visibility. for (let i = 0; i < this._clones.length; i++) this._clones[i].show(); }, /** * hide: * Hide the crosshairs. */ hide: function() { this._actor.hide(); // Clones don't share visibility. for (let i = 0; i < this._clones.length; i++) this._clones[i].hide(); }, /** * 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. * @clipSize: Optional. If present, an array of the form [width, height]. */ reCenter: function(clipSize) { let [groupWidth, groupHeight] = this._actor.get_size(); let leftLength = this._horizLeftHair.get_width(); let rightLength = this._horizRightHair.get_width(); let topLength = this._vertTopHair.get_height(); let bottomLength = this._vertBottomHair.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]; // Note that clip, if present, is not centred on the cross hair // intersection, but biased towards the top left. let left = groupWidth / 2 - clipWidth * 0.25 - leftLength; let right = groupWidth / 2 + clipWidth * 0.75; let top = groupHeight / 2 - clipHeight * 0.25 - topLength - thickness / 2; let bottom = groupHeight / 2 + clipHeight * 0.75 + 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); } }