1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2005, Günther
6 * Copyright (c) 2017-2023, 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#include "StdApp.h"
19#include "StdSync.h"
20#include "spdlog/common.h"
21
22#include <array>
23#include <string>
24
25#include <poll.h>
26#include <sys/time.h>
27
28#ifdef USE_X11
29#include <string_view>
30
31#include <X11/Xmd.h>
32
33#include <X11/Xlib.h>
34#include <X11/Xatom.h>
35#include <X11/Xutil.h>
36#include <X11/extensions/xf86vmode.h>
37#include <X11/XKBlib.h>
38#endif
39
40#ifdef WITH_GLIB
41#include <glib.h>
42#endif
43
44#include "C4Log.h"
45
46CStdApp::CStdApp()
47 : mainThread{std::this_thread::get_id()}
48{
49}
50
51CStdApp::~CStdApp()
52{
53#ifdef WITH_GLIB
54 if (glibLogger)
55 {
56 g_log_remove_handler(log_domain: nullptr, handler_id: glibLogHandlerId);
57 glibLogger.reset();
58 }
59#endif
60}
61
62#ifdef USE_X11
63static Atom ClipboardAtoms[1];
64#endif
65
66#ifdef WITH_GLIB
67template<auto Callback>
68static gboolean ForwardPipeInput(GIOChannel *, GIOCondition, gpointer data)
69{
70 (static_cast<CStdApp *>(data)->*Callback)();
71 return true;
72}
73
74static void gtkLogFunction([[maybe_unused]] const gchar* logDomain, GLogLevelFlags logLevel, const gchar* message, gpointer userData)
75{
76 const auto logger = *reinterpret_cast<std::shared_ptr<spdlog::logger>*>(userData);
77 const auto level = [logLevel]{
78 using enum spdlog::level::level_enum;
79 switch (logLevel & G_LOG_LEVEL_MASK)
80 {
81 // in glib error seems to be more severe than critical, but in spdlog it is the opposite
82 case G_LOG_LEVEL_ERROR: return critical;
83 case G_LOG_LEVEL_CRITICAL: return err;
84
85 case G_LOG_LEVEL_WARNING: return warn;
86 case G_LOG_LEVEL_MESSAGE: return info;
87 case G_LOG_LEVEL_INFO: return info;
88 case G_LOG_LEVEL_DEBUG: return debug;
89
90 case G_LOG_LEVEL_MASK:
91 case G_LOG_FLAG_RECURSION:
92 case G_LOG_FLAG_FATAL:
93 break;
94 }
95 return trace;
96 }();
97 logger->log(lvl: level, msg: message);
98
99 if (logLevel & G_LOG_FLAG_FATAL)
100 {
101 logger->flush();
102 }
103}
104#endif
105
106void CStdApp::Init(const int argc, char **const argv)
107{
108 setlocale(LC_ALL, locale: "");
109 setlocale(LC_NUMERIC, locale: "C");
110
111#ifdef USE_X11
112 // Clear XMODIFIERS as key input gets evaluated twice otherwise
113 unsetenv(name: "XMODIFIERS");
114#endif
115
116 this->argc = argc;
117 this->argv = argv;
118
119 static char dir[PATH_MAX];
120 SCopy(szSource: argv[0], sTarget: dir);
121
122#ifndef USE_SDL_MAINLOOP
123 if (dir[0] != '/')
124 {
125 SInsert(szString: dir, szInsert: "/");
126 SInsert(szString: dir, szInsert: GetWorkingDirectory());
127 }
128#endif
129
130 Location = dir;
131
132 // Build command line.
133 static std::string s("\"");
134 for (int i = 1; i < argc; ++i)
135 {
136 s.append(s: argv[i]);
137 s.append(s: "\" \"");
138 }
139 s.append(s: "\"");
140 szCmdLine = s.c_str();
141
142#ifdef WITH_GLIB
143
144
145
146 loop = g_main_loop_new(context: nullptr, is_running: false);
147#endif
148
149#ifdef USE_X11
150 dpy = XOpenDisplay(nullptr);
151 if (!dpy)
152 {
153 throw StartupException{"Error opening display."};
154 }
155
156 int eventBase;
157 int errorBase;
158 if (!XF86VidModeQueryExtension(dpy, &eventBase, &errorBase) ||
159 !XF86VidModeQueryVersion(dpy, &xf86vmode_major_version, &xf86vmode_minor_version))
160 {
161 xf86vmode_major_version = -1;
162 xf86vmode_minor_version = 0;
163 LogNTr(level: spdlog::level::err, message: "XF86VidMode Extension missing, resolution switching will not work");
164 }
165
166 // So a repeated keypress-event is not preceded with a keyrelease.
167 XkbSetDetectableAutoRepeat(dpy, true, &detectable_autorepeat_supported);
168
169 XSetLocaleModifiers("");
170
171 inputMethod = XOpenIM(dpy, nullptr, nullptr, nullptr);
172 if (!inputMethod)
173 {
174 LogNTr(level: spdlog::level::err, message: "Failed to open input method");
175 }
176
177 const char *names[]{"CLIPBOARD"};
178 XInternAtoms(dpy, const_cast<char **>(names), 1, false, ClipboardAtoms);
179
180#ifdef WITH_GLIB
181 xChannel = g_io_channel_unix_new(fd: XConnectionNumber(dpy));
182 g_io_add_watch(channel: xChannel, condition: G_IO_IN, func: &ForwardPipeInput<&CStdApp::OnXInput>, user_data: this);
183#endif
184#elif defined(USE_SDL_MAINLOOP)
185 try
186 {
187 sdlVideoSubSys.emplace(SDL_INIT_VIDEO);
188 }
189 catch (const std::runtime_error &)
190 {
191 throw StartupException{"Error initializing SDL."};
192 }
193#endif
194
195 if (pipe(pipedes: Pipe) != 0)
196 {
197 throw StartupException{"Error creating Pipe"};
198 }
199
200#ifdef WITH_GLIB
201 pipeChannel = g_io_channel_unix_new(fd: Pipe[0]);
202 g_io_add_watch(channel: pipeChannel, condition: G_IO_IN, func: &ForwardPipeInput<&CStdApp::OnPipeInput>, user_data: this);
203#endif
204
205 DoInit();
206
207#ifdef WITH_GLIB
208 glibLogger = CreateGLibLogger();
209
210 static constexpr auto allLevels = static_cast<GLogLevelFlags>(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION);
211 glibLogHandlerId = g_log_set_handler(log_domain: nullptr, log_levels: allLevels, log_func: gtkLogFunction, user_data: &glibLogger);
212#endif
213}
214
215bool CStdApp::InitTimer()
216{
217 gettimeofday(tv: &LastExecute, tz: nullptr);
218 return true;
219}
220
221void CStdApp::Clear()
222{
223#ifdef USE_X11
224 if (dpy)
225 {
226 XCloseDisplay(dpy);
227 dpy = nullptr;
228 }
229#endif
230
231 close(fd: Pipe[0]);
232 close(fd: Pipe[1]);
233
234#ifdef WITH_GLIB
235 g_main_loop_unref(loop);
236
237 if (pipeChannel)
238 {
239 g_io_channel_unref(channel: pipeChannel);
240 }
241
242#ifdef USE_X11
243 if (xChannel)
244 {
245 g_io_channel_unref(channel: xChannel);
246 }
247#endif
248#endif
249
250#ifdef USE_SDL_MAINLOOP
251 sdlVideoSubSys.reset();
252 SDL_Quit();
253#endif
254}
255
256void CStdApp::Quit()
257{
258 fQuitMsgReceived = true;
259}
260
261void CStdApp::Execute()
262{
263 const time_t seconds{LastExecute.tv_sec};
264 timeval tv;
265 gettimeofday(tv: &tv, tz: nullptr);
266
267 if (DoNotDelay)
268 {
269 DoNotDelay = false;
270 LastExecute = tv;
271 }
272 else if (LastExecute.tv_sec < tv.tv_sec - 2)
273 {
274 LastExecute = tv;
275 }
276 else
277 {
278 LastExecute.tv_usec += Delay;
279 if (LastExecute.tv_usec > 1000000)
280 {
281 ++LastExecute.tv_sec;
282 LastExecute.tv_usec -= 1000000;
283 }
284 }
285
286 // This will make the FPS look "prettier" in some situations
287 // But who cares...
288 if (seconds != LastExecute.tv_sec)
289 {
290 pWindow->Sec1Timer();
291 }
292}
293
294void CStdApp::NextTick(bool)
295{
296 DoNotDelay = true;
297}
298
299void CStdApp::Run()
300{
301 while (HandleMessage(iTimeout: StdSync::Infinite, fCheckTimer: true) != HR_Failure)
302 {
303 }
304}
305
306void CStdApp::ResetTimer(const unsigned int d)
307{
308 Delay = d * 1000;
309}
310
311C4AppHandleResult CStdApp::HandleMessage(const unsigned int timeout, const bool checkTimer)
312{
313 // quit check for nested HandleMessage-calls
314 if (fQuitMsgReceived) return HR_Failure;
315
316 bool doExecute{checkTimer};
317
318 timeval tv;
319 if (DoNotDelay)
320 {
321 tv = {.tv_sec: 0, .tv_usec: 0};
322 }
323 else if (checkTimer)
324 {
325 gettimeofday(tv: &tv, tz: nullptr);
326 tv.tv_usec = LastExecute.tv_usec - tv.tv_usec + Delay - 1000000 * (tv.tv_sec - LastExecute.tv_sec);
327
328 // Check if the given timeout comes first
329 // (don't call Execute then, because it assumes it has been called because of a timer event!)
330 if (timeout != StdSync::Infinite && timeout * 1000 < tv.tv_usec)
331 {
332 tv.tv_usec = timeout * 1000;
333 doExecute = false;
334 }
335
336 if (tv.tv_usec < 0)
337 {
338 tv.tv_usec = 0;
339 }
340 }
341 else
342 {
343 tv.tv_usec = timeout * 1000;
344 }
345
346 tv.tv_sec = 0;
347
348#ifdef USE_X11
349 while (XPending(dpy))
350 {
351 HandleXMessage();
352 }
353#elif defined(USE_SDL_MAINLOOP)
354 SDL_Event event;
355 while (SDL_PollEvent(&event))
356 {
357 HandleSDLEvent(event);
358 }
359#endif
360
361#ifdef WITH_GLIB
362 const auto tvTimeout = static_cast<unsigned int>(tv.tv_sec * 1000 + tv.tv_usec / 1000);
363 bool timeoutElapsed{false};
364 guint timeoutHandle{0};
365
366 // Guarantee that we do not block until something interesting occurs
367 // when using a timeout
368 if (checkTimer || timeout != StdSync::Infinite)
369 {
370 // The timeout handler sets timeout_elapsed to true when
371 // the timeout elapsed, this is required for a correct return
372 // value.
373 timeoutHandle = g_timeout_add_full(
374 G_PRIORITY_HIGH,
375 interval: tvTimeout,
376 function: [](gpointer data) -> gboolean { *static_cast<bool *>(data) = true; return false; },
377 data: &timeoutElapsed,
378 notify: nullptr
379 );
380 }
381
382 g_main_context_iteration(context: g_main_loop_get_context(loop), may_block: true);
383
384 if (timeoutHandle && !timeoutElapsed)
385 {
386 // FIXME: do not add a new timeout instead of deleting the old one in the next call
387 g_source_remove(tag: timeoutHandle);
388 }
389
390 if (timeoutElapsed && doExecute)
391 {
392 Execute();
393 }
394
395 while (g_main_context_pending(context: g_main_loop_get_context(loop)))
396 {
397 g_main_context_iteration(context: g_main_loop_get_context(loop), may_block: false);
398 }
399
400 return timeoutElapsed ? (doExecute ? HR_Timer : HR_Timeout) : HR_Message;
401#else
402 std::array<pollfd, 3> fds;
403 fds.fill({.fd = -1, .events = POLLIN});
404
405 fds[0].fd = Pipe[0];
406
407#ifdef USE_X11
408 // Stop waiting for the next frame when more events arrive
409 XFlush(dpy);
410 fds[1].fd = XConnectionNumber(dpy);
411#endif
412
413#ifdef USE_CONSOLE
414 fds[2].fd = STDIN_FILENO;
415#endif
416
417 switch (StdSync::Poll(fds, (checkTimer || timeout != StdSync::Infinite) ? tv.tv_usec / 1000 : StdSync::Infinite))
418 {
419 case -1:
420 LogNTr(spdlog::level::err, "poll error: {}", std::strerror(errno));
421 return HR_Failure;
422
423 // timeout
424 case 0:
425 if (doExecute)
426 {
427 Execute();
428 return HR_Timer;
429 }
430
431 return HR_Timeout;
432
433 default:
434 if (fds[0].revents & POLLIN)
435 {
436 OnPipeInput();
437 }
438
439#ifdef USE_X11
440 if (fds[1].revents & POLLIN)
441 {
442 OnXInput();
443 }
444#endif
445
446#ifdef USE_CONSOLE
447 if (fds[2].revents & POLLIN)
448 {
449 if (!ReadStdInCommand())
450 {
451 return HR_Failure;
452 }
453 }
454#endif
455
456 return HR_Message;
457 }
458#endif
459}
460
461bool CStdApp::SignalNetworkEvent()
462{
463 char c{1};
464 write(fd: Pipe[1], buf: &c, n: 1);
465 return true;
466}
467
468bool CStdApp::Copy(const std::string_view text, const bool clipboard)
469{
470#ifdef USE_X11
471 const auto copy = [=, this](std::string &data, const Atom atom)
472 {
473 XSetSelectionOwner(dpy, atom, pWindow->wnd, LastEventTime);
474 if (XGetSelectionOwner(dpy, atom) != pWindow->wnd)
475 {
476 return false;
477 }
478
479 data = text;
480 return true;
481 };
482
483 if (clipboard)
484 {
485 return copy(clipboardSelection, ClipboardAtoms[0]);
486 }
487 else
488 {
489 return copy(primarySelection, XA_PRIMARY);
490 }
491#elif defined(USE_SDL_MAINLOOP)
492 return !SDL_SetClipboardText(std::string{text}.c_str());
493#elif defined(USE_CONSOLE)
494 return false;
495#endif
496}
497
498std::string CStdApp::Paste(const bool clipboard)
499{
500#ifdef USE_X11
501 if (!IsClipboardFull())
502 {
503 return "";
504 }
505
506 XConvertSelection(dpy, clipboard ? ClipboardAtoms[0] : XA_PRIMARY, XA_STRING, XA_STRING, pWindow->wnd, LastEventTime);
507
508 // Give the owner some time to respond
509 HandleMessage(timeout: 50, checkTimer: false);
510
511 // Get the length of the data, so we can request it all at once
512 Atom type;
513 int format;
514 unsigned long size;
515 unsigned long bytesLeft;
516 unsigned char *data;
517
518 const auto getWindowProperty = [&](const bool deleteNow, unsigned long bytesToGet)
519 {
520 return XGetWindowProperty(
521 dpy,
522 pWindow->wnd,
523 XA_STRING,
524 0,
525 bytesToGet,
526 deleteNow,
527 AnyPropertyType,
528 &type,
529 &format,
530 &size,
531 &bytesLeft,
532 &data
533 );
534 };
535
536 getWindowProperty(false, 0);
537
538 if (bytesLeft == 0)
539 {
540 return "";
541 }
542
543 if (getWindowProperty(true, bytesLeft) != Success)
544 {
545 return "";
546 }
547
548 std::string result{reinterpret_cast<char *>(data)};
549 XFree(data);
550 return result;
551#elif defined(USE_SDL_MAINLOOP)
552 char *const text{SDL_GetClipboardText()};
553 if (text)
554 {
555 std::string ret{text};
556 free(text);
557 return ret;
558 }
559#endif
560 return "";
561}
562
563bool CStdApp::IsClipboardFull(const bool clipboard)
564{
565#ifdef USE_X11
566 return XGetSelectionOwner(dpy, clipboard ? ClipboardAtoms[0] : XA_PRIMARY) != None;
567#elif defined(USE_SDL_MAINLOOP)
568 return SDL_HasClipboardText() == SDL_TRUE;
569#elif defined(USE_CONSOLE)
570 return false;
571#endif
572}
573
574void CStdApp::OnPipeInput()
575{
576 char c;
577 read(fd: Pipe[0], buf: &c, nbytes: 1);
578 OnNetworkEvents();
579}
580
581bool CStdApp::ReadStdInCommand()
582{
583 // Surely not the most efficient way to do it, but we won't have to read much data anyway.
584 char c;
585 if (read(STDIN_FILENO, buf: &c, nbytes: 1) != 1)
586 {
587 return false;
588 }
589
590 if (c == '\n')
591 {
592 if (!CmdBuf.isNull())
593 {
594 OnCommand(szCmd: CmdBuf.getData());
595 CmdBuf.Clear();
596 }
597 }
598 else if (std::isprint(c))
599 {
600 CmdBuf.AppendChar(cChar: c);
601 }
602
603 return true;
604}
605
606#ifdef USE_X11
607static unsigned int KeyMaskFromKeyEvent(Display *const dpy, XKeyEvent *const key)
608{
609 unsigned int mask{key->state};
610 const KeySym sym{XkbKeycodeToKeysym(dpy, key->keycode, 0, 1)};
611
612 // We need to correct the keymask since the event.xkey.state
613 // is the state _before_ the event, but we want to store the
614 // current state.
615 if (sym == XK_Control_L || sym == XK_Control_R) mask ^= MK_CONTROL;
616 if (sym == XK_Shift_L || sym == XK_Shift_R) mask ^= MK_SHIFT;
617 if (sym == XK_Alt_L || sym == XK_Alt_R) mask ^= MK_ALT;
618 return mask;
619}
620
621void CStdApp::HandleXMessage()
622{
623 XEvent event;
624 XNextEvent(dpy, &event);
625 const auto filtered = static_cast<bool>(XFilterEvent(&event, event.xany.window));
626
627 switch (event.type)
628 {
629 case FocusIn:
630 if (inputContext)
631 {
632 XSetICFocus(inputContext);
633 }
634 break;
635
636 case FocusOut:
637 if (inputContext)
638 {
639 XUnsetICFocus(inputContext);
640 }
641 break;
642
643 case EnterNotify:
644 KeyMask = event.xcrossing.state;
645 break;
646
647 case KeyPress:
648 if (!filtered)
649 {
650 KeyMask = KeyMaskFromKeyEvent(dpy, key: &event.xkey);
651
652 std::array<char, 10> buf{};
653 if (inputContext)
654 {
655 Status status;
656 XmbLookupString(inputContext, &event.xkey, buf.data(), buf.size(), nullptr, &status);
657 if (status == XLookupKeySym)
658 {
659 fputs(s: "FIXME: XmbLookupString returned XLookupKeySym", stderr);
660 }
661 else if (status == XBufferOverflow)
662 {
663 fputs(s: "FIXME: XmbLookupString returned XBufferOverflow\n", stderr);
664 }
665 }
666 else
667 {
668 static XComposeStatus status;
669 XLookupString(&event.xkey, buf.data(), buf.size(), nullptr, &status);
670 }
671
672 if (buf[0])
673 {
674 if (const auto it = windows.find(x: event.xany.window); it != windows.end() && !IsAltDown())
675 {
676 it->second->CharIn(c: buf.data());
677 }
678 }
679 }
680 [[fallthrough]];
681
682 case KeyRelease:
683 KeyMask = KeyMaskFromKeyEvent(dpy, key: &event.xkey);
684 LastEventTime = event.xkey.time;
685 break;
686
687 case ButtonPress:
688 // We can take this directly since there are no key presses
689 // involved. TODO: We probably need to correct button state
690 // here though.
691 KeyMask = event.xbutton.state;
692 LastEventTime = event.xbutton.time;
693 break;
694
695 case SelectionRequest:
696 {
697 // We should compare the timestamp with the timespan when we owned the selection
698 // But slow network connections are not supported anyway, so do not bother
699 std::string &clipboardData{event.xselectionrequest.selection == XA_PRIMARY ? primarySelection : clipboardSelection};
700 XEvent response{
701 .xselection = {
702 .type = SelectionNotify,
703 .display = dpy,
704 .requestor = event.xselectionrequest.requestor,
705 .selection = event.xselectionrequest.selection,
706 .target = event.xselectionrequest.target,
707 .time = event.xselectionrequest.time
708 }
709 };
710
711 // Note: we're implementing the spec only partially here
712 if (!clipboardData.empty())
713 {
714 response.xselection.property = event.xselectionrequest.property;
715 XChangeProperty(
716 dpy,
717 response.xselection.requestor,
718 response.xselection.property,
719 response.xselection.target,
720 8,
721 PropModeReplace,
722 reinterpret_cast<const unsigned char *>(clipboardData.c_str()),
723 clipboardData.size()
724 );
725 }
726 else
727 {
728 response.xselection.property = None;
729 }
730
731 XSendEvent(dpy, response.xselection.requestor, false, NoEventMask, &response);
732 }
733 break;
734
735 case SelectionClear:
736 if (event.xselectionrequest.selection == XA_PRIMARY)
737 {
738 primarySelection.clear();
739 }
740 else
741 {
742 clipboardSelection.clear();
743 }
744
745 break;
746
747 case ClientMessage:
748 {
749 using namespace std::literals::string_view_literals;
750
751 if (XGetAtomName(dpy, event.xclient.message_type) == "WM_PROTOCOLS"sv)
752 {
753 const std::string_view data{XGetAtomName(dpy,event.xclient.data.l[0])};
754 if (data == "WM_DELETE_WINDOW")
755 {
756 if (const auto it = windows.find(x: event.xclient.window); it != windows.end())
757 {
758 it->second->Close();
759 }
760 }
761 else if (data == "_NET_WM_PING")
762 {
763 event.xclient.window = DefaultRootWindow(dpy);
764 XSendEvent(dpy, DefaultRootWindow(dpy), false, SubstructureNotifyMask | SubstructureRedirectMask, &event);
765 }
766 }
767 }
768 break;
769
770 case MappingNotify:
771 XRefreshKeyboardMapping(&event.xmapping);
772 break;
773
774 case DestroyNotify:
775 if (const auto it = windows.find(x: event.xany.window); it != windows.end())
776 {
777 it->second->wnd = 0;
778 it->second->Close();
779 windows.erase(position: it);
780 }
781
782 windows.erase(x: event.xany.window);
783 break;
784 }
785
786 if (const auto it = windows.find(x: event.xany.window); it != windows.end())
787 {
788 it->second->HandleMessage(event);
789 }
790}
791
792void CStdApp::OnXInput()
793{
794 while (XEventsQueued(dpy, QueuedAfterReading))
795 {
796 HandleXMessage();
797 }
798}
799
800void CStdApp::NewWindow(CStdWindow *const window)
801{
802 windows.emplace(args&: window->wnd, args: window);
803}
804#elif defined(USE_SDL_MAINLOOP)
805static void UpdateKeyMaskFromModifiers(const std::uint16_t modifiers)
806{
807}
808
809void CStdApp::HandleSDLEvent(SDL_Event &event)
810{
811 switch (event.type)
812 {
813 case SDL_QUIT:
814 Quit();
815 return;
816
817 case SDL_KEYDOWN:
818 case SDL_KEYUP:
819 {
820 KeyMask = event.key.keysym.mod;
821 break;
822 }
823 }
824
825 if (pWindow)
826 {
827 pWindow->HandleMessage(event);
828 }
829}
830#endif
831