[OSK] Restore the previous window coordination
[reactos.git] / base / applications / osk / main.c
1 /*
2 * PROJECT: ReactOS On-Screen Keyboard
3 * LICENSE: GPL - See COPYING in the top level directory
4 * PURPOSE: On-screen keyboard.
5 * COPYRIGHT: Denis ROBERT
6 * Copyright 2019 Bișoc George (fraizeraust99 at gmail dot com)
7 */
8
9 /* INCLUDES *******************************************************************/
10
11 #include "osk.h"
12 #include "settings.h"
13
14 /* GLOBALS ********************************************************************/
15
16 OSK_GLOBALS Globals;
17
18 /* Functions */
19 int OSK_SetImage(int IdDlgItem, int IdResource);
20 int OSK_DlgInitDialog(HWND hDlg);
21 int OSK_DlgClose(void);
22 int OSK_DlgTimer(void);
23 BOOL OSK_DlgCommand(WPARAM wCommand, HWND hWndControl);
24 BOOL OSK_ReleaseKey(WORD ScanCode);
25
26 INT_PTR APIENTRY OSK_DlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam);
27 int WINAPI wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int);
28
29 /* FUNCTIONS ******************************************************************/
30
31 /***********************************************************************
32 *
33 * OSK_SetImage
34 *
35 * Set an image on a button
36 */
37 int OSK_SetImage(int IdDlgItem, int IdResource)
38 {
39 HICON hIcon;
40 HWND hWndItem;
41
42 hIcon = (HICON)LoadImageW(Globals.hInstance, MAKEINTRESOURCEW(IdResource),
43 IMAGE_ICON, 16, 16, LR_DEFAULTCOLOR);
44 if (hIcon == NULL)
45 return FALSE;
46
47 hWndItem = GetDlgItem(Globals.hMainWnd, IdDlgItem);
48 if (hWndItem == NULL)
49 {
50 DestroyIcon(hIcon);
51 return FALSE;
52 }
53
54 SendMessageW(hWndItem, BM_SETIMAGE, (WPARAM)IMAGE_ICON, (LPARAM)hIcon);
55
56 /* The system automatically deletes these resources when the process that created them terminates (MSDN) */
57
58 return TRUE;
59 }
60
61 /***********************************************************************
62 *
63 * OSK_WarningProc
64 *
65 * Function handler for the warning dialog box on startup
66 */
67 INT_PTR CALLBACK OSK_WarningProc(HWND hDlg, UINT Msg, WPARAM wParam, LPARAM lParam)
68 {
69 UNREFERENCED_PARAMETER(lParam);
70
71 switch (Msg)
72 {
73 case WM_INITDIALOG:
74 {
75 return TRUE;
76 }
77
78 case WM_COMMAND:
79 {
80 switch (LOWORD(wParam))
81 {
82 case IDC_SHOWWARNINGCHECK:
83 {
84 Globals.bShowWarning = !IsDlgButtonChecked(hDlg, IDC_SHOWWARNINGCHECK);
85 return TRUE;
86 }
87
88 case IDOK:
89 case IDCANCEL:
90 {
91 EndDialog(hDlg, LOWORD(wParam));
92 return TRUE;
93 }
94 }
95 break;
96 }
97 }
98
99 return FALSE;
100 }
101
102 /***********************************************************************
103 *
104 * OSK_About
105 *
106 * Initializes the "About" dialog box
107 */
108 VOID OSK_About(VOID)
109 {
110 WCHAR szTitle[MAX_BUFF];
111 WCHAR szAuthors[MAX_BUFF];
112 HICON OSKIcon;
113
114 /* Load the icon */
115 OSKIcon = LoadImageW(Globals.hInstance, MAKEINTRESOURCEW(IDI_OSK), IMAGE_ICON, 0, 0, LR_DEFAULTSIZE);
116
117 /* Load the strings into the "About" dialog */
118 LoadStringW(Globals.hInstance, STRING_OSK, szTitle, countof(szTitle));
119 LoadStringW(Globals.hInstance, STRING_AUTHORS, szAuthors, countof(szAuthors));
120
121 /* Finally, execute the "About" dialog by using the Shell routine */
122 ShellAboutW(Globals.hMainWnd, szTitle, szAuthors, OSKIcon);
123
124 /* Once done, destroy the icon */
125 DestroyIcon(OSKIcon);
126 }
127
128
129 /***********************************************************************
130 *
131 * OSK_DlgInitDialog
132 *
133 * Handling of WM_INITDIALOG
134 */
135 int OSK_DlgInitDialog(HWND hDlg)
136 {
137 HICON hIcon, hIconSm;
138 HMONITOR monitor;
139 MONITORINFO info;
140 POINT Pt;
141 RECT rcWindow, rcDlgIntersect;
142
143 /* Save handle */
144 Globals.hMainWnd = hDlg;
145
146 /* Check the checked menu item before displaying the modal box */
147 if (Globals.bIsEnhancedKeyboard)
148 {
149 /* Enhanced keyboard dialog chosen, set the respective menu item as checked */
150 CheckMenuItem(GetMenu(hDlg), IDM_ENHANCED_KB, MF_BYCOMMAND | MF_CHECKED);
151 CheckMenuItem(GetMenu(hDlg), IDM_STANDARD_KB, MF_BYCOMMAND | MF_UNCHECKED);
152 }
153 else
154 {
155 /* Standard keyboard dialog chosen, set the respective menu item as checked */
156 CheckMenuItem(GetMenu(hDlg), IDM_STANDARD_KB, MF_BYCOMMAND | MF_CHECKED);
157 CheckMenuItem(GetMenu(hDlg), IDM_ENHANCED_KB, MF_BYCOMMAND | MF_UNCHECKED);
158 }
159
160 /* Check if the "Click Sound" option was chosen before (and if so, then tick the menu item) */
161 if (Globals.bSoundClick)
162 {
163 CheckMenuItem(GetMenu(hDlg), IDM_CLICK_SOUND, MF_BYCOMMAND | MF_CHECKED);
164 }
165
166 /* Set the application's icon */
167 hIcon = LoadImageW(Globals.hInstance, MAKEINTRESOURCEW(IDI_OSK), IMAGE_ICON, 0, 0, LR_SHARED | LR_DEFAULTSIZE);
168 hIconSm = CopyImage(hIcon, IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_COPYFROMRESOURCE);
169 if (hIcon || hIconSm)
170 {
171 /* Set the window icons (they are deleted when the process terminates) */
172 SendMessageW(Globals.hMainWnd, WM_SETICON, ICON_BIG, (LPARAM)hIcon);
173 SendMessageW(Globals.hMainWnd, WM_SETICON, ICON_SMALL, (LPARAM)hIconSm);
174 }
175
176 /* Get screen info */
177 memset(&Pt, 0, sizeof(Pt));
178 monitor = MonitorFromPoint(Pt, MONITOR_DEFAULTTOPRIMARY);
179 info.cbSize = sizeof(info);
180 GetMonitorInfoW(monitor, &info);
181 GetWindowRect(hDlg, &rcWindow);
182
183 /*
184 If the coordination values are default then re-initialize using the specific formulas
185 to move the dialog at the bottom of the screen.
186 */
187 if (Globals.PosX == CW_USEDEFAULT && Globals.PosY == CW_USEDEFAULT)
188 {
189 Globals.PosX = (info.rcMonitor.left + info.rcMonitor.right - (rcWindow.right - rcWindow.left)) / 2;
190 Globals.PosY = info.rcMonitor.bottom - (rcWindow.bottom - rcWindow.top);
191 }
192
193 /*
194 Calculate the intersection of two rectangle sources (dialog and work desktop area).
195 If such sources do not intersect, then the dialog is deemed as "off screen".
196 */
197 if (IntersectRect(&rcDlgIntersect, &rcWindow, &info.rcWork) == 0)
198 {
199 Globals.PosX = (info.rcMonitor.left + info.rcMonitor.right - (rcWindow.right - rcWindow.left)) / 2;
200 Globals.PosY = info.rcMonitor.bottom - (rcWindow.bottom - rcWindow.top);
201 }
202 else
203 {
204 /*
205 There's still some intersection but we're not for sure if it is sufficient (the dialog could also be partially hidden).
206 Therefore, check the remaining intersection if it's enough.
207 */
208 if (rcWindow.top < info.rcWork.top || rcWindow.left < info.rcWork.left || rcWindow.right > info.rcWork.right || rcWindow.bottom > info.rcWork.bottom)
209 {
210 Globals.PosX = (info.rcMonitor.left + info.rcMonitor.right - (rcWindow.right - rcWindow.left)) / 2;
211 Globals.PosY = info.rcMonitor.bottom - (rcWindow.bottom - rcWindow.top);
212 }
213 }
214
215 /* Move the dialog according to the placement coordination */
216 SetWindowPos(hDlg, HWND_TOP, Globals.PosX, Globals.PosY, 0, 0, SWP_NOSIZE);
217
218 /* Set icon on visual buttons */
219 OSK_SetImage(SCAN_CODE_15, IDI_BACK);
220 OSK_SetImage(SCAN_CODE_16, IDI_TAB);
221 OSK_SetImage(SCAN_CODE_30, IDI_CAPS_LOCK);
222 OSK_SetImage(SCAN_CODE_43, IDI_RETURN);
223 OSK_SetImage(SCAN_CODE_44, IDI_SHIFT);
224 OSK_SetImage(SCAN_CODE_57, IDI_SHIFT);
225 OSK_SetImage(SCAN_CODE_127, IDI_REACTOS);
226 OSK_SetImage(SCAN_CODE_128, IDI_REACTOS);
227 OSK_SetImage(SCAN_CODE_129, IDI_MENU);
228 OSK_SetImage(SCAN_CODE_80, IDI_HOME);
229 OSK_SetImage(SCAN_CODE_85, IDI_PG_UP);
230 OSK_SetImage(SCAN_CODE_86, IDI_PG_DOWN);
231 OSK_SetImage(SCAN_CODE_79, IDI_LEFT);
232 OSK_SetImage(SCAN_CODE_83, IDI_TOP);
233 OSK_SetImage(SCAN_CODE_84, IDI_BOTTOM);
234 OSK_SetImage(SCAN_CODE_89, IDI_RIGHT);
235
236 /* Create a green brush for leds */
237 Globals.hBrushGreenLed = CreateSolidBrush(RGB(0, 255, 0));
238
239 /* Set a timer for periodics tasks */
240 Globals.iTimer = SetTimer(hDlg, 0, 200, NULL);
241
242 return TRUE;
243 }
244
245 /***********************************************************************
246 *
247 * OSK_DlgClose
248 *
249 * Handling of WM_CLOSE
250 */
251 int OSK_DlgClose(void)
252 {
253 KillTimer(Globals.hMainWnd, Globals.iTimer);
254
255 /* Release Ctrl, Shift, Alt keys */
256 OSK_ReleaseKey(SCAN_CODE_44); // Left shift
257 OSK_ReleaseKey(SCAN_CODE_57); // Right shift
258 OSK_ReleaseKey(SCAN_CODE_58); // Left ctrl
259 OSK_ReleaseKey(SCAN_CODE_60); // Left alt
260 OSK_ReleaseKey(SCAN_CODE_62); // Right alt
261 OSK_ReleaseKey(SCAN_CODE_64); // Right ctrl
262
263 /* delete GDI objects */
264 if (Globals.hBrushGreenLed) DeleteObject(Globals.hBrushGreenLed);
265
266 /* Save the settings to the registry hive */
267 SaveDataToRegistry();
268
269 return TRUE;
270 }
271
272 /***********************************************************************
273 *
274 * OSK_DlgTimer
275 *
276 * Handling of WM_TIMER
277 */
278 int OSK_DlgTimer(void)
279 {
280 /* FIXME: To be deleted when ReactOS will support WS_EX_NOACTIVATE */
281 HWND hWndActiveWindow;
282
283 hWndActiveWindow = GetForegroundWindow();
284 if (hWndActiveWindow != NULL && hWndActiveWindow != Globals.hMainWnd)
285 {
286 Globals.hActiveWnd = hWndActiveWindow;
287 }
288
289 /* Always redraw leds because it can be changed by the real keyboard) */
290 InvalidateRect(GetDlgItem(Globals.hMainWnd, IDC_LED_NUM), NULL, TRUE);
291 InvalidateRect(GetDlgItem(Globals.hMainWnd, IDC_LED_CAPS), NULL, TRUE);
292 InvalidateRect(GetDlgItem(Globals.hMainWnd, IDC_LED_SCROLL), NULL, TRUE);
293
294 return TRUE;
295 }
296
297 /***********************************************************************
298 *
299 * OSK_DlgCommand
300 *
301 * All handling of dialog command
302 */
303 BOOL OSK_DlgCommand(WPARAM wCommand, HWND hWndControl)
304 {
305 WORD ScanCode;
306 INPUT Input;
307 BOOL bExtendedKey;
308 BOOL bKeyDown;
309 BOOL bKeyUp;
310 LONG WindowStyle;
311
312 /* FIXME: To be deleted when ReactOS will support WS_EX_NOACTIVATE */
313 if (Globals.hActiveWnd)
314 {
315 MSG msg;
316
317 SetForegroundWindow(Globals.hActiveWnd);
318 while (PeekMessageW(&msg, 0, 0, 0, PM_REMOVE))
319 {
320 TranslateMessage(&msg);
321 DispatchMessageW(&msg);
322 }
323 }
324
325 /* KeyDown and/or KeyUp ? */
326 WindowStyle = GetWindowLongW(hWndControl, GWL_STYLE);
327 if ((WindowStyle & BS_AUTOCHECKBOX) == BS_AUTOCHECKBOX)
328 {
329 /* 2-states key like Shift, Alt, Ctrl, ... */
330 if (SendMessageW(hWndControl, BM_GETCHECK, 0, 0) == BST_CHECKED)
331 {
332 bKeyDown = TRUE;
333 bKeyUp = FALSE;
334 }
335 else
336 {
337 bKeyDown = FALSE;
338 bKeyUp = TRUE;
339 }
340 }
341 else
342 {
343 /* Other key */
344 bKeyDown = TRUE;
345 bKeyUp = TRUE;
346 }
347
348 /* Extended key ? */
349 ScanCode = wCommand;
350 if (ScanCode & 0x0200)
351 bExtendedKey = TRUE;
352 else
353 bExtendedKey = FALSE;
354 ScanCode &= 0xFF;
355
356 /* Press and release the key */
357 if (bKeyDown)
358 {
359 Input.type = INPUT_KEYBOARD;
360 Input.ki.wVk = 0;
361 Input.ki.wScan = ScanCode;
362 Input.ki.time = GetTickCount();
363 Input.ki.dwExtraInfo = GetMessageExtraInfo();
364 Input.ki.dwFlags = KEYEVENTF_SCANCODE;
365 if (bExtendedKey) Input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
366 SendInput(1, &Input, sizeof(Input));
367 }
368
369 if (bKeyUp)
370 {
371 Input.type = INPUT_KEYBOARD;
372 Input.ki.wVk = 0;
373 Input.ki.wScan = ScanCode;
374 Input.ki.time = GetTickCount();
375 Input.ki.dwExtraInfo = GetMessageExtraInfo();
376 Input.ki.dwFlags = KEYEVENTF_SCANCODE | KEYEVENTF_KEYUP;
377 if (bExtendedKey) Input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
378 SendInput(1, &Input, sizeof(Input));
379 }
380
381 /* Play the sound during clicking event (only if "Use Click Sound" menu option is ticked) */
382 if (Globals.bSoundClick)
383 {
384 PlaySoundW(MAKEINTRESOURCEW(IDI_SOUNDCLICK), GetModuleHandle(NULL), SND_RESOURCE | SND_ASYNC);
385 }
386
387 return TRUE;
388 }
389
390 /***********************************************************************
391 *
392 * OSK_ReleaseKey
393 *
394 * Release the key of ID wCommand
395 */
396 BOOL OSK_ReleaseKey(WORD ScanCode)
397 {
398 INPUT Input;
399 BOOL bExtendedKey;
400 LONG WindowStyle;
401 HWND hWndControl;
402
403 /* Is it a 2-states key ? */
404 hWndControl = GetDlgItem(Globals.hMainWnd, ScanCode);
405 WindowStyle = GetWindowLongW(hWndControl, GWL_STYLE);
406 if ((WindowStyle & BS_AUTOCHECKBOX) != BS_AUTOCHECKBOX) return FALSE;
407
408 /* Is the key down ? */
409 if (SendMessageW(hWndControl, BM_GETCHECK, 0, 0) != BST_CHECKED) return TRUE;
410
411 /* Extended key ? */
412 if (ScanCode & 0x0200)
413 bExtendedKey = TRUE;
414 else
415 bExtendedKey = FALSE;
416 ScanCode &= 0xFF;
417
418 /* Release the key */
419 Input.type = INPUT_KEYBOARD;
420 Input.ki.wVk = 0;
421 Input.ki.wScan = ScanCode;
422 Input.ki.time = GetTickCount();
423 Input.ki.dwExtraInfo = GetMessageExtraInfo();
424 Input.ki.dwFlags = KEYEVENTF_SCANCODE | KEYEVENTF_KEYUP;
425 if (bExtendedKey) Input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
426 SendInput(1, &Input, sizeof(Input));
427
428 return TRUE;
429 }
430
431 /***********************************************************************
432 *
433 * OSK_DlgProc
434 */
435 INT_PTR APIENTRY OSK_DlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
436 {
437 switch (msg)
438 {
439 case WM_INITDIALOG:
440 OSK_DlgInitDialog(hDlg);
441 return TRUE;
442
443 case WM_TIMER:
444 OSK_DlgTimer();
445 return TRUE;
446
447 case WM_CTLCOLORSTATIC:
448 if ((HWND)lParam == GetDlgItem(hDlg, IDC_LED_NUM))
449 {
450 if (GetKeyState(VK_NUMLOCK) & 0x0001)
451 return (INT_PTR)Globals.hBrushGreenLed;
452 else
453 return (INT_PTR)GetStockObject(BLACK_BRUSH);
454 }
455 if ((HWND)lParam == GetDlgItem(hDlg, IDC_LED_CAPS))
456 {
457 if (GetKeyState(VK_CAPITAL) & 0x0001)
458 return (INT_PTR)Globals.hBrushGreenLed;
459 else
460 return (INT_PTR)GetStockObject(BLACK_BRUSH);
461 }
462 if ((HWND)lParam == GetDlgItem(hDlg, IDC_LED_SCROLL))
463 {
464 if (GetKeyState(VK_SCROLL) & 0x0001)
465 return (INT_PTR)Globals.hBrushGreenLed;
466 else
467 return (INT_PTR)GetStockObject(BLACK_BRUSH);
468 }
469 break;
470
471 case WM_COMMAND:
472 switch (LOWORD(wParam))
473 {
474 case IDCANCEL:
475 {
476 EndDialog(hDlg, FALSE);
477 break;
478 }
479
480 case IDM_EXIT:
481 {
482 EndDialog(hDlg, FALSE);
483 break;
484 }
485
486 case IDM_ENHANCED_KB:
487 {
488 if (!Globals.bIsEnhancedKeyboard)
489 {
490 /*
491 The user attempted to switch to enhanced keyboard dialog type.
492 Set the member value as TRUE, destroy the dialog and save the data configuration into the registry.
493 */
494 Globals.bIsEnhancedKeyboard = TRUE;
495 EndDialog(hDlg, FALSE);
496 SaveDataToRegistry();
497
498 /* Change the condition of enhanced keyboard item menu to checked */
499 CheckMenuItem(GetMenu(hDlg), IDM_ENHANCED_KB, MF_BYCOMMAND | MF_CHECKED);
500 CheckMenuItem(GetMenu(hDlg), IDM_STANDARD_KB, MF_BYCOMMAND | MF_UNCHECKED);
501
502 /* Finally, display the dialog modal box with the enhanced keyboard dialog */
503 DialogBoxW(Globals.hInstance,
504 MAKEINTRESOURCEW(MAIN_DIALOG_ENHANCED_KB),
505 GetDesktopWindow(),
506 OSK_DlgProc);
507 }
508
509 break;
510 }
511
512 case IDM_STANDARD_KB:
513 {
514 if (Globals.bIsEnhancedKeyboard)
515 {
516 /*
517 The user attempted to switch to standard keyboard dialog type.
518 Set the member value as FALSE, destroy the dialog and save the data configuration into the registry.
519 */
520 Globals.bIsEnhancedKeyboard = FALSE;
521 EndDialog(hDlg, FALSE);
522 SaveDataToRegistry();
523
524 /* Change the condition of standard keyboard item menu to checked */
525 CheckMenuItem(GetMenu(hDlg), IDM_ENHANCED_KB, MF_BYCOMMAND | MF_UNCHECKED);
526 CheckMenuItem(GetMenu(hDlg), IDM_STANDARD_KB, MF_BYCOMMAND | MF_CHECKED);
527
528 /* Finally, display the dialog modal box with the standard keyboard dialog */
529 DialogBoxW(Globals.hInstance,
530 MAKEINTRESOURCEW(MAIN_DIALOG_STANDARD_KB),
531 GetDesktopWindow(),
532 OSK_DlgProc);
533 }
534
535 break;
536 }
537
538 case IDM_CLICK_SOUND:
539 {
540 /*
541 This case is triggered when the user attempts to click on the menu item. Before doing anything,
542 we must check the condition state of such menu item so that we can tick/untick the menu item accordingly.
543 */
544 if (!Globals.bSoundClick)
545 {
546 Globals.bSoundClick = TRUE;
547 CheckMenuItem(GetMenu(hDlg), IDM_CLICK_SOUND, MF_BYCOMMAND | MF_CHECKED);
548 }
549 else
550 {
551 Globals.bSoundClick = FALSE;
552 CheckMenuItem(GetMenu(hDlg), IDM_CLICK_SOUND, MF_BYCOMMAND | MF_UNCHECKED);
553 }
554
555 break;
556 }
557
558 case IDM_ABOUT:
559 {
560 OSK_About();
561 break;
562 }
563
564 default:
565 OSK_DlgCommand(wParam, (HWND)lParam);
566 break;
567 }
568 break;
569
570 case WM_CLOSE:
571 OSK_DlgClose();
572 break;
573 }
574
575 return 0;
576 }
577
578 /***********************************************************************
579 *
580 * WinMain
581 */
582 int WINAPI wWinMain(HINSTANCE hInstance,
583 HINSTANCE prev,
584 LPWSTR cmdline,
585 int show)
586 {
587 HANDLE hMutex;
588 INT LayoutResource;
589
590 UNREFERENCED_PARAMETER(prev);
591 UNREFERENCED_PARAMETER(cmdline);
592 UNREFERENCED_PARAMETER(show);
593
594 ZeroMemory(&Globals, sizeof(Globals));
595 Globals.hInstance = hInstance;
596
597 /* Load the settings from the registry hive */
598 LoadDataFromRegistry();
599
600 /* If the member of the struct (bShowWarning) is set then display the dialog box */
601 if (Globals.bShowWarning)
602 {
603 DialogBoxW(Globals.hInstance, MAKEINTRESOURCEW(IDD_WARNINGDIALOG_OSK), Globals.hMainWnd, OSK_WarningProc);
604 }
605
606 /* Before initializing the dialog execution, check if the chosen keyboard type is standard or enhanced */
607 if (Globals.bIsEnhancedKeyboard)
608 {
609 LayoutResource = MAIN_DIALOG_ENHANCED_KB;
610 }
611 else
612 {
613 LayoutResource = MAIN_DIALOG_STANDARD_KB;
614 }
615
616 /* Rry to open a mutex for a single instance */
617 hMutex = OpenMutexW(MUTEX_ALL_ACCESS, FALSE, L"osk");
618
619 if (!hMutex)
620 {
621 /* Mutex doesn't exist. This is the first instance so create the mutex. */
622 hMutex = CreateMutexW(NULL, FALSE, L"osk");
623
624 /* Create the modal box based on the configuration registry */
625 DialogBoxW(hInstance,
626 MAKEINTRESOURCEW(LayoutResource),
627 GetDesktopWindow(),
628 OSK_DlgProc);
629
630 /* Delete the mutex */
631 if (hMutex) CloseHandle(hMutex);
632 }
633 else
634 {
635 /* Programme already launched */
636
637 /* Delete the mutex */
638 CloseHandle(hMutex);
639
640 ExitProcess(0);
641 }
642
643 return 0;
644 }
645
646 /* EOF */