1 // W32Compat.cc --- W32 compatibility
2 //
3 // Copyright (C) 2004, 2007, 2010, 2012 Rob Caelers, Raymond Penners, Ray Satiro
4 // All rights reserved.
5 //
6 // This program is free software: you can redistribute it and/or modify
7 // it under the terms of the GNU General Public License as published by
8 // the Free Software Foundation, either version 3 of the License, or
9 // (at your option) any later version.
10 //
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 // GNU General Public License for more details.
15 //
16 // You should have received a copy of the GNU General Public License
17 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 //
19 
20 #ifdef HAVE_CONFIG_H
21 #include "config.h"
22 #endif
23 
24 #include "debug.hh"
25 
26 #include <windows.h>
27 #include <tchar.h>
28 #include <wtsapi32.h>
29 
30 #ifdef PLATFORM_OS_WIN32_NATIVE
31 #undef max
32 #endif
33 #ifdef PLATFORM_OS_WIN32
34 #undef interface
35 #endif
36 
37 #include <gtkmm.h>
38 #include <gdk/gdkwin32.h>
39 
40 #include "IBreak.hh"
41 #include "BreakWindow.hh"
42 #include "CoreFactory.hh"
43 #include "IConfigurator.hh"
44 #include "GtkUtil.hh"
45 #include "W32ForceFocus.hh"
46 #include "W32CriticalSection.hh"
47 #include "W32Compat.hh"
48 
49 
50 using namespace workrave;
51 
52 bool W32Compat::ime_magic = false;
53 bool W32Compat::reset_window_always = false;
54 bool W32Compat::reset_window_never = false;
55 
56 W32Compat::SWITCHTOTHISWINDOWPROC W32Compat::switch_to_this_window_proc = NULL;
57 
58 
59 
60 typedef BOOLEAN (WINAPI * PWINSTATIONQUERYINFORMATIONW)(
61     HANDLE, ULONG, INT, PVOID, ULONG, PULONG );
62 namespace { PWINSTATIONQUERYINFORMATIONW dyn_WinStationQueryInformationW; }
63 
64 /* W32Compat::WinStationQueryInformationW()
65 
66 Query information about our winstation.
67 http://msdn.microsoft.com/en-us/library/windows/desktop/aa383827.aspx
68 
69 This API is deprecated so its use is limited. Some queries are problematic on x64:
70 http://www.remkoweijnen.nl/blog/2011/01/29/querying-a-user-token-under-64-bit-version-of-2003xp/
71 
72 Help for WinStationInformationClass info:
73 http://msdn.microsoft.com/en-us/library/cc248604.aspx
74 http://msdn.microsoft.com/en-us/library/cc248834.aspx
75 
76 returns whatever the API returns.
77 if the API is unavailable this function returns FALSE and last error is set ERROR_INVALID_FUNCTION.
78 */
WinStationQueryInformationW(HANDLE hServer,ULONG LogonId,INT WinStationInformationClass,PVOID pWinStationInformation,ULONG WinStationInformationLength,PULONG pReturnLength)79 BOOLEAN W32Compat::WinStationQueryInformationW(
80     HANDLE hServer,   // use WTS_CURRENT_SERVER_HANDLE
81     ULONG LogonId,   // use WTS_CURRENT_SESSION
82     INT WinStationInformationClass,   // review msdn links in comment block
83     PVOID pWinStationInformation,
84     ULONG WinStationInformationLength,
85     PULONG pReturnLength
86 )
87 {
88     init();
89 
90     if( !dyn_WinStationQueryInformationW )
91     {
92         SetLastError( ERROR_INVALID_FUNCTION );
93         return FALSE;
94     }
95 
96     return dyn_WinStationQueryInformationW(
97         hServer,
98         LogonId,
99         WinStationInformationClass,
100         pWinStationInformation,
101         WinStationInformationLength,
102         pReturnLength
103         );
104 }
105 
106 
107 
108 VOID
SwitchToThisWindow(HWND hwnd,BOOL emulate_alt_tab)109 W32Compat::SwitchToThisWindow( HWND hwnd, BOOL emulate_alt_tab )
110 {
111   init();
112 
113   if ( switch_to_this_window_proc != NULL )
114     {
115       ( *switch_to_this_window_proc )( hwnd, emulate_alt_tab );
116     }
117   return;
118 }
119 
120 
121 
122 static W32CriticalSection cs__init;
123 volatile LONG W32Compat::_initialized = 0;
124 
125 /* W32Compat::_init()
126 
127 Initialize the W32Compat class. The functions should call init(), which is inline, instead of this.
128 A call to init() should be the first line in each of the other functions in this class.
129 */
_init()130 void W32Compat::_init()
131 {
132     W32CriticalSection::Guard guard( cs__init );
133 
134     if( _initialized )
135         return;
136 
137     HMODULE user_lib = GetModuleHandleA( "user32.dll" );
138     if( user_lib )
139     {
140         switch_to_this_window_proc = (SWITCHTOTHISWINDOWPROC) GetProcAddress(user_lib, "SwitchToThisWindow");
141     }
142 
143     HMODULE winsta_lib = LoadLibraryA( "winsta" );
144     if( winsta_lib )
145     {
146         dyn_WinStationQueryInformationW =
147             (PWINSTATIONQUERYINFORMATIONW)GetProcAddress(
148             winsta_lib,
149             "WinStationQueryInformationW"
150             );
151     }
152 
153     // Should SetWindowOnTop() call IMEWindowMagic() ?
154     if( !CoreFactory::get_configurator()->get_value( "advanced/ime_magic", ime_magic ) )
155     {
156         ime_magic = false;
157     }
158 
159     // As of writing SetWindowOnTop() always calls ResetWindow()
160     // ResetWindow() determines whether to "reset" when both
161     // reset_window_always and reset_window_never are false.
162     //
163     // If reset_window_always is true, and if ResetWindow() is continually
164     // passed the same hwnd, hwnd will flicker as a result of the continual
165     // z-order position changes / resetting.
166     if( !CoreFactory::get_configurator()->get_value( "advanced/reset_window_always", reset_window_always ) )
167     {
168         reset_window_always = false;
169     }
170     // ResetWindow() will always abort when reset_window_never is true.
171     if( !CoreFactory::get_configurator()->get_value( "advanced/reset_window_never", reset_window_never ) )
172     {
173         reset_window_never = false;
174     }
175 
176     InterlockedExchange( &_initialized, 1 );
177 }
178 
179 
180 void
SetWindowOnTop(HWND hwnd,BOOL topmost)181 W32Compat::SetWindowOnTop( HWND hwnd, BOOL topmost )
182 {
183 	init();
184 
185 	SetWindowPos( hwnd, topmost ? HWND_TOPMOST : HWND_NOTOPMOST,
186 		0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE );
187 
188 	ResetWindow( hwnd, (bool)topmost );
189 
190 	if( ime_magic && topmost )
191 	{
192 		IMEWindowMagic( hwnd );
193 	}
194 
195 	if( W32ForceFocus::GetForceFocusValue() && topmost )
196 	{
197 		W32ForceFocus::ForceWindowFocus( hwnd );
198 	}
199 
200 	return;
201 }
202 
203 
204 // Bug 587 -  Vista: Workrave not modal / coming to front
205 // http://issues.workrave.org/show_bug.cgi?id=587
206 // There is an issue with IME and window z-ordering.
207 //
208 // hwnd == window to "reset" in z-order
209 // topmost == true if window should be topmost, false otherwise
210 void
ResetWindow(HWND hwnd,bool topmost)211 W32Compat::ResetWindow( HWND hwnd, bool topmost )
212 {
213 	init();
214 
215 	if( !IsWindow( hwnd ) || reset_window_never )
216 		return;
217 
218 	const bool DEBUG = false;
219 	bool reset = false;
220 	DWORD gwl_exstyle = 0;
221 	DWORD valid_exstyle_diff = 0;
222 
223 	WINDOWINFO gwi;
224 	ZeroMemory( &gwi, sizeof( gwi ) );
225 	gwi.cbSize = sizeof( WINDOWINFO );
226 
227 
228 	SetLastError( 0 );
229 	gwl_exstyle = (DWORD)GetWindowLong( hwnd, GWL_EXSTYLE );
230 	if( !GetLastError() )
231 	{
232 		// if desired and actual topmost style differ, plan to reset
233 		if( topmost != ( gwl_exstyle & WS_EX_TOPMOST ? true : false ) )
234 			reset = true;
235 	}
236 
237 	SetLastError( 0 );
238 	GetWindowInfo( hwnd, &gwi );
239 	if( !GetLastError() )
240 	{
241 		// if desired and actual topmost style differ, plan to reset
242 		if( topmost != ( gwi.dwExStyle & WS_EX_TOPMOST ? true : false ) )
243 			reset = true;
244 	}
245 
246 #ifdef BREAKAGE
247 	// GetWindowInfo() and GetWindowLong() extended style info can differ.
248 	// Compare the two results but filter valid values only.
249 	valid_exstyle_diff = ( gwl_exstyle ^ gwi.dwExStyle ) & ~0xF1A08802;
250 	if( valid_exstyle_diff || DEBUG )
251 	{
252 		// if the extended style info differs, plan to reset.
253 		// e.g. gwl returned ws_ex_toolwindow but gwi didn't
254 		reset = true;
255 
256 		// attempt to sync differences:
257 		DWORD swl_exstyle = ( valid_exstyle_diff | gwl_exstyle ) & ~0xF1A08802;
258 
259 		if( ( swl_exstyle & WS_EX_APPWINDOW ) && ( swl_exstyle & WS_EX_TOOLWINDOW ) )
260 		// this hasn't happened and shouldn't happen, but i suppose it could.
261 		// if both styles are set change to appwindow only.
262 		// why not toolwindow only? well, why are they both set in the first place?
263 		// in this case it's better to make hwnd visible on the taskbar.
264 		{
265 			swl_exstyle &= ~WS_EX_TOOLWINDOW;
266 		}
267 
268 		ShowWindow( hwnd, SW_HIDE );
269 		SetWindowLong( hwnd, GWL_EXSTYLE, (LONG)swl_exstyle );
270 		ShowWindow( hwnd, SW_SHOWNA );
271 	}
272 #endif
273 
274 	// "reset" window position in z-order.
275 	// if the window is supposed to be topmost but is really not:
276 	// set HWND_NOTOPMOST followed by HWND_TOPMOST
277 	// the above sequence is key: review test results in 587#c17
278 	//
279 	// if the window is not supposed to be topmost but is, reverse:
280 	// set HWND_TOPMOST followed by HWND_NOTOPMOST
281 	// the reverse is currently unproven.
282 	// i don't know of any problems removing the topmost style.
283 	if( IsWindow( hwnd ) && ( reset || reset_window_always ) )
284 	{
285 		SetWindowPos( hwnd, !topmost ? HWND_TOPMOST : HWND_NOTOPMOST,
286 			0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE );
287 		SetWindowPos( hwnd, topmost ? HWND_TOPMOST : HWND_NOTOPMOST,
288 			0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE );
289 	}
290 
291 	return;
292 }
293 
294 
295 // Bug 587 -  Vista: Workrave not modal / coming to front
296 // http://issues.workrave.org/show_bug.cgi?id=587
297 // There is an issue with IME and window z-ordering.
298 //
299 // ResetWindow() tests sufficient. This code is for troubleshooting.
300 // if all else fails request user enable advanced/ime_magic = "1"
301 void
IMEWindowMagic(HWND hwnd)302 W32Compat::IMEWindowMagic( HWND hwnd )
303 {
304 	init();
305 
306 	if( !IsWindow( hwnd ) )
307 		return;
308 
309 	// This message works to make hwnd topmost without activation or focus.
310 	// I found it by watching window messages. I don't know its intended use.
311 	SendMessage( hwnd, 0x287, 0x17/*0x18*/, (LPARAM)hwnd );
312 
313 	SetWindowPos( hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOOWNERZORDER |
314 		SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE );
315 }
316 
317 
318 
319 /* W32Compat::IsOurWinStationConnected()
320 
321 Check if our winstation is connected by querying terminal services.
322 
323 Note: Terminal services API can cause a harmless exception that it handles and should be ignored:
324 First-chance exception at 0x7656fc56 in workrave.exe: 0x000006BA: The RPC server is unavailable.
325 
326 returns true if our winstation is connected
327 */
IsOurWinStationConnected()328 bool W32Compat::IsOurWinStationConnected()
329 {
330 	init();
331 
332     bool func_retval = false;
333     DWORD bytes_returned = 0;
334 
335 #ifdef PLATFORM_OS_WIN32_NATIVE
336     enum WTS_INFO_CLASS *state = NULL;
337 #else
338     // TODO: check compiler warnings and native/mingw difference.
339     WTS_INFO_CLASS *state = NULL;
340 #endif
341 
342     if( WTSQuerySessionInformation(
343             WTS_CURRENT_SERVER_HANDLE,
344             WTS_CURRENT_SESSION,
345             WTSConnectState,
346             reinterpret_cast<LPTSTR*>(&state),
347             &bytes_returned
348             )
349             && state
350         )
351     {
352         if( ( bytes_returned == sizeof( *state ) )
353             && ( ( *state == WTSActive )
354                 || ( *state == WTSConnected ) )
355             )
356         {
357             func_retval = true;
358         }
359 
360         WTSFreeMemory( state );
361     }
362 
363     return func_retval;
364 }
365 
366 
367 
368 /* W32Compat::IsOurWinStationLocked()
369 
370 Check if our winstation is locked by querying terminal services.
371 
372 Note: Terminal services API can cause a harmless exception that it handles and should be ignored:
373 First-chance exception at 0x7656fc56 in workrave.exe: 0x000006BA: The RPC server is unavailable.
374 
375 returns true if our winstation is locked
376 */
IsOurWinStationLocked()377 bool W32Compat::IsOurWinStationLocked()
378 {
379 	init();
380 
381     BOOL locked = FALSE;
382     DWORD bytes_returned = 0;
383 
384     if( !WinStationQueryInformationW(
385             WTS_CURRENT_SERVER_HANDLE,   // SERVERNAME_CURRENT
386             WTS_CURRENT_SESSION,   // LOGONID_CURRENT
387             28,   // WinStationLockedState
388             &locked,
389             sizeof( locked ),
390             &bytes_returned
391             )
392         || ( bytes_returned != sizeof( locked ) )
393         )
394     {
395         return false;
396     }
397 
398     return !!locked;
399 }
400 
401 
402 
403 /* W32Compat::IsOurDesktopVisible()
404 
405 Check if our desktop can be viewed by the user: Check that our process' window station is connected
406 and its input desktop is the same as our calling thread's desktop.
407 
408 If our desktop is visible that implies that this process' workstation is unlocked.
409 
410 returns true if our desktop is visible
411 */
IsOurDesktopVisible()412 bool W32Compat::IsOurDesktopVisible()
413 {
414 	init();
415 
416     bool func_retval = false;
417 
418     HDESK input_desktop_handle = NULL;
419     HDESK our_desktop_handle = NULL;
420 
421     wchar_t input_desktop_name[ MAX_PATH ] = { L'\0', };
422     wchar_t our_desktop_name[ MAX_PATH ] = { L'\0', };
423 
424     BOOL ret = 0;
425     DWORD bytes_needed = 0;
426 
427 
428     /*
429     Get the input desktop name
430     */
431     input_desktop_handle = OpenInputDesktop( 0, false, GENERIC_READ );
432     if( !input_desktop_handle )
433         goto cleanup;
434 
435     bytes_needed = 0;
436     ret = GetUserObjectInformationW(
437         input_desktop_handle,
438         UOI_NAME,
439         input_desktop_name,
440         sizeof( input_desktop_name ),
441         &bytes_needed
442         );
443 
444     if( !ret || ( bytes_needed > sizeof( input_desktop_name ) ) )
445         goto cleanup;
446 
447 
448     /*
449     Get our calling thread's desktop name
450     */
451     our_desktop_handle = GetThreadDesktop( GetCurrentThreadId() );
452     if( !our_desktop_handle )
453         goto cleanup;
454 
455     bytes_needed = 0;
456     ret = GetUserObjectInformationW(
457         our_desktop_handle,
458         UOI_NAME,
459         our_desktop_name,
460         sizeof( our_desktop_name ),
461         &bytes_needed
462         );
463     if( !ret || ( bytes_needed > sizeof( our_desktop_name ) ) )
464         goto cleanup;
465 
466 
467     // If the desktop names are different then our thread is not associated with the input desktop
468     if( _wcsnicmp( input_desktop_name, our_desktop_name, MAX_PATH ) )
469         goto cleanup;
470 
471 
472     // If our winstation is not connected then our desktop is not visible
473     if( !IsOurWinStationConnected() )
474         goto cleanup;
475 
476 
477     func_retval = true;
478 cleanup:
479     if( input_desktop_handle )
480         CloseHandle( input_desktop_handle );
481 
482     if( our_desktop_handle )
483         CloseHandle( our_desktop_handle );
484 
485     return func_retval;
486 }
487 
488 
489 
490 /* W32Compat::RefreshBreakWindow()
491 
492 Refresh a BreakWindow:
493 - Make keyboard shortcuts available without pressing ALT after five seconds of inactivity
494 - Set our window topmost unless a tooltip is visible (tooltips are topmost when visible)
495 - Make sure the window manager has not disabled our topmost status; reset if necessary
496 - Force focus to the main break window if the preference advanced/force_focus is true
497 */
RefreshBreakWindow(BreakWindow & window)498 void W32Compat::RefreshBreakWindow( BreakWindow &window )
499 {
500     ICore *core = CoreFactory::get_core();
501     bool user_active = core->is_user_active();
502 
503     // GTK keyboard shortcuts can be accessed by using the ALT key. This appear
504     // to be non-standard behaviour on windows, so make shortcuts available
505     // without ALT after the user is idle for 5s
506     if ( !user_active && !window.accel_added )
507     {
508         IBreak *b = core->get_break( BreakId( window.break_id ) );
509         assert( b != NULL );
510 
511         //TRACE_MSG(b->get_elapsed_idle_time());
512         if( b->get_elapsed_idle_time() > 5 )
513         {
514             if( window.postpone_button != NULL )
515             {
516                 GtkUtil::update_mnemonic( window.postpone_button, window.accel_group );
517             }
518             if( window.skip_button != NULL )
519             {
520                 GtkUtil::update_mnemonic( window.skip_button, window.accel_group );
521             }
522             // FIXME:
523             // if( window.shutdown_button != NULL )
524             // {
525             //     GtkUtil::update_mnemonic( window.shutdown_button, window.accel_group );
526             // }
527             // if( window.lock_button != NULL )
528             // {
529             //     GtkUtil::update_mnemonic( window.lock_button, window.accel_group );
530             // }
531             window.accel_added = true;
532         }
533     }
534 
535     /* We can't call WindowHints::set_always_on_top() or W32Compat::SetWindowOnTop() for every
536     refresh. While the logic here is similar it is adjusted for the specific case of refreshing.
537     */
538 
539     HWND hwnd = (HWND)GDK_WINDOW_HWND( window.Gtk::Widget::gobj()->window );
540     if( !hwnd )
541         return;
542 
543     // don't enforce topmost while a tooltip is visible, otherwise we could cover the tooltip
544     if( !GtkUtil::get_visible_tooltip_window() )
545         SetWindowPos( hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE );
546 
547     // this checks if the window manager has disabled our topmost ability and resets it if necessary
548     W32Compat::ResetWindow( hwnd, true );
549 
550     /* If there are multiple break windows only force focus on the first, otherwise focus would be
551     continuously switched to each break window on every refresh, making interaction very difficult.
552     */
553     if( W32ForceFocus::GetForceFocusValue() && ( window.head.count == 0 ) )
554     {
555         W32ForceFocus::ForceWindowFocus( hwnd, 0 );   // try without blocking
556     }
557 }
558