| 1 | /* |
| 2 | * LegacyClonk |
| 3 | * |
| 4 | * Copyright (c) RedWolf Design |
| 5 | * Copyright (c) 2005, Günther |
| 6 | * Copyright (c) 2017-2021, The LegacyClonk Team and contributors |
| 7 | * |
| 8 | * Distributed under the terms of the ISC license; see accompanying file |
| 9 | * "COPYING" for details. |
| 10 | * |
| 11 | * "Clonk" is a registered trademark of Matthes Bender, used with permission. |
| 12 | * See accompanying file "TRADEMARK" for details. |
| 13 | * |
| 14 | * To redistribute this file separately, substitute the full license texts |
| 15 | * for the above references. |
| 16 | */ |
| 17 | |
| 18 | /* A wrapper class to OS dependent event and window interfaces, X11 version */ |
| 19 | |
| 20 | #include <Standard.h> |
| 21 | #include <StdApp.h> |
| 22 | #include <StdWindow.h> |
| 23 | #include <StdGL.h> |
| 24 | #include <StdDDraw2.h> |
| 25 | #include <StdFile.h> |
| 26 | #include <StdBuf.h> |
| 27 | |
| 28 | #ifdef USE_X11 |
| 29 | #include "res/lc.xpm" |
| 30 | #include <GL/glx.h> |
| 31 | #include <X11/Xlib.h> |
| 32 | #include <X11/xpm.h> |
| 33 | #include <X11/Xatom.h> |
| 34 | #include <X11/extensions/xf86vmode.h> |
| 35 | #endif |
| 36 | |
| 37 | #ifdef WITH_GLIB |
| 38 | #include <glib.h> |
| 39 | #endif |
| 40 | |
| 41 | #include <string> |
| 42 | #include <map> |
| 43 | #include <sstream> |
| 44 | #include <sys/time.h> |
| 45 | #include <time.h> |
| 46 | #include <errno.h> |
| 47 | #include <unistd.h> |
| 48 | |
| 49 | /* CStdWindow */ |
| 50 | |
| 51 | CStdWindow::~CStdWindow() |
| 52 | { |
| 53 | Clear(); |
| 54 | } |
| 55 | |
| 56 | bool CStdWindow::Init(CStdApp *const app, const char *const title, const C4Rect &bounds, CStdWindow *const parent) |
| 57 | { |
| 58 | #ifndef USE_X11 |
| 59 | return true; |
| 60 | #else |
| 61 | Active = true; |
| 62 | dpy = app->dpy; |
| 63 | |
| 64 | if (!FindInfo()) return false; |
| 65 | |
| 66 | // Various properties |
| 67 | XSetWindowAttributes attr; |
| 68 | attr.border_pixel = 0; |
| 69 | attr.background_pixel = 0; |
| 70 | // Which events we want to receive |
| 71 | attr.event_mask = |
| 72 | StructureNotifyMask | |
| 73 | FocusChangeMask | |
| 74 | KeyPressMask | |
| 75 | KeyReleaseMask | |
| 76 | PointerMotionMask | |
| 77 | ButtonPressMask | |
| 78 | ButtonReleaseMask; |
| 79 | attr.colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), static_cast<XVisualInfo *>(Info)->visual, AllocNone); |
| 80 | unsigned long attrmask = CWBackPixel | CWBorderPixel | CWColormap | CWEventMask; |
| 81 | Pixmap bitmap; |
| 82 | const bool hideCursor{HideCursor()}; |
| 83 | if (hideCursor) |
| 84 | { |
| 85 | // Hide the mouse cursor |
| 86 | // We do not care what color the invisible cursor has |
| 87 | XColor cursor_color{}; |
| 88 | bitmap = XCreateBitmapFromData(dpy, DefaultRootWindow(dpy), "\000" , 1, 1); |
| 89 | attr.cursor = XCreatePixmapCursor(dpy, bitmap, bitmap, &cursor_color, &cursor_color, 0, 0); |
| 90 | attrmask |= CWCursor; |
| 91 | } |
| 92 | |
| 93 | wnd = XCreateWindow(dpy, DefaultRootWindow(dpy), |
| 94 | 0, 0, 640, 480, 0, static_cast<XVisualInfo *>(Info)->depth, InputOutput, static_cast<XVisualInfo *>(Info)->visual, |
| 95 | attrmask, &attr); |
| 96 | if (hideCursor) |
| 97 | { |
| 98 | XFreeCursor(dpy, attr.cursor); |
| 99 | XFreePixmap(dpy, bitmap); |
| 100 | } |
| 101 | if (!wnd) |
| 102 | { |
| 103 | LogNTr(level: spdlog::level::err, message: "Error creating window." ); |
| 104 | return false; |
| 105 | } |
| 106 | // Update the XWindow->CStdWindow-Map |
| 107 | app->NewWindow(window: this); |
| 108 | if (!app->inputContext && app->inputMethod) |
| 109 | { |
| 110 | app->inputContext = XCreateIC(app->inputMethod, |
| 111 | XNClientWindow, wnd, |
| 112 | XNFocusWindow, wnd, |
| 113 | XNInputStyle, XIMPreeditNothing | XIMStatusNothing, |
| 114 | XNResourceName, STD_PRODUCT, |
| 115 | XNResourceClass, STD_PRODUCT, |
| 116 | nullptr); |
| 117 | if (!app->inputContext) |
| 118 | { |
| 119 | LogNTr(level: spdlog::level::err, message: "Failed to create input context." ); |
| 120 | XCloseIM(app->inputMethod); |
| 121 | app->inputMethod = nullptr; |
| 122 | } |
| 123 | else |
| 124 | { |
| 125 | long ic_event_mask; |
| 126 | if (XGetICValues(app->inputContext, XNFilterEvents, &ic_event_mask, nullptr) == nullptr) |
| 127 | attr.event_mask |= ic_event_mask; |
| 128 | XSelectInput(dpy, wnd, attr.event_mask); |
| 129 | XSetICFocus(app->inputContext); |
| 130 | } |
| 131 | } |
| 132 | // We want notification of closerequests and be killed if we hang |
| 133 | Atom WMProtocols[2]; |
| 134 | const char *WMProtocolnames[] = { "WM_DELETE_WINDOW" , "_NET_WM_PING" }; |
| 135 | XInternAtoms(dpy, const_cast<char **>(WMProtocolnames), 2, false, WMProtocols); |
| 136 | XSetWMProtocols(dpy, wnd, WMProtocols, 2); |
| 137 | // Let the window manager know our pid so it can kill us |
| 138 | Atom PID = XInternAtom(app->dpy, "_NET_WM_PID" , false); |
| 139 | int32_t pid = getpid(); |
| 140 | if (PID != None) XChangeProperty(app->dpy, wnd, PID, XA_CARDINAL, 32, PropModeReplace, reinterpret_cast<const unsigned char *>(&pid), 1); |
| 141 | // Title and stuff |
| 142 | XTextProperty title_property; |
| 143 | XStringListToTextProperty(const_cast<char **>(&title), 1, &title_property); |
| 144 | // State and Icon |
| 145 | XWMHints *wm_hint = XAllocWMHints(); |
| 146 | wm_hint->flags = StateHint | InputHint | IconPixmapHint | IconMaskHint; |
| 147 | wm_hint->initial_state = NormalState; |
| 148 | wm_hint->input = True; |
| 149 | // Trust XpmCreatePixmapFromData to not modify the xpm... |
| 150 | XpmCreatePixmapFromData(display: dpy, d: wnd, data: const_cast<char **>(c4x_xpm), pixmap_return: &wm_hint->icon_pixmap, shapemask_return: &wm_hint->icon_mask, attributes: nullptr); |
| 151 | // Window class |
| 152 | XClassHint *class_hint = XAllocClassHint(); |
| 153 | class_hint->res_name = const_cast<char *>(STD_PRODUCT); |
| 154 | class_hint->res_class = const_cast<char *>(STD_PRODUCT); |
| 155 | XSetWMProperties(dpy, wnd, &title_property, &title_property, app->argv, app->argc, nullptr, wm_hint, class_hint); |
| 156 | // Set "parent". Clonk does not use "real" parent windows, but multiple toplevel windows. |
| 157 | if (parent) XSetTransientForHint(dpy, wnd, parent->wnd); |
| 158 | // Show window |
| 159 | XMapWindow(dpy, wnd); |
| 160 | // Clean up |
| 161 | XFree(title_property.value); |
| 162 | Hints = wm_hint; |
| 163 | XFree(class_hint); |
| 164 | |
| 165 | // Render into whole window |
| 166 | renderwnd = wnd; |
| 167 | |
| 168 | return true; |
| 169 | #endif // USE_X11 |
| 170 | } |
| 171 | |
| 172 | void CStdWindow::Clear() |
| 173 | { |
| 174 | #ifdef USE_X11 |
| 175 | // Destroy window |
| 176 | if (wnd) |
| 177 | { |
| 178 | XUnmapWindow(dpy, wnd); |
| 179 | XDestroyWindow(dpy, wnd); |
| 180 | if (Info) XFree(Info); |
| 181 | if (Hints) XFree(Hints); |
| 182 | |
| 183 | // Might be necessary when the last window is closed |
| 184 | XFlush(dpy); |
| 185 | } |
| 186 | wnd = renderwnd = 0; |
| 187 | #endif |
| 188 | } |
| 189 | |
| 190 | #ifdef USE_X11 |
| 191 | bool CStdWindow::FindInfo() |
| 192 | { |
| 193 | #ifndef USE_CONSOLE |
| 194 | // get an appropriate visual |
| 195 | // attributes for a single buffered visual in RGBA format with at least 4 bits per color |
| 196 | static int attrListSgl[] = { GLX_RGBA, |
| 197 | GLX_RED_SIZE, 4, GLX_GREEN_SIZE, 4, GLX_BLUE_SIZE, 4, |
| 198 | None |
| 199 | }; |
| 200 | // attributes for a double buffered visual in RGBA format with at least 4 bits per color |
| 201 | static int attrListDbl[] = { GLX_RGBA, GLX_DOUBLEBUFFER, |
| 202 | GLX_RED_SIZE, 4, GLX_GREEN_SIZE, 4, GLX_BLUE_SIZE, 4, |
| 203 | GLX_X_VISUAL_TYPE, GLX_TRUE_COLOR, |
| 204 | None |
| 205 | }; |
| 206 | // doublebuffered is the best |
| 207 | Info = glXChooseVisual(dpy, DefaultScreen(dpy), attribList: attrListDbl); |
| 208 | if (!Info) |
| 209 | { |
| 210 | pGL->logger->error(msg: "no doublebuffered visual" ); |
| 211 | // a singlebuffered is probably better than the default |
| 212 | Info = glXChooseVisual(dpy, DefaultScreen(dpy), attribList: attrListSgl); |
| 213 | } |
| 214 | #endif |
| 215 | if (!Info) |
| 216 | { |
| 217 | pGL->logger->error(msg: "no singlebuffered visual, either." ); |
| 218 | // just try to get the default |
| 219 | XVisualInfo vitmpl; int blub; |
| 220 | vitmpl.visual = DefaultVisual(dpy, DefaultScreen(dpy)); |
| 221 | vitmpl.visualid = XVisualIDFromVisual(vitmpl.visual); |
| 222 | Info = XGetVisualInfo(dpy, VisualIDMask, &vitmpl, &blub); |
| 223 | } |
| 224 | if (!Info) |
| 225 | { |
| 226 | pGL->logger->error(msg: "no visual at all." ); |
| 227 | return false; |
| 228 | } |
| 229 | |
| 230 | return true; |
| 231 | } |
| 232 | #endif // USE_X11 |
| 233 | |
| 234 | bool CStdWindow::GetSize(C4Rect &rect) |
| 235 | { |
| 236 | #ifdef USE_X11 |
| 237 | Window winDummy; |
| 238 | unsigned int borderDummy; |
| 239 | int x, y; |
| 240 | unsigned int width, height; |
| 241 | unsigned int depth; |
| 242 | XGetGeometry(dpy, wnd, &winDummy, &x, &y, |
| 243 | &width, &height, &borderDummy, &depth); |
| 244 | rect.x = x; |
| 245 | rect.y = y; |
| 246 | rect.Wdt = width; |
| 247 | rect.Hgt = height; |
| 248 | #else |
| 249 | rect = {0, 0, 0, 0}; |
| 250 | #endif |
| 251 | return true; |
| 252 | } |
| 253 | |
| 254 | void CStdWindow::SetSize(const unsigned int X, const unsigned int Y) |
| 255 | { |
| 256 | #ifdef USE_X11 |
| 257 | XResizeWindow(dpy, wnd, X, Y); |
| 258 | #endif |
| 259 | } |
| 260 | |
| 261 | void CStdWindow::SetTitle(const char *const Title) |
| 262 | { |
| 263 | #ifdef USE_X11 |
| 264 | XTextProperty title_property; |
| 265 | XStringListToTextProperty(const_cast<char **>(&Title), 1, &title_property); |
| 266 | XSetWMName(dpy, wnd, &title_property); |
| 267 | #endif |
| 268 | } |
| 269 | |
| 270 | void CStdWindow::FlashWindow() |
| 271 | { |
| 272 | #ifdef USE_X11 |
| 273 | if (!HasFocus) |
| 274 | { |
| 275 | XWMHints *wm_hint = static_cast<XWMHints *>(Hints); |
| 276 | wm_hint->flags |= XUrgencyHint; |
| 277 | XSetWMHints(dpy, wnd, wm_hint); |
| 278 | } |
| 279 | #endif |
| 280 | } |
| 281 | |
| 282 | #ifdef USE_X11 |
| 283 | void CStdWindow::HandleMessage(XEvent &event) |
| 284 | { |
| 285 | if (event.type == FocusIn) |
| 286 | { |
| 287 | HasFocus = true; |
| 288 | |
| 289 | // Clear urgency flag |
| 290 | XWMHints *wm_hint = static_cast<XWMHints *>(Hints); |
| 291 | if (wm_hint->flags & XUrgencyHint) |
| 292 | { |
| 293 | wm_hint->flags &= ~XUrgencyHint; |
| 294 | XSetWMHints(dpy, wnd, wm_hint); |
| 295 | } |
| 296 | } |
| 297 | else if (event.type == FocusOut) |
| 298 | { |
| 299 | int detail = reinterpret_cast<XFocusChangeEvent *>(&event)->detail; |
| 300 | |
| 301 | // StdGtkWindow gets two FocusOut events, one of which comes |
| 302 | // directly after a FocusIn event even when the window has |
| 303 | // focus. For these FocusOut events, detail is set to |
| 304 | // NotifyInferior which is why we are ignoring it here. |
| 305 | if (detail != NotifyInferior) |
| 306 | { |
| 307 | HasFocus = false; |
| 308 | } |
| 309 | } |
| 310 | } |
| 311 | #endif |
| 312 | |
| 313 | void CStdWindow::SetDisplayMode(const DisplayMode mode) |
| 314 | { |
| 315 | #ifdef USE_X11 |
| 316 | const auto fullscreen = mode == DisplayMode::Fullscreen; |
| 317 | |
| 318 | static Atom atoms[4]; |
| 319 | static const char *names[] = {"_NET_WM_STATE" , "_NET_WM_STATE_FULLSCREEN" , "_NET_WM_MAXIMIZED_VERT" , "_NET_WM_MAXIMIZED_HORZ" }; |
| 320 | if (!atoms[0]) XInternAtoms(dpy, const_cast<char **>(names), 4, false, atoms); |
| 321 | XEvent e; |
| 322 | e.xclient.type = ClientMessage; |
| 323 | e.xclient.window = wnd; |
| 324 | e.xclient.message_type = atoms[0]; |
| 325 | e.xclient.format = 32; |
| 326 | e.xclient.data.l[0] = fullscreen ? 1 : 0; // _NET_WM_STATE_ADD : _NET_WM_STATE_DELETE |
| 327 | e.xclient.data.l[1] = atoms[1]; |
| 328 | e.xclient.data.l[2] = 0; // second property to alter |
| 329 | e.xclient.data.l[3] = 1; // source indication |
| 330 | e.xclient.data.l[4] = 0; |
| 331 | XSendEvent(dpy, DefaultRootWindow(dpy), false, SubstructureNotifyMask | SubstructureRedirectMask, &e); |
| 332 | #endif |
| 333 | } |
| 334 | |
| 335 | void CStdWindow::SetProgress(uint32_t) {} // stub |
| 336 | |