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
51CStdWindow::~CStdWindow()
52{
53 Clear();
54}
55
56bool 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
172void 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
191bool 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
234bool 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
254void 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
261void 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
270void 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
283void 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
313void 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
335void CStdWindow::SetProgress(uint32_t) {} // stub
336