mirror of
https://github.com/finos/SymphonyElectron.git
synced 2024-12-27 17:31:36 -06:00
parent
e3732b9de0
commit
abd538bfa0
@ -119,6 +119,9 @@ class Script
|
|||||||
new Dir(@"ffi-napi\build\Release",
|
new Dir(@"ffi-napi\build\Release",
|
||||||
new File(@"..\..\..\dist\win-unpacked\resources\app.asar.unpacked\node_modules\ffi-napi\build\Release\ffi_bindings.node")
|
new File(@"..\..\..\dist\win-unpacked\resources\app.asar.unpacked\node_modules\ffi-napi\build\Release\ffi_bindings.node")
|
||||||
),
|
),
|
||||||
|
new Dir(@"ffi-napi\node_modules\ref-napi\prebuilds\win32-x64",
|
||||||
|
new File(@"..\..\..\dist\win-unpacked\resources\app.asar.unpacked\node_modules\ffi-napi\node_modules\ref-napi\prebuilds\win32-x64\electron.napi.node")
|
||||||
|
),
|
||||||
new Dir(@"ref-napi\build\Release",
|
new Dir(@"ref-napi\build\Release",
|
||||||
new File(@"..\..\..\dist\win-unpacked\resources\app.asar.unpacked\node_modules\ref-napi\build\Release\binding.node")
|
new File(@"..\..\..\dist\win-unpacked\resources\app.asar.unpacked\node_modules\ref-napi\build\Release\binding.node")
|
||||||
),
|
),
|
||||||
|
159
spec/hwndHandler.spec.ts
Normal file
159
spec/hwndHandler.spec.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import { getContentWindowHandle } from '../src/app/hwnd-handler';
|
||||||
|
|
||||||
|
jest.mock('../src/common/env', () => {
|
||||||
|
return {
|
||||||
|
isWindowsOS: true,
|
||||||
|
isLinux: false,
|
||||||
|
isMac: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockFindWindowExA = jest.fn();
|
||||||
|
const mockGetWindowRect = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('ffi-napi', () => {
|
||||||
|
return {
|
||||||
|
Library: jest.fn(() => {
|
||||||
|
return {
|
||||||
|
FindWindowExA: mockFindWindowExA,
|
||||||
|
GetWindowRect: mockGetWindowRect,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function writeRect(
|
||||||
|
buffer: Buffer,
|
||||||
|
left: number,
|
||||||
|
top: number,
|
||||||
|
right: number,
|
||||||
|
bottom: number,
|
||||||
|
) {
|
||||||
|
buffer.writeInt32LE(left, 0);
|
||||||
|
buffer.writeInt32LE(top, 4);
|
||||||
|
buffer.writeInt32LE(right, 8);
|
||||||
|
buffer.writeInt32LE(bottom, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('hwnd handler', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks().resetModules();
|
||||||
|
|
||||||
|
mockGetWindowRect.mockImplementation((_hwnd: bigint, _rect: Buffer) => {
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
mockFindWindowExA.mockImplementation(() => {
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('not using windows', () => {
|
||||||
|
jest.mock('../src/common/env', () => {
|
||||||
|
return {
|
||||||
|
isWindowsOS: false,
|
||||||
|
isLinux: true,
|
||||||
|
isMac: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const { getContentWindowHandle } = require('../src/app/hwnd-handler');
|
||||||
|
const parent = Buffer.from('hwnd', 'utf8');
|
||||||
|
const hwnd = getContentWindowHandle(parent);
|
||||||
|
expect(hwnd).toBe(parent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unexpected buffer size', () => {
|
||||||
|
const parent = Buffer.from('hwnd', 'utf8');
|
||||||
|
const hwnd = getContentWindowHandle(parent);
|
||||||
|
expect(hwnd).toBe(parent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('no rect found for parent window', () => {
|
||||||
|
const parent = Buffer.from('validhwnd', 'utf8');
|
||||||
|
const hwnd = getContentWindowHandle(parent);
|
||||||
|
expect(hwnd).toBe(parent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('no child window found', () => {
|
||||||
|
mockGetWindowRect.mockImplementationOnce((_hwnd, rect) => {
|
||||||
|
writeRect(rect, 10, 10, 100, 100);
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const parent = Buffer.from('validhwnd', 'utf8');
|
||||||
|
const hwnd = getContentWindowHandle(parent);
|
||||||
|
|
||||||
|
expect(mockGetWindowRect).toBeCalledTimes(1);
|
||||||
|
expect(mockFindWindowExA).toBeCalledTimes(1);
|
||||||
|
expect(hwnd).toBe(parent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matching child window found', () => {
|
||||||
|
mockGetWindowRect.mockImplementationOnce((_hwnd, rect) => {
|
||||||
|
writeRect(rect, 10, 10, 100, 100);
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
mockGetWindowRect.mockImplementationOnce((_hwnd, rect) => {
|
||||||
|
writeRect(rect, 10, 20, 100, 100);
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
mockFindWindowExA.mockImplementationOnce(() => {
|
||||||
|
return 4711;
|
||||||
|
});
|
||||||
|
|
||||||
|
const parent = Buffer.from('validhwnd', 'utf8');
|
||||||
|
const hwnd = getContentWindowHandle(parent);
|
||||||
|
|
||||||
|
expect(mockGetWindowRect).toBeCalledTimes(2);
|
||||||
|
expect(mockFindWindowExA).toBeCalledTimes(1);
|
||||||
|
expect(hwnd.readInt32LE(0)).toBe(4711);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matching child window found second', () => {
|
||||||
|
mockGetWindowRect.mockImplementationOnce((_hwnd, rect) => {
|
||||||
|
writeRect(rect, 10, 10, 100, 100);
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
mockGetWindowRect.mockImplementationOnce((_hwnd, rect) => {
|
||||||
|
writeRect(rect, 100, 10, 100, 100);
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
mockGetWindowRect.mockImplementationOnce((_hwnd, rect) => {
|
||||||
|
writeRect(rect, 10, 20, 100, 100);
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
mockFindWindowExA.mockImplementationOnce(() => {
|
||||||
|
return 4711;
|
||||||
|
});
|
||||||
|
mockFindWindowExA.mockImplementationOnce(() => {
|
||||||
|
return 42;
|
||||||
|
});
|
||||||
|
|
||||||
|
const parent = Buffer.from('validhwnd', 'utf8');
|
||||||
|
const hwnd = getContentWindowHandle(parent);
|
||||||
|
|
||||||
|
expect(mockGetWindowRect).toBeCalledTimes(3);
|
||||||
|
expect(mockFindWindowExA).toBeCalledTimes(2);
|
||||||
|
expect(hwnd.readInt32LE(0)).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('no matching child window found', () => {
|
||||||
|
mockGetWindowRect.mockImplementationOnce((_hwnd, rect) => {
|
||||||
|
writeRect(rect, 10, 10, 100, 100);
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
mockGetWindowRect.mockImplementationOnce((_hwnd, rect) => {
|
||||||
|
writeRect(rect, 10, 10, 100, 100);
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
mockFindWindowExA.mockImplementationOnce(() => {
|
||||||
|
return 4711;
|
||||||
|
});
|
||||||
|
|
||||||
|
const parent = Buffer.from('validhwnd', 'utf8');
|
||||||
|
const hwnd = getContentWindowHandle(parent);
|
||||||
|
|
||||||
|
expect(mockGetWindowRect).toBeCalledTimes(2);
|
||||||
|
expect(mockFindWindowExA).toBeCalledTimes(2);
|
||||||
|
expect(hwnd).toBe(parent);
|
||||||
|
});
|
||||||
|
});
|
@ -58,6 +58,7 @@ jest.mock('../src/app/window-handler', () => {
|
|||||||
jest.mock('../src/app/window-utils', () => {
|
jest.mock('../src/app/window-utils', () => {
|
||||||
return {
|
return {
|
||||||
downloadManagerAction: jest.fn(),
|
downloadManagerAction: jest.fn(),
|
||||||
|
getWindowByName: jest.fn(),
|
||||||
isValidWindow: jest.fn(() => true),
|
isValidWindow: jest.fn(() => true),
|
||||||
sanitize: jest.fn(),
|
sanitize: jest.fn(),
|
||||||
setDataUrl: jest.fn(),
|
setDataUrl: jest.fn(),
|
||||||
@ -471,17 +472,35 @@ describe('main api handler', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should call `getNativeWindowHandle` correctly', () => {
|
it('should call `getNativeWindowHandle` correctly', () => {
|
||||||
const fromWebContentsMocked = {
|
const windows = {
|
||||||
getNativeWindowHandle: jest.fn(),
|
main: {
|
||||||
|
getNativeWindowHandle: jest.fn(),
|
||||||
|
},
|
||||||
|
popout1: {
|
||||||
|
getNativeWindowHandle: jest.fn(),
|
||||||
|
},
|
||||||
|
popout2: {
|
||||||
|
getNativeWindowHandle: jest.fn(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
jest.spyOn(BrowserWindow, 'fromWebContents').mockImplementation(() => {
|
jest
|
||||||
return fromWebContentsMocked;
|
.spyOn(utils, 'getWindowByName')
|
||||||
});
|
.mockImplementation((windowName: string) => {
|
||||||
const value = {
|
return windows[windowName];
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.send(apiName.symphonyApi, {
|
||||||
cmd: apiCmds.getNativeWindowHandle,
|
cmd: apiCmds.getNativeWindowHandle,
|
||||||
};
|
windowName: 'main',
|
||||||
ipcMain.send(apiName.symphonyApi, value);
|
});
|
||||||
expect(fromWebContentsMocked.getNativeWindowHandle).toBeCalledTimes(1);
|
expect(windows['main'].getNativeWindowHandle).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
ipcMain.send(apiName.symphonyApi, {
|
||||||
|
cmd: apiCmds.getNativeWindowHandle,
|
||||||
|
windowName: 'popout1',
|
||||||
|
});
|
||||||
|
expect(windows['popout1'].getNativeWindowHandle).toBeCalledTimes(1);
|
||||||
|
expect(windows['popout2'].getNativeWindowHandle).toBeCalledTimes(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
70
src/app/hwnd-handler.ts
Normal file
70
src/app/hwnd-handler.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { Library } from 'ffi-napi';
|
||||||
|
import { isWindowsOS } from '../common/env';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate the nativeWindowHandle of an Electron BrowserWindow to the handle
|
||||||
|
* of the window where the main content is hosted. On Windows, Chrome uses a separate
|
||||||
|
* window handle for the title bar and the main content has a different window handle
|
||||||
|
* that is positioned below the title bar.
|
||||||
|
* @returns translated window handle, or original handle if no applicable translation found
|
||||||
|
*/
|
||||||
|
export const getContentWindowHandle = (nativeWindowHandle: Buffer): Buffer => {
|
||||||
|
if (!isWindowsOS) {
|
||||||
|
return nativeWindowHandle;
|
||||||
|
}
|
||||||
|
if (nativeWindowHandle.byteLength < 8) {
|
||||||
|
return nativeWindowHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user32 = Library('user32.dll', {
|
||||||
|
FindWindowExA: ['int', ['int', 'int', 'string', 'string']],
|
||||||
|
GetWindowRect: ['int', ['int', 'pointer']],
|
||||||
|
});
|
||||||
|
|
||||||
|
const getWindowRect = (hwnd: bigint) => {
|
||||||
|
const rect = Buffer.alloc(16);
|
||||||
|
|
||||||
|
const ret = user32.GetWindowRect(hwnd.toString(), rect);
|
||||||
|
if (ret) {
|
||||||
|
return {
|
||||||
|
left: rect.readInt32LE(0),
|
||||||
|
top: rect.readInt32LE(4),
|
||||||
|
right: rect.readInt32LE(8),
|
||||||
|
bottom: rect.readInt32LE(12),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const parentHwnd = nativeWindowHandle.readBigUInt64LE();
|
||||||
|
const parentRect = getWindowRect(parentHwnd);
|
||||||
|
if (!parentRect) {
|
||||||
|
return nativeWindowHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
let child = user32.FindWindowExA(
|
||||||
|
parentHwnd.toString(),
|
||||||
|
0,
|
||||||
|
'Chrome_RenderWidgetHostHWND',
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
while (child !== 0) {
|
||||||
|
const rect = getWindowRect(child);
|
||||||
|
|
||||||
|
// The candidate child window is located at the same x position as the parent window, but
|
||||||
|
// has a higher y position (due to the window title frame at the top).
|
||||||
|
if (rect && parentRect.left === rect.left && parentRect.top < rect.top) {
|
||||||
|
const ret = Buffer.alloc(8);
|
||||||
|
ret.writeBigUInt64LE(BigInt(child));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
child = user32.FindWindowExA(
|
||||||
|
parentHwnd.toString(),
|
||||||
|
child,
|
||||||
|
'Chrome_RenderWidgetHostHWND',
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return nativeWindowHandle;
|
||||||
|
};
|
@ -21,6 +21,7 @@ import appStateHandler from './app-state-handler';
|
|||||||
import { getCitrixMediaRedirectionStatus } from './citrix-handler';
|
import { getCitrixMediaRedirectionStatus } from './citrix-handler';
|
||||||
import { CloudConfigDataTypes, config, ICloudConfig } from './config-handler';
|
import { CloudConfigDataTypes, config, ICloudConfig } from './config-handler';
|
||||||
import { downloadHandler } from './download-handler';
|
import { downloadHandler } from './download-handler';
|
||||||
|
import { getContentWindowHandle } from './hwnd-handler';
|
||||||
import { mainEvents } from './main-event-handler';
|
import { mainEvents } from './main-event-handler';
|
||||||
import { memoryMonitor } from './memory-monitor';
|
import { memoryMonitor } from './memory-monitor';
|
||||||
import notificationHelper from './notifications/notification-helper';
|
import notificationHelper from './notifications/notification-helper';
|
||||||
@ -31,6 +32,7 @@ import { activate, handleKeyPress } from './window-actions';
|
|||||||
import { ICustomBrowserWindow, windowHandler } from './window-handler';
|
import { ICustomBrowserWindow, windowHandler } from './window-handler';
|
||||||
import {
|
import {
|
||||||
downloadManagerAction,
|
downloadManagerAction,
|
||||||
|
getWindowByName,
|
||||||
isValidView,
|
isValidView,
|
||||||
isValidWindow,
|
isValidWindow,
|
||||||
sanitize,
|
sanitize,
|
||||||
@ -416,11 +418,10 @@ ipcMain.handle(
|
|||||||
thumbnailSize,
|
thumbnailSize,
|
||||||
});
|
});
|
||||||
case apiCmds.getNativeWindowHandle:
|
case apiCmds.getNativeWindowHandle:
|
||||||
const browserWin = BrowserWindow.fromWebContents(
|
const browserWin = getWindowByName(arg.windowName);
|
||||||
event.sender,
|
|
||||||
) as ICustomBrowserWindow;
|
|
||||||
if (browserWin && windowExists(browserWin)) {
|
if (browserWin && windowExists(browserWin)) {
|
||||||
return browserWin.getNativeWindowHandle();
|
const windowHandle = browserWin.getNativeWindowHandle();
|
||||||
|
return getContentWindowHandle(windowHandle);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case apiCmds.getCitrixMediaRedirectionStatus:
|
case apiCmds.getCitrixMediaRedirectionStatus:
|
||||||
|
@ -279,7 +279,10 @@
|
|||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
<p>Native Window Handle:</p>
|
<p>Native Window Handle:</p>
|
||||||
<button id="get-window-handle">Get window handle</button>
|
<button id="get-window-handle">
|
||||||
|
Get window handle (optionally enter a window name)
|
||||||
|
</button>
|
||||||
|
<input type="text" id="text-window-handle-name" />
|
||||||
<input type="text" id="text-window-handle" />
|
<input type="text" id="text-window-handle" />
|
||||||
<hr />
|
<hr />
|
||||||
<br />
|
<br />
|
||||||
@ -1200,12 +1203,15 @@
|
|||||||
.join('');
|
.join('');
|
||||||
document.getElementById('text-window-handle').value = handleStr;
|
document.getElementById('text-window-handle').value = handleStr;
|
||||||
};
|
};
|
||||||
|
const windowName = document.getElementById('text-window-handle-name')
|
||||||
|
.value;
|
||||||
if (window.ssf) {
|
if (window.ssf) {
|
||||||
window.ssf.getNativeWindowHandle().then(resultCallback);
|
window.ssf.getNativeWindowHandle(windowName).then(resultCallback);
|
||||||
} else if (window.manaSSF) {
|
} else if (window.manaSSF) {
|
||||||
window.manaSSF.getNativeWindowHandle().then(resultCallback);
|
window.manaSSF.getNativeWindowHandle(windowName).then(resultCallback);
|
||||||
} else {
|
} else {
|
||||||
postRequest(apiCmds.getNativeWindowHandle, null, {
|
postRequest(apiCmds.getNativeWindowHandle, null, {
|
||||||
|
windowName,
|
||||||
successCallback: resultCallback,
|
successCallback: resultCallback,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -737,12 +737,18 @@ export class SSFApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get native window handle of the window where the renderer is displayed
|
* Get native window handle of the window, by default where the renderer is displayed,
|
||||||
|
* or optionally another window identified by its name.
|
||||||
|
* @param windowName optional window name, defaults to current renderer window
|
||||||
* @returns the platform-specific handle of the window.
|
* @returns the platform-specific handle of the window.
|
||||||
*/
|
*/
|
||||||
public getNativeWindowHandle(): Promise<Buffer> {
|
public getNativeWindowHandle(windowName?: string): Promise<Buffer> {
|
||||||
|
if (!windowName) {
|
||||||
|
windowName = window.name || 'main';
|
||||||
|
}
|
||||||
return ipcRenderer.invoke(apiName.symphonyApi, {
|
return ipcRenderer.invoke(apiName.symphonyApi, {
|
||||||
cmd: apiCmds.getNativeWindowHandle,
|
cmd: apiCmds.getNativeWindowHandle,
|
||||||
|
windowName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user