UNPKG

gpii-windows

Version:

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

364 lines (322 loc) 14 kB
/* * DPI support for Windows 10 * * Copyright 2016 Raising the Floor - US * * Licensed under the New BSD license. You may not use this file except in * compliance with this License. * * You may obtain a copy of the License at * https://github.com/GPII/universal/blob/master/LICENSE.txt */ "use strict"; /* There is no documented method to change the DPI in Windows 10. The Display Settings applet in Windows calls * DisplayConfigSetDeviceInfo with an undocumented structure, which looks like the following: * * struct DISPLAYCONFIG_SET_DPI { * DISPLAYCONFIG_DEVICE_INFO_HEADER header; // https://msdn.microsoft.com/en-us/library/windows/hardware/ff553920.aspx * INT32 dpiOffset; // see below * }; * * The 'type' member of the header, which is used to identify the type of packet, is (uint) -4. * * The procedure to getting the current DPI is similar. The "official" way of getting it is using * GetDeviceCaps(LOGPIXELS[X/Y]), however that appears to only return the DPI that was set at the time of login. * The Display Settings applet uses DisplayConfigGetDeviceInfo, and the packet looks something like: * * struct DISPLAYCONFIG_GET_DPI { * DISPLAYCONFIG_DEVICE_INFO_HEADER header; // https://msdn.microsoft.com/en-us/library/windows/hardware/ff553920.aspx * INT32 minDpiOffset; // The minimum value of dpiOffset. * INT32 dpiOffset; // see below * INT32 maxDpiOffset; // The maximum value of dpiOffset, determined by the screen resolution. * }; * * The 'type' member of header is (uint) -3. * maxDpiOffset is the maximum DPI of the current resolution, dpiOffset can be higher but not the actual DPI. * * The dpiOffset is the number of values away from the display's recommended value, and can be negative. The values * (which are a percentage of 96 DPI), are: * [ 100, 125, 150, 175, 200, 225, 250, 300, 350, 400, 450, 500 ] * For example, if the recommended DPI is 175%, then a dpiOffset of 0 is 175, 2 is 225, and -1 is 150. */ var ref = require("ref"); var Struct = require("ref-struct"); var arrayType = require("ref-array"); var ffi = require("ffi-napi"); var fluid = require("gpii-universal"); var windows = fluid.registerNamespace("gpii.windows"); var t = windows.types; var c = windows.API_constants; // These API calls are for only Windows 10 var user32 = ffi.Library("user32", { // https://msdn.microsoft.com/en-us/library/windows/hardware/ff553903.aspx "DisplayConfigGetDeviceInfo": [ // DISPLAYCONFIG_DEVICE_INFO_HEADER* t.LONG, [ "pointer" ] ], // https://msdn.microsoft.com/en-us/library/windows/hardware/ff553909.aspx "DisplayConfigSetDeviceInfo": [ // DISPLAYCONFIG_DEVICE_INFO_HEADER* t.LONG, [ "pointer" ] ], // https://msdn.microsoft.com/en-us/library/windows/hardware/ff566772.aspx "GetDisplayConfigBufferSizes": [ t.LONG, [ "uint32", "uint32*", "uint32*" ] ], // https://msdn.microsoft.com/en-us/library/windows/hardware/ff569215.aspx "QueryDisplayConfig":[ t.LONG, [ "uint32", "uint32*", "pointer", "uint*", "pointer", "pointer" ] ] }); // sizeof(DISPLAYCONFIG_MODE_INFO) var DISPLAYCONFIG_MODE_INFO_SIZE = 64; // THe types of packets send to DisplayConfigGetDeviceInfo/DisplayConfigSetDeviceInfo // https://msdn.microsoft.com/en-us/library/windows/hardware/ff553924.aspx var DISPLAYCONFIG_DEVICE_INFO_TYPE = { GET_SOURCE_NAME: 1, GET_TARGET_NAME: 2, GET_TARGET_PREFERRED_MODE: 3, GET_ADAPTER_NAME: 4, SET_TARGET_PERSISTENCE: 5, GET_TARGET_BASE_TYPE: 6, GET_SUPPORT_VIRTUAL_RESOLUTION: 7, SET_SUPPORT_VIRTUAL_RESOLUTION: 8, // undocumented: GET_DPI: 0xfffffffd, SET_DPI: 0xfffffffc }; // https://msdn.microsoft.com/en-us/library/dd145066.aspx windows.MONITORINFOEX = new Struct([ [t.DWORD, "cbSize"], [arrayType(t.LONG, 4), "rcMonitor"], [arrayType(t.LONG, 4), "rcWork"], [t.DWORD, "dwFlags"], [arrayType(t.TCHAR, c.CCHDEVICENAME), "szDevice"] ]); // https://msdn.microsoft.com/en-us/library/windows/hardware/ff553945.aspx windows.DISPLAYCONFIG_PATH_INFO = new Struct([ [t.LUID, "source_adapterId"], ["uint32", "source_id"], ["uint32", "source_modeInfoIdx"], ["uint32", "source_statusFlags"], [t.LUID, "target_adapterId"], ["uint32", "target_id"], ["uint32", "target_modeInfoIdx"], ["uint32", "target_outputTechnology"], ["uint32", "target_rotation"], ["uint32", "target_scaling"], ["uint32", "target_refreshRateNumerator"], ["uint32", "target_refreshRateDenominator"], ["uint32", "target_scanLineOrdering"], [t.BOOL, "target_targetAvailable"], ["uint32", "target_statusFlags"], ["uint32", "flags"] ], { packed: true }); // https://msdn.microsoft.com/en-us/library/windows/hardware/ff553933.aspx // This structure serves no useful purpose (to GPII), so an anonymous blob of memory is used, instead of defining a // handful of nested structures. windows.DISPLAYCONFIG_MODE_INFO = new Struct([ [arrayType("char", DISPLAYCONFIG_MODE_INFO_SIZE), "reserved"] ]); // https://msdn.microsoft.com/en-us/library/windows/hardware/ff553920.aspx windows.DISPLAYCONFIG_DEVICE_INFO_HEADER = new Struct([ // DISPLAYCONFIG_DEVICE_INFO_TYPE enum ["uint32", "type"], ["uint32", "size"], [t.LUID, "adapterId"], ["uint32", "id"] ], { packed: true }); // https://msdn.microsoft.com/en-us/library/windows/hardware/ff553983.aspx windows.DISPLAYCONFIG_SOURCE_DEVICE_NAME = new Struct([ [windows.DISPLAYCONFIG_DEVICE_INFO_HEADER, "header"], [arrayType(t.TCHAR, c.CCHDEVICENAME), "viewGdiDeviceName"] ]); // This structure is not documented or defined in the SDK, but it's what control panel appears to use when querying the // current DPI setting. windows.DISPLAYCONFIG_GET_DPI = new Struct([ [windows.DISPLAYCONFIG_DEVICE_INFO_HEADER, "header"], ["int32", "minDpiOffset"], ["int32", "dpiOffset"], ["int32", "maxDpiOffset"] ], {packed: true}); // This structure is not documented or defined in the SDK, but it's what control panel appears to use when adjusting // the DPI. windows.DISPLAYCONFIG_SET_DPI = new Struct([ [windows.DISPLAYCONFIG_DEVICE_INFO_HEADER, "header"], ["int32", "dpiOffset"] ]); /** * Create and initialise a new new MONITORINFOEX structure * @return {MONITORINFOEX} The structure. */ windows.display.createMonitorInfoStruct = function () { var obj = new windows.MONITORINFOEX(); obj.ref().fill(0); obj.cbSize = windows.MONITORINFOEX.size; return obj; }; /** * Create and initialise a new DISPLAYCONFIG_SOURCE_DEVICE_NAME structure * @return {DISPLAYCONFIG_SOURCE_DEVICE_NAME} The structure. */ windows.display.createSourceDeviceNameStruct = function () { var obj = new windows.DISPLAYCONFIG_SOURCE_DEVICE_NAME(); obj.ref().fill(0); obj.header.size = windows.DISPLAYCONFIG_SOURCE_DEVICE_NAME.size; obj.header.type = DISPLAYCONFIG_DEVICE_INFO_TYPE.GET_SOURCE_NAME; return obj; }; /** * Create and initialise a new DISPLAYCONFIG_SET_DPI structure * @return {DISPLAYCONFIG_SET_DPI} The structure. */ windows.display.createSetDpiStruct = function () { var obj = new windows.DISPLAYCONFIG_SET_DPI(); obj.ref().fill(0); obj.header.size = windows.DISPLAYCONFIG_SET_DPI.size; obj.header.type = DISPLAYCONFIG_DEVICE_INFO_TYPE.SET_DPI; return obj; }; /** * Create and initialise a new DISPLAYCONFIG_GET_DPI structure * @return {DISPLAYCONFIG_GET_DPI} The structure. */ windows.display.createGetDpiStruct = function () { var obj = new windows.DISPLAYCONFIG_GET_DPI(); obj.ref().fill(0); obj.header.size = windows.DISPLAYCONFIG_GET_DPI.size; obj.header.type = DISPLAYCONFIG_DEVICE_INFO_TYPE.GET_DPI; return obj; }; /** * Gets the adapter and monitor (or socket?) id pair of a given device name, by iterating through all display * configurations until there's a match. * Inspired by https://msdn.microsoft.com/en-us/library/windows/hardware/dn690039.aspx * * @param {String} deviceName Device name of the display (eg, \\.\DISPLAY1). * @return {Object} The adapterId and sourceId of the display. */ windows.display.getAdapter = function (deviceName) { // Get the array sizes for the configuration var pathCountPtr = ref.alloc("uint32"); var modeInfoCountPtr = ref.alloc("uint32"); var ret = user32.GetDisplayConfigBufferSizes(c.QDC_ONLY_ACTIVE_PATHS, pathCountPtr, modeInfoCountPtr); windows.checkReturnCode(ret); var pathCount = pathCountPtr.deref(); var modeInfoCount = modeInfoCountPtr.deref(); // Allocate the arrays var PathInfoArray = arrayType(windows.DISPLAYCONFIG_PATH_INFO); var ModeInfoArray = arrayType(windows.DISPLAYCONFIG_MODE_INFO); var pathInfo = new PathInfoArray(pathCount); var modeInfo = new ModeInfoArray(modeInfoCount); // Get the display configurations ret = user32.QueryDisplayConfig(c.QDC_ONLY_ACTIVE_PATHS, pathCountPtr, pathInfo.buffer, modeInfoCountPtr, modeInfo.buffer, ref.NULL); windows.checkReturnCode(ret); var adapterToGo = null; // Find a matching display. var sourceName = windows.display.createSourceDeviceNameStruct(); for (var n = 0; n < pathCount; n++) { // Get the device name. sourceName.header.adapterId = pathInfo[n].source_adapterId; sourceName.header.id = pathInfo[n].source_id; ret = user32.DisplayConfigGetDeviceInfo(sourceName.ref()); if (ret) { // There was some problem with this display - ignore it and move on. continue; } var name = windows.fromWideChar(sourceName.viewGdiDeviceName.buffer); if (name === deviceName) { // There could be more than one matching monitor (a clone). The first one is preferred, but if there's an // internal/built-in display later, then let's use that one. var isInternal = (pathInfo[n].target_outputTechnology === c.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EMBEDDED) || (pathInfo[n].target_outputTechnology === c.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_UDI_EMBEDDED) || (pathInfo[n].target_outputTechnology === c.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INTERNAL); if (adapterToGo === null || isInternal) { adapterToGo = { adapterId: sourceName.header.adapterId, sourceId: sourceName.header.id }; } } } return adapterToGo; }; /** * Gets the device name of the primary monitor. (eg, \\.\DISPLAY1) * @return {String} The device name of the primary monitor. */ windows.display.getPrimaryMonitorName = function () { var monitor = windows.user32.MonitorFromWindow(ref.NULL_POINTER, c.MONITOR_DEFAULTTOPRIMARY); var monitorInfo = windows.display.createMonitorInfoStruct(); windows.user32.GetMonitorInfoW(monitor, monitorInfo.ref()); return windows.fromWideChar(monitorInfo.szDevice.buffer); }; /** * Sets the DPI scale of the primary display, by specifying the number of values away from an from the recommended * setting. * * @param {Number} offset The offset from the recommended setting. * @return {DpiConfig} The newly configured, actual, and minimum/maximum DPI offsets (see getScreenDpi). */ windows.display.setScreenDpi = function (offset) { // Only change the primary monitor, because that's what setScreenResolution does. var monitorName = windows.display.getPrimaryMonitorName(); var adapter = windows.display.getAdapter(monitorName); var setPacket = windows.display.createSetDpiStruct(); setPacket.header.adapterId = adapter.adapterId; setPacket.header.id = adapter.sourceId; if (offset < -3) { // There isn't a lower limit, so add an artificial bottom cap. If the display supports lower settings then it // would probably be unusable beyond this point. offset = -3; } setPacket.dpiOffset = offset; var ret = user32.DisplayConfigSetDeviceInfo(setPacket.ref()); windows.checkReturnCode(ret); // Return the new configuration. return windows.display.getScreenDpi(adapter); }; /** * Get the configured, maximum, and actual DPI values of the primary display. * * The value is the number of "notches" away from the recommended setting of the display. * * The configured scale is what DPI should be if the resolution is high enough, the maximum scale is the highest DPI * scale that the current screen resolution supports. The actual scale (what the user is looking at) is the configured * scale, capped at the maximum. * * @param {Object} adapter [optional] The adapter id pair (from getAdapter()). * @return {DpiConfig} The newly configured, actual, and minimum/maximum DPI offsets */ windows.display.getScreenDpi = function (adapter) { if (!adapter) { var monitorName = windows.display.getPrimaryMonitorName(); adapter = windows.display.getAdapter(monitorName); } var getPacket = windows.display.createGetDpiStruct(); getPacket.header.adapterId = adapter.adapterId; getPacket.header.id = adapter.sourceId; var ret = user32.DisplayConfigGetDeviceInfo(getPacket.ref()); windows.checkReturnCode(ret); var configured = getPacket.dpiOffset; var maximum = getPacket.maxDpiOffset; return { configured: configured, minimum: getPacket.minDpiOffset, maximum: maximum, actual: Math.min(configured, maximum) }; }; /** * DPI Configuration. * @typedef {Object} DpiConfig * @property {Number} configured The desired DPI setting. * @property {Number} minimum The minimum available DPI setting. * @property {Number} maximum The maximum available DPI setting. * @property {Number} actual The actual DPI setting - the same as configured, but clamped to minimum and maximum. */ fluid.defaults("gpii.windows.display.getScreenDpi", { gradeNames: "fluid.function", argumentMap: {} });