These solutions use js-ctypes winapi. This is a Windows solution. OSX and Linux don't have this issue, it is easy for steal focus there.
It would be easy if we could just do SetForegroundWindow
winapi method, however the process calling this method must have user focus. If it does not, then it does nothing, so here are the ways to force it:
Attach to thread method (recommended method)
This method does the following:
- Gets the window currently in the foreground
- If none found, the the regular
SetForegroundWindow
will work so it does that and quits
- If window in foreground found it then gets the id of its thread
- It then compares that to the thread id of the current firefox (which is the target windows thread)
- If it is same then it uses regular
SetForegroundWindow
and quits
- If it is not same then it uses
AttachThreadInput
to attach the firefox's thread to the thread of the currently in foreground window
- If call in step 6 fails, then it gives up and quits the function, it throws an error
- If call in step 6 doesn't fail then it calls
SetForegroundWindow
as it WILL work now
- It then undoes the attachment done in step 6, so if you do a
SetForegroundWindow
call now it will fail as it normally should
I can't think of a reason AttachThreadInput
will fail, but if it does, that is the only non-foolproof part of this method. If anyone knows why that will fail then please share a solution for that failure reason. However I left the SetCursorPos
method below as an emergency fallback.
Cu.import('resource://gre/modules/Services.jsm');
Cu.import('resource://gre/modules/ctypes.jsm');
if (ctypes.voidptr_t.size == 4 /* 32-bit */ ) {
var is64bit = false;
} else if (ctypes.voidptr_t.size == 8 /* 64-bit */ ) {
var is64bit = true;
} else {
throw new Error('huh??? not 32 or 64 bit?!?!');
}
var user32 = ctypes.open('user32.dll');
var GetForegroundWindow = user32.declare('GetForegroundWindow', ctypes.winapi_abi,
ctypes.voidptr_t // return
);
var DWORD = ctypes.uint32_t;
var LPDWORD = DWORD.ptr;
var GetWindowThreadProcessId = user32.declare('GetWindowThreadProcessId', ctypes.winapi_abi,
DWORD, // return
ctypes.voidptr_t, // hWnd
LPDWORD // lpdwProcessId
);
var AttachThreadInput = user32.declare('AttachThreadInput', ctypes.winapi_abi,
ctypes.bool, // return
DWORD, // idAttach
DWORD, // idAttachTo
ctypes.bool // fAttach
);
var SetForegroundWindow = user32.declare('SetForegroundWindow', ctypes.winapi_abi,
ctypes.bool, // return BOOL
ctypes.voidptr_t // HWND
);
function forceFocus() {
var hToDOMWindow = Services.wm.getMostRecentWindow('navigator:browser');
if (!hToDOMWindow) {
throw new Error('No browser window found');
}
var hToBaseWindow = hToDOMWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.treeOwner
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIBaseWindow);
var hToString = hToBaseWindow.nativeHandle;
var hTo = ctypes.voidptr_t(ctypes.UInt64(hToString));
var hFrom = GetForegroundWindow();
if (hFrom.isNull()) {
var rez_SetSetForegroundWindow = SetForegroundWindow(hTo);
console.log('rez_SetSetForegroundWindow:', rez_SetSetForegroundWindow);
return true;
}
if (hTo.toString() == hFrom.toString()) {
console.log('window is already focused');
return true;
}
var pid = GetWindowThreadProcessId(hFrom, null);
console.info('pid:', pid);
var _threadid = GetWindowThreadProcessId(hTo, null); // _threadid is thread of my firefox id, and hTo is that of my firefox id so this is possible to do
console.info('_threadid:', _threadid);
if (pid == _threadid) {
var rez_SetSetForegroundWindow = SetForegroundWindow(hTo);
console.log('rez_SetSetForegroundWindow:', rez_SetSetForegroundWindow);
return true;
}
var rez_AttachThreadInput = AttachThreadInput(_threadid, pid, true)
console.info('rez_AttachThreadInput:', rez_AttachThreadInput);
if (!rez_AttachThreadInput) {
throw new Error('failed to attach thread input');
}
var rez_SetSetForegroundWindow = SetForegroundWindow(hTo);
console.log('rez_SetSetForegroundWindow:', rez_SetSetForegroundWindow);
var rez_AttachThreadInput = AttachThreadInput(_threadid, pid, false)
console.info('rez_AttachThreadInput:', rez_AttachThreadInput);
}
setTimeout(function() {
forceFocus();
user32.close();
}, 5000);
Steal then restore cursor position method (not recommended method - is not fool proof, and since it simulates click - it may click on something like a link if user has window in fullscreen and the link was at position 0,0 of the window)
This is a solution that uses js-ctypes and works only on Windows operating system, that does the following:
- Sets the target window to be "always on top" (but focus is not in the window yet)
- Gets the current cursor position
- Sets the cursor position to the top left of the window
- Clicks to give it focus
- Restores the cursor position back to what it was
- Sets the window back to normal (not "always on top")
It has the issue where the x and y coords of the syntehsized mouse click in SendInput
are not being respected, that's why I had to use SetCursorPos
, in a future revision I want to fix this up so it doesn't use SetCursorPos
as moving the users mouse is annoying to the user, but the benefit far outweights the slight annoyance, because its very important to give the window focus, I am temporarily accepting this SetCursorPos
method because I do a "restore cursor position to before stealing", so here it is:
Cu.import('resource://gre/modules/Services.jsm');
Cu.import('resource://gre/modules/ctypes.jsm');
function myFocus() {
//////// START Ctypes DECLARES
if (ctypes.voidptr_t.size == 4 /* 32-bit */ ) {
var is64bit = false;
} else if (ctypes.voidptr_t.size == 8 /* 64-bit */ ) {
var is64bit = true;
} else {
throw new Error('huh??? not 32 or 64 bit?!?!');
}
var user32 = ctypes.open('user32.dll');
var SetWindowPos = user32.declare('SetWindowPos', ctypes.winapi_abi,
ctypes.bool, //return
ctypes.voidptr_t, //hwnd
ctypes.voidptr_t, //hWndInsertAfter
ctypes.int, //X
ctypes.int, //Y
ctypes.int, //cx
ctypes.int, //cy
ctypes.unsigned_int //uFlags
);
var RECT = ctypes.StructType('_RECT', [
{left: ctypes.long},
{top: ctypes.long},
{right: ctypes.long},
{bottom: ctypes.long}
]);
var LPRECT = RECT.ptr;
var GetWindowRect = user32.declare('GetWindowRect', ctypes.winapi_abi,
ctypes.bool, // return
ctypes.voidptr_t, // hwnd
LPRECT // lpRect
);
var SetCursorPos = user32.declare('SetCursorPos', ctypes.winapi_abi,
ctypes.bool, // return
ctypes.int, // x
ctypes.int // y
);
var POINT = ctypes.StructType('_tagPoint', [
{x: ctypes.long},
{y: ctypes.long}
]);
var LPPOINT = POINT.ptr;
var GetCursorPos = user32.declare('GetCursorPos', ctypes.winapi_abi,
ctypes.bool, // return
LPPOINT // lpPoint
);
// send mouse stuff
var ULONG_PTR = is64bit ? ctypes.uint64_t : ctypes.unsigned_long;
var DWORD = ctypes.uint32_t;
var MOUSEINPUT = ctypes.StructType('tagMOUSEINPUT', [
{'dx': ctypes.long},
{'dy': ctypes.long},
{'mouseData': DWORD},
{'dwFlags': DWORD},
{'time': ULONG_PTR},
{'dwExtraInfo': DWORD}
]);
var INPUT = ctypes.StructType('tagINPUT', [
{'type': DWORD},
{'mi': MOUSEINPUT} // union, pick which one you want, we want keyboard input
]);
var LPINPUT = INPUT.ptr;
var SendInput = user32.declare('SendInput', ctypes.winapi_abi, ctypes.unsigned_int, ctypes.unsigned_int, LPINPUT, ctypes.int);
var INPUT_MOUSE = 0;
var MOUSEEVENTF_LEFTDOWN = 2;
var MOUSEEVENTF_LEFTUP = 4;
var MOUSEEVENTF_ABSOLUTE = 0x8000;
// end send mouse stuff
var HWND_TOP = ctypes.voidptr_t(-1); //ctypes.cast(ctypes.int(-1), ctypes.voidptr_t);
var HWND_NOTOPMOST = ctypes.voidptr_t(-2);
var SWP_NOMOVE = 2;
var SWP_NOSIZE = 1;
//////// END Ctypes DECLARES
// pick a one of our navigator:browser firefox windows to focus
var browserWindow = Services.wm.getMostRecentWindow('navigator:browser');
if (!browserWindow) {
throw new Error('No browser window found');
}
// convert our DOMWindow to a HWND
var baseWindow = browserWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.treeOwner
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIBaseWindow);
var hwndString = baseWindow.nativeHandle;
var hwnd = ctypes.voidptr_t(ctypes.UInt64(hwndString));
browserWindow.focus(); // this is important, withou this, i dont know why, but the window will not become "always on top" from the SetWindowPos code on the next line
var rez_SetWindowPos = SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
console.log('rez_SetWindowPos:', rez_SetWindowPos);
var myRect = RECT();
var rez_GetWindowRect = GetWindowRect(hwnd, myRect.address());
console.log('rez_SetWindowPos:', rez_SetWindowPos);
var myRectLeft = parseInt(myRect.left.toString());
var myRectTop = parseInt(myRect.top.toString());
console.log('myRect.left', myRectLeft);
console.log('myRect.top', myRectTop);
var myPoint = POINT();
var rez_GetCursorPos = GetCursorPos(myPoint.address());
console.log('rez_GetCursorPos:', rez_GetCursorPos);
var myPointX = parseInt(myPoint.x.toString());
var myPointY = parseInt(myPoint.y.toString());
console.log('myPoint.x', myPointX);
console.log('myPoint.y', myPointY);
var rez_SetCursorPos = SetCursorPos(myRect.left, myRect.top);
console.log('rez_SetWindowPos:', rez_SetWindowPos);
// may need to wait for the window to come to top
// send click - i dont know why but the x and y coords of these send clicks is not being respected, it is just clicking where the mouse is, so im having to use SetCursorPos as temp hack
var js_pInputs = [
INPUT(INPUT_MOUSE, MOUSEINPUT(myRectLeft, myRectTop, 0, MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_ABSOLUTE, 0, 0)),
INPUT(INPUT_MOUSE, MOUSEINPUT(myRectLeft, myRectTop, 0, MOUSEEVENTF_LEFTUP | MOUSEEVENTF_ABSOLUTE, 0, 0))
];
var pInputs = INPUT.array()(js_pInputs);
var rez_SI = SendInput(pInputs.length, pInputs, INPUT.size);
console.log('rez_SI:', rez_SI.toString());
// end send click
var rez_SetCursorPos = SetCursorPos(myPoint.x, myPoint.y);
console.log('rez_SetWindowPos:', rez_SetWindowPos);
var rez_SetWindowPos = SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
console.log('rez_SetWindowPos:', rez_SetWindowPos);
user32.close();
}
setTimeout(function() {
myFocus();
}, 5000);
This is the approach that was needed, because SetForegroundWindow
does nothing if the process cal