gpii-windows
Version:
Components of the GPII personalization infrastructure for use on Microsoft's "Windows" ™
336 lines (299 loc) • 10.7 kB
JavaScript
/*
* App Bar - Makes a window an App Bar, like the taskbar.
*
* Copyright 2020 Raising the Floor - International
*
* Licensed under the New BSD license. You may not use this file except in
* compliance with this License.
*
* The R&D leading to these results received funding from the
* Department of Education - Grant H421A150005 (GPII-APCP). However,
* these results do not necessarily represent the policy of the
* Department of Education, and you should not assume endorsement by the
* Federal Government.
*
* 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");
var gpii = fluid.registerNamespace("gpii");
fluid.registerNamespace("gpii.windows.appBar");
fluid.defaults("gpii.windows.appBar", {
gradeNames: ["fluid.component"],
listeners: {
"onCreate.appBar": {
funcName: "gpii.windows.appBar.created",
args: ["{that}"]
},
"onDestroy.appBar": {
funcName: "gpii.windows.appBar.destroyed",
args: ["{that}"]
}
},
invokers: {
// Set these to the corresponding functions of the electron window.
getNativeWindowHandle: "fluid.notImplemented",
hookWindowMessage: "fluid.notImplemented",
unhookWindowMessage: "fluid.notImplemented",
listenForMessage: {
funcName: "gpii.windows.appBar.listenForMessage",
args: ["{that}", "{arguments}.0"]
},
updateAppBar: {
funcName: "gpii.windows.appBar.updateAppBar", // // message, appBarData, (out) approveRect
args: [ "{that}.hwnd", "{arguments}.0", "{arguments}.1", "{arguments}.2" ]
},
enable: {
funcName: "gpii.windows.appBar.enable",
args: ["{that}"]
},
disable: {
funcName: "gpii.windows.appBar.disable",
args: ["{that}"]
},
getTaskbarPos: {
funcName: "gpii.windows.appBar.getTaskbarPos"
},
getWorkAreaBottom: {
funcName: "gpii.windows.appBar.getWorkAreaBottom"
}
},
events: {
onAppBarChange: null,
onPositionChange: null
},
members: {
hwnd: 0,
hookedMessages: [],
WM_GPII_APPBAR: "@expand:gpii.windows.messages.registeredMessage(WM_GPII_APPBAR)",
enabled: false,
bottomTaskbar: true
}
});
/**
* @typedef {Object} AppBarData
* @property {Number} callbackMessage
* @property {Number} edge
* @property {RECT} rc
* @property {Number} lParam
*/
/**
* Start some message hooks on the QSS window.
* @param {Component} that The gpii.windows.appBar instance.
*/
gpii.windows.appBar.created = function (that) {
that.hwnd = that.getNativeWindowHandle().readInt32LE();
that.listenForMessage(that.WM_GPII_APPBAR);
that.listenForMessage(gpii.windows.API_constants.WM_WINDOWPOSCHANGING);
that.listenForMessage(gpii.windows.API_constants.WM_WINDOWPOSCHANGED);
that.listenForMessage(gpii.windows.API_constants.WM_ACTIVATE);
that.updateAppBar(gpii.windows.API_constants.ABM_NEW, {
callbackMessage: that.WM_GPII_APPBAR
});
};
/**
* Release the message hooks on the QSS window.
* @param {Component} that The gpii.windows.appBar instance.
*/
gpii.windows.appBar.destroyed = function (that) {
fluid.each(that.hookedMessages, function (msg) {
that.unhookWindowMessage(msg);
});
};
/**
* Adds a message hook to the QSS window
* @param {Component} that The gpii.windows.appBar instance.
* @param {Number} msg The window message to listen for.
*/
gpii.windows.appBar.listenForMessage = function (that, msg) {
if (!that.hookedMessages.includes(msg)) {
that.hookedMessages.push(msg);
that.hookWindowMessage(msg, function (wParam, lParam) {
return gpii.windows.appBar.windowMessage(that, that.hwnd, msg, wParam, lParam);
});
}
};
/**
* Handles messages from the window.
*
* @param {Component} that An instance of gpii.app
* @param {Number} hwnd Window handle.
* @param {Number} msg The message.
* @param {Number} wParam Message parameter.
* @param {Number} lParam Message parameter.
*/
gpii.windows.appBar.windowMessage = function (that, hwnd, msg, wParam, lParam) {
switch (msg) {
case that.WM_GPII_APPBAR:
if (wParam.readUInt32LE() === gpii.windows.API_constants.ABN_POSCHANGED) {
if (that.enabled) {
gpii.windows.appBar.updatePosition(that);
}
}
break;
case gpii.windows.API_constants.WM_ACTIVATE:
that.updateAppBar(gpii.windows.API_constants.ABM_ACTIVATE);
break;
case gpii.windows.API_constants.WM_WINDOWPOSCHANGED:
that.updateAppBar(gpii.windows.API_constants.ABM_WINDOWPOSCHANGED);
break;
case gpii.windows.API_constants.WM_WINDOWPOSCHANGING:
if (that.enabled) {
// Put the memory pointed to by (WINDOWPOS)lParam into windowPos
var windowPos = new gpii.windows.WINDOWPOS();
var windowPosPtr = lParam.readPointer().reinterpret(gpii.windows.WINDOWPOS.size);
windowPosPtr.copy(windowPos.ref());
var SWP_NOMOVE = 0x2;
var SWP_NOREDRAW = 0x8;
var windowRect = gpii.windows.getWindowRect(that.hwnd);
if (!windowPos.cy) {
windowPos.cy = windowRect.height;
}
windowPos.flags |= SWP_NOREDRAW;
windowPos.flags &= ~(0x100 | 0x0020);
// Just after the work area change, Windows sometimes adjusts the position to inside the work area.
// Remove the SWP_NOMOVE bit and set the location to prevent this.
if (windowPos.flags & SWP_NOMOVE) {
windowPos.flags &= ~SWP_NOMOVE;
windowPos.x = windowRect.left;
windowPos.y = windowRect.top;
}
// Keep it at the bottom
var bottom = that.getWorkAreaBottom();
if (windowPos.y + windowPos.cy < bottom) {
windowPos.y = bottom - windowPos.cy;
}
// Copy the struct back to the pointer.
windowPos.ref().copy(windowPosPtr);
}
break;
}
};
/**
* Update the app bar state.
*
* A wrapper for SHAppBarMessage: https://docs.microsoft.com/windows/win32/api/shellapi/nf-shellapi-shappbarmessage
*
* @param {Number} hwnd The window handle.
* @param {Number} message The message (ABM_*)
* @param {AppBarData} appBarData Information about the state.
* @return {Number} The return of SHAppBarMessage. Message specific, normally !0 for success.
*/
gpii.windows.appBar.updateAppBar = function (hwnd, message, appBarData) {
if (!appBarData) {
appBarData = {};
}
var data = new gpii.windows.APPBARDATA();
data.ref().fill(0);
data.cbSize = gpii.windows.APPBARDATA.size;
data.hWnd = hwnd;
data.uCallbackMessage = appBarData.callbackMessage || 0;
data.uEdge = appBarData.edge || 0;
data.lParam = appBarData.lParam || 0;
if (appBarData.rc) {
data.rc.left = appBarData.rc.left;
data.rc.top = appBarData.rc.top;
data.rc.right = appBarData.rc.right === undefined
? appBarData.rc.left + appBarData.rc.width
: appBarData.rc.right;
data.rc.bottom = appBarData.rc.bottom === undefined
? appBarData.rc.top + appBarData.rc.height
: appBarData.rc.bottom;
}
return gpii.windows.shell32.SHAppBarMessage(message, data.ref());
};
/**
* Update the position of the window.
* @param {Component} that The gpii.windows.appBar instance.
*/
gpii.windows.appBar.updatePosition = function (that) {
var rect;
var windowRect = gpii.windows.getWindowRect(that.hwnd);
var taskbarRect = gpii.windows.getWindowRect(gpii.windows.getTasktrayWindow());
if (taskbarRect.left === 0 && taskbarRect.top > 0) {
// Taskbar is at the bottom (the usual position)
rect = {
left: 0,
top: taskbarRect.top - windowRect.height - 1,
right: taskbarRect.width,
bottom: taskbarRect.top
};
} else {
var desktopRect = gpii.windows.display.getDesktopSize();
rect = {
left: 0,
top: desktopRect.height - windowRect.height - 1,
width: desktopRect.width,
bottom: desktopRect.height
};
}
var destRect = {};
that.updateAppBar(gpii.windows.API_constants.ABM_SETPOS, {
edge: gpii.windows.API_constants.ABE_BOTTOM,
rc: rect
}, destRect);
that.events.onPositionChange.fire();
};
/**
* Sets the window to be an app bar.
* @param {Component} that The gpii.windows.appBar instance.
*/
gpii.windows.appBar.enable = function (that) {
if (!that.enabled) {
that.enabled = true;
that.updateAppBar(gpii.windows.API_constants.ABM_NEW, {callbackMessage: that.WM_GPII_APPBAR});
gpii.windows.appBar.updatePosition(that);
}
};
/**
* Stops the window from being an app bar.
* @param {Component} that The gpii.windows.appBar instance.
*/
gpii.windows.appBar.disable = function (that) {
if (that.enabled) {
that.enabled = false;
that.updateAppBar(gpii.windows.API_constants.ABM_REMOVE);
}
};
/**
* Gets the position of the taskbar.
*
* @param {Rectangle} rect [optional] The bounding rectangle of the taskbar.
* @return {String} "left", "top", "right", or "bottom".
*/
gpii.windows.appBar.getTaskbarPos = function (rect) {
if (!rect) {
rect = gpii.windows.getWindowRect(gpii.windows.getTasktrayWindow());
}
var togo;
if (rect.top > 0) {
togo = "bottom";
} else if (rect.left > 0) {
togo = "right";
} else {
var desktop = gpii.windows.display.getDesktopSize();
if (rect.width < desktop.width) {
togo = "left";
} else {
togo = "top";
}
}
return togo;
};
/**
* Gets the lowest part of the screen.
* @return {Number} The top of the taskbar (if it's at the bottom of the screen), or the bottom of the screen.
*/
gpii.windows.appBar.getWorkAreaBottom = function () {
var bottom;
var taskRect = gpii.windows.getWindowRect(gpii.windows.getTasktrayWindow());
var taskPos = gpii.windows.appBar.getTaskbarPos(taskRect);
if (taskPos === "bottom") {
bottom = taskRect.top;
} else {
var desktop = gpii.windows.display.getDesktopSize();
bottom = desktop.height;
}
return bottom;
};