Commit | Line | Data |
---|---|---|
a09e091a JB |
1 | /* |
2 | *Copyright (C) 1994-2000 The XFree86 Project, Inc. All Rights Reserved. | |
3 | * | |
4 | *Permission is hereby granted, free of charge, to any person obtaining | |
5 | * a copy of this software and associated documentation files (the | |
6 | *"Software"), to deal in the Software without restriction, including | |
7 | *without limitation the rights to use, copy, modify, merge, publish, | |
8 | *distribute, sublicense, and/or sell copies of the Software, and to | |
9 | *permit persons to whom the Software is furnished to do so, subject to | |
10 | *the following conditions: | |
11 | * | |
12 | *The above copyright notice and this permission notice shall be | |
13 | *included in all copies or substantial portions of the Software. | |
14 | * | |
15 | *THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
16 | *EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
17 | *MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
18 | *NONINFRINGEMENT. IN NO EVENT SHALL THE XFREE86 PROJECT BE LIABLE FOR | |
19 | *ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF | |
20 | *CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |
21 | *WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
22 | * | |
23 | *Except as contained in this notice, the name of the XFree86 Project | |
24 | *shall not be used in advertising or otherwise to promote the sale, use | |
25 | *or other dealings in this Software without prior written authorization | |
26 | *from the XFree86 Project. | |
27 | * | |
28 | * Authors: Dakshinamurthy Karra | |
29 | * Suhaib M Siddiqi | |
30 | * Peter Busch | |
31 | * Harold L Hunt II | |
32 | */ | |
33 | ||
34 | #ifdef HAVE_XWIN_CONFIG_H | |
35 | #include <xwin-config.h> | |
36 | #endif | |
37 | #include "win.h" | |
38 | #include "winkeybd.h" | |
39 | #include "winconfig.h" | |
40 | #include "winmsg.h" | |
41 | ||
42 | #include "xkbsrv.h" | |
43 | ||
44 | /* C does not have a logical XOR operator, so we use a macro instead */ | |
45 | #define LOGICAL_XOR(a,b) ((!(a) && (b)) || ((a) && !(b))) | |
46 | ||
47 | static Bool g_winKeyState[NUM_KEYCODES]; | |
48 | ||
49 | /* | |
50 | * Local prototypes | |
51 | */ | |
52 | ||
53 | static void | |
54 | winKeybdBell(int iPercent, DeviceIntPtr pDeviceInt, pointer pCtrl, int iClass); | |
55 | ||
56 | static void | |
57 | winKeybdCtrl(DeviceIntPtr pDevice, KeybdCtrl * pCtrl); | |
58 | ||
59 | /* | |
60 | * Translate a Windows WM_[SYS]KEY(UP/DOWN) message | |
61 | * into an ASCII scan code. | |
62 | * | |
63 | * We do this ourselves, rather than letting Windows handle it, | |
64 | * because Windows tends to munge the handling of special keys, | |
65 | * like AltGr on European keyboards. | |
66 | */ | |
67 | ||
68 | int | |
69 | winTranslateKey(WPARAM wParam, LPARAM lParam) | |
70 | { | |
71 | int iKeyFixup = g_iKeyMap[wParam * WIN_KEYMAP_COLS + 1]; | |
72 | int iKeyFixupEx = g_iKeyMap[wParam * WIN_KEYMAP_COLS + 2]; | |
73 | int iParam = HIWORD(lParam); | |
74 | int iParamScanCode = LOBYTE(iParam); | |
75 | int iScanCode; | |
76 | ||
77 | winDebug("winTranslateKey: wParam %08x lParam %08x\n", wParam, lParam); | |
78 | ||
79 | /* WM_ key messages faked by Vista speech recognition (WSR) don't have a | |
80 | * scan code. | |
81 | * | |
82 | * Vocola 3 (Rick Mohr's supplement to WSR) uses | |
83 | * System.Windows.Forms.SendKeys.SendWait(), which appears always to give a | |
84 | * scan code of 1 | |
85 | */ | |
86 | if (iParamScanCode <= 1) { | |
87 | if (VK_PRIOR <= wParam && wParam <= VK_DOWN) | |
88 | /* Trigger special case table to translate to extended | |
89 | * keycode, otherwise if num_lock is on, we can get keypad | |
90 | * numbers instead of navigation keys. */ | |
91 | iParam |= KF_EXTENDED; | |
92 | else | |
93 | iParamScanCode = MapVirtualKeyEx(wParam, | |
94 | /*MAPVK_VK_TO_VSC */ 0, | |
95 | GetKeyboardLayout(0)); | |
96 | } | |
97 | ||
98 | /* Branch on special extended, special non-extended, or normal key */ | |
99 | if ((iParam & KF_EXTENDED) && iKeyFixupEx) | |
100 | iScanCode = iKeyFixupEx; | |
101 | else if (iKeyFixup) | |
102 | iScanCode = iKeyFixup; | |
103 | else if (wParam == 0 && iParamScanCode == 0x70) | |
104 | iScanCode = KEY_HKTG; | |
105 | else | |
106 | switch (iParamScanCode) { | |
107 | case 0x70: | |
108 | iScanCode = KEY_HKTG; | |
109 | break; | |
110 | case 0x73: | |
111 | iScanCode = KEY_BSlash2; | |
112 | break; | |
113 | default: | |
114 | iScanCode = iParamScanCode; | |
115 | break; | |
116 | } | |
117 | ||
118 | return iScanCode; | |
119 | } | |
120 | ||
121 | /* Ring the keyboard bell (system speaker on PCs) */ | |
122 | static void | |
123 | winKeybdBell(int iPercent, DeviceIntPtr pDeviceInt, pointer pCtrl, int iClass) | |
124 | { | |
125 | /* | |
126 | * We can't use Beep () here because it uses the PC speaker | |
127 | * on NT/2000. MessageBeep (MB_OK) will play the default system | |
128 | * sound on systems with a sound card or it will beep the PC speaker | |
129 | * on systems that do not have a sound card. | |
130 | */ | |
131 | MessageBeep(MB_OK); | |
132 | } | |
133 | ||
134 | /* Change some keyboard configuration parameters */ | |
135 | static void | |
136 | winKeybdCtrl(DeviceIntPtr pDevice, KeybdCtrl * pCtrl) | |
137 | { | |
138 | } | |
139 | ||
140 | /* | |
141 | * See Porting Layer Definition - p. 18 | |
142 | * winKeybdProc is known as a DeviceProc. | |
143 | */ | |
144 | ||
145 | int | |
146 | winKeybdProc(DeviceIntPtr pDeviceInt, int iState) | |
147 | { | |
148 | DevicePtr pDevice = (DevicePtr) pDeviceInt; | |
149 | XkbSrvInfoPtr xkbi; | |
150 | XkbControlsPtr ctrl; | |
151 | ||
152 | switch (iState) { | |
153 | case DEVICE_INIT: | |
154 | winConfigKeyboard(pDeviceInt); | |
155 | ||
156 | /* FIXME: Maybe we should use winGetKbdLeds () here? */ | |
157 | defaultKeyboardControl.leds = g_winInfo.keyboard.leds; | |
158 | ||
159 | winErrorFVerb(2, "Rules = \"%s\" Model = \"%s\" Layout = \"%s\"" | |
160 | " Variant = \"%s\" Options = \"%s\"\n", | |
161 | g_winInfo.xkb.rules ? g_winInfo.xkb.rules : "none", | |
162 | g_winInfo.xkb.model ? g_winInfo.xkb.model : "none", | |
163 | g_winInfo.xkb.layout ? g_winInfo.xkb.layout : "none", | |
164 | g_winInfo.xkb.variant ? g_winInfo.xkb.variant : "none", | |
165 | g_winInfo.xkb.options ? g_winInfo.xkb.options : "none"); | |
166 | ||
167 | InitKeyboardDeviceStruct(pDeviceInt, | |
168 | &g_winInfo.xkb, winKeybdBell, winKeybdCtrl); | |
169 | ||
170 | xkbi = pDeviceInt->key->xkbInfo; | |
171 | if ((xkbi != NULL) && (xkbi->desc != NULL)) { | |
172 | ctrl = xkbi->desc->ctrls; | |
173 | ctrl->repeat_delay = g_winInfo.keyboard.delay; | |
174 | ctrl->repeat_interval = 1000 / g_winInfo.keyboard.rate; | |
175 | } | |
176 | else { | |
177 | winErrorFVerb(1, | |
178 | "winKeybdProc - Error initializing keyboard AutoRepeat\n"); | |
179 | } | |
180 | ||
181 | break; | |
182 | ||
183 | case DEVICE_ON: | |
184 | pDevice->on = TRUE; | |
185 | ||
186 | // immediately copy the state of this keyboard device to the VCK | |
187 | // (which otherwise happens lazily after the first keypress) | |
188 | CopyKeyClass(pDeviceInt, inputInfo.keyboard); | |
189 | break; | |
190 | ||
191 | case DEVICE_CLOSE: | |
192 | case DEVICE_OFF: | |
193 | pDevice->on = FALSE; | |
194 | break; | |
195 | } | |
196 | ||
197 | return Success; | |
198 | } | |
199 | ||
200 | /* | |
201 | * Detect current mode key states upon server startup. | |
202 | * | |
203 | * Simulate a press and release of any key that is currently | |
204 | * toggled. | |
205 | */ | |
206 | ||
207 | void | |
208 | winInitializeModeKeyStates(void) | |
209 | { | |
210 | /* Restore NumLock */ | |
211 | if (GetKeyState(VK_NUMLOCK) & 0x0001) { | |
212 | winSendKeyEvent(KEY_NumLock, TRUE); | |
213 | winSendKeyEvent(KEY_NumLock, FALSE); | |
214 | } | |
215 | ||
216 | /* Restore CapsLock */ | |
217 | if (GetKeyState(VK_CAPITAL) & 0x0001) { | |
218 | winSendKeyEvent(KEY_CapsLock, TRUE); | |
219 | winSendKeyEvent(KEY_CapsLock, FALSE); | |
220 | } | |
221 | ||
222 | /* Restore ScrollLock */ | |
223 | if (GetKeyState(VK_SCROLL) & 0x0001) { | |
224 | winSendKeyEvent(KEY_ScrollLock, TRUE); | |
225 | winSendKeyEvent(KEY_ScrollLock, FALSE); | |
226 | } | |
227 | ||
228 | /* Restore KanaLock */ | |
229 | if (GetKeyState(VK_KANA) & 0x0001) { | |
230 | winSendKeyEvent(KEY_HKTG, TRUE); | |
231 | winSendKeyEvent(KEY_HKTG, FALSE); | |
232 | } | |
233 | } | |
234 | ||
235 | /* | |
236 | * Upon regaining the keyboard focus we must | |
237 | * resynchronize our internal mode key states | |
238 | * with the actual state of the keys. | |
239 | */ | |
240 | ||
241 | void | |
242 | winRestoreModeKeyStates(void) | |
243 | { | |
244 | DWORD dwKeyState; | |
245 | BOOL processEvents = TRUE; | |
246 | unsigned short internalKeyStates; | |
247 | ||
248 | /* X server is being initialized */ | |
249 | if (!inputInfo.keyboard) | |
250 | return; | |
251 | ||
252 | /* Only process events if the rootwindow is mapped. The keyboard events | |
253 | * will cause segfaults otherwise */ | |
254 | if (screenInfo.screens[0]->root && | |
255 | screenInfo.screens[0]->root->mapped == FALSE) | |
256 | processEvents = FALSE; | |
257 | ||
258 | /* Force to process all pending events in the mi event queue */ | |
259 | if (processEvents) | |
260 | mieqProcessInputEvents(); | |
261 | ||
262 | /* Read the mode key states of our X server */ | |
263 | /* (stored in the virtual core keyboard) */ | |
264 | internalKeyStates = | |
265 | XkbStateFieldFromRec(&inputInfo.keyboard->key->xkbInfo->state); | |
266 | winDebug("winRestoreModeKeyStates: state %d\n", internalKeyStates); | |
267 | ||
268 | /* Check if modifier keys are pressed, and if so, fake a press */ | |
269 | { | |
270 | ||
271 | BOOL lctrl = (GetAsyncKeyState(VK_LCONTROL) < 0); | |
272 | BOOL rctrl = (GetAsyncKeyState(VK_RCONTROL) < 0); | |
273 | BOOL lshift = (GetAsyncKeyState(VK_LSHIFT) < 0); | |
274 | BOOL rshift = (GetAsyncKeyState(VK_RSHIFT) < 0); | |
275 | BOOL alt = (GetAsyncKeyState(VK_LMENU) < 0); | |
276 | BOOL altgr = (GetAsyncKeyState(VK_RMENU) < 0); | |
277 | ||
278 | /* | |
279 | If AltGr and CtrlL appear to be pressed, assume the | |
280 | CtrL is a fake one | |
281 | */ | |
282 | if (lctrl && altgr) | |
283 | lctrl = FALSE; | |
284 | ||
285 | if (lctrl) | |
286 | winSendKeyEvent(KEY_LCtrl, TRUE); | |
287 | ||
288 | if (rctrl) | |
289 | winSendKeyEvent(KEY_RCtrl, TRUE); | |
290 | ||
291 | if (lshift) | |
292 | winSendKeyEvent(KEY_ShiftL, TRUE); | |
293 | ||
294 | if (rshift) | |
295 | winSendKeyEvent(KEY_ShiftL, TRUE); | |
296 | ||
297 | if (alt) | |
298 | winSendKeyEvent(KEY_Alt, TRUE); | |
299 | ||
300 | if (altgr) | |
301 | winSendKeyEvent(KEY_AltLang, TRUE); | |
302 | } | |
303 | ||
304 | /* | |
305 | Check if latching modifier key states have changed, and if so, | |
306 | fake a press and a release to toggle the modifier to the correct | |
307 | state | |
308 | */ | |
309 | dwKeyState = GetKeyState(VK_NUMLOCK) & 0x0001; | |
310 | if (LOGICAL_XOR(internalKeyStates & NumLockMask, dwKeyState)) { | |
311 | winSendKeyEvent(KEY_NumLock, TRUE); | |
312 | winSendKeyEvent(KEY_NumLock, FALSE); | |
313 | } | |
314 | ||
315 | dwKeyState = GetKeyState(VK_CAPITAL) & 0x0001; | |
316 | if (LOGICAL_XOR(internalKeyStates & LockMask, dwKeyState)) { | |
317 | winSendKeyEvent(KEY_CapsLock, TRUE); | |
318 | winSendKeyEvent(KEY_CapsLock, FALSE); | |
319 | } | |
320 | ||
321 | dwKeyState = GetKeyState(VK_SCROLL) & 0x0001; | |
322 | if (LOGICAL_XOR(internalKeyStates & ScrollLockMask, dwKeyState)) { | |
323 | winSendKeyEvent(KEY_ScrollLock, TRUE); | |
324 | winSendKeyEvent(KEY_ScrollLock, FALSE); | |
325 | } | |
326 | ||
327 | dwKeyState = GetKeyState(VK_KANA) & 0x0001; | |
328 | if (LOGICAL_XOR(internalKeyStates & KanaMask, dwKeyState)) { | |
329 | winSendKeyEvent(KEY_HKTG, TRUE); | |
330 | winSendKeyEvent(KEY_HKTG, FALSE); | |
331 | } | |
332 | ||
333 | /* | |
334 | For strict correctness, we should also press any non-modifier keys | |
335 | which are already down when we gain focus, but nobody has complained | |
336 | yet :-) | |
337 | */ | |
338 | } | |
339 | ||
340 | /* | |
341 | * Look for the lovely fake Control_L press/release generated by Windows | |
342 | * when AltGr is pressed/released on a non-U.S. keyboard. | |
343 | */ | |
344 | ||
345 | Bool | |
346 | winIsFakeCtrl_L(UINT message, WPARAM wParam, LPARAM lParam) | |
347 | { | |
348 | MSG msgNext; | |
349 | LONG lTime; | |
350 | Bool fReturn; | |
351 | ||
352 | static Bool lastWasControlL = FALSE; | |
353 | static LONG lastTime; | |
354 | ||
355 | /* | |
356 | * Fake Ctrl_L presses will be followed by an Alt_R press | |
357 | * with the same timestamp as the Ctrl_L press. | |
358 | */ | |
359 | if ((message == WM_KEYDOWN || message == WM_SYSKEYDOWN) | |
360 | && wParam == VK_CONTROL && (HIWORD(lParam) & KF_EXTENDED) == 0) { | |
361 | /* Got a Ctrl_L press */ | |
362 | ||
363 | /* Get time of current message */ | |
364 | lTime = GetMessageTime(); | |
365 | ||
366 | /* Look for next press message */ | |
367 | fReturn = PeekMessage(&msgNext, NULL, | |
368 | WM_KEYDOWN, WM_SYSKEYDOWN, PM_NOREMOVE); | |
369 | ||
370 | if (fReturn && msgNext.message != WM_KEYDOWN && | |
371 | msgNext.message != WM_SYSKEYDOWN) | |
372 | fReturn = 0; | |
373 | ||
374 | if (!fReturn) { | |
375 | lastWasControlL = TRUE; | |
376 | lastTime = lTime; | |
377 | } | |
378 | else { | |
379 | lastWasControlL = FALSE; | |
380 | } | |
381 | ||
382 | /* Is next press an Alt_R with the same timestamp? */ | |
383 | if (fReturn && msgNext.wParam == VK_MENU | |
384 | && msgNext.time == lTime | |
385 | && (HIWORD(msgNext.lParam) & KF_EXTENDED)) { | |
386 | /* | |
387 | * Next key press is Alt_R with same timestamp as current | |
388 | * Ctrl_L message. Therefore, this Ctrl_L press is a fake | |
389 | * event, so discard it. | |
390 | */ | |
391 | return TRUE; | |
392 | } | |
393 | } | |
394 | /* | |
395 | * Sometimes, the Alt_R press message is not yet posted when the | |
396 | * fake Ctrl_L press message arrives (even though it has the | |
397 | * same timestamp), so check for an Alt_R press message that has | |
398 | * arrived since the last Ctrl_L message. | |
399 | */ | |
400 | else if ((message == WM_KEYDOWN || message == WM_SYSKEYDOWN) | |
401 | && wParam == VK_MENU && (HIWORD(lParam) & KF_EXTENDED)) { | |
402 | /* Got a Alt_R press */ | |
403 | ||
404 | if (lastWasControlL) { | |
405 | lTime = GetMessageTime(); | |
406 | ||
407 | if (lastTime == lTime) { | |
408 | /* Undo the fake Ctrl_L press by sending a fake Ctrl_L release */ | |
409 | winSendKeyEvent(KEY_LCtrl, FALSE); | |
410 | } | |
411 | lastWasControlL = FALSE; | |
412 | } | |
413 | } | |
414 | /* | |
415 | * Fake Ctrl_L releases will be followed by an Alt_R release | |
416 | * with the same timestamp as the Ctrl_L release. | |
417 | */ | |
418 | else if ((message == WM_KEYUP || message == WM_SYSKEYUP) | |
419 | && wParam == VK_CONTROL && (HIWORD(lParam) & KF_EXTENDED) == 0) { | |
420 | /* Got a Ctrl_L release */ | |
421 | ||
422 | /* Get time of current message */ | |
423 | lTime = GetMessageTime(); | |
424 | ||
425 | /* Look for next release message */ | |
426 | fReturn = PeekMessage(&msgNext, NULL, | |
427 | WM_KEYUP, WM_SYSKEYUP, PM_NOREMOVE); | |
428 | ||
429 | if (fReturn && msgNext.message != WM_KEYUP && | |
430 | msgNext.message != WM_SYSKEYUP) | |
431 | fReturn = 0; | |
432 | ||
433 | lastWasControlL = FALSE; | |
434 | ||
435 | /* Is next press an Alt_R with the same timestamp? */ | |
436 | if (fReturn | |
437 | && (msgNext.message == WM_KEYUP || msgNext.message == WM_SYSKEYUP) | |
438 | && msgNext.wParam == VK_MENU | |
439 | && msgNext.time == lTime | |
440 | && (HIWORD(msgNext.lParam) & KF_EXTENDED)) { | |
441 | /* | |
442 | * Next key release is Alt_R with same timestamp as current | |
443 | * Ctrl_L message. Therefore, this Ctrl_L release is a fake | |
444 | * event, so discard it. | |
445 | */ | |
446 | return TRUE; | |
447 | } | |
448 | } | |
449 | else { | |
450 | /* On any other press or release message, we don't have a | |
451 | potentially fake Ctrl_L to worry about anymore... */ | |
452 | lastWasControlL = FALSE; | |
453 | } | |
454 | ||
455 | /* Not a fake control left press/release */ | |
456 | return FALSE; | |
457 | } | |
458 | ||
459 | /* | |
460 | * Lift any modifier keys that are pressed | |
461 | */ | |
462 | ||
463 | void | |
464 | winKeybdReleaseKeys(void) | |
465 | { | |
466 | int i; | |
467 | ||
468 | #ifdef HAS_DEVWINDOWS | |
469 | /* Verify that the mi input system has been initialized */ | |
470 | if (g_fdMessageQueue == WIN_FD_INVALID) | |
471 | return; | |
472 | #endif | |
473 | ||
474 | /* Loop through all keys */ | |
475 | for (i = 0; i < NUM_KEYCODES; ++i) { | |
476 | /* Pop key if pressed */ | |
477 | if (g_winKeyState[i]) | |
478 | winSendKeyEvent(i, FALSE); | |
479 | ||
480 | /* Reset pressed flag for keys */ | |
481 | g_winKeyState[i] = FALSE; | |
482 | } | |
483 | } | |
484 | ||
485 | /* | |
486 | * Take a raw X key code and send an up or down event for it. | |
487 | * | |
488 | * Thanks to VNC for inspiration, though it is a simple function. | |
489 | */ | |
490 | ||
491 | void | |
492 | winSendKeyEvent(DWORD dwKey, Bool fDown) | |
493 | { | |
494 | /* | |
495 | * When alt-tabing between screens we can get phantom key up messages | |
496 | * Here we only pass them through it we think we should! | |
497 | */ | |
498 | if (g_winKeyState[dwKey] == FALSE && fDown == FALSE) | |
499 | return; | |
500 | ||
501 | /* Update the keyState map */ | |
502 | g_winKeyState[dwKey] = fDown; | |
503 | ||
504 | QueueKeyboardEvents(g_pwinKeyboard, fDown ? KeyPress : KeyRelease, | |
505 | dwKey + MIN_KEYCODE, NULL); | |
506 | ||
507 | winDebug("winSendKeyEvent: dwKey: %d, fDown: %d\n", dwKey, fDown); | |
508 | } | |
509 | ||
510 | BOOL | |
511 | winCheckKeyPressed(WPARAM wParam, LPARAM lParam) | |
512 | { | |
513 | switch (wParam) { | |
514 | case VK_CONTROL: | |
515 | if ((lParam & 0x1ff0000) == 0x11d0000 && g_winKeyState[KEY_RCtrl]) | |
516 | return TRUE; | |
517 | if ((lParam & 0x1ff0000) == 0x01d0000 && g_winKeyState[KEY_LCtrl]) | |
518 | return TRUE; | |
519 | break; | |
520 | case VK_SHIFT: | |
521 | if ((lParam & 0x1ff0000) == 0x0360000 && g_winKeyState[KEY_ShiftR]) | |
522 | return TRUE; | |
523 | if ((lParam & 0x1ff0000) == 0x02a0000 && g_winKeyState[KEY_ShiftL]) | |
524 | return TRUE; | |
525 | break; | |
526 | default: | |
527 | return TRUE; | |
528 | } | |
529 | return FALSE; | |
530 | } | |
531 | ||
532 | /* Only one shift release message is sent even if both are pressed. | |
533 | * Fix this here | |
534 | */ | |
535 | void | |
536 | winFixShiftKeys(int iScanCode) | |
537 | { | |
538 | if (GetKeyState(VK_SHIFT) & 0x8000) | |
539 | return; | |
540 | ||
541 | if (iScanCode == KEY_ShiftL && g_winKeyState[KEY_ShiftR]) | |
542 | winSendKeyEvent(KEY_ShiftR, FALSE); | |
543 | if (iScanCode == KEY_ShiftR && g_winKeyState[KEY_ShiftL]) | |
544 | winSendKeyEvent(KEY_ShiftL, FALSE); | |
545 | } |