/* Copyright (c) 2017 Alex Diener This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. Alex Diener alex@ludobloom.com */ #define _GNU_SOURCE #include "glxshell/GLXShell.h" #include "glxshell/GLXShellCallbacks.h" #include "glxshell/GLXTarget.h" #include "shell/ShellBatteryInfo.h" #include "shell/ShellCallbacks.h" #include "shell/ShellKeyCodes.h" #include "shell/ShellThreads.h" #include "shell/Shell.h" #include "utilities/Log.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define WINDOW_TITLE_BAR_HEIGHT_ESTIMATE 36 #define SHELL_KEY_CODE_MAX 0xE7 struct GLXShellTimer { double interval; double nextFireTime; unsigned int flags; bool canceled; unsigned int id; void (* callback)(unsigned int timerID, void * context); void * context; }; #if defined(__linux) #define VSYNC_DEFAULT_WINDOW false #define VSYNC_DEFAULT_FULLSCREEN true #else #define VSYNC_DEFAULT_WINDOW true #define VSYNC_DEFAULT_FULLSCREEN true #endif static Display * display; static int screen; static GLXContext context; static Window window; static Window rootWindow; static unsigned long rootFrameExtents[4] = {0, 0, WINDOW_TITLE_BAR_HEIGHT_ESTIMATE, 0}; static Atom deleteWindowAtom, dialogClosedAtom, ignoreNextMotionNotifyAtom, mouseDeltaModeAtom; static bool windowCreated; static int initialWindowX, initialWindowY; static bool redisplayNeeded; static unsigned int buttonMask = 0; static unsigned int modifiers = 0, lastModifiers = 0; static int lastMouseX, lastMouseY; static double mainLoopTargetInterval; static bool inFullScreenMode = false; static bool vsyncWindow = VSYNC_DEFAULT_WINDOW, vsyncFullscreen = VSYNC_DEFAULT_FULLSCREEN; static struct GLXShellConfiguration configuration; static unsigned int nextTimerID; static size_t timerCount; static struct GLXShellTimer * timers; static int lastWidth, lastHeight; static bool backgrounded; static bool coalesceMouseMotionEvents; static bool sendActivationClicks = false; static bool cursorHiddenByHide; static bool showCursorOnNextMouseMove; static ShellCursorID lastUnhiddenCursor = ShellCursor_arrow; static float cursorUnhideThreshold = 0.0f; static float mouseDeltaXSinceHide, mouseDeltaYSinceHide; static bool mouseDeltaMode, eventQueueSynchronizedMouseDeltaMode; static int restoreMouseX, restoreMouseY; static int lastMouseX, lastMouseY; static unsigned int ignoredMotionNotifyEventCount, unignoredMotionNotifyEventCount; static bool nextKeyDownIsRepeat; static char * clipboardText; static double lastDrawTime; static char * windowTitle; static bool documentEdited; static bool runningDialog; static unsigned int screensaverCookie; static struct { double lastEventTime; bool held; } keyStates[SHELL_KEY_CODE_MAX + 1]; static ShellMutex callbackQueueHeadMutex; static volatile unsigned int queuedCallbackHead, queuedCallbackTail; #define QUEUED_CALLBACK_MAX 500 static struct { void (* callback)(void * context); void * context; } queuedCallbacks[QUEUED_CALLBACK_MAX]; static int pselectPipe[2]; #define CUSTOM_CURSOR_ID_OFFSET 1000 static unsigned int customCursorCount; static struct { unsigned int representationCount; struct { float scaleFactor; Cursor cursor; } * representations; } * customCursors; // From dbus/dbus.h (don't want to include) typedef struct DBusConnection DBusConnection; typedef struct DBusError DBusError; typedef struct DBusMessage DBusMessage; typedef uint32_t dbus_uint32_t; typedef dbus_uint32_t dbus_bool_t; typedef enum { DBUS_BUS_SESSION, /**< The login session bus */ DBUS_BUS_SYSTEM, /**< The systemwide bus */ DBUS_BUS_STARTER /**< The bus that started us, if any */ } DBusBusType; struct DBusError { const char *name; /**< public error name field */ const char *message; /**< public error message field */ unsigned int dummy1 : 1; /**< placeholder */ unsigned int dummy2 : 1; /**< placeholder */ unsigned int dummy3 : 1; /**< placeholder */ unsigned int dummy4 : 1; /**< placeholder */ unsigned int dummy5 : 1; /**< placeholder */ void *padding1; /**< placeholder */ }; #define DBUS_TYPE_INVALID ((int) '\0') #define DBUS_TYPE_ARRAY ((int) 'a') #define DBUS_TYPE_UINT32 ((int) 'u') #define DBUS_TYPE_STRING ((int) 's') #define DBUS_LOAD_STATUS_UNINITIALIZED 0 #define DBUS_LOAD_STATUS_INITIALIZING 1 #define DBUS_LOAD_STATUS_SUCCEEDED 2 #define DBUS_LOAD_STATUS_FAILED 3 static unsigned int dbusLoadStatus = DBUS_LOAD_STATUS_UNINITIALIZED; static struct { DBusConnection * connection; DBusConnection * (* bus_get_private)(DBusBusType, DBusError *); DBusMessage * (* message_new_method_call)(const char *, const char *, const char *, const char *); dbus_bool_t (* message_append_args_valist)(DBusMessage *, int, va_list); void (* connection_set_exit_on_disconnect)(DBusConnection *, dbus_bool_t); dbus_bool_t (* connection_send)(DBusConnection *, DBusMessage *, dbus_uint32_t *); DBusMessage * (* connection_send_with_reply_and_block)(DBusConnection *, DBusMessage *, int, DBusError *); void (* connection_close)(DBusConnection *); void (* connection_unref)(DBusConnection *); void (* connection_flush)(DBusConnection *); dbus_bool_t (* message_get_args_valist)(DBusMessage *, DBusError *, int, va_list); void (* message_unref)(DBusMessage *); dbus_bool_t (* threads_init_default)(void); void (* error_init)(DBusError *); dbus_bool_t (* error_is_set)(const DBusError *); void (* error_free)(DBusError *); } dbusInterface; static void setEmptyCursor() { static Cursor emptyCursor = None; if (emptyCursor == None) { char emptyBits[32]; XColor color; Pixmap pixmap; memset(emptyBits, 0, sizeof(emptyBits)); memset(&color, 0, sizeof(color)); pixmap = XCreateBitmapFromData(display, window, emptyBits, 16, 16); if (pixmap != None) { emptyCursor = XCreatePixmapCursor(display, pixmap, pixmap, &color, &color, 0, 0); XFreePixmap(display, pixmap); } } XDefineCursor(display, window, emptyCursor); } static void setShellCursor(ShellCursorID cursor) { static struct {unsigned int name; Cursor cursor;} cursors[] = { {XC_top_left_arrow, None}, // ShellCursor_arrow {XC_xterm, None}, // ShellCursor_iBeam {XC_crosshair, None}, // ShellCursor_crosshair {XC_hand2, None}, // ShellCursor_hand {XC_watch, None} // ShellCursor_wait }; if (cursor >= CUSTOM_CURSOR_ID_OFFSET) { cursor -= CUSTOM_CURSOR_ID_OFFSET; if ((unsigned int) cursor >= customCursorCount) { return; } float scaleFactor = Shell_getDisplayScaleFactor(Shell_getDisplayIndexFromWindow()); unsigned int bestRepresentationIndex = 0; float bestScaleFactor = 0.0f; for (unsigned int representationIndex = 0; representationIndex < customCursors[cursor].representationCount; representationIndex++) { if (customCursors[cursor].representations[representationIndex].scaleFactor > bestScaleFactor && customCursors[cursor].representations[representationIndex].scaleFactor <= scaleFactor) { bestRepresentationIndex = representationIndex; bestScaleFactor = customCursors[cursor].representations[representationIndex].scaleFactor; } } XDefineCursor(display, window, customCursors[cursor].representations[bestRepresentationIndex].cursor); } else { if ((unsigned int) cursor > ShellCursor_wait) { return; } // XC_top_left_arrow doesn't match the default arrow cursor, so use None instead (which is equivalent to calling XUndefineCursor) if (cursors[cursor].cursor == None && cursor != ShellCursor_arrow) { cursors[cursor].cursor = XCreateFontCursor(display, cursors[cursor].name); } XDefineCursor(display, window, cursors[cursor].cursor); } } static void sendClientMessage(Atom message_type, long data0, long data1, long data2) { XEvent event; memset(&event, 0, sizeof(event)); event.type = ClientMessage; event.xclient.window = window; event.xclient.message_type = message_type; event.xclient.format = 32; event.xclient.data.l[0] = data0; event.xclient.data.l[1] = data1; event.xclient.data.l[2] = data2; XSendEvent(display, window, False, 0, &event); } static void warpPointerAndIgnoreEvent(int x, int y) { sendClientMessage(ignoreNextMotionNotifyAtom, 1, x, y); XWarpPointer(display, None, window, 0, 0, 0, 0, x, y); //XSync(display, False); sendClientMessage(ignoreNextMotionNotifyAtom, 0, x, y); } static unsigned int xkeyCodeToShellKeyCode(unsigned int xkeyCode) { switch (xkeyCode) { #define KEY_CODE_PAIR(shell_key_code, x_key_code) \ case x_key_code: \ return shell_key_code; #include "glxshell/GLXShell_keyCodes.h" #undef KEY_CODE_PAIR #ifdef DEBUG default: fprintf(stderr, "Warning: Unknown key code %u\n", xkeyCode); #endif } return 0; } static unsigned int shellKeyCodeToXKeyCode(unsigned int shellKeyCode) { switch (shellKeyCode) { #define KEY_CODE_PAIR(shell_key_code, x_key_code) \ case shell_key_code: \ return x_key_code; #include "glxshell/GLXShell_keyCodes.h" #undef KEY_CODE_PAIR #ifdef DEBUG default: fprintf(stderr, "Warning: Unknown key code %u\n", shellKeyCode); #endif } return 0; } /* static unsigned int xkeyStateToShellModifiers(unsigned int xkeyState) { unsigned int state = 0; if (xkeyState & ShiftMask) { state |= MODIFIER_SHIFT_BIT; } if (xkeyState & ControlMask) { state |= MODIFIER_CONTROL_BIT; } if (xkeyState & LockMask) { state |= MODIFIER_CAPS_LOCK_BIT; } if (xkeyState & Mod1Mask) { state |= MODIFIER_ALT_BIT; } return state; } */ static void updateKeyModifierState(void) { char keys[32]; XQueryKeymap(display, keys); XKeyboardState keyboardState; XGetKeyboardControl(display, &keyboardState); bool leftShift = keys[6] & 0x4; bool rightShift = keys[7] & 0x40; bool leftCtrl = keys[4] & 0x20; bool rightCtrl = keys[13] & 0x2; bool leftAlt = keys[8] & 0x1; bool rightAlt = keys[13] & 0x10; bool capsLock = keyboardState.led_mask & 0x1; if (leftShift || rightShift) { modifiers |= MODIFIER_SHIFT_BIT; } else { modifiers &= ~MODIFIER_SHIFT_BIT; } if (leftCtrl || rightCtrl) { modifiers |= MODIFIER_CONTROL_BIT; } else { modifiers &= ~MODIFIER_CONTROL_BIT; } if (leftAlt) { modifiers |= MODIFIER_ALT_BIT; } else { modifiers &= ~MODIFIER_ALT_BIT; } if (rightAlt) { modifiers |= MODIFIER_ALTGR_BIT; } else { modifiers &= ~MODIFIER_ALTGR_BIT; } if (capsLock) { modifiers |= MODIFIER_CAPS_LOCK_BIT; } else { modifiers &= ~MODIFIER_CAPS_LOCK_BIT; } if (lastModifiers != modifiers) { if (keyModifiersChangedCallback != NULL) { keyModifiersChangedCallback(modifiers, lastModifiers, Shell_getCurrentTime()); } lastModifiers = modifiers; } } static void releaseAllKeys(void) { for (unsigned int key = 0; key <= SHELL_KEY_CODE_MAX; key++) { if (keyStates[key].held) { keyStates[key].held = false; if (keyUpCallback != NULL) { keyUpCallback(key, modifiers, Shell_getCurrentTime()); } } } } static void sleepEventLoop(double currentTime, bool coordinateWithTimers) { fd_set fdset; int socket; struct timespec wait; socket = ConnectionNumber(display); FD_ZERO(&fdset); FD_SET(socket, &fdset); FD_SET(pselectPipe[0], &fdset); if (coordinateWithTimers) { double closestNextFireTime = 1000.0; for (unsigned int timerIndex = 0; timerIndex < timerCount; timerIndex++) { if (!timers[timerIndex].canceled && (!backgrounded || (timers[timerIndex].flags & TIMER_FLAG_RUN_IN_BACKGROUND)) && (!runningDialog || (timers[timerIndex].flags & TIMER_FLAG_RUN_DURING_FILE_DIALOGS)) && // Note: This function currently never executes during file dialogs timers[timerIndex].nextFireTime - currentTime < closestNextFireTime) { closestNextFireTime = timers[timerIndex].nextFireTime - currentTime; } } if (closestNextFireTime < 0.0) { closestNextFireTime = 0.0; } wait.tv_sec = closestNextFireTime; wait.tv_nsec = fmod(closestNextFireTime, 1.0) * 1000000000; } else { wait.tv_sec = INT_MAX; wait.tv_nsec = 0; } sigset_t emptyset; sigemptyset(&emptyset); int nfds = socket + 1; if (pselectPipe[0] + 1 > nfds) { nfds = pselectPipe[0] + 1; } pselect(nfds, &fdset, NULL, NULL, &wait, &emptyset); } static double XEventTimeToShellTime(Time time) { // XEvent time is a 32-bit milliseconds value, which rolls over every 49 days. CLOCK_MONOTONIC does not overflow. // Converting the CLOCK_MONOTONIC measurement to milliseconds and adding the high bits to event times allows // their values to line up with the results of Shell_getCurrentTime() when the system's uptime is above 49 days. struct timespec currentTime; clock_gettime(CLOCK_MONOTONIC, ¤tTime); uint64_t millisecondsMonotonic = currentTime.tv_sec * 1000 + currentTime.tv_nsec / 1000000; millisecondsMonotonic &= ~0xFFFFFFFFull; return (time + millisecondsMonotonic) / 1000.0; } static unsigned int translateXButtonToShellButtonIndex(unsigned int xbutton) { switch (xbutton) { case Button1: return MOUSE_BUTTON_NUMBER_LEFT; case Button2: return MOUSE_BUTTON_NUMBER_MIDDLE; case Button3: return MOUSE_BUTTON_NUMBER_RIGHT; } return xbutton - 5; } #define BUTTON_NUMBER_SCROLL_WHEEL_UP 4 #define BUTTON_NUMBER_SCROLL_WHEEL_DOWN 5 #define BUTTON_NUMBER_SCROLL_WHEEL_LEFT 6 #define BUTTON_NUMBER_SCROLL_WHEEL_RIGHT 7 static bool isScrollWheelXButtonEvent(unsigned int button, int * outDeltaX, int * outDeltaY) { switch (button) { case BUTTON_NUMBER_SCROLL_WHEEL_UP: *outDeltaY = -1; return true; case BUTTON_NUMBER_SCROLL_WHEEL_DOWN: *outDeltaY = 1; return true; case BUTTON_NUMBER_SCROLL_WHEEL_LEFT: *outDeltaX = -1; return true; case BUTTON_NUMBER_SCROLL_WHEEL_RIGHT: *outDeltaY = 1; return true; } return false; } static void processMouseMoveEvent(XEvent event) { if (event.xbutton.x == lastMouseX && event.xbutton.y == lastMouseY) { return; } int reportedDeltaX = event.xbutton.x - lastMouseX; int reportedDeltaY = event.xbutton.y - lastMouseY; int reportedX, reportedY; if (eventQueueSynchronizedMouseDeltaMode) { reportedX = restoreMouseX; reportedY = restoreMouseY; } else { reportedX = event.xbutton.x; reportedY = event.xbutton.y; lastMouseX = event.xbutton.x; lastMouseY = event.xbutton.y; } if (showCursorOnNextMouseMove) { mouseDeltaXSinceHide += reportedDeltaX; mouseDeltaYSinceHide += reportedDeltaY; if (fabs(mouseDeltaXSinceHide) >= cursorUnhideThreshold || fabs(mouseDeltaYSinceHide) >= cursorUnhideThreshold) { setShellCursor(lastUnhiddenCursor); showCursorOnNextMouseMove = false; if (cursorUnhiddenCallback != NULL) { cursorUnhiddenCallback(true, Shell_getCurrentTime()); } } } if (buttonMask != 0) { if (mouseDraggedCallback != NULL) { mouseDraggedCallback(buttonMask, reportedX, reportedY, reportedDeltaX, reportedDeltaY, modifiers, event.xbutton.time / 1000.0); } } else { if (mouseMovedCallback != NULL) { mouseMovedCallback(reportedX, reportedY, reportedDeltaX, reportedDeltaY, modifiers, event.xbutton.time / 1000.0); } } if (mouseDeltaMode) { XWindowAttributes windowAttributes; XGetWindowAttributes(display, window, &windowAttributes); warpPointerAndIgnoreEvent(windowAttributes.width / 2, windowAttributes.height / 2); } } // Set = 1 (true), clear = 0 (false) #define _NET_WM_STATE_TOGGLE 2 bool setFullScreenEWMH(bool fullscreen) { XEvent event; event.type = ClientMessage; event.xclient.window = window; event.xclient.message_type = XInternAtom(display, "_NET_WM_STATE", False); event.xclient.format = 32; event.xclient.data.l[0] = fullscreen; event.xclient.data.l[1] = XInternAtom(display, "_NET_WM_STATE_FULLSCREEN", False); event.xclient.data.l[2] = 0; event.xclient.data.l[3] = 1; event.xclient.data.l[4] = 0; return !!XSendEvent(display, rootWindow, 0, SubstructureRedirectMask | SubstructureNotifyMask, &event); } void Shell_mainLoop(void) { Window rootWindowResult, childWindowResult; int root_x, root_y, win_x, win_y; unsigned int mask; if (XQueryPointer(display, window, &rootWindowResult, &childWindowResult, &root_x, &root_y, &win_x, &win_y, &mask)) { lastMouseX = win_x; lastMouseY = win_y; if (mouseMovedCallback != NULL) { mouseMovedCallback(win_x, win_y, 0.0f, 0.0f, 0, Shell_getCurrentTime()); } } XEvent event, lastMouseMoveEvent = {.type = 0}; int deltaX, deltaY; bool ignoreMotionNotifyEvents = false; XWindowAttributes windowAttributes; unsigned int charCode; unsigned int keyCode; char keys[32]; //bool frameExtentsSet = false; bool extraRedisplayPosted = false; double loopStartTime = Shell_getCurrentTime(); double lastFocusInTime = 0.0; XkbStateRec state; XkbGetState(display, XkbUseCoreKbd, &state); int lastKeyboardGroup = state.group; int lastKeyboardDeviceID = INT_MIN; int xkbEventType = 0; XkbQueryExtension(display, 0, &xkbEventType, 0, 0, 0); XkbSelectEvents(display, XkbUseCoreKbd, XkbNewKeyboardNotifyMask | XkbStateNotifyMask, XkbNewKeyboardNotifyMask | XkbStateNotifyMask); XSync(display, False); for (;;) { while (XPending(display)) { XNextEvent(display, &event); if (coalesceMouseMotionEvents && event.type == MotionNotify) { if (!ignoreMotionNotifyEvents) { lastMouseMoveEvent = event; unignoredMotionNotifyEventCount++; } else { ignoredMotionNotifyEventCount++; } continue; } if (lastMouseMoveEvent.type == MotionNotify) { processMouseMoveEvent(lastMouseMoveEvent); lastMouseMoveEvent.type = 0; } switch (event.type) { case Expose: Shell_redisplay(); /* fall through */ case ConfigureNotify: /*if (!frameExtentsSet) { // Some sources claim XCreateWindow specifies content rect rather than frame rect, // but observed behavior on Ubuntu 20.10 Unity was that it specifies frame rect. // Have to wait for window manager to asynchronously set _NET_FRAME_EXTENTS, then // adjust by that amount to get content rect sizing. Atom actualType; int actualFormat; unsigned long itemCountReturned = 0, itemCountUnread = 0; unsigned long frameExtentsArray[4] = {0, 0, 0, 0}; unsigned long * frameExtents = frameExtentsArray; if (XGetWindowProperty(display, window, XInternAtom(display, "_NET_FRAME_EXTENTS", False), 0, 4, False, AnyPropertyType, &actualType, &actualFormat, &itemCountReturned, &itemCountUnread, (unsigned char **) &frameExtents) == Success) { if (!inFullScreenMode) { XMoveWindow(display, window, initialWindowX - frameExtents[0], initialWindowY - frameExtents[2]); } frameExtentsSet = true; } }*/ XGetWindowAttributes(display, window, &windowAttributes); if (windowAttributes.width != lastWidth || windowAttributes.height != lastHeight) { lastWidth = windowAttributes.width; lastHeight = windowAttributes.height; if (resizeCallback != NULL) { resizeCallback(windowAttributes.width, windowAttributes.height, Shell_getCurrentTime()); } Shell_redisplay(); } if (inFullScreenMode) { // Ubuntu 24.04 seems to force an application that enters full screen at startup back into a window; // if this has happened and ConfigureNotify comes in, reapply fullscreen setFullScreenEWMH(true); } break; case KeyPress: { if (runningDialog) { break; } KeySym keysym = 0; char asciiCharCode = 0; int lookupResult = XLookupString(&event.xkey, &asciiCharCode, 1, &keysym, NULL); if (lookupResult == 0) { charCode = xkb_keysym_to_utf32(keysym); } else { charCode = asciiCharCode; } keyCode = xkeyCodeToShellKeyCode(event.xkey.keycode); switch (keyCode) { case KEY_CODE_CAPS_LOCK: if (event.xkey.state & LockMask) { modifiers &= ~MODIFIER_CAPS_LOCK_BIT; } else { modifiers |= MODIFIER_CAPS_LOCK_BIT; } break; case KEY_CODE_LEFT_SHIFT: case KEY_CODE_RIGHT_SHIFT: modifiers |= MODIFIER_SHIFT_BIT; break; case KEY_CODE_LEFT_CONTROL: case KEY_CODE_RIGHT_CONTROL: modifiers |= MODIFIER_CONTROL_BIT; break; case KEY_CODE_LEFT_ALT: modifiers |= MODIFIER_ALT_BIT; break; case KEY_CODE_RIGHT_ALT: modifiers |= MODIFIER_ALTGR_BIT; break; } double timestamp = XEventTimeToShellTime(event.xkey.time); if (keyCode != 0 && keyCode <= SHELL_KEY_CODE_MAX) { keyStates[keyCode].held = true; if (timestamp == keyStates[keyCode].lastEventTime) { LOG(2, "Key down %u was debounced\n", keyCode); } else if (keyDownCallback != NULL) { keyDownCallback(charCode, keyCode, modifiers, nextKeyDownIsRepeat, timestamp); } nextKeyDownIsRepeat = false; } if (lastModifiers != modifiers) { if (keyModifiersChangedCallback != NULL) { keyModifiersChangedCallback(modifiers, lastModifiers, timestamp); } lastModifiers = modifiers; } break; } case KeyRelease: { if (runningDialog) { break; } if (event.xkey.keycode > 255) { break; } XQueryKeymap(display, keys); if (keys[event.xkey.keycode >> 3] & 1 << (event.xkey.keycode % 8)) { // Xlib sends key ups for key repeats; this one is a repeat, so skip it and mark the next keyDown as a repeat nextKeyDownIsRepeat = true; break; } keyCode = xkeyCodeToShellKeyCode(event.xkey.keycode); switch (keyCode) { case KEY_CODE_LEFT_SHIFT: case KEY_CODE_RIGHT_SHIFT: modifiers &= ~MODIFIER_SHIFT_BIT; break; case KEY_CODE_LEFT_CONTROL: case KEY_CODE_RIGHT_CONTROL: modifiers &= ~MODIFIER_CONTROL_BIT; break; case KEY_CODE_LEFT_ALT: modifiers &= ~MODIFIER_ALT_BIT; break; case KEY_CODE_RIGHT_ALT: modifiers &= ~MODIFIER_ALTGR_BIT; break; } double timestamp = XEventTimeToShellTime(event.xkey.time); if (keyCode != 0) { keyStates[keyCode].held = false; keyStates[keyCode].lastEventTime = timestamp; if (keyUpCallback != NULL) { keyUpCallback(keyCode, modifiers, timestamp); } } if (lastModifiers != modifiers) { if (keyModifiersChangedCallback != NULL) { keyModifiersChangedCallback(modifiers, lastModifiers, timestamp); } lastModifiers = modifiers; } break; } case ButtonPress: deltaX = deltaY = 0; if (isScrollWheelXButtonEvent(event.xbutton.button, &deltaX, &deltaY)) { if (scrollWheelCallback != NULL) { scrollWheelCallback(event.xbutton.x, event.xbutton.y, deltaX, deltaY, buttonMask, modifiers, event.xbutton.time / 1000.0); } } else { double timestamp = XEventTimeToShellTime(event.xbutton.time); if (!sendActivationClicks && timestamp <= lastFocusInTime) { break; } if (showCursorOnNextMouseMove) { setShellCursor(lastUnhiddenCursor); showCursorOnNextMouseMove = false; if (cursorUnhiddenCallback != NULL) { cursorUnhiddenCallback(false, timestamp); } } unsigned int shellButtonIndex = translateXButtonToShellButtonIndex(event.xbutton.button); buttonMask |= 1 << shellButtonIndex; if (mouseDownCallback != NULL) { float reportedX, reportedY; if (mouseDeltaMode) { reportedX = restoreMouseX; reportedY = restoreMouseY; } else { reportedX = event.xbutton.x; reportedY = event.xbutton.y; } mouseDownCallback(shellButtonIndex, buttonMask, reportedX, reportedY, modifiers, timestamp); } } break; case ButtonRelease: if (!isScrollWheelXButtonEvent(event.xbutton.button, &deltaX, &deltaY)) { unsigned int shellButtonIndex = translateXButtonToShellButtonIndex(event.xbutton.button); buttonMask &= ~(1 << shellButtonIndex); if (mouseUpCallback != NULL) { float reportedX, reportedY; if (mouseDeltaMode) { reportedX = restoreMouseX; reportedY = restoreMouseY; } else { reportedX = event.xbutton.x; reportedY = event.xbutton.y; } mouseUpCallback(shellButtonIndex, buttonMask, reportedX, reportedY, modifiers, event.xbutton.time / 1000.0); } } break; case MotionNotify: if (!coalesceMouseMotionEvents && !ignoreMotionNotifyEvents) { processMouseMoveEvent(event); } break; case LeaveNotify: if (buttonMask == 0 && mouseLeaveCallback != NULL) { mouseLeaveCallback(modifiers, event.xcrossing.time / 1000.0); } break; case FocusIn: updateKeyModifierState(); if (backgrounded && (event.xfocus.mode != NotifyGrab || event.xfocus.detail != NotifyPointer)) { backgrounded = false; double timestamp = Shell_getCurrentTime(); lastFocusInTime = timestamp; if (foregroundedCallback != NULL) { foregroundedCallback(timestamp); } } break; case FocusOut: if (!backgrounded && (event.xfocus.mode != NotifyGrab && event.xfocus.mode != NotifyUngrab)) { backgrounded = true; releaseAllKeys(); if (backgroundedCallback != NULL) { backgroundedCallback(Shell_getCurrentTime()); } } break; case SelectionClear: free(clipboardText); clipboardText = NULL; break; case SelectionRequest: { XSelectionEvent selectionEvent = { .type = SelectionNotify, .requestor = event.xselectionrequest.requestor, .selection = event.xselectionrequest.selection, .target = event.xselectionrequest.target, .time = event.xselectionrequest.time }; Atom utf8Atom = XInternAtom(display, "UTF8_STRING", False); if (event.xselectionrequest.target != utf8Atom || event.xselectionrequest.property == None) { selectionEvent.property = None; } else { selectionEvent.property = event.xselectionrequest.property, XChangeProperty(display, event.xselectionrequest.requestor, event.xselectionrequest.property, utf8Atom, 8, PropModeReplace, (unsigned char *) clipboardText, strlen(clipboardText)); } XSendEvent(display, event.xselectionrequest.requestor, True, NoEventMask, (XEvent *) &selectionEvent); break; } case ClientMessage: if (event.xclient.message_type == dialogClosedAtom) { runningDialog = false; } else if (event.xclient.message_type == ignoreNextMotionNotifyAtom) { ignoreMotionNotifyEvents = event.xclient.data.l[0] == 1; lastMouseX = event.xclient.data.l[1]; lastMouseY = event.xclient.data.l[2]; if (ignoreMotionNotifyEvents) { ignoredMotionNotifyEventCount = 0; } else { unignoredMotionNotifyEventCount = 0; } } else if (event.xclient.message_type == mouseDeltaModeAtom) { eventQueueSynchronizedMouseDeltaMode = event.xclient.data.l[0]; } else if ((Atom) event.xclient.data.l[0] == deleteWindowAtom) { if (confirmQuitCallback != NULL && !confirmQuitCallback(Shell_getCurrentTime())) { XRaiseWindow(display, window); break; } glXMakeCurrent(display, None, NULL); glXDestroyContext(display, context); XDestroyWindow(display, window); XCloseDisplay(display); exit(EXIT_SUCCESS); } break; } if (event.type == xkbEventType) { XkbEvent * xkbEvent = (XkbEvent *) &event; if (xkbEvent->any.xkb_type == XkbStateNotify) { lastKeyboardDeviceID = xkbEvent->any.device; if (xkbEvent->state.group != lastKeyboardGroup) { lastKeyboardGroup = xkbEvent->state.group; if (keyboardLayoutChangedCallback != NULL) { keyboardLayoutChangedCallback(Shell_getCurrentTime()); } } } else if (xkbEvent->any.xkb_type == XkbNewKeyboardNotify && xkbEvent->new_kbd.device == lastKeyboardDeviceID) { if (keyboardLayoutChangedCallback != NULL) { keyboardLayoutChangedCallback(Shell_getCurrentTime()); } } } } if (lastMouseMoveEvent.type == MotionNotify) { processMouseMoveEvent(lastMouseMoveEvent); lastMouseMoveEvent.type = 0; } bool neededRedisplayBeforeTimers = redisplayNeeded; double currentTime = Shell_getCurrentTime(); for (unsigned int timerIndex = 0; timerIndex < timerCount; timerIndex++) { if (!timers[timerIndex].canceled && (!backgrounded || (timers[timerIndex].flags & TIMER_FLAG_RUN_IN_BACKGROUND)) && (!runningDialog || (timers[timerIndex].flags & TIMER_FLAG_RUN_DURING_FILE_DIALOGS)) && // Note: This function currently never executes during file dialogs currentTime >= timers[timerIndex].nextFireTime) { timers[timerIndex].callback(timers[timerIndex].id, timers[timerIndex].context); if (timers[timerIndex].flags & TIMER_FLAG_REPEAT) { //timers[timerIndex].nextFireTime += timers[timerIndex].interval; // Similar to above line, but ensures that next fire time is in the future timers[timerIndex].nextFireTime += ceil((currentTime - timers[timerIndex].nextFireTime) / timers[timerIndex].interval) * timers[timerIndex].interval; } else { timers[timerIndex].canceled = true; } } if (timers[timerIndex].canceled) { timerCount--; for (unsigned int timerIndex2 = timerIndex; timerIndex2 < timerCount; timerIndex2++) { timers[timerIndex2] = timers[timerIndex2 + 1]; } timerIndex--; } } while (queuedCallbackTail != queuedCallbackHead) { queuedCallbacks[queuedCallbackTail].callback(queuedCallbacks[queuedCallbackTail].context); queuedCallbackTail = (queuedCallbackTail + 1) % QUEUED_CALLBACK_MAX; unsigned char byte; read(pselectPipe[0], &byte, 1); } currentTime = Shell_getCurrentTime(); if (redisplayNeeded) { if (mainLoopTargetInterval > 0.0) { double timeAllocationUsed = fmod(currentTime - loopStartTime, mainLoopTargetInterval); struct timespec sleepTimeSpec = {.tv_sec = 0, .tv_nsec = (mainLoopTargetInterval - timeAllocationUsed) * 1000000000}; struct timespec elapsedTimeSpec = sleepTimeSpec; nanosleep(&sleepTimeSpec, &elapsedTimeSpec); } if (lastDrawTime == 0.0 || !neededRedisplayBeforeTimers) { lastDrawTime = currentTime; } redisplayNeeded = false; if (drawCallback != NULL && drawCallback(currentTime, currentTime - lastDrawTime)) { glXSwapBuffers(display, window); if (redisplayNeeded) { extraRedisplayPosted = false; } else if (!extraRedisplayPosted) { Shell_redisplay(); extraRedisplayPosted = true; } else { extraRedisplayPosted = false; } } lastDrawTime = currentTime; #ifdef DEBUG GLenum error = glGetError(); while (error != GL_NO_ERROR) { fprintf(stderr, "GL error: %s\n", gluErrorString(error)); error = glGetError(); } #endif } currentTime = Shell_getCurrentTime(); if (!redisplayNeeded && !XPending(display)) { sleepEventLoop(currentTime, timerCount > 0); } } } void Shell_redisplay(void) { if (!redisplayNeeded) { lastDrawTime = Shell_getCurrentTime(); } redisplayNeeded = true; } bool Shell_needsDisplay(void) { return redisplayNeeded; } unsigned int Shell_getDisplayCount(void) { return ScreenCount(display); } unsigned int Shell_getDisplayIndexFromWindow(void) { return 0; } void Shell_getDisplayBounds(unsigned int displayIndex, int * outOffsetX, int * outOffsetY, unsigned int * outWidth, unsigned int * outHeight) { if (outOffsetX != NULL) { *outOffsetX = 0; } if (outOffsetY != NULL) { *outOffsetY = 0; } if (outWidth != NULL) { *outWidth = DisplayWidth(display, screen); } if (outHeight != NULL) { *outHeight = DisplayHeight(display, screen); } } double Shell_getDisplayRefreshRate(unsigned int displayIndex) { // Note: XRRGetScreenInfo is extremely slow! Cache the result of this function or otherwise avoid calling it if possible XRRScreenConfiguration * configuration = XRRGetScreenInfo(display, rootWindow); double refreshRate = XRRConfigCurrentRate(configuration); XRRFreeScreenConfigInfo(configuration); return refreshRate; } float Shell_getDisplayScaleFactor(unsigned int displayIndex) { return gdk_screen_get_monitor_scale_factor(gdk_display_get_screen(gdk_display_get_default(), displayIndex), 0); } #define INCHES_PER_MILLIMETER 0.03937007874 float Shell_getDisplayDPI(unsigned int displayIndex) { float monitorHeightInches = gdk_screen_get_monitor_height_mm(gdk_display_get_screen(gdk_display_get_default(), displayIndex), 0) * INCHES_PER_MILLIMETER; if (monitorHeightInches <= 0.0f) { return 72.0f; } return DisplayHeight(display, screen) / monitorHeightInches; } static bool getWindowCardinalProperty(Display * display, Window window, const char * propertyName, unsigned long * outValue) { Atom actualType; int actualFormat; unsigned long dataSize, dataSizeUnread; unsigned char * data; int result = XGetWindowProperty(display, window, XInternAtom(display, propertyName, False), 0, LONG_MAX, False, XA_CARDINAL, &actualType, &actualFormat, &dataSize, &dataSizeUnread, &data); if (result == Success && actualType == XA_CARDINAL) { *outValue = *(unsigned long *) data; XFree(data); return true; } return false; } void Shell_getSafeWindowRect(unsigned int displayIndex, int * outOffsetX, int * outOffsetY, unsigned int * outWidth, unsigned int * outHeight) { Atom workareaAtom = XInternAtom(display, "_NET_WORKAREA", False); Window rootWindow = XRootWindow(display, displayIndex); Atom actualType; int actualFormat; unsigned long dataSize, dataSizeUnread; unsigned char * data; int result = XGetWindowProperty(display, rootWindow, workareaAtom, 0, LONG_MAX, False, AnyPropertyType, &actualType, &actualFormat, &dataSize, &dataSizeUnread, &data); int x = 0; int y = 0; unsigned int width = DisplayWidth(display, screen); unsigned int height = DisplayHeight(display, screen); if (result == Success && actualType != None && actualFormat != 0 && dataSizeUnread == 0 && dataSize % 4 == 0) { unsigned long currentDesktop = 0; getWindowCardinalProperty(display, rootWindow, "_NET_CURRENT_DESKTOP", ¤tDesktop); if (currentDesktop < dataSize / 4) { long * workareas = (long *) data; x = workareas[currentDesktop * 4]; y = workareas[currentDesktop * 4 + 1]; width = workareas[currentDesktop * 4 + 2]; height = workareas[currentDesktop * 4 + 3]; } } height -= rootFrameExtents[2] * Shell_getDisplayScaleFactor(0); if (outOffsetX != NULL) { *outOffsetX = x; } if (outOffsetY != NULL) { *outOffsetY = y; } if (outWidth != NULL) { *outWidth = width; } if (outHeight != NULL) { *outHeight = height; } } bool Shell_isFullScreen(void) { return inFullScreenMode; } #define loadDBusSymbol2(symbol, dbus_symbol) \ dbusInterface.symbol = dlsym(dbusLibrary, #dbus_symbol); \ if (dbusInterface.symbol == NULL) { \ dbusLoadStatus = DBUS_LOAD_STATUS_FAILED; \ return; \ } #define loadDBusSymbol(symbol) loadDBusSymbol2(symbol, dbus_##symbol) static void initDBus(void) { if (dbusLoadStatus == DBUS_LOAD_STATUS_UNINITIALIZED) { dbusLoadStatus = DBUS_LOAD_STATUS_INITIALIZING; void * dbusLibrary = dlopen("libdbus-1.so.3", RTLD_NOW | RTLD_LOCAL); if (dbusLibrary == NULL) { dbusLoadStatus = DBUS_LOAD_STATUS_FAILED; return; } loadDBusSymbol(bus_get_private); loadDBusSymbol(message_new_method_call); loadDBusSymbol(message_append_args_valist); loadDBusSymbol(connection_set_exit_on_disconnect); loadDBusSymbol(connection_send); loadDBusSymbol(connection_send_with_reply_and_block); loadDBusSymbol(connection_close); loadDBusSymbol(connection_unref); loadDBusSymbol(connection_flush); loadDBusSymbol(message_get_args_valist); loadDBusSymbol(message_unref); loadDBusSymbol(threads_init_default); loadDBusSymbol(error_init); loadDBusSymbol(error_is_set); loadDBusSymbol(error_free); if (!dbusInterface.threads_init_default()) { dbusLoadStatus = DBUS_LOAD_STATUS_FAILED; return; } DBusError error; dbusInterface.error_init(&error); dbusInterface.connection = dbusInterface.bus_get_private(DBUS_BUS_SESSION, &error); if (dbusInterface.error_is_set(&error)) { dbusInterface.error_free(&error); dbusLoadStatus = DBUS_LOAD_STATUS_FAILED; return; } dbusInterface.connection_set_exit_on_disconnect(dbusInterface.connection, 0); dbusInterface.error_free(&error); dbusLoadStatus = DBUS_LOAD_STATUS_SUCCEEDED; } } static bool callDBusMethodInternal(DBusConnection * connection, const char * node, const char * path, const char * interface, const char * method, va_list argsMessage) { bool success = false; if (connection != NULL) { DBusMessage * message = dbusInterface.message_new_method_call(node, path, interface, method); if (message != NULL) { va_list argsReply; va_copy(argsReply, argsMessage); int argument = va_arg(argsMessage, int); if (argument == DBUS_TYPE_INVALID || dbusInterface.message_append_args_valist(message, argument, argsMessage)) { DBusMessage * reply = dbusInterface.connection_send_with_reply_and_block(connection, message, 300, NULL); if (reply != NULL) { while ((argument = va_arg(argsReply, int)) != DBUS_TYPE_INVALID) { void * discard = va_arg(argsReply, void *); (void) discard; if (argument == DBUS_TYPE_ARRAY) { int discard = va_arg(argsReply, int); (void) discard; } } argument = va_arg(argsReply, int); if (argument == DBUS_TYPE_INVALID || dbusInterface.message_get_args_valist(reply, NULL, argument, argsReply)) { success = true; } dbusInterface.message_unref(reply); } } va_end(argsReply); dbusInterface.message_unref(message); } } return success; } static bool callDBusMethod(const char * node, const char * path, const char * interface, const char * method, ...) { va_list args; va_start(args, method); bool success = callDBusMethodInternal(dbusInterface.connection, node, path, interface, method, args); va_end(args); return success; } static bool callDBusVoidMethodInternal(DBusConnection * connection, const char * node, const char * path, const char * interface, const char * method, va_list argsMessage) { bool success = false; if (connection != NULL) { DBusMessage * message = dbusInterface.message_new_method_call(node, path, interface, method); if (message != NULL) { int argument = va_arg(argsMessage, int); if (argument == DBUS_TYPE_INVALID || dbusInterface.message_append_args_valist(message, argument, argsMessage)) { if (dbusInterface.connection_send(connection, message, NULL)) { dbusInterface.connection_flush(connection); success = true; } } dbusInterface.message_unref(message); } } return success; } static bool callDBusVoidMethod(const char * node, const char * path, const char * interface, const char * method, ...) { va_list args; va_start(args, method); bool success = callDBusVoidMethodInternal(dbusInterface.connection, node, path, interface, method, args); va_end(args); return success; } void Shell_setDisplaySleepEnabled(bool enabled) { // This appears not to work; have to use dbus instead /*if (enabled && screenSaverSuspended) { XScreenSaverSuspend(display, False); XResetScreenSaver(display); screenSaverSuspended = false; } else if (!enabled && !screenSaverSuspended) { XScreenSaverSuspend(display, True); XResetScreenSaver(display); screenSaverSuspended = true; }*/ initDBus(); if (dbusLoadStatus != DBUS_LOAD_STATUS_SUCCEEDED) { #ifdef DEBUG if (!enabled) { fprintf(stderr, "Warning: Couldn't init dbus interface; display sleep cannot be disabled\n"); } #endif return; } bool alreadyEnabled = screensaverCookie == 0; if (alreadyEnabled == enabled) { return; } const char * node = "org.freedesktop.ScreenSaver"; const char * path = "/org/freedesktop/ScreenSaver"; const char * interface = "org.freedesktop.ScreenSaver"; if (enabled) { callDBusVoidMethod(node, path, interface, "UnInhibit", DBUS_TYPE_UINT32, &screensaverCookie, DBUS_TYPE_INVALID); screensaverCookie = 0; } else { const char * applicationString = configuration.applicationName; const char * reasonString = "Screensaver disabled while running"; callDBusMethod(node, path, interface, "Inhibit", DBUS_TYPE_STRING, &applicationString, DBUS_TYPE_STRING, &reasonString, DBUS_TYPE_INVALID, DBUS_TYPE_UINT32, &screensaverCookie, DBUS_TYPE_INVALID); } } void Shell_setDrawThrottleRate(double targetInterval) { mainLoopTargetInterval = targetInterval; } static void setVSync(bool sync) { const char * extensions = glXQueryExtensionsString(glXGetCurrentDisplay(), 0); if (strstr(extensions, "GLX_EXT_swap_control")) { PFNGLXSWAPINTERVALEXTPROC glXSwapIntervalEXT = (PFNGLXSWAPINTERVALEXTPROC) glXGetProcAddress((const GLubyte *) "glXSwapIntervalEXT"); glXSwapIntervalEXT(glXGetCurrentDisplay(), glXGetCurrentDrawable(), sync); } else if (strstr(extensions, "GLX_SGI_swap_control")) { PFNGLXSWAPINTERVALSGIPROC glxSwapIntervalSGI = (PFNGLXSWAPINTERVALSGIPROC) glXGetProcAddress((const GLubyte *) "glXSwapIntervalSGI"); glxSwapIntervalSGI(sync); } } bool Shell_enterFullScreen(unsigned int displayIndex, bool captureDisplay) { if (!inFullScreenMode) { if (!setFullScreenEWMH(true)) { return false; } setVSync(vsyncFullscreen); inFullScreenMode = true; } return true; } void Shell_exitFullScreen(void) { if (inFullScreenMode) { if (!setFullScreenEWMH(false)) { return; } inFullScreenMode = false; setVSync(vsyncWindow); } } double Shell_getCurrentTime(void) { struct timespec currentTime; clock_gettime(CLOCK_MONOTONIC, ¤tTime); return currentTime.tv_sec + currentTime.tv_nsec * 0.000000001; } #define RESOURCE_PATH_MAX 2048 const char * Shell_getResourcePath(void) { static char * resourcePath; if (resourcePath == NULL) { resourcePath = malloc(RESOURCE_PATH_MAX); ssize_t bytesRead = readlink("/proc/self/exe", resourcePath, RESOURCE_PATH_MAX - 1); int charIndex; if (bytesRead == -1) { charIndex = 0; } else { for (charIndex = bytesRead - 1; charIndex > 0; charIndex--) { if (resourcePath[charIndex] == '/') {\ charIndex++; break; } } } strncpy(resourcePath + charIndex, "Resources", RESOURCE_PATH_MAX - charIndex); } return resourcePath; } const char * Shell_getSupportPath(const char * subdirectory) { static char supportPath[PATH_MAX]; if (subdirectory == NULL) { snprintf(supportPath, PATH_MAX, "%s/.local/share", getenv("HOME")); } else { snprintf(supportPath, PATH_MAX, "%s/.local/share/%s", getenv("HOME"), subdirectory); } mkdir(supportPath, 0777); return supportPath; } void Shell_openURL(const char * url) { if (!fork()) { execl("/usr/bin/x-www-browser", "/usr/bin/x-www-browser", url, NULL); exit(EXIT_SUCCESS); } } struct batteryInfo { enum ShellBatteryState state; float level; }; static bool readNextKeyValuePair(size_t * ioPosition, const char * contents, size_t length, char * outKey, size_t keySize, char * outValue, size_t valueSize) { size_t charIndex, lastCharIndex, tokenLength; lastCharIndex = *ioPosition; while (lastCharIndex < length && isspace(contents[lastCharIndex])) { lastCharIndex++; } if (lastCharIndex >= length) { return false; } charIndex = lastCharIndex; while (charIndex < length && contents[charIndex] != ':') { charIndex++; } if (charIndex >= length) { return false; } tokenLength = charIndex - lastCharIndex; if (tokenLength > keySize) { tokenLength = keySize - 1; } memcpy(outKey, contents + lastCharIndex, tokenLength); outKey[tokenLength] = '\x00'; lastCharIndex = charIndex + 1; while (lastCharIndex < length && isspace(contents[lastCharIndex])) { lastCharIndex++; } if (lastCharIndex >= length) { return false; } charIndex = lastCharIndex; while (charIndex < length && contents[charIndex] != '\n') { charIndex++; } if (charIndex >= length) { return false; } tokenLength = charIndex - lastCharIndex; if (tokenLength > valueSize) { tokenLength = valueSize - 1; } memcpy(outValue, contents + lastCharIndex, tokenLength); outValue[tokenLength] = '\x00'; *ioPosition = charIndex + 1; return true; } static char * readVirtualFile(const char * path, size_t * outLength) { FILE * file; int byte; size_t allocatedSize = 16; char * data; size_t length = 0; file = fopen(path, "rb"); if (file == NULL) { return NULL; } data = malloc(allocatedSize); for (;;) { byte = fgetc(file); if (feof(file)) { break; } if (length >= allocatedSize) { allocatedSize *= 2; data = realloc(data, allocatedSize); } data[length++] = byte; } fclose(file); *outLength = length; return data; } #define KEY_SIZE 24 #define VALUE_SIZE 16 static struct batteryInfo getBatteryInfo(void) { struct batteryInfo info = {ShellBatteryState_unknown, -1.0f}; DIR * dir; struct dirent * dirent; char path[PATH_MAX]; unsigned int batteryNumber; size_t length, position; char * contents; char key[KEY_SIZE], value[VALUE_SIZE]; bool present = true; unsigned int lastFullCapacity_mWh = 0; unsigned int remainingCapacity_mWh = 0; // TODO: /proc/acpi is deprecated; use /sys/class/power_supply (see https://askubuntu.com/questions/69556/how-do-i-check-the-batterys-status-via-the-terminal) dir = opendir("/proc/acpi"); if (dir == NULL) { return info; } closedir(dir); dir = opendir("/proc/acpi/battery"); if (dir == NULL) { info.state = ShellBatteryState_notBatteryPowered; return info; } while ((dirent = readdir(dir)) != NULL) { if (sscanf(dirent->d_name, "BAT%u", &batteryNumber)) { snprintf(path, PATH_MAX, "/proc/acpi/battery/BAT%u/info", batteryNumber); contents = readVirtualFile(path, &length); if (contents == NULL) { continue; } position = 0; for (;;) { if (!readNextKeyValuePair(&position, contents, length, key, KEY_SIZE, value, VALUE_SIZE)) { break; } if (!strcmp(key, "present")) { if (!strcmp(value, "no")) { present = false; break; } } else if (!strcmp(key, "last full capacity")) { sscanf(value, "%u mWh", &lastFullCapacity_mWh); } } free(contents); if (!present) { info.state = ShellBatteryState_batteryMissing; break; } snprintf(path, PATH_MAX, "/proc/acpi/battery/BAT%u/state", batteryNumber); contents = readVirtualFile(path, &length); if (contents == NULL) { continue; } position = 0; for (;;) { if (!readNextKeyValuePair(&position, contents, length, key, KEY_SIZE, value, VALUE_SIZE)) { break; } if (!strcmp(key, "present")) { if (!strcmp(value, "no")) { present = false; break; } } else if (!strcmp(key, "remaining capacity")) { sscanf(value, "%u mWh", &remainingCapacity_mWh); } else if (!strcmp(key, "charging state")) { if (!strcmp(value, "discharging")) { info.state = ShellBatteryState_unplugged; } else if (!strcmp(value, "charging")) { info.state = ShellBatteryState_charging; } else if (!strcmp(value, "charged")) { info.state = ShellBatteryState_full; } else { info.state = ShellBatteryState_unknown; } } } free(contents); if (!present) { info.state = ShellBatteryState_batteryMissing; break; } if (lastFullCapacity_mWh > 0) { if (remainingCapacity_mWh >= lastFullCapacity_mWh) { info.level = 1.0f; } else { info.level = remainingCapacity_mWh / (float) lastFullCapacity_mWh; } } // Only support for one battery break; } } closedir(dir); return info; } enum ShellBatteryState Shell_getBatteryState(void) { return getBatteryInfo().state; } float Shell_getBatteryLevel(void) { return getBatteryInfo().level; } unsigned int Shell_setTimer(double interval, unsigned int flags, void (* callback)(unsigned int timerID, void * context), void * context) { timers = realloc(timers, (timerCount + 1) * sizeof(*timers)); timers[timerCount].interval = interval; timers[timerCount].nextFireTime = Shell_getCurrentTime() + interval; timers[timerCount].flags = flags; timers[timerCount].canceled = false; timers[timerCount].id = nextTimerID++; timers[timerCount].callback = callback; timers[timerCount].context = context; return timers[timerCount++].id; } void Shell_cancelTimer(unsigned int timerID) { for (unsigned int timerIndex = 0; timerIndex < timerCount; timerIndex++) { if (timers[timerIndex].id == timerID) { timers[timerIndex].canceled = true; break; } } } void Shell_setCursorVisible(bool visible) { if (visible && cursorHiddenByHide) { cursorHiddenByHide = false; if (!mouseDeltaMode) { setShellCursor(lastUnhiddenCursor); } } else if (!visible && !cursorHiddenByHide) { cursorHiddenByHide = true; setEmptyCursor(); } } void Shell_hideCursorUntilMouseMoves(void) { if (!cursorHiddenByHide) { showCursorOnNextMouseMove = true; mouseDeltaXSinceHide = 0.0f; mouseDeltaYSinceHide = 0.0f; setEmptyCursor(); } } void Shell_setUnhideCursorMovementThreshold(float threshold) { cursorUnhideThreshold = threshold; } void Shell_setCursor(int cursor) { lastUnhiddenCursor = cursor; if (cursorHiddenByHide || mouseDeltaMode) { return; } setShellCursor(cursor); } ShellCursorID Shell_registerCustomCursor(unsigned int representationCount, struct ShellCustomCursorImage * representations, unsigned int hotspotX, unsigned int hotspotY) { customCursors = realloc(customCursors, sizeof(*customCursors) * (customCursorCount + 1)); customCursors[customCursorCount].representationCount = representationCount; customCursors[customCursorCount].representations = malloc(sizeof(*customCursors[customCursorCount].representations) * customCursors[customCursorCount].representationCount); for (unsigned int representationIndex = 0; representationIndex < representationCount; representationIndex++) { customCursors[customCursorCount].representations[representationIndex].scaleFactor = representations[representationIndex].scaleFactor; XcursorImage * image = XcursorImageCreate(representations[representationIndex].width, representations[representationIndex].height); image->xhot = hotspotX * representations[representationIndex].scaleFactor; image->yhot = hotspotY * representations[representationIndex].scaleFactor; image->delay = 0; unsigned char * pixels = representations[representationIndex].pixels; for (unsigned int rowIndex = 0; rowIndex < representations[representationIndex].height; rowIndex++) { for (unsigned int columnIndex = 0; columnIndex < representations[representationIndex].width; columnIndex++) { unsigned int pixelIndex = rowIndex * representations[representationIndex].width + columnIndex; unsigned int alpha = pixels[pixelIndex * 4 + 3]; unsigned int red = pixels[pixelIndex * 4 + 0]; unsigned int green = pixels[pixelIndex * 4 + 1]; unsigned int blue = pixels[pixelIndex * 4 + 2]; XcursorPixel argbPixel = alpha << 24 | (red * alpha / 0xFF) << 16 | (green * alpha / 0xFF) << 8 | (blue * alpha / 0xFF); image->pixels[pixelIndex] = argbPixel; } } customCursors[customCursorCount].representations[representationIndex].cursor = XcursorImageLoadCursor(display, image); XcursorImageDestroy(image); } return CUSTOM_CURSOR_ID_OFFSET + customCursorCount++; } void Shell_getMousePosition(float * outX, float * outY) { if (outX != NULL) { *outX = lastMouseX; } if (outY != NULL) { *outY = lastMouseY; } } void Shell_setMousePosition(float x, float y, bool sendEvent) { if (sendEvent) { XWarpPointer(display, None, window, 0, 0, 0, 0, x, y); } else { warpPointerAndIgnoreEvent(x, y); } } void Shell_setMouseDeltaMode(bool deltaMode) { if (!mouseDeltaMode && deltaMode) { setEmptyCursor(); restoreMouseX = lastMouseX; restoreMouseY = lastMouseY; XWindowAttributes windowAttributes; XGetWindowAttributes(display, window, &windowAttributes); warpPointerAndIgnoreEvent(windowAttributes.width / 2, windowAttributes.height / 2); mouseDeltaMode = true; sendClientMessage(mouseDeltaModeAtom, 1, 0, 0); } else if (mouseDeltaMode && !deltaMode) { warpPointerAndIgnoreEvent(restoreMouseX, restoreMouseY); mouseDeltaMode = false; sendClientMessage(mouseDeltaModeAtom, 0, 0, 0); setShellCursor(lastUnhiddenCursor); } } bool Shell_getMouseDeltaMode(void) { return mouseDeltaMode; } void Shell_setVSync(bool sync, bool fullscreen) { if (fullscreen) { vsyncFullscreen = sync; if (inFullScreenMode) { setVSync(sync); } } else { vsyncWindow = sync; if (!inFullScreenMode) { setVSync(sync); } } } static void addFileDialogFilters(GtkFileChooser * chooser, unsigned int filterCount, struct ShellFileDialogFilter * filters) { if (filterCount > 0) { for (unsigned int filterIndex = 0; filterIndex < filterCount; filterIndex++) { GtkFileFilter * fileFilter = gtk_file_filter_new(); for (unsigned int extensionIndex = 0; extensionIndex < filters[filterIndex].fileExtensionCount; extensionIndex++) { char filterWildcardString[256]; snprintf(filterWildcardString, sizeof(filterWildcardString), "*.%s", filters[filterIndex].fileExtensions[extensionIndex]); gtk_file_filter_add_pattern(fileFilter, filterWildcardString); } gtk_file_filter_set_name(fileFilter, filters[filterIndex].filterName); gtk_file_chooser_add_filter(chooser, fileFilter); } GtkFileFilter * fileFilter = gtk_file_filter_new(); gtk_file_filter_add_pattern(fileFilter, "*"); gtk_file_filter_set_name(fileFilter, "All files"); gtk_file_chooser_add_filter(chooser, fileFilter); } } static gint runGTKDialog(GtkWidget * dialog) { GdkWindow * dialogWindow = gtk_widget_get_window(dialog); if (dialogWindow == NULL) { gtk_widget_show(dialog); dialogWindow = gtk_widget_get_window(dialog); } if (dialogWindow != NULL && GDK_IS_X11_WINDOW(dialogWindow)) { XSetTransientForHint(display, GDK_WINDOW_XID(dialogWindow), window); } setShellCursor(lastUnhiddenCursor); XFlush(display); // TODO: May be able to run manually to oblige TIMER_FLAG_RUN_DURING_FILE_DIALOGS // https://discourse.gnome.org/t/how-should-i-replace-a-gtk-dialog-run-in-gtk-4/3501 // https://stackoverflow.com/questions/23037921/manually-run-gtk-event-loop gint result = gtk_dialog_run(GTK_DIALOG(dialog)); if (cursorHiddenByHide || mouseDeltaMode) { setEmptyCursor(); } if (showCursorOnNextMouseMove) { showCursorOnNextMouseMove = false; if (cursorUnhiddenCallback != NULL) { cursorUnhiddenCallback(true, Shell_getCurrentTime()); } } return result; } static void postDialogClosedEvent() { XEvent event; event.type = ClientMessage; event.xclient.window = window; event.xclient.message_type = dialogClosedAtom; event.xclient.format = 32; event.xclient.data.l[0] = 0; event.xclient.data.l[1] = 0; event.xclient.data.l[2] = 0; event.xclient.data.l[3] = 0; event.xclient.data.l[4] = 0; XSendEvent(display, window, 0, 0, &event); } const char * Shell_openFileDialog(const char * title, const char * defaultDirectory, const char * defaultFileName, unsigned int filterCount, struct ShellFileDialogFilter * filters) { GtkWidget * dialog; GtkFileChooser * chooser; bool success = false; static char * returnedFilePath; dialog = gtk_file_chooser_dialog_new(title, NULL, GTK_FILE_CHOOSER_ACTION_OPEN, "_Cancel", GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, NULL); chooser = GTK_FILE_CHOOSER(dialog); addFileDialogFilters(chooser, filterCount, filters); if (defaultDirectory != NULL) { if (defaultFileName != NULL) { char defaultFilePath[4096]; snprintf(defaultFilePath, sizeof(defaultFilePath), "%s/%s", defaultDirectory, defaultFileName); GFile * gfile = g_file_new_for_path(defaultFilePath); if (!gtk_file_chooser_set_file(chooser, gfile, NULL)) { gtk_file_chooser_set_current_folder(chooser, defaultDirectory); } g_object_unref(gfile); } else { gtk_file_chooser_set_current_folder(chooser, defaultDirectory); } } runningDialog = true; if (runGTKDialog(dialog) == GTK_RESPONSE_ACCEPT) { char * filename = gtk_file_chooser_get_filename(chooser); free(returnedFilePath); returnedFilePath = strdup(filename); g_free(filename); success = true; } gtk_widget_destroy(dialog); while (gtk_events_pending()) { gtk_main_iteration(); } postDialogClosedEvent(); updateKeyModifierState(); if (!success) { return NULL; } return returnedFilePath; } const char ** Shell_openFileDialogMultiple(const char * title, const char * defaultDirectory, unsigned int defaultFileNameCount, const char ** defaultFileNames, unsigned int * outFilePathCount, unsigned int filterCount, struct ShellFileDialogFilter * filters) { GtkWidget * dialog; GtkFileChooser * chooser; bool success = false; static unsigned int returnedFilePathCount; static char ** returnedFilePaths; dialog = gtk_file_chooser_dialog_new(title, NULL, GTK_FILE_CHOOSER_ACTION_OPEN, "_Cancel", GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, NULL); chooser = GTK_FILE_CHOOSER(dialog); gtk_file_chooser_set_select_multiple(chooser, TRUE); addFileDialogFilters(chooser, filterCount, filters); if (defaultDirectory != NULL) { gtk_file_chooser_set_current_folder(chooser, defaultDirectory); for (unsigned int fileNameIndex = 0; fileNameIndex < defaultFileNameCount; fileNameIndex++) { gtk_file_chooser_select_filename(chooser, defaultFileNames[fileNameIndex]); } } runningDialog = true; if (runGTKDialog(dialog) == GTK_RESPONSE_ACCEPT) { GSList * fileList = gtk_file_chooser_get_filenames(chooser); for (unsigned int filePathIndex = 0; filePathIndex < returnedFilePathCount; filePathIndex++) { free(returnedFilePaths[filePathIndex]); } free(returnedFilePaths); returnedFilePathCount = g_slist_length(fileList); returnedFilePaths = malloc(returnedFilePathCount * sizeof(*returnedFilePaths)); returnedFilePathCount = 0; for (GSList * listNode = fileList; listNode != NULL; listNode = listNode->next) { returnedFilePaths[returnedFilePathCount++] = strdup(listNode->data); g_free(listNode->data); } g_slist_free(fileList); *outFilePathCount = returnedFilePathCount; success = true; } gtk_widget_destroy(dialog); while (gtk_events_pending()) { gtk_main_iteration(); } postDialogClosedEvent(); updateKeyModifierState(); if (!success) { return NULL; } return (const char **) returnedFilePaths; } const char * Shell_chooseFolderDialog(const char * title, const char * defaultDirectory) { GtkWidget * dialog; GtkFileChooser * chooser; bool success = false; static char * returnedFolderPath; dialog = gtk_file_chooser_dialog_new(title, NULL, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, "_Cancel", GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, NULL); chooser = GTK_FILE_CHOOSER(dialog); gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE); if (defaultDirectory != NULL) { gtk_file_chooser_set_current_folder(chooser, defaultDirectory); } runningDialog = true; if (runGTKDialog(dialog) == GTK_RESPONSE_ACCEPT) { char * filename = gtk_file_chooser_get_filename(chooser); free(returnedFolderPath); returnedFolderPath = strdup(filename); g_free(filename); success = true; } gtk_widget_destroy(dialog); while (gtk_events_pending()) { gtk_main_iteration(); } postDialogClosedEvent(); updateKeyModifierState(); if (!success) { return NULL; } return returnedFolderPath; } const char * Shell_saveFileDialog(const char * title, const char * defaultDirectory, const char * defaultFileName, unsigned int filterCount, struct ShellFileDialogFilter * filters) { GtkWidget * dialog; GtkFileChooser * chooser; bool success = false; static char * returnedFilePath; dialog = gtk_file_chooser_dialog_new(title, NULL, GTK_FILE_CHOOSER_ACTION_SAVE, "Cancel", GTK_RESPONSE_CANCEL, "Save", GTK_RESPONSE_ACCEPT, NULL); chooser = GTK_FILE_CHOOSER(dialog); addFileDialogFilters(chooser, filterCount, filters); gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE); if (defaultDirectory != NULL) { gtk_file_chooser_set_current_folder(chooser, defaultDirectory); } if (defaultFileName != NULL) { gtk_file_chooser_set_current_name(chooser, defaultFileName); } runningDialog = true; if (runGTKDialog(dialog) == GTK_RESPONSE_ACCEPT) { char * filename; filename = gtk_file_chooser_get_filename(chooser); free(returnedFilePath); returnedFilePath = strdup(filename); g_free(filename); success = true; } while (gtk_events_pending()) { gtk_main_iteration(); } gtk_widget_destroy(dialog); while (gtk_events_pending()) { gtk_main_iteration(); } postDialogClosedEvent(); updateKeyModifierState(); if (!success) { return NULL; } return returnedFilePath; } bool Shell_copyTextToClipboard(const char * text) { free(clipboardText); clipboardText = strdup(text); XSetSelectionOwner(display, XInternAtom(display, "CLIPBOARD", False), window, CurrentTime); return true; } #define PASTE_SIZE_MAX 0x1000000 const char * Shell_getClipboardText(void) { Atom selectionAtom, utf8Atom, targetPropertyAtom; Window owner; bool success = false; XEvent * delayedEvents = NULL; unsigned int delayedEventCount = 0; static char * lastPaste; selectionAtom = XInternAtom(display, "CLIPBOARD", False); owner = XGetSelectionOwner(display, selectionAtom); if (owner == None) { return NULL; } if (owner == window) { return clipboardText; } targetPropertyAtom = XInternAtom(display, "PASTE_BUFFER", False); utf8Atom = XInternAtom(display, "UTF8_STRING", False); XConvertSelection(display, selectionAtom, utf8Atom, targetPropertyAtom, window, CurrentTime); double timeoutTime = Shell_getCurrentTime() + 1.0; do { XEvent event; if (XPending(display)) { XNextEvent(display, &event); if (event.type == SelectionNotify) { if (event.xselection.property != None) { success = true; } break; } delayedEvents = realloc(delayedEvents, (delayedEventCount + 1) * sizeof(*delayedEvents)); delayedEvents[delayedEventCount++] = event; } if (!XPending(display)) { sleepEventLoop(0.0, false); } } while (Shell_getCurrentTime() < timeoutTime); for (unsigned int eventIndex = 0; eventIndex < delayedEventCount; eventIndex++) { XPutBackEvent(display, &delayedEvents[eventIndex]); } free(delayedEvents); if (!success) { return NULL; } Atom actualType; int actualFormat; unsigned long dataSize, dataSizeUnread; unsigned char * data; if (XGetWindowProperty(display, window, targetPropertyAtom, 0, PASTE_SIZE_MAX, False, AnyPropertyType, &actualType, &actualFormat, &dataSize, &dataSizeUnread, &data) != Success) { return NULL; } if (actualType != utf8Atom) { XFree(data); return NULL; } free(lastPaste); lastPaste = malloc(dataSize + 1); memcpy(lastPaste, data, dataSize); lastPaste[dataSize] = '\0'; XFree(data); return lastPaste; } unsigned int Shell_getModifierKeys(void) { return modifiers; } unsigned int Shell_getButtonMask(void) { return buttonMask; } static int shellModifiersToXkbLevel(unsigned int modifiers) { if (modifiers & MODIFIER_ALTGR_BIT) { if (modifiers & MODIFIER_SHIFT_BIT) { return 3; } return 2; } if (modifiers & MODIFIER_SHIFT_BIT) { return 1; } return 0; } unsigned int Shell_getKeyLabel(unsigned int keyCode, unsigned int modifiers) { XkbStateRec state; XkbGetState(display, XkbUseCoreKbd, &state); int level = shellModifiersToXkbLevel(modifiers); if (keyCode == KEY_CODE_NUMPAD_PERIOD) { level = 1; } KeySym keysym = XkbKeycodeToKeysym(display, shellKeyCodeToXKeyCode(keyCode), state.group, level); if (modifiers & MODIFIER_CAPS_LOCK_BIT) { KeySym lowerKeysym; XConvertCase(keysym, &lowerKeysym, &keysym); } return xkb_keysym_to_utf32(keysym); } const char * Shell_getKeyboardLayoutName(void) { XkbStateRec state; XkbGetState(display, XkbUseCoreKbd, &state); XkbDescPtr desc = XkbGetKeyboard(display, XkbAllComponentsMask, XkbUseCoreKbd); return XGetAtomName(display, desc->names->groups[state.group]); } void Shell_systemBeep(void) { XBell(display, 100); } static void updateWindowTitle(void) { if (windowCreated) { if (documentEdited) { char adornedTitle[256]; snprintf(adornedTitle, sizeof(adornedTitle), "*%s", windowTitle); XStoreName(display, window, adornedTitle); } else { XStoreName(display, window, windowTitle); } } } void Shell_setWindowTitle(const char * title) { free(windowTitle); windowTitle = strdup(title); updateWindowTitle(); } void Shell_setWindowDocumentEdited(bool edited) { documentEdited = edited; updateWindowTitle(); } void Shell_moveWindow(int x, int y) { if (windowCreated) { XMoveWindow(display, window, x, y); } } void Shell_resizeWindow(unsigned int width, unsigned int height) { if (windowCreated) { XResizeWindow(display, window, width, height); } } void Shell_moveResizeWindow(int x, int y, unsigned int width, unsigned int height) { if (windowCreated) { XMoveWindow(display, window, x, y); XResizeWindow(display, window, width, height); } } #ifndef SEM_VALUE_MAX #define SEM_VALUE_MAX 32767 #endif struct threadFuncInvocation { int (* threadFunction)(void * context); void * context; }; static void * callThreadFunc(void * context) { struct threadFuncInvocation * invocation = context; int (* threadFunction)(void * context); void * threadContext; threadFunction = invocation->threadFunction; threadContext = invocation->context; free(invocation); return (void *) threadFunction(threadContext); } ShellThread Shell_createThread(int (* threadFunction)(void * context), void * context) { struct threadFuncInvocation * invocation = malloc(sizeof(*invocation)); invocation->threadFunction = threadFunction; invocation->context = context; pthread_t thread; pthread_create(&thread, NULL, callThreadFunc, invocation); return (ShellThread) thread; } void Shell_detachThread(ShellThread thread) { pthread_detach((pthread_t) thread); } int Shell_joinThread(ShellThread thread) { int status; void * returnValue; status = pthread_join((pthread_t) thread, &returnValue); if (status == 0) { return (int) returnValue; } return status; } void Shell_exitThread(int statusCode) { pthread_exit((void *) statusCode); } ShellThread Shell_getCurrentThread(void) { return (ShellThread) pthread_self(); } ShellMutex Shell_createMutex(void) { pthread_mutex_t * mutex; pthread_mutexattr_t recursiveLock; pthread_mutexattr_init(&recursiveLock); pthread_mutexattr_settype(&recursiveLock, PTHREAD_MUTEX_RECURSIVE); mutex = malloc(sizeof(*mutex)); pthread_mutex_init(mutex, &recursiveLock); pthread_mutexattr_destroy(&recursiveLock); return (ShellMutex) mutex; } void Shell_disposeMutex(ShellMutex mutex) { pthread_mutex_destroy((pthread_mutex_t *) mutex); free(mutex); } void Shell_lockMutex(ShellMutex mutex) { pthread_mutex_lock((pthread_mutex_t *) mutex); } bool Shell_tryLockMutex(ShellMutex mutex) { return !pthread_mutex_trylock((pthread_mutex_t *) mutex); } void Shell_unlockMutex(ShellMutex mutex) { pthread_mutex_unlock((pthread_mutex_t *) mutex); } ShellSemaphore Shell_createSemaphore(unsigned int value) { sem_t * semaphore = malloc(sizeof(*semaphore)); sem_init(semaphore, 0, value > SEM_VALUE_MAX ? SEM_VALUE_MAX : value); return (ShellSemaphore) semaphore; } void Shell_disposeSemaphore(ShellSemaphore semaphore) { sem_destroy((sem_t *) semaphore); free(semaphore); } void Shell_postSemaphore(ShellSemaphore semaphore) { sem_post((sem_t *) semaphore); } void Shell_waitSemaphore(ShellSemaphore semaphore) { sem_wait((sem_t *) semaphore); } bool Shell_tryWaitSemaphore(ShellSemaphore semaphore) { return !sem_trywait((sem_t *) semaphore); } void Shell_executeOnMainThread(void (* callback)(void * context), void * context) { Shell_lockMutex(callbackQueueHeadMutex); if ((queuedCallbackHead + 1) % QUEUED_CALLBACK_MAX == queuedCallbackTail) { #ifdef DEBUG fprintf(stderr, "Error: Shell_executeOnMainThread() called when the maximum number of callbacks (%u) is already queued. Couldn't add to queue.\n", QUEUED_CALLBACK_MAX); #endif Shell_unlockMutex(callbackQueueHeadMutex); return; } queuedCallbacks[queuedCallbackHead].callback = callback; queuedCallbacks[queuedCallbackHead].context = context; queuedCallbackHead = (queuedCallbackHead + 1) % QUEUED_CALLBACK_MAX; Shell_unlockMutex(callbackQueueHeadMutex); unsigned char byte = 0; write(pselectPipe[1], &byte, 1); } void GLXShell_coalesceMouseMotionEvents(bool coalesce) { coalesceMouseMotionEvents = coalesce; } void GLXShell_sendActivationClicks(bool send) { sendActivationClicks = send; } static void requestRootFrameExtents(void) { Atom actualType; int actualFormat; unsigned long itemCountReturned = 0, itemCountUnread = 0; unsigned long * rootFrameExtentsPointer = rootFrameExtents; XGetWindowProperty(display, rootWindow, XInternAtom(display, "_NET_REQUEST_FRAME_EXTENTS", False), 0, 4, False, AnyPropertyType, &actualType, &actualFormat, &itemCountReturned, &itemCountUnread, (unsigned char **) &rootFrameExtentsPointer); } unsigned int getGLXAttributes(struct GLXShellConfiguration configuration, int * outAttributes) { unsigned int attributeCount = 0; outAttributes[attributeCount++] = GLX_RGBA; if (configuration.displayMode.doubleBuffer) { outAttributes[attributeCount++] = GLX_DOUBLEBUFFER; } if (configuration.displayMode.depthBuffer) { outAttributes[attributeCount++] = GLX_DEPTH_SIZE; outAttributes[attributeCount++] = configuration.displayMode.depthSize; } if (configuration.displayMode.multisample) { outAttributes[attributeCount++] = GLX_SAMPLE_BUFFERS; outAttributes[attributeCount++] = configuration.displayMode.sampleBuffers; outAttributes[attributeCount++] = GLX_SAMPLES; outAttributes[attributeCount++] = configuration.displayMode.samples; } outAttributes[attributeCount++] = None; return attributeCount; } int main(int argc, char ** argv) { XVisualInfo * visualInfo = NULL; XSetWindowAttributes windowAttributes; int attributes[7]; Colormap colormap; gtk_init(&argc, &argv); display = XOpenDisplay(NULL); if (display == NULL) { #ifdef DEBUG fprintf(stderr, "XOpenDisplay failed\n"); #endif return EXIT_FAILURE; } screen = DefaultScreen(display); rootWindow = RootWindow(display, screen); float scaleFactor = Shell_getDisplayScaleFactor(0); configuration.windowX = 50; configuration.windowY = 80; configuration.windowWidth = 800 * scaleFactor; configuration.windowHeight = 600 * scaleFactor; configuration.windowTitle = "GLXShell"; configuration.applicationName = "GLXShell Application"; configuration.icon.data = NULL; configuration.icon.size = 0; configuration.displayMode.doubleBuffer = true; configuration.displayMode.depthBuffer = false; configuration.displayMode.depthSize = 24; configuration.displayMode.stencilBuffer = false; configuration.displayMode.stencilSize = 16; configuration.displayMode.multisample = false; configuration.displayMode.sampleBuffers = 1; configuration.displayMode.samples = 4; GLXTarget_configure(argc, (const char **) argv, &configuration); getGLXAttributes(configuration, attributes); visualInfo = glXChooseVisual(display, 0, attributes); if (visualInfo == NULL && configuration.displayMode.multisample) { #ifdef DEBUG fprintf(stderr, "Requested pixel format unavailable; trying fallback non-multisampled format\n"); #endif configuration.displayMode.multisample = false; getGLXAttributes(configuration, attributes); visualInfo = glXChooseVisual(display, 0, attributes); } if (visualInfo == NULL) { #ifdef DEBUG fprintf(stderr, "Requested pixel format unavailable\n"); #endif return EXIT_FAILURE; } colormap = XCreateColormap(display, rootWindow, visualInfo->visual, AllocNone); windowAttributes.colormap = colormap; windowAttributes.event_mask = ExposureMask | KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | ButtonMotionMask | LeaveWindowMask | FocusChangeMask | StructureNotifyMask; windowAttributes.background_pixmap = None; windowAttributes.background_pixel = 0; windowAttributes.border_pixel = 0; requestRootFrameExtents(); window = XCreateWindow(display, rootWindow, configuration.windowX, configuration.windowY - rootFrameExtents[2] * scaleFactor, configuration.windowWidth, configuration.windowHeight, 0, visualInfo->depth, InputOutput, visualInfo->visual, CWColormap | CWEventMask | CWBackPixmap | CWBorderPixel, &windowAttributes); deleteWindowAtom = XInternAtom(display, "WM_DELETE_WINDOW", False); dialogClosedAtom = XInternAtom(display, "glxshell_dialogClosed", False); ignoreNextMotionNotifyAtom = XInternAtom(display, "glxshell_ignoreNextMotionNotify", False); mouseDeltaModeAtom = XInternAtom(display, "glxshell_mouseDeltaModeAtom", False); XSetWMProtocols(display, window, &deleteWindowAtom, 1); windowTitle = strdup(configuration.windowTitle); XStoreName(display, window, windowTitle); XClassHint * classHint = XAllocClassHint(); classHint->res_name = configuration.applicationName; classHint->res_class = configuration.applicationName; XSetClassHint(display, window, classHint); free(classHint); XMapWindow(display, window); initialWindowX = configuration.windowX; initialWindowY = configuration.windowY; XMoveWindow(display, window, initialWindowX, initialWindowY); if (configuration.icon.data != NULL) { Atom iconAtom = XInternAtom(display, "_NET_WM_ICON", False); XChangeProperty(display, window, iconAtom, XA_CARDINAL, 32, PropModeReplace, (const unsigned char *) configuration.icon.data, configuration.icon.size); XFlush(display); } windowCreated = true; context = glXCreateContext(display, visualInfo, NULL, GL_TRUE); glXMakeCurrent(display, window, context); setVSync(vsyncWindow); callbackQueueHeadMutex = Shell_createMutex(); pipe2(pselectPipe, O_DIRECT); Target_init(); return EXIT_SUCCESS; } void Shell_msleep(unsigned int milliseconds) { struct timespec sleepTimeSpec = {.tv_sec = milliseconds / 1000, .tv_nsec = (milliseconds % 1000) * 1000000}; struct timespec elapsedTimeSpec = sleepTimeSpec; nanosleep(&sleepTimeSpec, &elapsedTimeSpec); } void Shell_preciseSleep(double seconds) { struct timespec sleepTimeSpec = {.tv_sec = seconds, .tv_nsec = fmod(seconds, 1.0) * 1000000000.0}; struct timespec elapsedTimeSpec = sleepTimeSpec; nanosleep(&sleepTimeSpec, &elapsedTimeSpec); }