#ifdef __WIN32 // Q_OS_WIN32 isn't available at this point #define _WIN32_WINNT 0x0430 #include #endif #include "hotkeywrapper.hh" #include "gddebug.hh" #include #include #include #ifdef Q_OS_WIN #include "mainwindow.hh" #endif ////////////////////////////////////////////////////////////////////////// QHotkeyApplication::QHotkeyApplication( int & argc, char ** argv ): QtSingleApplication( argc, argv ) { connect( this, &QGuiApplication::commitDataRequest, this, &QHotkeyApplication::hotkeyAppCommitData, Qt::DirectConnection ); connect( this, &QGuiApplication::saveStateRequest, this, &QHotkeyApplication::hotkeyAppSaveState, Qt::DirectConnection ); #if defined( Q_OS_WIN ) installNativeEventFilter( this ); #endif } QHotkeyApplication::QHotkeyApplication( QString const & id, int & argc, char ** argv ): QtSingleApplication( id, argc, argv ) { connect( this, &QGuiApplication::commitDataRequest, this, &QHotkeyApplication::hotkeyAppCommitData, Qt::DirectConnection ); connect( this, &QGuiApplication::saveStateRequest, this, &QHotkeyApplication::hotkeyAppSaveState, Qt::DirectConnection ); #if defined( Q_OS_WIN ) installNativeEventFilter( this ); #endif } void QHotkeyApplication::addDataCommiter( DataCommitter & d ) { dataCommitters.append( &d ); } void QHotkeyApplication::removeDataCommiter( DataCommitter & d ) { dataCommitters.removeAll( &d ); } void QHotkeyApplication::hotkeyAppCommitData( QSessionManager & mgr ) { for ( int x = 0; x < dataCommitters.size(); ++x ) dataCommitters[ x ]->commitData( mgr ); } void QHotkeyApplication::hotkeyAppSaveState( QSessionManager & mgr ) { mgr.setRestartHint( QSessionManager::RestartNever ); } void QHotkeyApplication::registerWrapper( HotkeyWrapper * wrapper ) { if ( wrapper && !hotkeyWrappers.contains( wrapper ) ) hotkeyWrappers.append( wrapper ); } void QHotkeyApplication::unregisterWrapper( HotkeyWrapper * wrapper ) { if ( wrapper && hotkeyWrappers.contains( wrapper ) ) hotkeyWrappers.removeAll( wrapper ); } ////////////////////////////////////////////////////////////////////////// HotkeyStruct::HotkeyStruct( quint32 key_, quint32 key2_, quint32 modifier_, int handle_, int id_ ): key( key_ ), key2( key2_ ), modifier( modifier_ ), handle( handle_ ), id( id_ ) #ifdef Q_OS_MAC , hkRef( 0 ), hkRef2( 0 ) #endif { } ////////////////////////////////////////////////////////////////////////// #if !defined( Q_OS_MAC ) && !defined( Q_WS_QWS ) HotkeyWrapper::HotkeyWrapper( QObject * parent ): QThread( parent ), state2( false ) { #ifdef Q_OS_WIN hwnd = (HWND)( ( static_cast< QMainWindow * >( parent ) )->winId() ); gdDebug( "Handle global hotkeys via RegisterHotkey()" ); #else init(); #endif ( static_cast< QHotkeyApplication * >( qApp ) )->registerWrapper( this ); } HotkeyWrapper::~HotkeyWrapper() { unregister(); } void HotkeyWrapper::waitKey2() { state2 = false; #ifdef HAVE_X11 if ( keyToUngrab != grabbedKeys.end() ) { ungrabKey( keyToUngrab ); keyToUngrab = grabbedKeys.end(); } #endif } bool HotkeyWrapper::checkState( quint32 vk, quint32 mod ) { if ( state2 ) { // wait for 2nd key waitKey2(); // Cancel the 2nd-key wait stage if ( state2waiter.key2 == vk && state2waiter.modifier == mod ) { emit hotkeyActivated( state2waiter.handle ); return true; } } for ( int i = 0; i < hotkeys.count(); i++ ) { const HotkeyStruct & hs = hotkeys.at( i ); if ( hs.key == vk && hs.modifier == mod ) { #ifdef Q_OS_WIN32 if ( hs.key2 != 0 || ( mod == MOD_CONTROL && ( vk == VK_INSERT || vk == 'c' || vk == 'C' ) ) ) { // Pass-through first part of compound hotkey or clipdoard copy command INPUT i[ 10 ]; memset( i, 0, sizeof( i ) ); int nextKeyNom = 0; short emulateModKeys = 0; // If modifier keys aren't pressed it looks like emulation // We then also emulate full sequence if ( ( mod & MOD_ALT ) != 0 && ( GetAsyncKeyState( VK_MENU ) & 0x8000 ) == 0 ) { emulateModKeys |= MOD_ALT; i[ nextKeyNom ].type = INPUT_KEYBOARD; i[ nextKeyNom ].ki.wVk = VK_MENU; nextKeyNom += 1; } if ( ( mod & MOD_CONTROL ) != 0 && ( GetAsyncKeyState( VK_CONTROL ) & 0x8000 ) == 0 ) { emulateModKeys |= MOD_CONTROL; i[ nextKeyNom ].type = INPUT_KEYBOARD; i[ nextKeyNom ].ki.wVk = VK_CONTROL; nextKeyNom += 1; } if ( ( mod & MOD_SHIFT ) != 0 && ( GetAsyncKeyState( VK_SHIFT ) & 0x8000 ) == 0 ) { emulateModKeys |= MOD_SHIFT; i[ nextKeyNom ].type = INPUT_KEYBOARD; i[ nextKeyNom ].ki.wVk = VK_SHIFT; nextKeyNom += 1; } if ( ( mod & MOD_WIN ) != 0 && ( GetAsyncKeyState( VK_LWIN ) & 0x8000 ) == 0 && ( GetAsyncKeyState( VK_RWIN ) & 0x8000 ) == 0 ) { emulateModKeys |= MOD_WIN; i[ nextKeyNom ].type = INPUT_KEYBOARD; i[ nextKeyNom ].ki.wVk = VK_LWIN; nextKeyNom += 1; } i[ nextKeyNom ].type = INPUT_KEYBOARD; i[ nextKeyNom ].ki.wVk = vk; nextKeyNom += 1; i[ nextKeyNom ].type = INPUT_KEYBOARD; i[ nextKeyNom ].ki.wVk = vk; i[ nextKeyNom ].ki.dwFlags = KEYEVENTF_KEYUP; nextKeyNom += 1; if ( emulateModKeys & MOD_WIN ) { i[ nextKeyNom ].type = INPUT_KEYBOARD; i[ nextKeyNom ].ki.wVk = VK_LWIN; i[ nextKeyNom ].ki.dwFlags = KEYEVENTF_KEYUP; nextKeyNom += 1; } if ( emulateModKeys & MOD_SHIFT ) { i[ nextKeyNom ].type = INPUT_KEYBOARD; i[ nextKeyNom ].ki.wVk = VK_SHIFT; i[ nextKeyNom ].ki.dwFlags = KEYEVENTF_KEYUP; nextKeyNom += 1; } if ( emulateModKeys & MOD_CONTROL ) { i[ nextKeyNom ].type = INPUT_KEYBOARD; i[ nextKeyNom ].ki.wVk = VK_CONTROL; i[ nextKeyNom ].ki.dwFlags = KEYEVENTF_KEYUP; nextKeyNom += 1; } if ( emulateModKeys & MOD_ALT ) { i[ nextKeyNom ].type = INPUT_KEYBOARD; i[ nextKeyNom ].ki.wVk = VK_MENU; i[ nextKeyNom ].ki.dwFlags = KEYEVENTF_KEYUP; nextKeyNom += 1; } UnregisterHotKey( hwnd, hs.id ); SendInput( nextKeyNom, i, sizeof( *i ) ); RegisterHotKey( hwnd, hs.id, hs.modifier, hs.key ); } #endif if ( hs.key2 == 0 ) { emit hotkeyActivated( hs.handle ); return true; } state2 = true; state2waiter = hs; QTimer::singleShot( 500, this, &HotkeyWrapper::waitKey2 ); #ifdef HAVE_X11 // Grab the second key, unless it's grabbed already // Note that we only grab the clipboard key only if // the sequence didn't begin with it if ( ( isCopyToClipboardKey( hs.key, hs.modifier ) || !isCopyToClipboardKey( hs.key2, hs.modifier ) ) && !isKeyGrabbed( hs.key2, hs.modifier ) ) keyToUngrab = grabKey( hs.key2, hs.modifier ); #endif return true; } } state2 = false; return false; } ////////////////////////////////////////////////////////////////////////// #ifdef Q_OS_WIN void HotkeyWrapper::init() { QWidget * root = qApp->topLevelWidgets().value( 0 ); hwnd = (HWND)root->winId(); } bool HotkeyWrapper::setGlobalKey( QKeySequence const & seq, int handle ) { Config::HotKey hk( seq ); return setGlobalKey( hk.key1, hk.key2, hk.modifiers, handle ); } bool HotkeyWrapper::setGlobalKey( int key, int key2, Qt::KeyboardModifiers modifier, int handle ) { if ( !key ) return false; // We don't monitor empty combinations static int id = 0; if ( id > 0xBFFF - 1 ) id = 0; quint32 mod = 0; if ( modifier & Qt::CTRL ) mod |= MOD_CONTROL; if ( modifier & Qt::ALT ) mod |= MOD_ALT; if ( modifier & Qt::SHIFT ) mod |= MOD_SHIFT; if ( modifier & Qt::META ) mod |= MOD_WIN; quint32 vk = nativeKey( key ); quint32 vk2 = key2 ? nativeKey( key2 ) : 0; hotkeys.append( HotkeyStruct( vk, vk2, mod, handle, id ) ); if ( !RegisterHotKey( hwnd, id++, mod, vk ) ) return false; if ( key2 && key2 != key ) return RegisterHotKey( hwnd, id++, mod, vk2 ); return true; } #if ( QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 ) ) bool HotkeyWrapper::winEvent( MSG * message, long * result ) #else bool HotkeyWrapper::winEvent( MSG * message, qintptr * result ) #endif { (void)result; if ( message->message == WM_HOTKEY ) return checkState( ( message->lParam >> 16 ), ( message->lParam & 0xffff ) ); return false; } quint32 HotkeyWrapper::nativeKey( int key ) { if ( key >= Qt::Key_0 && key <= Qt::Key_9 ) return VK_NUMPAD0 + ( key - Qt::Key_0 ); if ( key >= Qt::Key_A && key <= Qt::Key_Z ) return key; switch ( key ) { case Qt::Key_Space: return VK_SPACE; case Qt::Key_Asterisk: return VK_MULTIPLY; case Qt::Key_Plus: return VK_ADD; case Qt::Key_Comma: return VK_SEPARATOR; case Qt::Key_Minus: return VK_SUBTRACT; case Qt::Key_Slash: return VK_DIVIDE; case Qt::Key_Tab: case Qt::Key_Backtab: return VK_TAB; case Qt::Key_Backspace: return VK_BACK; case Qt::Key_Return: case Qt::Key_Escape: return VK_ESCAPE; case Qt::Key_Enter: return VK_RETURN; case Qt::Key_Insert: return VK_INSERT; case Qt::Key_Delete: return VK_DELETE; case Qt::Key_Pause: return VK_PAUSE; case Qt::Key_Print: return VK_PRINT; case Qt::Key_Clear: return VK_CLEAR; case Qt::Key_Home: return VK_HOME; case Qt::Key_End: return VK_END; case Qt::Key_Up: return VK_UP; case Qt::Key_Down: return VK_DOWN; case Qt::Key_Left: return VK_LEFT; case Qt::Key_Right: return VK_RIGHT; case Qt::Key_PageUp: return VK_PRIOR; case Qt::Key_PageDown: return VK_NEXT; case Qt::Key_F1: return VK_F1; case Qt::Key_F2: return VK_F2; case Qt::Key_F3: return VK_F3; case Qt::Key_F4: return VK_F4; case Qt::Key_F5: return VK_F5; case Qt::Key_F6: return VK_F6; case Qt::Key_F7: return VK_F7; case Qt::Key_F8: return VK_F8; case Qt::Key_F9: return VK_F9; case Qt::Key_F10: return VK_F10; case Qt::Key_F11: return VK_F11; case Qt::Key_F12: return VK_F12; case Qt::Key_F13: return VK_F13; case Qt::Key_F14: return VK_F14; case Qt::Key_F15: return VK_F15; case Qt::Key_F16: return VK_F16; case Qt::Key_F17: return VK_F17; case Qt::Key_F18: return VK_F18; case Qt::Key_F19: return VK_F19; case Qt::Key_F20: return VK_F20; case Qt::Key_F21: return VK_F21; case Qt::Key_F22: return VK_F22; case Qt::Key_F23: return VK_F23; case Qt::Key_F24: return VK_F24; case Qt::Key_Colon: case Qt::Key_Semicolon: return VK_OEM_1; case Qt::Key_Question: return VK_OEM_2; case Qt::Key_AsciiTilde: case Qt::Key_QuoteLeft: return VK_OEM_3; case Qt::Key_BraceLeft: case Qt::Key_BracketLeft: return VK_OEM_4; case Qt::Key_Bar: case Qt::Key_Backslash: return VK_OEM_5; case Qt::Key_BraceRight: case Qt::Key_BracketRight: return VK_OEM_6; case Qt::Key_QuoteDbl: case Qt::Key_Apostrophe: return VK_OEM_7; case Qt::Key_Less: return VK_OEM_COMMA; case Qt::Key_Greater: return VK_OEM_PERIOD; case Qt::Key_Equal: return VK_OEM_PLUS; case Qt::Key_ParenRight: return 0x30; case Qt::Key_Exclam: return 0x31; case Qt::Key_At: return 0x32; case Qt::Key_NumberSign: return 0x33; case Qt::Key_Dollar: return 0x34; case Qt::Key_Percent: return 0x35; case Qt::Key_AsciiCircum: return 0x36; case Qt::Key_Ampersand: return 0x37; case Qt::Key_copyright: return 0x38; case Qt::Key_ParenLeft: return 0x39; case Qt::Key_Underscore: return VK_OEM_MINUS; default:; } return key; } void HotkeyWrapper::unregister() { for ( int i = 0; i < hotkeys.count(); i++ ) { HotkeyStruct const & hk = hotkeys.at( i ); UnregisterHotKey( hwnd, hk.id ); if ( hk.key2 && hk.key2 != hk.key ) UnregisterHotKey( hwnd, hk.id + 1 ); } ( static_cast< QHotkeyApplication * >( qApp ) )->unregisterWrapper( this ); } #if ( QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 ) ) bool QHotkeyApplication::nativeEventFilter( const QByteArray & /*eventType*/, void * message, long * result ) #else bool QHotkeyApplication::nativeEventFilter( const QByteArray & /*eventType*/, void * message, qintptr * result ) #endif { MSG * msg = reinterpret_cast< MSG * >( message ); if ( msg->message == WM_HOTKEY ) { for ( int i = 0; i < hotkeyWrappers.size(); i++ ) { if ( hotkeyWrappers.at( i )->winEvent( msg, result ) ) return true; } } return false; } ////////////////////////////////////////////////////////////////////////// #else ////////////////////////////////////////////////////////////////////////// #include void HotkeyWrapper::init() { keyToUngrab = grabbedKeys.end(); #if QT_VERSION < 0x060000 Display * displayID = QX11Info::display(); #else QNativeInterface::QX11Application * x11AppInfo = qApp->nativeInterface< QNativeInterface::QX11Application >(); Display * displayID = x11AppInfo->display(); #endif // We use RECORD extension instead of XGrabKey. That's because XGrabKey // prevents other clients from getting their input if it's grabbed. Display * display = displayID; lShiftCode = XKeysymToKeycode( display, XK_Shift_L ); rShiftCode = XKeysymToKeycode( display, XK_Shift_R ); lCtrlCode = XKeysymToKeycode( display, XK_Control_L ); rCtrlCode = XKeysymToKeycode( display, XK_Control_R ); lAltCode = XKeysymToKeycode( display, XK_Alt_L ); rAltCode = XKeysymToKeycode( display, XK_Alt_R ); lMetaCode = XKeysymToKeycode( display, XK_Super_L ); rMetaCode = XKeysymToKeycode( display, XK_Super_R ); cCode = XKeysymToKeycode( display, XK_c ); insertCode = XKeysymToKeycode( display, XK_Insert ); kpInsertCode = XKeysymToKeycode( display, XK_KP_Insert ); currentModifiers = 0; // This one will be used to read the recorded content dataDisplay = XOpenDisplay( 0 ); if ( !dataDisplay ) throw exInit(); recordRange = XRecordAllocRange(); if ( !recordRange ) { XCloseDisplay( dataDisplay ); throw exInit(); } recordRange->device_events.first = KeyPress; recordRange->device_events.last = KeyRelease; recordClientSpec = XRecordAllClients; recordContext = XRecordCreateContext( display, 0, &recordClientSpec, 1, &recordRange, 1 ); if ( !recordContext ) { XFree( recordRange ); XCloseDisplay( dataDisplay ); throw exInit(); } // This is required to ensure context was indeed created XSync( display, False ); connect( this, &HotkeyWrapper::keyRecorded, this, &HotkeyWrapper::checkState, Qt::QueuedConnection ); start(); } void HotkeyWrapper::run() // Runs in a separate thread { if ( !XRecordEnableContext( dataDisplay, recordContext, recordEventCallback, (XPointer)this ) ) GD_DPRINTF( "Failed to enable record context\n" ); } void HotkeyWrapper::recordEventCallback( XPointer ptr, XRecordInterceptData * data ) { ( (HotkeyWrapper *)ptr )->handleRecordEvent( data ); } void HotkeyWrapper::handleRecordEvent( XRecordInterceptData * data ) { if ( data->category == XRecordFromServer ) { xEvent * event = (xEvent *)data->data; if ( event->u.u.type == KeyPress ) { KeyCode key = event->u.u.detail; if ( key == lShiftCode || key == rShiftCode ) currentModifiers |= ShiftMask; else if ( key == lCtrlCode || key == rCtrlCode ) currentModifiers |= ControlMask; else if ( key == lAltCode || key == rAltCode ) currentModifiers |= Mod1Mask; else if ( key == lMetaCode || key == rMetaCode ) currentModifiers |= Mod4Mask; else { // Here we employ a kind of hack translating KP_Insert key // to just Insert. This allows reacting to both Insert keys. if ( key == kpInsertCode ) key = insertCode; emit keyRecorded( key, currentModifiers ); } } else if ( event->u.u.type == KeyRelease ) { KeyCode key = event->u.u.detail; if ( key == lShiftCode || key == rShiftCode ) currentModifiers &= ~ShiftMask; else if ( key == lCtrlCode || key == rCtrlCode ) currentModifiers &= ~ControlMask; else if ( key == lAltCode || key == rAltCode ) currentModifiers &= ~Mod1Mask; else if ( key == lMetaCode || key == rMetaCode ) currentModifiers &= ~Mod4Mask; } } XRecordFreeData( data ); } bool HotkeyWrapper::setGlobalKey( QKeySequence const & seq, int handle ) { Config::HotKey hk( seq ); return setGlobalKey( hk.key1, hk.key2, hk.modifiers, handle ); } bool HotkeyWrapper::setGlobalKey( int key, int key2, Qt::KeyboardModifiers modifier, int handle ) { if ( !key ) return false; // We don't monitor empty combinations int vk = nativeKey( key ); int vk2 = key2 ? nativeKey( key2 ) : 0; quint32 mod = 0; if ( modifier & Qt::ShiftModifier ) mod |= ShiftMask; if ( modifier & Qt::ControlModifier ) mod |= ControlMask; if ( modifier & Qt::AltModifier ) mod |= Mod1Mask; if ( modifier & Qt::MetaModifier ) mod |= Mod4Mask; hotkeys.append( HotkeyStruct( vk, vk2, mod, handle, 0 ) ); if ( !isCopyToClipboardKey( vk, mod ) ) grabKey( vk, mod ); // Make sure it doesn't get caught by other apps return true; } bool HotkeyWrapper::isCopyToClipboardKey( quint32 keyCode, quint32 modifiers ) const { return modifiers == ControlMask && ( keyCode == cCode || keyCode == insertCode || keyCode == kpInsertCode ); } bool HotkeyWrapper::isKeyGrabbed( quint32 keyCode, quint32 modifiers ) const { GrabbedKeys::const_iterator i = grabbedKeys.find( std::make_pair( keyCode, modifiers ) ); return i != grabbedKeys.end(); } namespace { typedef int ( *X11ErrorHandler )( Display * display, XErrorEvent * event ); class X11GrabUngrabErrorHandler { public: static bool error; static int grab_ungrab_error_handler( Display *, XErrorEvent * event ) { qDebug() << "grab_ungrab_error_handler is invoked"; switch ( event->error_code ) { case BadAccess: case BadValue: case BadWindow: if ( event->request_code == 33 /* X_GrabKey */ || event->request_code == 34 /* X_UngrabKey */ ) { error = true; } } return 0; } X11GrabUngrabErrorHandler() { error = false; previousErrorHandler_ = XSetErrorHandler( grab_ungrab_error_handler ); } ~X11GrabUngrabErrorHandler() { #if QT_VERSION < 0x060000 Display * displayID = QX11Info::display(); #else QNativeInterface::QX11Application * x11AppInfo = qApp->nativeInterface< QNativeInterface::QX11Application >(); Display * displayID = x11AppInfo->display(); #endif XFlush( displayID ); (void)XSetErrorHandler( previousErrorHandler_ ); } bool isError() const { #if QT_VERSION < 0x060000 Display * displayID = QX11Info::display(); #else QNativeInterface::QX11Application * x11AppInfo = qApp->nativeInterface< QNativeInterface::QX11Application >(); Display * displayID = x11AppInfo->display(); #endif XFlush( displayID ); return error; } private: X11ErrorHandler previousErrorHandler_; }; bool X11GrabUngrabErrorHandler::error = false; } // anonymous namespace HotkeyWrapper::GrabbedKeys::iterator HotkeyWrapper::grabKey( quint32 keyCode, quint32 modifiers ) { std::pair< GrabbedKeys::iterator, bool > result = grabbedKeys.insert( std::make_pair( keyCode, modifiers ) ); if ( result.second ) { #if QT_VERSION < 0x060000 Display * displayID = QX11Info::display(); #else QNativeInterface::QX11Application * x11AppInfo = qApp->nativeInterface< QNativeInterface::QX11Application >(); Display * displayID = x11AppInfo->display(); #endif X11GrabUngrabErrorHandler errorHandler; XGrabKey( displayID, keyCode, modifiers, DefaultRootWindow( displayID ), True, GrabModeAsync, GrabModeAsync ); if ( errorHandler.isError() ) { gdWarning( "Possible hotkeys conflict. Check your hotkeys options." ); ungrabKey( result.first ); } } return result.first; } void HotkeyWrapper::ungrabKey( GrabbedKeys::iterator i ) { #if QT_VERSION < 0x060000 Display * displayID = QX11Info::display(); #else QNativeInterface::QX11Application * x11AppInfo = qApp->nativeInterface< QNativeInterface::QX11Application >(); Display * displayID = x11AppInfo->display(); #endif X11GrabUngrabErrorHandler errorHandler; XUngrabKey( displayID, i->first, i->second, XDefaultRootWindow( displayID ) ); grabbedKeys.erase( i ); if ( errorHandler.isError() ) { gdWarning( "Cannot ungrab the hotkey" ); } } quint32 HotkeyWrapper::nativeKey( int key ) { QString keySymName; switch ( key ) { case Qt::Key_Insert: keySymName = "Insert"; break; default: keySymName = QKeySequence( key ).toString(); break; } #if QT_VERSION < 0x060000 Display * displayID = QX11Info::display(); #else QNativeInterface::QX11Application * x11AppInfo = qApp->nativeInterface< QNativeInterface::QX11Application >(); Display * displayID = x11AppInfo->display(); #endif Display * display = displayID; return XKeysymToKeycode( display, XStringToKeysym( keySymName.toLatin1().data() ) ); } void HotkeyWrapper::unregister() { #if QT_VERSION < 0x060000 Display * displayID = QX11Info::display(); #else QNativeInterface::QX11Application * x11AppInfo = qApp->nativeInterface< QNativeInterface::QX11Application >(); Display * displayID = x11AppInfo->display(); #endif Display * display = displayID; XRecordDisableContext( display, recordContext ); XSync( display, False ); wait(); XRecordFreeContext( display, recordContext ); XFree( recordRange ); XCloseDisplay( dataDisplay ); while ( grabbedKeys.size() ) ungrabKey( grabbedKeys.begin() ); ( static_cast< QHotkeyApplication * >( qApp ) )->unregisterWrapper( this ); } #endif #endif