UNPKG

gpii-windows

Version:

Components of the GPII personalization infrastructure for use on Microsoft's "Windows" ™

499 lines (449 loc) 17.2 kB
/* * Application Zoom * * * Copyright 2017 Raising the Floor - International * * Licensed under the New BSD license. You may not use this file except in * compliance with this License. * * The research leading to these results has received funding from the European Union's * Seventh Framework Programme (FP7/2007-2013) * under grant agreement no. 289016. * * You may obtain a copy of the License at * https://github.com/GPII/universal/blob/master/LICENSE.txt */ "use strict"; var fluid = require("gpii-universal"), path = require("path"); var gpii = fluid.registerNamespace("gpii"); var windows = fluid.registerNamespace("gpii.windows"); fluid.registerNamespace("gpii.windows.appZoom"); fluid.defaults("gpii.windows.appZoom", { gradeNames: ["fluid.component"], invokers: { startMessages: "{gpii.windows.messages}.start({that})", stopMessages: "{gpii.windows.messages}.stop({that})", getMessageWindow: "{gpii.windows.messages}.getWindowHandle()", windowActivated: { funcName: "gpii.windows.appZoom.windowActivated", args: ["{that}", "{arguments}.0"] // [hwnd] }, getConfig: { funcName: "gpii.windows.appZoom.getConfig", args: ["{that}", "{arguments}.0"] // [WindowInfo] }, sendZoom: { funcName: "gpii.windows.appZoom.sendZoom", args: ["{that}", "{arguments}.0"] // [direction] } }, events: { // The current window has changed. onApplicationActivated: null // [{that}, WindowInfo] }, listeners: { "onCreate": "gpii.windows.appZoom.start({that})", "{gpii.windows.messages}.events.onMessage": { funcName: "gpii.windows.appZoom.windowMessage", // that, hwnd, msg, wParam, lParam args: [ "{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "{arguments}.3" ] } }, members: { // The window that should receive the zoom. currentWindow: null }, configurations: { ignored: { // "ignored" means they don't become the current window. Windows owned by this process are included. ignore: true, match: ["explorer.exe"] }, generic: { // All unmatched windows - the wheel message, with simulated ctrl press. wheel: {}, ctrl: true }, wheel: { // Well behaved applications, that just require the wheel message (which includes the ctrl key). match: ["chrome.exe"], wheel: {} }, standardKey: { // Presses ctrl + "-" or "=" match: ["acrord32.exe", "firefox.exe" ], ctrl: true, key: { decrease: "-", increase: "=" } }, word: { match: ["winword.exe"], wheel: { delay: 500 }, ctrl: true, childWindow: "_WwG" }, edge: { match: ["microsoftedgecp.exe"], // The active window reported by the shell message is wrong. getForegroundWindow: true, childWindow: "Windows.UI.Core.CoreWindow", ctrl: true, wheel: { delay: 500, simulate: true } }, uwp: { match: ["applicationframehost.exe"], getForegroundWindow: true, childWindow: "Windows.UI.Core.CoreWindow", ctrl: true, wheel: { simulate: true } } } }); /** * Information about a window. * @typedef {Object} WindowInfo * @property {Number} hwnd The window handle. * @property {Number} pid The process ID. * @property {String} exe The executable name (lower-cased, without the directory) * @property {ZoomConfig} config The configuration to use for the zooming. */ /** * Information about a configuration. * * @typedef {Object} ZoomConfig * @property {String} name Config name. * @property {String[]} match The executable files (without the directory) to match against. * @property {String} [optional] The class-name of a child window to sent the notifications to. * @property {Boolean} getForegroundWindow true to call GetForegroundWindow instead of trusting the handle passed by * WM_SHELLHOOKMESSAGE. * * @property {Boolean} ctrl Explicitly hold the control key down. * * @property {Object} wheel Use ctrl+mouse wheel. * @property {Boolean} wheel.delay Wait a number of milliseconds before and after. * @property {Boolean} wheel.simulate Simulate the mouse action by moving the cursor and injecting a wheel movement, * rather than just sending WM_MOUSEWHEEL. * * @property {Object} keys The keys to send to adjust the zoom level. * @property {String|Number} keys.decrease The key (character or virtual key code) to send to reduce the zoom. * @property {String|Number} keys.increase The key (character or virtual key code) to send to increase the zoom. */ /** * Sends the zoom command to the current window. * * @param {Component} that The gpii.windows.appZoom component. * @param {String} direction The direction to zoom, "decrease" "increase". * @return {Promise} Resolves when the actions have been sent. */ windows.appZoom.sendZoom = function (that, direction) { var window = that.currentWindow; var config = window && window.config; var promise = fluid.promise(); fluid.log("sendZoom(" + direction + "): ", window); var minimised = windows.user32.IsIconic(window.hwnd); if (minimised) { promise.resolve(); } else if (config) { if (config.ctrl) { // Even though the WM_MOUSEWHEEL message does provide the modifier key state, some applications will // still use GetKeyState/GetAsyncKeyState. For this case, simulate the control key press. windows.appZoom.setControlKeyState(true); } var increment = direction === "increase" ? 1 : -1; if (increment < 0 && direction !== "decrease") { fluid.fail("sendZoom: direction should be either 'decrease' or 'increase'"); } var hwnd = 0; if (config.childWindow) { // Send messages to a child window - find the child. hwnd = windows.appZoom.findChildWindow(window, config); } if (!hwnd) { hwnd = window.hwnd; } if (config.wheel) { setTimeout(function () { windows.appZoom.sendWheel(hwnd, increment, window); setTimeout(function () { // Release the ctrl key after a short delay windows.appZoom.setControlKeyState(false); promise.resolve(); }, 500); }, config.wheel.delay); } else if (config.key) { var key = config.key[direction]; if (key) { var keyCode = parseInt(key); if (!keyCode) { keyCode = windows.user32.VkKeyScanW(key.charCodeAt(0)) & 0xff; } windows.user32.PostMessageW(hwnd, windows.API_constants.WM_KEYDOWN, keyCode, 0); windows.user32.PostMessageW(hwnd, windows.API_constants.WM_KEYUP, keyCode, 1); setTimeout(function () { // Release the ctrl key after a short delay windows.appZoom.setControlKeyState(false); promise.resolve(); }, 500); } } } else { promise.reject({ isError: true, message: "Tried to zoom an unrecognised window" }); } return promise; }; /** * Set the state of the control key. * * @param {Boolean} down True to put the key in the pressed state, otherwise de-pressed. */ windows.appZoom.setControlKeyState = function (down) { var KEYEVENTF_EXTENDEDKEY = 0x1; var KEYEVENTF_KEYUP = 0x2; var flags = down ? 0 : KEYEVENTF_KEYUP; if (down) { // Ensure the keys are released first, MS Word sometimes needs this. windows.appZoom.setControlKeyState(!down); } windows.user32.keybd_event(windows.API_constants.virtualKeyCodes.VK_CONTROL, 0, flags, 0); if (!down) { // ensure both keys are up windows.user32.keybd_event( windows.API_constants.virtualKeyCodes.VK_CONTROL, 0, flags | KEYEVENTF_EXTENDEDKEY, 0); } }; /** * Send the mouse-wheel event to a given window. * @param {Number} hwnd The window handle to send events to. * @param {Number} direction The direction to spin the wheel: > 0 for up, < 0 for down. * @param {WindowInfo} window The window of the application that is being zoomed. */ windows.appZoom.sendWheel = function (hwnd, direction, window) { var MK_CONTROL = 0x8; var WHEEL_DELTA = 120; var MOUSEEVENTF_WHEEL = 0x800; var config = window.config; // Say that the mouse cursor is in the middle of the window. var distance = WHEEL_DELTA * Math.sign(direction); // Some (or most?) applications will not zoom if their window isn't top-most at that point. var targetPoint = windows.appZoom.findUncoveredPoint(hwnd, window); if (config.wheel.simulate) { // Move the cursor on the window and simulate wheel movement. var cursorPos = new windows.POINT(); windows.user32.GetCursorPos(cursorPos.ref()); windows.user32.SetCursorPos(targetPoint.x, targetPoint.y); windows.user32.mouse_event(MOUSEEVENTF_WHEEL, 0, 0, distance, 0); // Put it back again. windows.user32.SetCursorPos(cursorPos.x, cursorPos.y); } else { // Send the wheel message. var lParam = windows.makeLong(targetPoint.x, targetPoint.y); var wParam = windows.makeLong(MK_CONTROL, distance); windows.user32.PostMessageW(hwnd, windows.API_constants.WM_MOUSEWHEEL, wParam, lParam); } }; /** * Returns a point closest to the centre of the target window that isn't covered by another Window owned by a different * process. * * This function assumes only one window could be covering the target. * * The target window is the active (or most recently active) Window, so its not expected to have any window above it * (especially in the middle). The covering window is most likely to be the QSS. * * @param {Number} hwnd The target window handle. * @param {WindowInfo} window The window of the application that is being zoomed. * @return {Object} A point {x,y} that's close to the centre of the target window. */ windows.appZoom.findUncoveredPoint = function (hwnd, window) { var rect = windows.getWindowRect(hwnd); var result; if (rect) { var centre = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; result = { x: centre.x, y: centre.y }; var windowOver = windows.user32.WindowFromPoint(centre.x, centre.y); var windowPid = windows.getWindowProcessId(windowOver); if (windowOver && windowOver !== hwnd && windowPid !== window.pid) { // The window at the point could be a child window, so get the top-level owner. var owner = windows.getTopParent(windowOver); if (owner) { windowOver = owner; } var points = windows.appZoom.getOverlappingPoints(rect, centre, windowOver); if (points.length === 1) { result = points[0]; } else if (points.length > 1) { // Get the closest to the centre, in case it matters for zoom-points. var closest = null; fluid.each(points, function (pt) { var dist = Math.hypot(centre.x - pt.x, centre.y - pt.y); if (closest === null || dist < closest) { closest = dist; result = pt; } }); } } } else { result = { x: 0, y: 0 }; } return result; }; /** * Find the side(s) of the overlapping window that's within the target window. * @param {Object} rect A rect object. * @param {Object} centre The center point of the window. * @param {WindowInfo} windowOver a window handle * @return {Array} Array of points. */ windows.appZoom.getOverlappingPoints = function (rect, centre, windowOver) { // var points = []; var overRect = windows.getWindowRect(windowOver); if (overRect) { overRect.left -= 1; overRect.top -= 1; if (rect.left < overRect.left) { points.push({x: overRect.left, y: centre.y}); } if (rect.top < overRect.top) { points.push({x: centre.x, y: overRect.top}); } if (rect.right > overRect.right) { points.push({x: overRect.right, y: centre.y}); } if (rect.bottom > overRect.bottom) { points.push({x: centre.x, y: overRect.bottom}); } } return points; }; /** * Gets some information about a given window. * @param {Number} hwnd The window handle. * @return {WindowInfo} Information about the window. */ windows.appZoom.getWindowInfo = function (hwnd) { var pid = hwnd && windows.getWindowProcessId(hwnd); var result; if (pid) { var processPath = windows.getProcessPath(pid); result = { hwnd: hwnd, pid: pid, exe: processPath && path.basename(processPath).toLowerCase() }; } return result; }; /** * Gets the zoom configuration that should be used for given window. * * @param {Component} that The gpii.windows.appZoom component. * @param {WindowInfo} window The window. * @return {ZoomConfig} The configuration. */ windows.appZoom.getConfig = function (that, window) { var config = fluid.find(that.options.configurations, function (config) { return fluid.makeArray(config.match).indexOf(window.exe) >= 0 ? config : undefined; }); return config || that.options.configurations.generic; }; /** * A window has been activated. * @param {Component} that The gpii.windows.appZoom component. * @param {Number} hwnd The handle to the activated window. */ windows.appZoom.windowActivated = function (that, hwnd) { if (that.activeWindow !== hwnd) { that.activeWindow = hwnd; var window = windows.appZoom.getWindowInfo(hwnd); // Get the configuration for the window. if (window && (window.pid !== process.pid)) { window.config = that.getConfig(window); if (!window.config.ignore) { that.currentWindow = window; if (window.config.getForegroundWindow) { // Edge's edge case. The window reported by the notification isn't the right window. window.hwnd = windows.user32.GetForegroundWindow(); } } } if (that.currentWindow) { that.events.onApplicationActivated.fire(that, that.currentWindow); } } }; /** * Called when an event has been received by the message window. * * Handles the WM_SHELLHOOKMESSAGE message. * * @param {Component} that The gpii.windows.appZoom component. * @param {Number} hwnd The window handle of the message window. * @param {Number|String} msg The message identifier. * @param {Number} wParam Message specific data. * @param {Buffer} lParam Additional message specific data. */ gpii.windows.appZoom.windowMessage = function (that, hwnd, msg, wParam, lParam) { var HSHELL_WINDOWACTIVATED = 4; var HSHELL_RUDEAPPACTIVATED = 0x8004; if (msg === windows.API_constants.WM_SHELLHOOK) { if (wParam === HSHELL_WINDOWACTIVATED || wParam === HSHELL_RUDEAPPACTIVATED) { // Run the code in the next tick so this function can return soon, as it's a window procedure. process.nextTick(that.windowActivated, lParam.address()); } } }; /** * Start monitoring window activation. * * @param {Component} that The gpii.windows.appZoom component. */ windows.appZoom.start = function (that) { that.startMessages(); // Tell Windows to send WM_SHELLHOOKMESSAGE. gpii.windows.user32.RegisterShellHookWindow(that.getMessageWindow()); }; /** * Find child window. * * @param {WindowInfo} window The window. * @param {Config} config The configuration to use. * @return {String} The window handleThe window handle. */ windows.appZoom.findChildWindow = function (window, config) { var classBuffer = Buffer.alloc(0xff); var hwnd = windows.enumerateWindows(window.hwnd, function (hwndChild) { if (windows.user32.GetClassNameW(hwndChild, classBuffer, classBuffer.length)) { var cls = windows.stringFromWideChar(classBuffer); return cls === config.childWindow ? hwndChild : undefined; } }); if (!hwnd) { fluid.log("appZoom: child window '" + config.childWindow + "' not found."); } return hwnd; };