diff options
107 files changed, 9735 insertions, 1902 deletions
@@ -2,5 +2,5 @@ sibs-build/ compile_commands.json -gpu-screen-recorder-overlay-daemon/sibs-build/ -gpu-screen-recorder-overlay-daemon/compile_commands.json +**/xdg-output-unstable-v1-client-protocol.h +**/xdg-output-unstable-v1-protocol.c @@ -2,16 +2,19 @@ # GPU Screen Recorder UI A fullscreen overlay UI for [GPU Screen Recorder](https://git.dec05eba.com/gpu-screen-recorder/about/) in the style of ShadowPlay.\ -The application is currently primarly designed for X11 but it can run on Wayland as well through XWayland, with some caveats because of Wayland limitations.\ -Note: This software is still in early alpha. Expect bugs, and please report any if you experience them. Some are already known, but it doesn't hurt to report them anyways.\ -You can report an issue by emailing the issue to dec05eba@protonmail.com. +The application is currently primarly designed for X11 but it can run on Wayland as well through XWayland, with some caveats because of Wayland limitations. # Usage -Run `gsr-ui` and press `Alt+Z` to show/hide the UI. You can start the overlay UI at system startup by running `systemctl enable --now --user gpu-screen-recorder-ui`. +You can start the overlay UI and make it start automatically on system startup by running `systemctl enable --now --user gpu-screen-recorder-ui`. +Alternatively you can run `gsr-ui` and go into settings and enable start on system startup setting.\ +Press `Left Alt+Z` to show/hide the UI. Go into settings to view all of the different hotkeys configured.\ +If you use a non-systemd distro and want to start the UI on system startup then you have to manually add `gsr-ui` to your system startup script.\ +A program called `gsr-ui-cli` is also installed when installing this software. This can be used to remotely control the UI. Run `gsr-ui-cli --help` to list the available commands. # Installation If you are using an Arch Linux based distro then you can find gpu screen recorder ui on aur under the name gpu-screen-recorder-ui (`yay -S gpu-screen-recorder-ui`).\ -If you are running another distro then you can run `sudo ./install.sh`, but you need to manually install the dependencies, as described below. +If you are running another distro then you can run `sudo ./install.sh`, but you need to manually install the dependencies, as described below.\ +You can also install gpu screen recorder from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder) which includes this UI. # Dependencies GPU Screen Recorder UI uses meson build system so you need to install `meson` to build GPU Screen Recorder UI. @@ -19,35 +22,52 @@ GPU Screen Recorder UI uses meson build system so you need to install `meson` to ## Build dependencies These are the dependencies needed to build GPU Screen Recorder UI: -* x11 (libx11, libxrandr, libxrender, libxcomposite, libxfixes, libxi) +* x11 (libx11, libxrandr, libxrender, libxcomposite, libxfixes, libxext, libxi, libxcursor) * libglvnd (which provides libgl, libglx and libegl) -* libevdev -* libudev (systemd-libs) -* libinput -* libxkbcommon +* linux-api-headers +* libpulse (libpulse-simple) +* libdrm +* wayland (wayland-client, wayland-egl, wayland-scanner) +* setcap (libcap) ## Runtime dependencies There are also additional dependencies needed at runtime: -* [GPU Screen Recorder](https://git.dec05eba.com/gpu-screen-recorder/) +* [GPU Screen Recorder](https://git.dec05eba.com/gpu-screen-recorder/) (version 5.0.0 or later) * [GPU Screen Recorder Notification](https://git.dec05eba.com/gpu-screen-recorder-notification/) +## Program behavior notes +This program has to grab all keyboards and create a virtual keyboard (`gsr-ui virtual keyboard`) to make global hotkeys work on all Wayland compositors.\ +This might cause issues for you if you use keyboard remapping software. To workaround this you can go into settings and select "Only grab virtual devices".\ +If you use keyboard remapping software such as keyd then make sure to make it ignore "gsr-ui virtual keyboard" (dec0:5eba device id), otherwise your keyboard can get locked +as gpu screen recorder tries to grab keys and keyd grabs gpu screen recorder, leading to a lock.\ +If you are stuck in such a lock where you cant press and keyboard keys you can press (left) ctrl+shift+alt+esc to close gpu screen recorder and remove it from system startup. + # License -This software is licensed under GPL3.0-only. Files under `fonts/` directory are licensed under `SIL Open Font License`. +This software is licensed under GPL3.0-only. Files under `fonts/` directory belong to the Noto Sans Google fonts project and they are licensed under `SIL Open Font License`.\ +`images/default.cur` it part of the [Adwaita icon theme](https://gitlab.gnome.org/GNOME/adwaita-icon-theme/-/tree/master) which is licensed under `CC BY-SA 3.0`.\ +The controller buttons under `images/` were created by [Julio Cacko](https://juliocacko.itch.io/free-input-prompts) and they are licensed under `CC0 1.0 Universal`.\ +The PlayStation logo under `images/` was created by [ArksDigital](https://arks.itch.io/ps4-buttons) and it's licensed under `CC BY 4.0`. + +# Reporting bugs, contributing patches, questions or donation +See [https://git.dec05eba.com/?p=about](https://git.dec05eba.com/?p=about).\ +I'm looking for somebody that can create sound effects for the notifications. # Demo [](https://www.youtube.com/watch?v=SOqXusCTXXA) # Screenshots - - - -# Donations -If you want to donate you can donate via bitcoin or monero. -* Bitcoin: bc1qqvuqnwrdyppf707ge27fqz2n9y9gu7lf5ypyuf -* Monero: 4An9kp2qW1C9Gah7ewv4JzcNFQ5TAX7ineGCqXWK6vQnhsGGcRpNgcn8r9EC3tMcgY7vqCKs3nSRXhejMHBaGvFdN2egYet + + # Known issues -* Some games receive mouse input while the UI is open -* Global hotkeys on Wayland can clash with keys used by other applications. This is primarly because Wayland compositors are missing support for global hotkey so this software uses a global hotkey system that works on all Wayland compositors. -* When the UI is open the wallpaper is shown instead of the game on Hyprland and Sway. This is an issue with Hyprland and Sway. It cant be fixed until the UI is redesigned to not be a fullscreen overlay. +* When the UI is open the wallpaper is shown instead of the game on Hyprland. This is an issue with Hyprland. It cant be fixed until the UI is redesigned to not be a fullscreen overlay. Change your waybar dock mode to "dock" in its config to fix this. +* Opening the UI when a game is fullscreen can mess up the game window a bit on Hyprland. This is an issue with Hyprland. Change your waybar dock mode to "dock" in its config to fix this. +* The background of the UI is black when opening the UI while a Wayland application is focused on COSMIC. This is an issue with COSMIC. +* Unable to close the region selection with escape key while a Wayland application is focused on COSMIC. This is an issue with COSMIC. + +# FAQ +## I get an error when trying to start the gpu-screen-recorder-ui.service systemd service +If you have previously used the flatpak version of GPU Screen Recorder with the new UI then the non-flatpak version of the systemd service will conflict with that. Run `gsr-ui` to fix that. +## I use a non-qwerty keyboard layout and I have an issue with incorrect keys registered in the software +This is a KDE Plasma Wayland issue. Use `setxkbmap <language>` command, for example `setxkbmap se` to make sure X11 applications (such as this one) gets updated to use your languages keyboard layout. @@ -1,12 +1,4 @@ -setcap nice for good performance when opening overlay when game is running below 60 fps. -Maybe grab cursor with xi, as that will prevent games from detecting movement with xi2 api. - -Fullscreen on wayland doesn't render windows behind because it's a compositor optimization, to not draw anything behind (only draw the window directly without compositing). -Fix this by drawing the window smaller, or have two windows (left and right half monitor width). -Maybe change design to have black triangles appear and get larger until they fill the screen, with even spaces being left with no triangles. - Exclude triangles from a diagonal line across the screen. - Have buttons appear slanted in 3D. - All of these things should be done with vertex buffer, for real 3D. +setcap nice for good performance when opening overlay when game is running below 60 fps (on amd). WAYLAND_DISPLAY gamescope-0, DISPLAY=:1 (gamescope xwayland) @@ -20,16 +12,8 @@ Handle events in draw function because the render position of elements is availa Add nvidia overclock option. -Add support for window selection in capture. - -Add option to record the focused monitor. This works on wayland too when using kms capture since we can get cursor position without root and see which monitor (crtc) the cursor is on. Or use create_window_get_center_position. - -Make hotkeys configurable. - Filechooser should have the option to select list view, search bar and common folders/mounted drives on the left side for quick navigation. Also a button to create a new directory. -Support wayland (excluding gnome, or force xwayland on gnome). - Restart replay on system start if monitor resolution changes. Show warning when selecting hevc/av1 on amd because of amd driver/ffmpeg bug. @@ -47,12 +31,10 @@ Add global setting. In that setting there should be an option to enable/disable Add profiles and hotkey to switch between profiles (show notification when switching profile). -Fix first frame being black. +Fix first frame being black when running without a compositor. Add support for systray. -Add option to take screenshot. - Move event callbacks to a global list instead of std::function object in each widget. This reduces the size of widgets, since most widgets wont have the event callback set. This event callback would pass the widget as an argument. @@ -66,41 +48,150 @@ Make save-video-in-game-folder.sh and notify-saved-name.sh run ~/.config/gpu-scr if the profile is called 4chan. Create a directory of such example scripts, including 4chan webm one. -On nvidia check if suspend fix is applied. If not, show a popup asking the user to apply it (and apply it automatically). This is a requirement before this package is made to a flatpak. +On nvidia check if suspend fix is applied. If not, show a popup asking the user to apply it (and apply it automatically). Show warning when using steam deck or when trying to capture hevc/av1 on amd (the same warnings as gpu screen recorder gtk). -Add option to capture application audio. This should show a popup where you can use one of the available applications or a custom one and choose to record that application or all applications except that one. - Add profile option. Convert view to profile, add an option at the bottom that says "Edit profiles..." which should show a popup where you can create/remove profiles. New profiles should always be in advanced view. Verify monitor/audio when starting recording. Give an error if the options are no longer valid. -Get focused window when opening gsr-ui and pass that to the save replay script, to ignore gsr-ui when getting game name. - -gsr ui window has _NET_WM_STATE _NET_WM_STATE_ABOVE, not _NET_WM_STATE_FULLSCREEN +gsr ui window has _NET_WM_STATE _NET_WM_STATE_ABOVE, not _NET_WM_STATE_FULLSCREEN. For replay on fullscreen detect focused fullscreen window by checking if the window size is the same as the monitor size instead of _NET_WM_STATE_FULLSCREEN. -Add audio devices/app refresh button. - Play camera shutter sound when saving recording. When another sound when starting recording. Some games such as "The Finals" crashes/freezes when they lose focus when running them on x11 on a laptop with prime setup and the monitor runs on the iGPU while the game runs on the dGPU. -Try to reproduce this and if it happens try grab cursor and keyboard instead of setting gsr ui focus and make gsr ui click through like gsr notify. This might fix the issue. Run `systemctl status --user gpu-screen-recorder` when starting recording and give a notification warning if it returns 0 (running). Or run pidof gpu-screen-recorder. Add option to select which gpu to record with, or list all monitors and automatically use the gpu associated with the monitor. Do the same in gtk application. -Remove all dependencies from tools/gsr-global-hotkeys and roll our own keyboard events code. +Use global shortcuts desktop portal protocol on wayland when available. + +Support CJK. -Test global hotkeys with azerty instead of qwerty. +Move ui hover code from ::draw to ::on_event, to properly handle widget event stack. -Fix cursor grab not working in owlboy, need to use xigrab. +Save audio devices by name instead of id. This is more robust since audio id can change(?). -Dont allow autostart of replay if capture option is window recording (when window recording is added). +Improve linux global hotkeys startup time by parsing /proc/bus/input/devices instead of ioctl. <- Do this! -Use global shortcuts desktop portal protocol on wayland when available. +We can get the name of the running steam game without x11 by listing processes and finding the one that runs a program called "reaper" with the arguments SteamLaunch AppId=<number>. The binary comes after the -- argument, get the name of the game by parsing out name from that, in the format steamapps/common/<name>/. + +All steam game names by ID are available at https://api.steampowered.com/ISteamApps/GetAppList/v2/. The name of a single game can be retrieved from http://store.steampowered.com/api/appdetails?appids=115800. + +Dont put widget position to int position when scrolling. This makes the UI jitter when it's coming to a halt. + +Show warning if another instance of gpu screen recorder is already running when starting recording? + +Keyboard leds get turned off when stopping gsr-global-hotkeys (for example numlock). The numlock key has to be pressed twice again to make it look correct to match its state. + +Make gsr-ui flatpak systemd work nicely with non-flatpak gsr-ui. Maybe change ExecStart to do flatpak run ... || gsr-ui, but make it run as a shell command first with /bin/sh -c "". + +When enabling X11 global hotkey again only grab lalt, not ralt. + +Show an error that prime run will be disabled when using desktop portal capture option. This can cause issues as the user may have selected a video codec option that isn't available on their iGPU but is available on the prime-run dGPU. + +For keyboards that report supporting mice the keyboard grab will be delayed until any key has been pressed (and then released), see: https://github.com/dec05eba/gpu-screen-recorder-issues/issues/97 + See if there is any way around this. + +Check if "modprobe uinput" is needed on some systems (old fedora?). + +Add recording timer to see duration of recording/streaming. + +Make folder with window name work when using gamescope. Gamescope runs x11 itself so to get the window name inside that we have to connect to the gamescope X11 server (DISPLAY=:1 on x11 and DISPLAY=:2 on wayland, but not always). + Detect if the window is gamescope automatically (WM_CLASS = "gamescope") and get the x11 display automatically and connect to it to get the application its running. + This seems to only be an issue on wayland? the window title of the gamescope/steam bigpicture mode is the title of the game on x11, so it works automatically on x11. + +When clicking on current directory in file manager show a dropdown menu where you can select common directories (HOME, Videos, Downloads and mounted drives) for quick navigation. Maybe even button to search. + +Maybe change gsr-ui startup retry time in the systemd service, from 5 seconds to 2 seconds. + +Make it possible to take a screenshot through a button in the ui instead of having to use hotkey. + +Handle failing to save a replay. gsr should output "failed to save replay, or something like that" to make it possible to detect that. + +Dont allow saving replay while a replay save is in progress. + +Make input work with cjk input systems (such as fcitx). + +System startup option should also support runit and some other init systems, not only soystemd. + +Use x11 shm instead of XGetImage (https://stackoverflow.com/questions/43442675/how-to-use-xshmgetimage-and-xshmputimage). + +Add a hotkey to record/stream/replay region. + +Do xi grab for keys as well. Otherwise the ui cant be used for keyboard input if a program has grabbed the keyboard, and there could possibly be a game that grabs the keyboard as well. + +Make inactive buttons gray (in dropdown boxes and in the front page with save, etc when replay is not running). + +Add option to do screen-direct recording. But make it clear that it should not be used, except for gsync on x11 nvidia. + +Add systray for recording status. + +Add a desktop icon when gsr-ui has a window mode option (which should be the default launch option). + +Verify if cursor tracker monitor name is always correct. It uses the wayland monitor name for recording, but gpu screen recorder uses a custom name created from the drm connector name. + +Notification with the focused monitor (with CursorTrackerWayland) assumes that the x11 monitor name is the same as the drm monitor name. Same for find_monitor_by_name. + +If CursorTrackerWayland fails then fallback to getting focused monitor by window creation trick. Need to take into consideration prime laptop with dGPU that controls external monitors which cant be captured (different /dev/dri/card device). + Maybe automatically switch to recording with the device that controls the monitor. + In that case also add all monitors available to capture in the capture list and automatically choose the gpu that controls the monitor. + +Support cjk font. Use fc-match to find the location of the font. This also works in flatpak, in which case the fonts are in /run/host/..., where it lists system fonts. + +Keyboard layout is incorrect on wayland when using kde plasma keyboard settings to setup multiple keyboards, for example when changing to french. + Text input is correct, but hotkey is incorrect. + Need to use "setxkbmap fr" as well. + This happens only when grabbing keyboard (gsr-global-hotkeys). Same thing is seen with xev. + +Getting focused monitor on wayland doesn't work when vrr is enabled. This is because it uses software cursor instead (at least on kde plasma wayland). + Right now it falls back to create window & getting window position trick if there is no cursor visible (or a software cursor) and one monitor has vrr enabled. + Remove this when linux & wayland supports vrr with hardware cursor plane. + Find out another way to get cursor position on wayland. + This was fixed in linux 6.11 and in kde plasma in this commit: https://invent.kde.org/plasma/kwin/-/merge_requests/7582/diffs. + +Add option to start recording/replay/stream after the notification has disappeared. Show "Starting recording on this monitor in 3 seconds". + See if we can use hardware overlay plane instead somehow. + +When using wayland for mgl try using wlr-layer-shell and set layer to overlay and keyboard interactivity to exclusive. Do something similar for notifications. + +When starting gsr-ui remove any temporary replay disk data that has possibly remained from a crash, by looking for all folders that starts with gsr-replay and end with .gsr, in the replay directory. + +Add restart program button, in global settings. It should do almost the same thing as exit program, execept execv gsr-ui. + +When gpu screen recorder ui can run as a regular window (and supports tray icon and global shortcut portal) remove gpu screen recorder gtk. Then all error checking needs to be moved from that project to this project. + May need support for multi windows, or create a small project to display dialogs. + +Add a bug report page that automatically includes system info (make this clear to the user). + Do this by sending the report to a custom server that stores that data. + The server should limit reports per IP to prevent spam. + +Make it possible to change controller hotkeys. Also read from /dev/input/eventN instead of /dev/input/jsN. This is readable for controllers. + +Add option to copy screenshot to clipboard. Does it work properly on Wayland compositors? Maybe need to wait until the application becomes Wayland native instead of XWayland. + +Show message that replay/streaming has to be restarted if recording settings are changed while replay/streaming is ongoing. + +Support vector graphics. Maybe support svg, rendering it to a texture for better performance. + +Support freetype for text rendering. Maybe load freetype as runtime (with dlopen) and use that when available and fallback to stb_freetype if not available. + +Show .webm container option. It's currently chosen automatically if vp8/vp9 is chosen. The available containers should automatically switch depending on the video codec. + +In settings show audio levels for each audio. Maybe show audio level image beside the audio name in the dropdown box and switch to a different image (have 3-4 different images for each level) depending on the volume. + +Only use fake cursor on wayland if the focused x11 window is fullscreen. + +Create window as a real overlay window, using layer shell protocol, when possible. This will however minimize windows on floating wms. Check if this can be fixed somehow, or only use layer shell in tiling wms. + +Add timeout option for screenshots. + +Add a window that shows a warning for wayland users, that wayland doesn't support this software and if they experience issues then they should use x11 instead. + +Add a window that shows a warning if gpu video encoding isn't supported. -When support for window capture is enabled on x11 then make sure to not save the window except temporary while the program is open.
\ No newline at end of file +Disable system notifications when recording. Does the notification dbus interface support pausing notifications? diff --git a/depends/mglpp b/depends/mglpp -Subproject 0c8ccb86a55e9b5b98ab68ca538ad7374308bae +Subproject a784fdb62b1ddfc8c38733c3a16cd1f39e5d415 diff --git a/extra/gpu-screen-recorder-ui.service b/extra/gpu-screen-recorder-ui.service index 8abb3de..e6120e6 100644 --- a/extra/gpu-screen-recorder-ui.service +++ b/extra/gpu-screen-recorder-ui.service @@ -2,7 +2,7 @@ Description=GPU Screen Recorder UI Service [Service] -ExecStart=gsr-ui +ExecStart=gsr-ui launch-daemon KillSignal=SIGINT Restart=on-failure RestartSec=5s diff --git a/flatpak/gpu-screen-recorder-ui.service b/flatpak/gpu-screen-recorder-ui.service new file mode 100644 index 0000000..3ed7f4b --- /dev/null +++ b/flatpak/gpu-screen-recorder-ui.service @@ -0,0 +1,11 @@ +[Unit] +Description=GPU Screen Recorder UI Service + +[Service] +ExecStart=flatpak run com.dec05eba.gpu_screen_recorder gsr-ui +KillSignal=SIGINT +Restart=on-failure +RestartSec=5s + +[Install] +WantedBy=default.target
\ No newline at end of file diff --git a/images/default.cur b/images/default.cur Binary files differnew file mode 100644 index 0000000..c7da315 --- /dev/null +++ b/images/default.cur diff --git a/images/delete.png b/images/delete.png Binary files differnew file mode 100644 index 0000000..f4ac335 --- /dev/null +++ b/images/delete.png diff --git a/images/ps4_cross.png b/images/ps4_cross.png Binary files differnew file mode 100644 index 0000000..fc14b2b --- /dev/null +++ b/images/ps4_cross.png diff --git a/images/ps4_dpad_down.png b/images/ps4_dpad_down.png Binary files differnew file mode 100644 index 0000000..727fdd3 --- /dev/null +++ b/images/ps4_dpad_down.png diff --git a/images/ps4_dpad_left.png b/images/ps4_dpad_left.png Binary files differnew file mode 100644 index 0000000..e114ed7 --- /dev/null +++ b/images/ps4_dpad_left.png diff --git a/images/ps4_dpad_right.png b/images/ps4_dpad_right.png Binary files differnew file mode 100644 index 0000000..6ebd88e --- /dev/null +++ b/images/ps4_dpad_right.png diff --git a/images/ps4_dpad_up.png b/images/ps4_dpad_up.png Binary files differnew file mode 100644 index 0000000..e287b3d --- /dev/null +++ b/images/ps4_dpad_up.png diff --git a/images/ps4_home.png b/images/ps4_home.png Binary files differnew file mode 100644 index 0000000..d17adc0 --- /dev/null +++ b/images/ps4_home.png diff --git a/images/ps4_options.png b/images/ps4_options.png Binary files differnew file mode 100644 index 0000000..99787fa --- /dev/null +++ b/images/ps4_options.png diff --git a/images/ps4_triangle.png b/images/ps4_triangle.png Binary files differnew file mode 100644 index 0000000..ff07fcd --- /dev/null +++ b/images/ps4_triangle.png diff --git a/images/replay.png b/images/replay.png Binary files differindex 65c9339..e9ec83b 100644 --- a/images/replay.png +++ b/images/replay.png diff --git a/images/screenshot.png b/images/screenshot.png Binary files differnew file mode 100644 index 0000000..d67acf6 --- /dev/null +++ b/images/screenshot.png diff --git a/images/settings.png b/images/settings.png Binary files differindex 5f8d203..efc19a1 100644 --- a/images/settings.png +++ b/images/settings.png diff --git a/images/settings_extra_small.png b/images/settings_extra_small.png Binary files differnew file mode 100644 index 0000000..71770fe --- /dev/null +++ b/images/settings_extra_small.png diff --git a/images/settings_small.png b/images/settings_small.png Binary files differindex 30534e6..dcb896d 100644 --- a/images/settings_small.png +++ b/images/settings_small.png diff --git a/images/trash.png b/images/trash.png Binary files differnew file mode 100644 index 0000000..9ae2191 --- /dev/null +++ b/images/trash.png diff --git a/include/AudioPlayer.hpp b/include/AudioPlayer.hpp new file mode 100644 index 0000000..22c3be8 --- /dev/null +++ b/include/AudioPlayer.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include <thread> + +namespace gsr { + // Only plays raw stereo PCM audio in 48000hz in s16le format. + // Use this command to convert an audio file (input.wav) to a format playable by this class (output.pcm): + // ffmpeg -i input.wav -f s16le -acodec pcm_s16le -ar 48000 output.pcm + class AudioPlayer { + public: + AudioPlayer() = default; + ~AudioPlayer(); + AudioPlayer(const AudioPlayer&) = delete; + AudioPlayer& operator=(const AudioPlayer&) = delete; + + bool play(const char *filepath); + private: + std::thread thread; + bool stop_playing_audio = false; + int audio_file_fd = -1; + }; +}
\ No newline at end of file diff --git a/include/Config.hpp b/include/Config.hpp index c61ca10..7c2aeda 100644 --- a/include/Config.hpp +++ b/include/Config.hpp @@ -6,12 +6,36 @@ #include <vector> #include <optional> +#define GSR_CONFIG_FILE_VERSION 2 + namespace gsr { struct SupportedCaptureOptions; + enum class ReplayStartupMode { + DONT_TURN_ON_AUTOMATICALLY, + TURN_ON_AT_SYSTEM_STARTUP, + TURN_ON_AT_FULLSCREEN, + TURN_ON_AT_POWER_SUPPLY_CONNECTED + }; + + ReplayStartupMode replay_startup_string_to_type(const char *startup_mode_str); + struct ConfigHotkey { - int64_t keysym = 0; - uint32_t modifiers = 0; + int64_t key = 0; // Mgl key + uint32_t modifiers = 0; // HotkeyModifier + + bool operator==(const ConfigHotkey &other) const; + bool operator!=(const ConfigHotkey &other) const; + + std::string to_string(bool spaces = true, bool modifier_side = true) const; + }; + + struct AudioTrack { + std::vector<std::string> audio_inputs; // ids + bool application_audio_invert = false; + + bool operator==(const AudioTrack &other) const; + bool operator!=(const AudioTrack &other) const; }; struct RecordOptions { @@ -21,16 +45,17 @@ namespace gsr { int32_t video_width = 0; int32_t video_height = 0; int32_t fps = 60; - int32_t video_bitrate = 15000; - bool merge_audio_tracks = true; - bool application_audio_invert = false; + int32_t video_bitrate = 8000; + bool merge_audio_tracks = true; // TODO: Remove in the future + bool application_audio_invert = false; // TODO: Remove in the future bool change_video_resolution = false; - std::vector<std::string> audio_tracks; + std::vector<std::string> audio_tracks; // ids, TODO: Remove in the future + std::vector<AudioTrack> audio_tracks_list; std::string color_range = "limited"; std::string video_quality = "very_high"; std::string video_codec = "auto"; std::string audio_codec = "opus"; - std::string framerate_mode = "vfr"; + std::string framerate_mode = "auto"; bool advanced_view = false; bool overclock = false; bool record_cursor = true; @@ -38,9 +63,12 @@ namespace gsr { }; struct MainConfig { - int32_t config_file_version = 0; + int32_t config_file_version = GSR_CONFIG_FILE_VERSION; bool software_encoding_warning_shown = false; + std::string hotkeys_enable_option = "enable_hotkeys"; + std::string joystick_hotkeys_enable_option = "disable_hotkeys"; std::string tint_color; + ConfigHotkey show_hide_hotkey; }; struct YoutubeStreamConfig { @@ -51,6 +79,10 @@ namespace gsr { std::string stream_key; }; + struct RumbleStreamConfig { + std::string stream_key; + }; + struct CustomStreamConfig { std::string url; std::string container = "flv"; @@ -63,8 +95,9 @@ namespace gsr { std::string streaming_service = "twitch"; YoutubeStreamConfig youtube; TwitchStreamConfig twitch; + RumbleStreamConfig rumble; CustomStreamConfig custom; - ConfigHotkey start_stop_recording_hotkey; + ConfigHotkey start_stop_hotkey; }; struct RecordConfig { @@ -72,33 +105,60 @@ namespace gsr { bool save_video_in_game_folder = false; bool show_recording_started_notifications = true; bool show_video_saved_notifications = true; + bool show_video_paused_notifications = true; std::string save_directory; std::string container = "mp4"; - ConfigHotkey start_stop_recording_hotkey; - ConfigHotkey pause_unpause_recording_hotkey; + ConfigHotkey start_stop_hotkey; + ConfigHotkey pause_unpause_hotkey; }; struct ReplayConfig { RecordOptions record_options; std::string turn_on_replay_automatically_mode = "dont_turn_on_automatically"; bool save_video_in_game_folder = false; + bool restart_replay_on_save = false; bool show_replay_started_notifications = true; bool show_replay_stopped_notifications = true; bool show_replay_saved_notifications = true; std::string save_directory; std::string container = "mp4"; int32_t replay_time = 60; - ConfigHotkey start_stop_recording_hotkey; - ConfigHotkey save_recording_hotkey; + std::string replay_storage = "ram"; + ConfigHotkey start_stop_hotkey; + ConfigHotkey save_hotkey; + ConfigHotkey save_1_min_hotkey; + ConfigHotkey save_10_min_hotkey; + }; + + struct ScreenshotConfig { + std::string record_area_option = "screen"; + int32_t image_width = 0; + int32_t image_height = 0; + bool change_image_resolution = false; + std::string image_quality = "very_high"; + std::string image_format = "jpg"; + bool record_cursor = true; + bool restore_portal_session = true; + + bool save_screenshot_in_game_folder = false; + bool show_screenshot_saved_notifications = true; + std::string save_directory; + ConfigHotkey take_screenshot_hotkey; + ConfigHotkey take_screenshot_region_hotkey; }; struct Config { Config(const SupportedCaptureOptions &capture_options); + bool operator==(const Config &other); + bool operator!=(const Config &other); + + void set_hotkeys_to_default(); MainConfig main_config; StreamingConfig streaming_config; RecordConfig record_config; ReplayConfig replay_config; + ScreenshotConfig screenshot_config; }; std::optional<Config> read_config(const SupportedCaptureOptions &capture_options); diff --git a/include/CursorTracker/CursorTracker.hpp b/include/CursorTracker/CursorTracker.hpp new file mode 100644 index 0000000..ff7374f --- /dev/null +++ b/include/CursorTracker/CursorTracker.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include <optional> +#include <string> +#include <mglpp/system/vec.hpp> + +namespace gsr { + struct CursorInfo { + mgl::vec2i position; + std::string monitor_name; + }; + + class CursorTracker { + public: + CursorTracker() = default; + CursorTracker(const CursorTracker&) = delete; + CursorTracker& operator=(const CursorTracker&) = delete; + virtual ~CursorTracker() = default; + + virtual void update() = 0; + virtual std::optional<CursorInfo> get_latest_cursor_info() = 0; + }; +}
\ No newline at end of file diff --git a/include/CursorTracker/CursorTrackerWayland.hpp b/include/CursorTracker/CursorTrackerWayland.hpp new file mode 100644 index 0000000..1eeee83 --- /dev/null +++ b/include/CursorTracker/CursorTrackerWayland.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "CursorTracker.hpp" +#include <stdint.h> +#include <vector> + +struct wl_display; +struct wl_registry; +struct wl_output; +struct zxdg_output_manager_v1; +struct zxdg_output_v1; + +namespace gsr { + struct WaylandOutput { + uint32_t wl_name; + struct wl_output *output; + struct zxdg_output_v1 *xdg_output; + mgl::vec2i pos; + mgl::vec2i size; + int32_t transform; + std::string name; + }; + + class CursorTrackerWayland : public CursorTracker { + public: + CursorTrackerWayland(const char *card_path); + CursorTrackerWayland(const CursorTrackerWayland&) = delete; + CursorTrackerWayland& operator=(const CursorTrackerWayland&) = delete; + ~CursorTrackerWayland(); + + void update() override; + std::optional<CursorInfo> get_latest_cursor_info() override; + + std::vector<WaylandOutput> monitors; + struct zxdg_output_manager_v1 *xdg_output_manager = nullptr; + private: + void set_monitor_outputs_from_xdg_output(struct wl_display *dpy); + private: + int drm_fd = -1; + mgl::vec2i latest_cursor_position; // Position of the cursor within the monitor + int latest_crtc_id = -1; + }; +}
\ No newline at end of file diff --git a/include/CursorTracker/CursorTrackerX11.hpp b/include/CursorTracker/CursorTrackerX11.hpp new file mode 100644 index 0000000..66618c4 --- /dev/null +++ b/include/CursorTracker/CursorTrackerX11.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "CursorTracker.hpp" + +typedef struct _XDisplay Display; + +namespace gsr { + class CursorTrackerX11 : public CursorTracker { + public: + CursorTrackerX11(Display *dpy); + CursorTrackerX11(const CursorTrackerX11&) = delete; + CursorTrackerX11& operator=(const CursorTrackerX11&) = delete; + ~CursorTrackerX11() = default; + + void update() override {} + std::optional<CursorInfo> get_latest_cursor_info() override; + private: + Display *dpy = nullptr; + }; +}
\ No newline at end of file diff --git a/include/GlobalHotkeys.hpp b/include/GlobalHotkeys/GlobalHotkeys.hpp index 27fca07..2927fa7 100644 --- a/include/GlobalHotkeys.hpp +++ b/include/GlobalHotkeys/GlobalHotkeys.hpp @@ -9,9 +9,20 @@ namespace mgl { } namespace gsr { + enum HotkeyModifier : uint32_t { + HOTKEY_MOD_LSHIFT = 1 << 0, + HOTKEY_MOD_RSHIFT = 1 << 1, + HOTKEY_MOD_LCTRL = 1 << 2, + HOTKEY_MOD_RCTRL = 1 << 3, + HOTKEY_MOD_LALT = 1 << 4, + HOTKEY_MOD_RALT = 1 << 5, + HOTKEY_MOD_LSUPER = 1 << 6, + HOTKEY_MOD_RSUPER = 1 << 7 + }; + struct Hotkey { - uint64_t key = 0; - uint32_t modifiers = 0; + uint32_t key = 0; // X11 keysym + uint32_t modifiers = 0; // HotkeyModifier }; using GlobalHotkeyCallback = std::function<void(const std::string &id)>; diff --git a/include/GlobalHotkeys/GlobalHotkeysJoystick.hpp b/include/GlobalHotkeys/GlobalHotkeysJoystick.hpp new file mode 100644 index 0000000..0177d29 --- /dev/null +++ b/include/GlobalHotkeys/GlobalHotkeysJoystick.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include "GlobalHotkeys.hpp" +#include "../Hotplug.hpp" +#include <unordered_map> +#include <thread> +#include <poll.h> +#include <linux/joystick.h> + +namespace gsr { + static constexpr int max_js_poll_fd = 16; + + class GlobalHotkeysJoystick : public GlobalHotkeys { + class GlobalHotkeysJoystickHotplugDelegate; + public: + GlobalHotkeysJoystick() = default; + GlobalHotkeysJoystick(const GlobalHotkeysJoystick&) = delete; + GlobalHotkeysJoystick& operator=(const GlobalHotkeysJoystick&) = delete; + ~GlobalHotkeysJoystick() override; + + bool start(); + // Currently valid ids: + // save_replay + // save_1_min_replay + // save_10_min_replay + // take_screenshot + // toggle_record + // toggle_replay + // toggle_show + bool bind_action(const std::string &id, GlobalHotkeyCallback callback) override; + void poll_events() override; + private: + void read_events(); + void process_js_event(int fd, js_event &event); + bool add_device(const char *dev_input_filepath, bool print_error = true); + bool remove_device(const char *dev_input_filepath); + bool remove_poll_fd(int index); + // Returns -1 if not found + int get_poll_fd_index_by_dev_input_id(int dev_input_id) const; + private: + struct ExtraData { + int dev_input_id = 0; + }; + + std::unordered_map<std::string, GlobalHotkeyCallback> bound_actions_by_id; + std::thread read_thread; + + pollfd poll_fd[max_js_poll_fd]; + ExtraData extra_data[max_js_poll_fd]; + int num_poll_fd = 0; + int event_fd = -1; + int event_index = -1; + + bool playstation_button_pressed = false; + bool up_pressed = false; + bool down_pressed = false; + bool left_pressed = false; + bool right_pressed = false; + bool l3_button_pressed = false; + bool r3_button_pressed = false; + + bool save_replay = false; + bool save_1_min_replay = false; + bool save_10_min_replay = false; + bool take_screenshot = false; + bool toggle_record = false; + bool toggle_replay = false; + bool toggle_show = false; + int hotplug_poll_index = -1; + Hotplug hotplug; + }; +}
\ No newline at end of file diff --git a/include/GlobalHotkeysLinux.hpp b/include/GlobalHotkeys/GlobalHotkeysLinux.hpp index 62da74e..959d095 100644 --- a/include/GlobalHotkeysLinux.hpp +++ b/include/GlobalHotkeys/GlobalHotkeysLinux.hpp @@ -7,18 +7,28 @@ namespace gsr { class GlobalHotkeysLinux : public GlobalHotkeys { public: - GlobalHotkeysLinux(); + enum class GrabType { + ALL, + VIRTUAL + }; + + GlobalHotkeysLinux(GrabType grab_type); GlobalHotkeysLinux(const GlobalHotkeysLinux&) = delete; GlobalHotkeysLinux& operator=(const GlobalHotkeysLinux&) = delete; ~GlobalHotkeysLinux() override; bool start(); - bool bind_action(const std::string &id, GlobalHotkeyCallback callback) override; + bool bind_key_press(Hotkey hotkey, const std::string &id, GlobalHotkeyCallback callback) override; + void unbind_all_keys() override; void poll_events() override; private: + void close_fds(); + private: pid_t process_id = 0; - int pipes[2]; + int read_pipes[2]; + int write_pipes[2]; FILE *read_file = nullptr; std::unordered_map<std::string, GlobalHotkeyCallback> bound_actions_by_id; + GrabType grab_type; }; }
\ No newline at end of file diff --git a/include/GlobalHotkeysX11.hpp b/include/GlobalHotkeys/GlobalHotkeysX11.hpp index 610399a..610399a 100644 --- a/include/GlobalHotkeysX11.hpp +++ b/include/GlobalHotkeys/GlobalHotkeysX11.hpp diff --git a/include/GsrInfo.hpp b/include/GsrInfo.hpp index 6ec8e23..156125b 100644 --- a/include/GsrInfo.hpp +++ b/include/GsrInfo.hpp @@ -2,6 +2,7 @@ #include <string> #include <vector> +#include <stdint.h> #include <mglpp/system/vec.hpp> @@ -19,15 +20,35 @@ namespace gsr { bool vp9 = false; }; + struct SupportedImageFormats { + bool jpeg = false; + bool png = false; + }; + struct GsrMonitor { std::string name; mgl::vec2i size; }; + struct GsrVersion { + uint8_t major = 0; + uint8_t minor = 0; + uint8_t patch = 0; + + bool operator>(const GsrVersion &other) const; + bool operator>=(const GsrVersion &other) const; + bool operator<(const GsrVersion &other) const; + bool operator<=(const GsrVersion &other) const; + bool operator==(const GsrVersion &other) const; + bool operator!=(const GsrVersion &other) const; + + std::string to_string() const; + }; + struct SupportedCaptureOptions { bool window = false; + bool region = false; bool focused = false; - bool screen = false; bool portal = false; std::vector<GsrMonitor> monitors; }; @@ -41,13 +62,15 @@ namespace gsr { struct SystemInfo { DisplayServer display_server = DisplayServer::UNKNOWN; bool supports_app_audio = false; + GsrVersion gsr_version; }; enum class GpuVendor { UNKNOWN, AMD, INTEL, - NVIDIA + NVIDIA, + BROADCOM }; struct GpuInfo { @@ -59,6 +82,7 @@ namespace gsr { SystemInfo system_info; GpuInfo gpu_info; SupportedVideoCodecs supported_video_codecs; + SupportedImageFormats supported_image_formats; }; enum class GsrInfoExitStatus { diff --git a/include/Hotplug.hpp b/include/Hotplug.hpp new file mode 100644 index 0000000..38fe25d --- /dev/null +++ b/include/Hotplug.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include <functional> + +namespace gsr { + enum class HotplugAction { + ADD, + REMOVE + }; + + using HotplugEventCallback = std::function<void(HotplugAction hotplug_action, const char *devname)>; + + class Hotplug { + public: + Hotplug() = default; + Hotplug(const Hotplug&) = delete; + Hotplug& operator=(const Hotplug&) = delete; + ~Hotplug(); + + bool start(); + int steal_fd(); + void process_event_data(int fd, const HotplugEventCallback &callback); + private: + void parse_netlink_data(const char *line, const HotplugEventCallback &callback); + private: + int fd = -1; + bool started = false; + bool event_is_add = false; + bool event_is_remove = false; + bool subsystem_is_input = false; + char event_data[1024]; + }; +}
\ No newline at end of file diff --git a/include/Overlay.hpp b/include/Overlay.hpp index 283f2b1..3de89c2 100644 --- a/include/Overlay.hpp +++ b/include/Overlay.hpp @@ -5,6 +5,12 @@ #include "GsrInfo.hpp" #include "Config.hpp" #include "window_texture.h" +#include "WindowUtils.hpp" +#include "GlobalHotkeys/GlobalHotkeysJoystick.hpp" +#include "AudioPlayer.hpp" +#include "RegionSelector.hpp" +#include "WindowSelector.hpp" +#include "CursorTracker/CursorTracker.hpp" #include <mglpp/window/Window.hpp> #include <mglpp/window/Event.hpp> @@ -31,7 +37,8 @@ namespace gsr { NONE, RECORD, REPLAY, - STREAM + STREAM, + SCREENSHOT }; class Overlay { @@ -41,8 +48,7 @@ namespace gsr { Overlay& operator=(const Overlay&) = delete; ~Overlay(); - void handle_events(gsr::GlobalHotkeys *global_hotkeys); - void on_event(mgl::Event &event); + void handle_events(); // Returns false if not visible bool draw(); @@ -54,29 +60,47 @@ namespace gsr { void toggle_stream(); void toggle_replay(); void save_replay(); - void show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type); + void save_replay_1_min(); + void save_replay_10_min(); + void take_screenshot(); + void take_screenshot_region(); + void show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type, const char *capture_target = nullptr); bool is_open() const; + bool should_exit(std::string &reason) const; + void exit(); + void go_back_to_old_ui(); + + const Config& get_config() const; + + void unbind_all_keyboard_hotkeys(); + void rebind_all_keyboard_hotkeys(); private: + void handle_keyboard_mapping_event(); + void on_event(mgl::Event &event); + + void create_frontpage_ui_components(); void xi_setup(); void handle_xi_events(); void process_key_bindings(mgl::Event &event); void grab_mouse_and_keyboard(); void xi_setup_fake_cursor(); - void xi_grab_all_devices(); - void xi_warp_pointer(mgl::vec2i position); void close_gpu_screen_recorder_output(); void update_notification_process_status(); void save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type); - void update_gsr_replay_save(); + void on_replay_saved(const char *replay_saved_filepath); + void process_gsr_output(); + void on_gsr_process_error(int exit_code, NotificationType notification_type); void update_gsr_process_status(); + void update_gsr_screenshot_process_status(); void replay_status_update_status(); void update_focused_fullscreen_status(); void update_power_supply_status(); + void update_system_startup_status(); - void on_stop_recording(int exit_code); + void on_stop_recording(int exit_code, const std::string &video_filepath); void update_ui_recording_paused(); void update_ui_recording_unpaused(); @@ -90,11 +114,17 @@ namespace gsr { void update_ui_replay_started(); void update_ui_replay_stopped(); + void prepare_gsr_output_for_reading(); void on_press_save_replay(); - void on_press_start_replay(bool disable_notification); - void on_press_start_record(); - void on_press_start_stream(); - bool update_compositor_texture(const mgl_monitor *monitor); + void on_press_save_replay_1_min_replay(); + void on_press_save_replay_10_min_replay(); + bool on_press_start_replay(bool disable_notification, bool finished_selection); + void on_press_start_record(bool finished_selection); + void on_press_start_stream(bool finished_selection); + void on_press_take_screenshot(bool finished_selection, bool force_region_capture); + bool update_compositor_texture(const Monitor &monitor); + + std::string get_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options); void force_window_on_top(); private: @@ -122,7 +152,6 @@ namespace gsr { mgl::Texture cursor_texture; mgl::Sprite cursor_sprite; mgl::vec2i cursor_hotspot; - bool cursor_drawn = false; WindowTexture window_texture; PageStack page_stack; @@ -137,6 +166,7 @@ namespace gsr { pid_t notification_process = -1; int gpu_screen_recorder_process_output_fd = -1; FILE *gpu_screen_recorder_process_output_file = nullptr; + pid_t gpu_screen_recorder_screenshot_process = -1; DropdownButton *replay_dropdown_button_ptr = nullptr; DropdownButton *record_dropdown_button_ptr = nullptr; @@ -153,6 +183,7 @@ namespace gsr { bool focused_window_is_fullscreen = false; std::string record_filepath; + std::string screenshot_filepath; Display *xi_display = nullptr; int xi_opcode = 0; @@ -161,5 +192,42 @@ namespace gsr { std::array<KeyBinding, 1> key_bindings; bool drawn_first_frame = false; + + bool do_exit = false; + std::string exit_reason; + + mgl::vec2i window_size = { 1280, 720 }; + mgl::vec2i window_pos = { 0, 0 }; + + mgl::Clock show_overlay_clock; + double show_overlay_timeout_seconds = 0.0; + + std::unique_ptr<GlobalHotkeys> global_hotkeys = nullptr; + std::unique_ptr<GlobalHotkeysJoystick> global_hotkeys_js = nullptr; + Display *x11_mapping_display = nullptr; + XEvent x11_mapping_xev; + + mgl::Clock replay_save_clock; + bool replay_save_show_notification = false; + ReplayStartupMode replay_startup_mode = ReplayStartupMode::TURN_ON_AT_SYSTEM_STARTUP; + bool try_replay_startup = true; + bool replay_recording = false; + int replay_save_duration_min = 0; + + AudioPlayer audio_player; + + RegionSelector region_selector; + bool start_region_capture = false; + std::function<void()> on_region_selected; + + WindowSelector window_selector; + bool start_window_capture = false; + std::function<void()> on_window_selected; + + std::string recording_capture_target; + std::string screenshot_capture_target; + + std::unique_ptr<CursorTracker> cursor_tracker; + mgl::Clock cursor_tracker_update_clock; }; }
\ No newline at end of file diff --git a/include/Process.hpp b/include/Process.hpp index 40373b5..1789fb1 100644 --- a/include/Process.hpp +++ b/include/Process.hpp @@ -12,11 +12,14 @@ namespace gsr { }; // Arguments ending with NULL - bool exec_program_daemonized(const char **args); + bool exec_program_daemonized(const char **args, bool debug = true); // Arguments ending with NULL. |read_fd| can be NULL - pid_t exec_program(const char **args, int *read_fd); + pid_t exec_program(const char **args, int *read_fd, bool debug = true); // Arguments ending with NULL. Returns the exit status of the program or -1 on error - int exec_program_get_stdout(const char **args, std::string &result); - // |output_buffer| should be at least PATH_MAX in size - bool read_cmdline_arg0(const char *filepath, char *output_buffer); + int exec_program_get_stdout(const char **args, std::string &result, bool debug = true); + // Arguments ending with NULL. Returns the exit status of the program or -1 on error. + // This works the same as |exec_program_get_stdout|, except on flatpak where this runs the program on the + // host machine with flatpak-spawn --host + int exec_program_on_host_get_stdout(const char **args, std::string &result, bool debug = true); + pid_t pidof(const char *process_name, pid_t ignore_pid); }
\ No newline at end of file diff --git a/include/RegionSelector.hpp b/include/RegionSelector.hpp new file mode 100644 index 0000000..ef0bc0e --- /dev/null +++ b/include/RegionSelector.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include "WindowUtils.hpp" +#include <mglpp/system/vec.hpp> +#include <mglpp/graphics/Color.hpp> +#include <vector> + +#include <X11/Xlib.h> + +namespace gsr { + struct Region { + mgl::vec2i pos; + mgl::vec2i size; + }; + + class RegionSelector { + public: + RegionSelector(); + RegionSelector(const RegionSelector&) = delete; + RegionSelector& operator=(const RegionSelector&) = delete; + ~RegionSelector(); + + bool start(mgl::Color border_color); + void stop(); + bool is_started() const; + + bool failed() const; + bool poll_events(); + bool take_selection(); + bool take_canceled(); + Region get_selection() const; + private: + void on_button_press(const void *de); + void on_button_release(const void *de); + void on_mouse_motion(const void *de); + private: + Display *dpy = nullptr; + unsigned long region_window = 0; + unsigned long cursor_window = 0; + unsigned long region_window_colormap = 0; + int xi_opcode = 0; + GC region_gc = nullptr; + GC cursor_gc = nullptr; + + Region region; + bool selecting_region = false; + bool selected = false; + bool canceled = false; + bool is_wayland = false; + std::vector<Monitor> monitors; + mgl::vec2i cursor_pos; + }; +}
\ No newline at end of file diff --git a/include/Rpc.hpp b/include/Rpc.hpp new file mode 100644 index 0000000..d6db218 --- /dev/null +++ b/include/Rpc.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include <stddef.h> +#include <functional> +#include <unordered_map> +#include <string> + +typedef struct _IO_FILE FILE; + +namespace gsr { + using RpcCallback = std::function<void(const std::string &name)>; + + class Rpc { + public: + Rpc() = default; + Rpc(const Rpc&) = delete; + Rpc& operator=(const Rpc&) = delete; + ~Rpc(); + + bool create(const char *name); + bool open(const char *name); + bool write(const char *str, size_t size); + void poll(); + + bool add_handler(const std::string &name, RpcCallback callback); + private: + bool open_filepath(const char *filepath); + private: + int fd = 0; + FILE *file = nullptr; + std::string fifo_filepath; + std::unordered_map<std::string, RpcCallback> handlers_by_name; + }; +}
\ No newline at end of file diff --git a/include/SafeVector.hpp b/include/SafeVector.hpp index 6cb4725..63125ad 100644 --- a/include/SafeVector.hpp +++ b/include/SafeVector.hpp @@ -1,8 +1,15 @@ #pragma once #include <vector> -#include <memory> #include <functional> +#include <assert.h> +#include "gui/Widget.hpp" + +template <typename T> +struct SafeVectorItem { + T item; + bool alive = false; +}; // A vector that can be modified while iterating template <typename T> @@ -10,64 +17,84 @@ class SafeVector { public: using PointerType = typename std::pointer_traits<T>::element_type*; + SafeVector() = default; + SafeVector(const SafeVector&) = delete; + SafeVector& operator=(const SafeVector&) = delete; + ~SafeVector() { + clear(); + } + void push_back(T item) { - data.push_back(std::move(item)); + data.push_back({std::move(item), true}); + ++num_items_alive; } // Safe to call when vector is empty // TODO: Make this iterator safe void pop_back() { - if(!data.empty()) + if(!data.empty()) { + gsr::add_widget_to_remove(std::move(data.back().item)); data.pop_back(); + --num_items_alive; + } } // Might not remove the data immediately if inside for_each loop. // In that case the item is removed at the end of the loop. void remove(PointerType item_to_remove) { - if(for_each_depth == 0) + if(for_each_depth == 0) { remove_item(item_to_remove); - else - remove_queue.push_back(item_to_remove); + return; + } + + SafeVectorItem<T> *item = get_item(item_to_remove); + if(item && item->alive) { + item->alive = false; + --num_items_alive; + has_items_to_remove = true; + } } // Safe to call when vector is empty, in which case it returns nullptr T* back() { - if(data.empty()) - return nullptr; - else - return &data.back(); + for(auto it = data.rbegin(), end = data.rend(); it != end; ++it) { + if(it->alive) + return &it->item; + } + return nullptr; } // TODO: Make this iterator safe void clear() { + for(auto &item : data) { + gsr::add_widget_to_remove(std::move(item.item)); + } data.clear(); - remove_queue.clear(); + num_items_alive = 0; } // Return true from |callback| to continue. This function returns false if |callback| returned false - bool for_each(std::function<bool(T &t)> callback) { + bool for_each(std::function<bool(T &t)> callback, bool include_dead = false) { bool result = true; ++for_each_depth; for(size_t i = 0; i < data.size(); ++i) { - result = callback(data[i]); - if(!result) - break; + if(data[i].alive || include_dead) { + result = callback(data[i].item); + if(!result) + break; + } } --for_each_depth; - if(for_each_depth == 0) { - for(PointerType item_to_remove : remove_queue) { - remove_item(item_to_remove); - } - remove_queue.clear(); - } + if(for_each_depth == 0) + remove_dead_items(); return result; } // Return true from |callback| to continue. This function returns false if |callback| returned false - bool for_each_reverse(std::function<bool(T &t)> callback) { + bool for_each_reverse(std::function<bool(T &t)> callback, bool include_dead = false) { bool result = true; ++for_each_depth; @@ -80,50 +107,84 @@ public: if(i < 0) break; - result = callback(data[i]); - if(!result) - break; + if(data[i].alive || include_dead) { + result = callback(data[i].item); + if(!result) + break; + } --i; } --for_each_depth; - if(for_each_depth == 0) { - for(PointerType item_to_remove : remove_queue) { - remove_item(item_to_remove); - } - remove_queue.clear(); - } + if(for_each_depth == 0) + remove_dead_items(); return result; } T& operator[](size_t index) { - return data[index]; + assert(index < data.size()); + return data[index].item; } const T& operator[](size_t index) const { - return data[index]; + assert(index < data.size()); + return data[index].item; } size_t size() const { - return data.size(); + return (size_t)num_items_alive; } bool empty() const { - return data.empty(); + return num_items_alive == 0; + } + + void replace_item(PointerType item_to_replace, T new_item) { + SafeVectorItem<T> *item = get_item(item_to_replace); + if(item->alive) { + gsr::add_widget_to_remove(std::move(item->item)); + item->item = std::move(new_item); + } } private: void remove_item(PointerType item_to_remove) { for(auto it = data.begin(), end = data.end(); it != end; ++it) { - if(&*(*it) == item_to_remove) { + if(&*(it->item) == item_to_remove) { + gsr::add_widget_to_remove(std::move(it->item)); data.erase(it); + --num_items_alive; return; } } } + + SafeVectorItem<T>* get_item(PointerType item_to_remove) { + for(auto &item : data) { + if(&*(item.item) == item_to_remove) + return &item; + } + return nullptr; + } + + void remove_dead_items() { + if(!has_items_to_remove) + return; + + for(auto it = data.begin(); it != data.end();) { + if(it->alive) { + ++it; + } else { + gsr::add_widget_to_remove(std::move(it->item)); + it = data.erase(it); + } + } + has_items_to_remove = false; + } private: - std::vector<T> data; - std::vector<PointerType> remove_queue; + std::vector<SafeVectorItem<T>> data; int for_each_depth = 0; + int num_items_alive = 0; + bool has_items_to_remove = false; };
\ No newline at end of file diff --git a/include/Theme.hpp b/include/Theme.hpp index 185bcdc..1390182 100644 --- a/include/Theme.hpp +++ b/include/Theme.hpp @@ -28,6 +28,7 @@ namespace gsr { mgl::Texture combobox_arrow_texture; mgl::Texture settings_texture; mgl::Texture settings_small_texture; + mgl::Texture settings_extra_small_texture; mgl::Texture folder_texture; mgl::Texture up_arrow_texture; mgl::Texture replay_button_texture; @@ -41,6 +42,17 @@ namespace gsr { mgl::Texture stop_texture; mgl::Texture pause_texture; mgl::Texture save_texture; + mgl::Texture screenshot_texture; + mgl::Texture trash_texture; + + mgl::Texture ps4_home_texture; + mgl::Texture ps4_options_texture; + mgl::Texture ps4_dpad_up_texture; + mgl::Texture ps4_dpad_down_texture; + mgl::Texture ps4_dpad_left_texture; + mgl::Texture ps4_dpad_right_texture; + mgl::Texture ps4_cross_texture; + mgl::Texture ps4_triangle_texture; double double_click_timeout_seconds = 0.4; diff --git a/include/Utils.hpp b/include/Utils.hpp index e7bb3bc..3d3c029 100644 --- a/include/Utils.hpp +++ b/include/Utils.hpp @@ -4,7 +4,6 @@ #include <string_view> #include <map> #include <string> -#include <optional> namespace gsr { struct KeyValue { @@ -15,6 +14,9 @@ namespace gsr { using StringSplitCallback = std::function<bool(std::string_view line)>; void string_split_char(std::string_view str, char delimiter, StringSplitCallback callback_func); + bool starts_with(std::string_view str, const char *substr); + bool ends_with(std::string_view str, const char *substr); + std::string strip(const std::string &str); std::string get_home_dir(); std::string get_config_dir(); @@ -24,6 +26,8 @@ namespace gsr { std::map<std::string, std::string> get_xdg_variables(); std::string get_videos_dir(); + std::string get_pictures_dir(); + // Returns 0 on success int create_directory_recursive(char *path); bool file_get_content(const char *filepath, std::string &file_content); diff --git a/include/WindowSelector.hpp b/include/WindowSelector.hpp new file mode 100644 index 0000000..ab4a85d --- /dev/null +++ b/include/WindowSelector.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include <X11/Xlib.h> + +#include <mglpp/graphics/Color.hpp> + +namespace gsr { + class WindowSelector { + public: + WindowSelector(); + WindowSelector(const WindowSelector&) = delete; + WindowSelector& operator=(const WindowSelector&) = delete; + ~WindowSelector(); + + bool start(mgl::Color border_color); + void stop(); + bool is_started() const; + + bool failed() const; + bool poll_events(); + bool take_selection(); + bool take_canceled(); + Window get_selection() const; + private: + Display *dpy = nullptr; + Cursor crosshair_cursor = None; + Colormap border_window_colormap = None; + Window border_window = None; + Window selected_window = None; + bool selected = false; + bool canceled = false; + }; +}
\ No newline at end of file diff --git a/include/WindowUtils.hpp b/include/WindowUtils.hpp index e647785..5c4d39a 100644 --- a/include/WindowUtils.hpp +++ b/include/WindowUtils.hpp @@ -1,6 +1,9 @@ #pragma once +#include <mglpp/system/vec.hpp> #include <string> +#include <vector> +#include <optional> #include <X11/Xlib.h> namespace gsr { @@ -9,6 +12,31 @@ namespace gsr { CURSOR }; + struct Monitor { + mgl::vec2i position; + mgl::vec2i size; + std::string name; + }; + + std::optional<std::string> get_window_title(Display *dpy, Window window); Window get_focused_window(Display *dpy, WindowCaptureType cap_type); std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type); + std::string get_window_name_at_position(Display *dpy, mgl::vec2i position, Window ignore_window); + std::string get_window_name_at_cursor_position(Display *dpy, Window ignore_window); + void set_window_size_not_resizable(Display *dpy, Window window, int width, int height); + Window window_get_target_window_child(Display *display, Window window); + mgl::vec2i get_cursor_position(Display *dpy, Window *window); + mgl::vec2i create_window_get_center_position(Display *display); + std::string get_window_manager_name(Display *display); + bool is_compositor_running(Display *dpy, int screen); + std::vector<Monitor> get_monitors(Display *dpy); + void xi_grab_all_mouse_devices(Display *dpy); + void xi_ungrab_all_mouse_devices(Display *dpy); + void xi_warp_all_mouse_devices(Display *dpy, mgl::vec2i position); + void window_set_fullscreen(Display *dpy, Window window, bool fullscreen); + bool window_is_fullscreen(Display *display, Window window); + bool set_window_wm_state(Display *dpy, Window window, Atom atom); + void make_window_click_through(Display *display, Window window); + bool make_window_sticky(Display *dpy, Window window); + bool hide_window_from_taskbar(Display *dpy, Window window); }
\ No newline at end of file diff --git a/include/gui/Button.hpp b/include/gui/Button.hpp index eb68e99..f412521 100644 --- a/include/gui/Button.hpp +++ b/include/gui/Button.hpp @@ -21,6 +21,7 @@ namespace gsr { mgl::vec2f get_size() override; void set_border_scale(float scale); + void set_icon_padding_scale(float scale); void set_bg_hover_color(mgl::Color color); void set_icon(mgl::Texture *texture); @@ -30,6 +31,7 @@ namespace gsr { std::function<void()> on_click; private: void scale_sprite_to_button_size(); + float get_button_height(); private: mgl::vec2f size; mgl::Color bg_color; @@ -37,5 +39,6 @@ namespace gsr { mgl::Text text; mgl::Sprite sprite; float border_scale = 0.0015f; + float icon_padding_scale = 1.0f; }; }
\ No newline at end of file diff --git a/include/gui/DropdownButton.hpp b/include/gui/DropdownButton.hpp index cbbcda2..f613d86 100644 --- a/include/gui/DropdownButton.hpp +++ b/include/gui/DropdownButton.hpp @@ -20,6 +20,8 @@ namespace gsr { void add_item(const std::string &text, const std::string &id, const std::string &description = ""); void set_item_label(const std::string &id, const std::string &new_label); void set_item_icon(const std::string &id, mgl::Texture *texture); + void set_item_description(const std::string &id, const std::string &new_description); + void set_item_enabled(const std::string &id, bool enabled); void set_description(std::string description_text); void set_activated(bool activated); @@ -35,6 +37,7 @@ namespace gsr { mgl::Text description_text; mgl::Texture *icon_texture = nullptr; std::string id; + bool enabled = true; }; std::vector<Item> items; diff --git a/include/gui/GlobalSettingsPage.hpp b/include/gui/GlobalSettingsPage.hpp index cd4a50c..d96397d 100644 --- a/include/gui/GlobalSettingsPage.hpp +++ b/include/gui/GlobalSettingsPage.hpp @@ -4,31 +4,107 @@ #include "../GsrInfo.hpp" #include "../Config.hpp" +#include <functional> +#include <mglpp/window/Event.hpp> + namespace gsr { + class Overlay; class GsrPage; class PageStack; class ScrollablePage; class Subsection; class RadioButton; + class Button; + class List; + class CustomRendererWidget; + + enum ConfigureHotkeyType { + NONE, + REPLAY_START_STOP, + REPLAY_SAVE, + REPLAY_SAVE_1_MIN, + REPLAY_SAVE_10_MIN, + RECORD_START_STOP, + RECORD_PAUSE_UNPAUSE, + STREAM_START_STOP, + TAKE_SCREENSHOT, + TAKE_SCREENSHOT_REGION, + SHOW_HIDE + }; class GlobalSettingsPage : public StaticPage { public: - GlobalSettingsPage(const GsrInfo *gsr_info, Config &config, PageStack *page_stack); + GlobalSettingsPage(Overlay *overlay, const GsrInfo *gsr_info, Config &config, PageStack *page_stack); GlobalSettingsPage(const GlobalSettingsPage&) = delete; GlobalSettingsPage& operator=(const GlobalSettingsPage&) = delete; void load(); void save(); void on_navigate_away_from_page() override; + + bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override; + + std::function<void(bool enable, int exit_status)> on_startup_changed; + std::function<void(const char *reason)> on_click_exit_program_button; + std::function<void(const char *hotkey_option)> on_keyboard_hotkey_changed; + std::function<void(const char *hotkey_option)> on_joystick_hotkey_changed; + std::function<void()> on_page_closed; private: + void load_hotkeys(); + std::unique_ptr<Subsection> create_appearance_subsection(ScrollablePage *parent_page); + std::unique_ptr<Subsection> create_startup_subsection(ScrollablePage *parent_page); + std::unique_ptr<RadioButton> create_enable_keyboard_hotkeys_button(); + std::unique_ptr<RadioButton> create_enable_joystick_hotkeys_button(); + std::unique_ptr<List> create_show_hide_hotkey_options(); + std::unique_ptr<List> create_replay_hotkey_options(); + std::unique_ptr<List> create_replay_partial_save_hotkey_options(); + std::unique_ptr<List> create_record_hotkey_options(); + std::unique_ptr<List> create_stream_hotkey_options(); + std::unique_ptr<List> create_screenshot_hotkey_options(); + std::unique_ptr<List> create_screenshot_region_hotkey_options(); + std::unique_ptr<List> create_hotkey_control_buttons(); + std::unique_ptr<Subsection> create_keyboard_hotkey_subsection(ScrollablePage *parent_page); + std::unique_ptr<Subsection> create_controller_hotkey_subsection(ScrollablePage *parent_page); + std::unique_ptr<Button> create_exit_program_button(); + std::unique_ptr<Button> create_go_back_to_old_ui_button(); + std::unique_ptr<Subsection> create_application_options_subsection(ScrollablePage *parent_page); + std::unique_ptr<Subsection> create_application_info_subsection(ScrollablePage *parent_page); void add_widgets(); + + Button* configure_hotkey_get_button_by_active_type(); + ConfigHotkey* configure_hotkey_get_config_by_active_type(); + void for_each_config_hotkey(std::function<void(ConfigHotkey *config_hotkey)> callback); + void configure_hotkey_start(ConfigureHotkeyType hotkey_type); + void configure_hotkey_cancel(); + void configure_hotkey_stop_and_save(); private: + Overlay *overlay = nullptr; Config &config; const GsrInfo *gsr_info = nullptr; GsrPage *content_page_ptr = nullptr; PageStack *page_stack = nullptr; RadioButton *tint_color_radio_button_ptr = nullptr; + RadioButton *startup_radio_button_ptr = nullptr; + RadioButton *enable_keyboard_hotkeys_radio_button_ptr = nullptr; + RadioButton *enable_joystick_hotkeys_radio_button_ptr = nullptr; + + Button *turn_replay_on_off_button_ptr = nullptr; + Button *save_replay_button_ptr = nullptr; + Button *save_replay_1_min_button_ptr = nullptr; + Button *save_replay_10_min_button_ptr = nullptr; + Button *start_stop_recording_button_ptr = nullptr; + Button *pause_unpause_recording_button_ptr = nullptr; + Button *start_stop_streaming_button_ptr = nullptr; + Button *take_screenshot_button_ptr = nullptr; + Button *take_screenshot_region_button_ptr = nullptr; + Button *show_hide_button_ptr = nullptr; + + ConfigHotkey configure_config_hotkey; + ConfigureHotkeyType configure_hotkey_type = ConfigureHotkeyType::NONE; + + CustomRendererWidget *hotkey_overlay_ptr = nullptr; + std::string hotkey_configure_action_name; }; }
\ No newline at end of file diff --git a/include/gui/GsrPage.hpp b/include/gui/GsrPage.hpp index 1d298f4..76c28a6 100644 --- a/include/gui/GsrPage.hpp +++ b/include/gui/GsrPage.hpp @@ -9,7 +9,7 @@ namespace gsr { class GsrPage : public Page { public: - GsrPage(); + GsrPage(const char *top_text, const char *bottom_text); GsrPage(const GsrPage&) = delete; GsrPage& operator=(const GsrPage&) = delete; @@ -42,7 +42,8 @@ namespace gsr { float margin_bottom_scale = 0.0f; float margin_left_scale = 0.0f; float margin_right_scale = 0.0f; - mgl::Text label_text; + mgl::Text top_text; + mgl::Text bottom_text; std::vector<ButtonItem> buttons; }; }
\ No newline at end of file diff --git a/include/gui/Image.hpp b/include/gui/Image.hpp new file mode 100644 index 0000000..d72c5c1 --- /dev/null +++ b/include/gui/Image.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "Widget.hpp" + +#include <mglpp/graphics/Sprite.hpp> + +namespace gsr { + class Image : public Widget { + public: + enum class ScaleBehavior { + SCALE, + CLAMP + }; + + // Set size to {0.0f, 0.0f} for no limit. The image is scaled to the size while keeping its aspect ratio + Image(mgl::Texture *texture, mgl::vec2f size, ScaleBehavior scale_behavior); + Image(const Image&) = delete; + Image& operator=(const Image&) = delete; + + bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override; + void draw(mgl::Window &window, mgl::vec2f offset) override; + + mgl::vec2f get_size() override; + private: + mgl::Sprite sprite; + mgl::vec2f size; + ScaleBehavior scale_behavior; + }; +}
\ No newline at end of file diff --git a/include/gui/List.hpp b/include/gui/List.hpp index 72c5353..f79d165 100644 --- a/include/gui/List.hpp +++ b/include/gui/List.hpp @@ -21,19 +21,20 @@ namespace gsr { List(Orientation orientation, Alignment content_alignment = Alignment::START); List(const List&) = delete; List& operator=(const List&) = delete; + virtual ~List() override; bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override; void draw(mgl::Window &window, mgl::vec2f offset) override; - //void remove_child_widget(Widget *widget) override; - void add_widget(std::unique_ptr<Widget> widget); void remove_widget(Widget *widget); + void replace_widget(Widget *widget, std::unique_ptr<Widget> new_widget); void clear(); // Return true from |callback| to continue void for_each_child_widget(std::function<bool(std::unique_ptr<Widget> &widget)> callback); // Returns nullptr if index is invalid Widget* get_child_widget_by_index(size_t index) const; + size_t get_num_children() const; void set_spacing(float spacing); diff --git a/include/gui/Page.hpp b/include/gui/Page.hpp index 0d8536a..00a53c6 100644 --- a/include/gui/Page.hpp +++ b/include/gui/Page.hpp @@ -10,13 +10,11 @@ namespace gsr { Page() = default; Page(const Page&) = delete; Page& operator=(const Page&) = delete; - virtual ~Page() = default; + virtual ~Page() override; virtual void on_navigate_to_page() {} virtual void on_navigate_away_from_page() {} - //void remove_child_widget(Widget *widget) override; - virtual void add_widget(std::unique_ptr<Widget> widget); protected: SafeVector<std::unique_ptr<Widget>> widgets; diff --git a/include/gui/RadioButton.hpp b/include/gui/RadioButton.hpp index a009eab..e319aa0 100644 --- a/include/gui/RadioButton.hpp +++ b/include/gui/RadioButton.hpp @@ -23,11 +23,13 @@ namespace gsr { void add_item(const std::string &text, const std::string &id); void set_selected_item(const std::string &id, bool trigger_event = true, bool trigger_event_even_if_selection_not_changed = true); - const std::string get_selected_id() const; + const std::string& get_selected_id() const; + const std::string& get_selected_text() const; mgl::vec2f get_size() override; - std::function<void(const std::string &text, const std::string &id)> on_selection_changed; + // Return false to revert the change + std::function<bool(const std::string &text, const std::string &id)> on_selection_changed; private: void update_if_dirty(); private: diff --git a/include/gui/ScreenshotSettingsPage.hpp b/include/gui/ScreenshotSettingsPage.hpp new file mode 100644 index 0000000..db66d66 --- /dev/null +++ b/include/gui/ScreenshotSettingsPage.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include "StaticPage.hpp" +#include "List.hpp" +#include "ComboBox.hpp" +#include "Entry.hpp" +#include "CheckBox.hpp" +#include "../GsrInfo.hpp" +#include "../Config.hpp" + +namespace gsr { + class PageStack; + class GsrPage; + class ScrollablePage; + class Button; + + class ScreenshotSettingsPage : public StaticPage { + public: + ScreenshotSettingsPage(const GsrInfo *gsr_info, Config &config, PageStack *page_stack); + ScreenshotSettingsPage(const ScreenshotSettingsPage&) = delete; + ScreenshotSettingsPage& operator=(const ScreenshotSettingsPage&) = delete; + + void load(); + void save(); + void on_navigate_away_from_page() override; + private: + std::unique_ptr<ComboBox> create_record_area_box(); + std::unique_ptr<Widget> create_record_area(); + std::unique_ptr<Entry> create_image_width_entry(); + std::unique_ptr<Entry> create_image_height_entry(); + std::unique_ptr<List> create_image_resolution(); + std::unique_ptr<List> create_image_resolution_section(); + std::unique_ptr<CheckBox> create_restore_portal_session_checkbox(); + std::unique_ptr<List> create_restore_portal_session_section(); + std::unique_ptr<Widget> create_change_image_resolution_section(); + std::unique_ptr<Widget> create_capture_target_section(); + std::unique_ptr<List> create_image_quality_section(); + std::unique_ptr<Widget> create_record_cursor_section(); + std::unique_ptr<Widget> create_image_section(); + std::unique_ptr<List> create_save_directory(const char *label); + std::unique_ptr<ComboBox> create_image_format_box(); + std::unique_ptr<List> create_image_format_section(); + std::unique_ptr<Widget> create_file_info_section(); + std::unique_ptr<CheckBox> create_save_screenshot_in_game_folder(); + std::unique_ptr<Widget> create_general_section(); + std::unique_ptr<Widget> create_notifications_section(); + std::unique_ptr<Widget> create_settings(); + void add_widgets(); + + void save(RecordOptions &record_options); + private: + Config &config; + const GsrInfo *gsr_info = nullptr; + SupportedCaptureOptions capture_options; + + GsrPage *content_page_ptr = nullptr; + ScrollablePage *settings_scrollable_page_ptr = nullptr; + List *image_resolution_list_ptr = nullptr; + List *restore_portal_session_list_ptr = nullptr; + List *color_range_list_ptr = nullptr; + Widget *image_format_ptr = nullptr; + ComboBox *record_area_box_ptr = nullptr; + Entry *image_width_entry_ptr = nullptr; + Entry *image_height_entry_ptr = nullptr; + CheckBox *record_cursor_checkbox_ptr = nullptr; + CheckBox *restore_portal_session_checkbox_ptr = nullptr; + CheckBox *change_image_resolution_checkbox_ptr = nullptr; + ComboBox *image_quality_box_ptr = nullptr; + ComboBox *image_format_box_ptr = nullptr; + Button *save_directory_button_ptr = nullptr; + CheckBox *save_screenshot_in_game_folder_checkbox_ptr = nullptr; + CheckBox *show_screenshot_saved_notification_checkbox_ptr = nullptr; + + PageStack *page_stack = nullptr; + }; +}
\ No newline at end of file diff --git a/include/gui/ScrollablePage.hpp b/include/gui/ScrollablePage.hpp index 452d0e9..54ec2cb 100644 --- a/include/gui/ScrollablePage.hpp +++ b/include/gui/ScrollablePage.hpp @@ -12,6 +12,7 @@ namespace gsr { ScrollablePage(mgl::vec2f size); ScrollablePage(const ScrollablePage&) = delete; ScrollablePage& operator=(const ScrollablePage&) = delete; + virtual ~ScrollablePage() override; bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override; void draw(mgl::Window &window, mgl::vec2f offset) override; diff --git a/include/gui/SettingsPage.hpp b/include/gui/SettingsPage.hpp index 981c99a..1810de5 100644 --- a/include/gui/SettingsPage.hpp +++ b/include/gui/SettingsPage.hpp @@ -10,12 +10,20 @@ #include "../GsrInfo.hpp" #include "../Config.hpp" +#include <functional> + namespace gsr { class GsrPage; class PageStack; class ScrollablePage; class Label; class LineSeparator; + class Subsection; + + enum class AudioDeviceType { + OUTPUT, + INPUT + }; class SettingsPage : public StaticPage { public: @@ -32,11 +40,12 @@ namespace gsr { void load(); void save(); void on_navigate_away_from_page() override; + + std::function<void()> on_config_changed; private: std::unique_ptr<RadioButton> create_view_radio_button(); std::unique_ptr<ComboBox> create_record_area_box(); std::unique_ptr<Widget> create_record_area(); - std::unique_ptr<List> create_select_window(); std::unique_ptr<Entry> create_area_width_entry(); std::unique_ptr<Entry> create_area_height_entry(); std::unique_ptr<List> create_area_size(); @@ -48,24 +57,26 @@ namespace gsr { std::unique_ptr<CheckBox> create_restore_portal_session_checkbox(); std::unique_ptr<List> create_restore_portal_session_section(); std::unique_ptr<Widget> create_change_video_resolution_section(); - std::unique_ptr<Widget> create_capture_target(); - std::unique_ptr<ComboBox> create_audio_device_selection_combobox(); - std::unique_ptr<Button> create_remove_audio_device_button(List *audio_device_list_ptr); - std::unique_ptr<List> create_audio_device(); - std::unique_ptr<Button> create_add_audio_device_button(); - std::unique_ptr<ComboBox> create_application_audio_selection_combobox(); - std::unique_ptr<List> create_application_audio(); - std::unique_ptr<List> create_custom_application_audio(); - std::unique_ptr<Button> create_add_application_audio_button(); - std::unique_ptr<Button> create_add_custom_application_audio_button(); - std::unique_ptr<List> create_add_audio_buttons(); - std::unique_ptr<List> create_audio_track_track_section(); - std::unique_ptr<CheckBox> create_merge_audio_tracks_checkbox(); + std::unique_ptr<Widget> create_capture_target_section(); + std::unique_ptr<ComboBox> create_audio_device_selection_combobox(AudioDeviceType device_type); + std::unique_ptr<Button> create_remove_audio_device_button(List *audio_input_list_ptr, List *audio_device_list_ptr); + std::unique_ptr<List> create_audio_device(AudioDeviceType device_type, List *audio_input_list_ptr); + std::unique_ptr<Button> create_add_audio_track_button(); + std::unique_ptr<Button> create_add_audio_output_device_button(List *audio_input_list_ptr); + std::unique_ptr<Button> create_add_audio_input_device_button(List *audio_input_list_ptr); + std::unique_ptr<ComboBox> create_application_audio_selection_combobox(List *application_audio_row); + std::unique_ptr<List> create_application_audio(List *audio_input_list_ptr); + std::unique_ptr<List> create_custom_application_audio(List *audio_input_list_ptr); + std::unique_ptr<Button> create_add_application_audio_button(List *audio_input_list_ptr); + std::unique_ptr<List> create_add_audio_buttons(List *audio_input_list_ptr); + std::unique_ptr<List> create_audio_input_section(); std::unique_ptr<CheckBox> create_application_audio_invert_checkbox(); - std::unique_ptr<Widget> create_audio_track_section(); + std::unique_ptr<List> create_audio_track_title_and_remove(Subsection *audio_track_subsection, const char *title); + std::unique_ptr<Subsection> create_audio_track_section(Widget *parent_widget); + std::unique_ptr<List> create_audio_track_section_list(); std::unique_ptr<Widget> create_audio_section(); std::unique_ptr<List> create_video_quality_box(); - std::unique_ptr<Entry> create_video_bitrate_entry(); + std::unique_ptr<List> create_video_bitrate_entry(); std::unique_ptr<List> create_video_bitrate(); std::unique_ptr<ComboBox> create_color_range_box(); std::unique_ptr<List> create_color_range(); @@ -89,13 +100,18 @@ namespace gsr { std::unique_ptr<List> create_save_directory(const char *label); std::unique_ptr<ComboBox> create_container_box(); std::unique_ptr<List> create_container_section(); - std::unique_ptr<Entry> create_replay_time_entry(); + std::unique_ptr<List> create_replay_time_entry(); std::unique_ptr<List> create_replay_time(); + std::unique_ptr<List> create_replay_storage(); std::unique_ptr<RadioButton> create_start_replay_automatically(); std::unique_ptr<CheckBox> create_save_replay_in_game_folder(); - std::unique_ptr<Label> create_estimated_file_size(); - void update_estimated_file_size(); + std::unique_ptr<CheckBox> create_restart_replay_on_save(); + std::unique_ptr<Label> create_estimated_replay_file_size(); + void update_estimated_replay_file_size(const std::string &replay_storage_type); + void update_replay_time_text(); std::unique_ptr<CheckBox> create_save_recording_in_game_folder(); + std::unique_ptr<Label> create_estimated_record_file_size(); + void update_estimated_record_file_size(); void add_replay_widgets(); void add_record_widgets(); @@ -117,6 +133,8 @@ namespace gsr { void save_replay(); void save_record(); void save_stream(); + + void view_changed(bool advanced_view, Subsection *notifications_subsection_ptr); private: Type type; Config &config; @@ -128,7 +146,6 @@ namespace gsr { GsrPage *content_page_ptr = nullptr; ScrollablePage *settings_scrollable_page_ptr = nullptr; List *settings_list_ptr = nullptr; - List *select_window_list_ptr = nullptr; List *area_size_list_ptr = nullptr; List *video_resolution_list_ptr = nullptr; List *restore_portal_session_list_ptr = nullptr; @@ -144,11 +161,6 @@ namespace gsr { Entry *framerate_entry_ptr = nullptr; Entry *video_bitrate_entry_ptr = nullptr; List *video_bitrate_list_ptr = nullptr; - List *audio_track_list_ptr = nullptr; - Button *add_application_audio_button_ptr = nullptr; - Button *add_custom_application_audio_button_ptr = nullptr; - CheckBox *merge_audio_tracks_checkbox_ptr = nullptr; - CheckBox *application_audio_invert_checkbox_ptr = nullptr; CheckBox *change_video_resolution_checkbox_ptr = nullptr; ComboBox *color_range_box_ptr = nullptr; ComboBox *video_quality_box_ptr = nullptr; @@ -164,6 +176,7 @@ namespace gsr { List *stream_url_list_ptr = nullptr; List *container_list_ptr = nullptr; CheckBox *save_replay_in_game_folder_ptr = nullptr; + CheckBox *restart_replay_on_save = nullptr; Label *estimated_file_size_ptr = nullptr; CheckBox *show_replay_started_notification_checkbox_ptr = nullptr; CheckBox *show_replay_stopped_notification_checkbox_ptr = nullptr; @@ -171,14 +184,20 @@ namespace gsr { CheckBox *save_recording_in_game_folder_ptr = nullptr; CheckBox *show_recording_started_notification_checkbox_ptr = nullptr; CheckBox *show_video_saved_notification_checkbox_ptr = nullptr; + CheckBox *show_video_paused_notification_checkbox_ptr = nullptr; CheckBox *show_streaming_started_notification_checkbox_ptr = nullptr; CheckBox *show_streaming_stopped_notification_checkbox_ptr = nullptr; Button *save_directory_button_ptr = nullptr; Entry *twitch_stream_key_entry_ptr = nullptr; Entry *youtube_stream_key_entry_ptr = nullptr; + Entry *rumble_stream_key_entry_ptr = nullptr; Entry *stream_url_entry_ptr = nullptr; Entry *replay_time_entry_ptr = nullptr; + RadioButton *replay_storage_button_ptr = nullptr; + Label *replay_time_label_ptr = nullptr; RadioButton *turn_on_replay_automatically_mode_ptr = nullptr; + Subsection *audio_section_ptr = nullptr; + List *audio_track_section_list_ptr = nullptr; PageStack *page_stack = nullptr; }; diff --git a/include/gui/Subsection.hpp b/include/gui/Subsection.hpp index 4da6baf..88953d2 100644 --- a/include/gui/Subsection.hpp +++ b/include/gui/Subsection.hpp @@ -11,15 +11,20 @@ namespace gsr { Subsection(const char *title, std::unique_ptr<Widget> inner_widget, mgl::vec2f size); Subsection(const Subsection&) = delete; Subsection& operator=(const Subsection&) = delete; + virtual ~Subsection() override; bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override; void draw(mgl::Window &window, mgl::vec2f offset) override; mgl::vec2f get_size() override; mgl::vec2f get_inner_size() override; + + Widget* get_inner_widget(); + void set_bg_color(mgl::Color color); private: Label label; std::unique_ptr<Widget> inner_widget; mgl::vec2f size; + mgl::Color bg_color{25, 30, 34}; }; }
\ No newline at end of file diff --git a/include/gui/Utils.hpp b/include/gui/Utils.hpp index 35b2bb7..542e4ea 100644 --- a/include/gui/Utils.hpp +++ b/include/gui/Utils.hpp @@ -3,9 +3,6 @@ #include <mglpp/system/vec.hpp> #include <mglpp/graphics/Color.hpp> -#include <functional> -#include <string_view> - namespace mgl { class Window; } @@ -16,4 +13,5 @@ namespace gsr { double get_frame_delta_seconds(); void set_frame_delta_seconds(double frame_delta); mgl::vec2f scale_keep_aspect_ratio(mgl::vec2f from, mgl::vec2f to); + mgl::vec2f clamp_keep_aspect_ratio(mgl::vec2f from, mgl::vec2f to); }
\ No newline at end of file diff --git a/include/gui/Widget.hpp b/include/gui/Widget.hpp index 57424cd..131e756 100644 --- a/include/gui/Widget.hpp +++ b/include/gui/Widget.hpp @@ -1,6 +1,7 @@ #pragma once #include <mglpp/system/vec.hpp> +#include <memory> namespace mgl { class Event; @@ -31,8 +32,6 @@ namespace gsr { virtual void draw(mgl::Window &window, mgl::vec2f offset) = 0; virtual void set_position(mgl::vec2f position); - //virtual void remove_child_widget(Widget *widget) { (void)widget; } - virtual mgl::vec2f get_position() const; virtual mgl::vec2f get_size() = 0; // This can be different from get_size, for example with ScrollablePage this excludes the margins @@ -61,4 +60,7 @@ namespace gsr { bool visible = true; }; + + void add_widget_to_remove(std::unique_ptr<Widget> widget); + void remove_widgets_to_be_removed(); }
\ No newline at end of file diff --git a/meson.build b/meson.build index 3b21a93..002c8b1 100644 --- a/meson.build +++ b/meson.build @@ -1,4 +1,4 @@ -project('gsr-ui', ['c', 'cpp'], version : '1.0.0', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends') +project('gsr-ui', ['c', 'cpp'], version : '1.6.7', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends') if get_option('buildtype') == 'debug' add_project_arguments('-g3', language : ['c', 'cpp']) @@ -23,24 +23,37 @@ src = [ 'src/gui/Utils.cpp', 'src/gui/DropdownButton.cpp', 'src/gui/Label.cpp', + 'src/gui/Image.cpp', 'src/gui/LineSeparator.cpp', 'src/gui/CustomRendererWidget.cpp', 'src/gui/FileChooser.cpp', 'src/gui/SettingsPage.cpp', + 'src/gui/ScreenshotSettingsPage.cpp', 'src/gui/GlobalSettingsPage.cpp', 'src/gui/GsrPage.cpp', 'src/gui/Subsection.cpp', + 'src/GlobalHotkeys/GlobalHotkeysX11.cpp', + 'src/GlobalHotkeys/GlobalHotkeysLinux.cpp', + 'src/GlobalHotkeys/GlobalHotkeysJoystick.cpp', + 'src/CursorTracker/CursorTrackerX11.cpp', + 'src/CursorTracker/CursorTrackerWayland.cpp', 'src/Utils.cpp', 'src/WindowUtils.cpp', + 'src/RegionSelector.cpp', + 'src/WindowSelector.cpp', 'src/Config.cpp', 'src/GsrInfo.cpp', 'src/Process.cpp', 'src/Overlay.cpp', - 'src/GlobalHotkeysX11.cpp', - 'src/GlobalHotkeysLinux.cpp', + 'src/AudioPlayer.cpp', + 'src/Hotplug.cpp', + 'src/Rpc.cpp', 'src/main.cpp', ] +subdir('protocol') +src += protocol_src + mglpp_proj = subproject('mglpp') mglpp_dep = mglpp_proj.get_variable('mglpp_dep') @@ -48,36 +61,47 @@ prefix = get_option('prefix') datadir = get_option('datadir') gsr_ui_resources_path = join_paths(prefix, datadir, 'gsr-ui') +add_project_arguments('-DGSR_UI_VERSION="' + meson.project_version() + '"', language: ['c', 'cpp']) +add_project_arguments('-DGSR_FLATPAK_VERSION="5.7.3"', language: ['c', 'cpp']) + executable( meson.project_name(), src, install : true, dependencies : [ mglpp_dep, + dependency('threads'), dependency('xcomposite'), dependency('xfixes'), + dependency('xext'), dependency('xi'), + dependency('xcursor'), + dependency('xrandr'), + dependency('libpulse-simple'), + dependency('libdrm'), + dependency('wayland-client'), ], cpp_args : '-DGSR_UI_RESOURCES_PATH="' + gsr_ui_resources_path + '"', ) executable( - 'gsr-window-name', - ['tools/gsr-window-name/main.c'], - install : true, - dependencies : [dependency('x11')], + 'gsr-global-hotkeys', + [ + 'tools/gsr-global-hotkeys/hotplug.c', + 'tools/gsr-global-hotkeys/keyboard_event.c', + 'tools/gsr-global-hotkeys/keys.c', + 'tools/gsr-global-hotkeys/main.c' + ], + c_args : '-fstack-protector-all', + install : true ) executable( - 'gsr-global-hotkeys', - ['tools/gsr-global-hotkeys/main.c'], - install : true, - dependencies : [ - dependency('libevdev'), - dependency('libudev'), - dependency('libinput'), - dependency('xkbcommon') + 'gsr-ui-cli', + [ + 'tools/gsr-ui-cli/main.c' ], + install : true ) install_subdir('images', install_dir : gsr_ui_resources_path) @@ -89,4 +113,4 @@ endif if get_option('capabilities') == true meson.add_install_script('meson_post_install.sh') -endif
\ No newline at end of file +endif diff --git a/project.conf b/project.conf index acf3b2f..54f9172 100644 --- a/project.conf +++ b/project.conf @@ -1,7 +1,7 @@ [package] name = "gsr-ui" type = "executable" -version = "0.1.0" +version = "1.6.7" platforms = ["posix"] [lang.cpp] @@ -13,4 +13,10 @@ ignore_dirs = ["build", "tools"] [dependencies] xcomposite = ">=0" xfixes = ">=0" -xi = ">=0"
\ No newline at end of file +xext = ">=0" +xi = ">=0" +xcursor = ">=1" +xrandr = ">=0.5" +libpulse-simple = ">=0" +libdrm = ">=2" +wayland-client = ">=1" diff --git a/protocol/meson.build b/protocol/meson.build new file mode 100644 index 0000000..bbdccba --- /dev/null +++ b/protocol/meson.build @@ -0,0 +1,25 @@ +wayland_scanner = dependency('wayland-scanner', native: true) +wayland_scanner_path = wayland_scanner.get_variable(pkgconfig: 'wayland_scanner') +wayland_scanner_prog = find_program(wayland_scanner_path, native: true) + +wayland_scanner_code = generator( + wayland_scanner_prog, + output: '@BASENAME@-protocol.c', + arguments: ['private-code', '@INPUT@', '@OUTPUT@'], +) + +wayland_scanner_client = generator( + wayland_scanner_prog, + output: '@BASENAME@-client-protocol.h', + arguments: ['client-header', '@INPUT@', '@OUTPUT@'], +) + +protocols = [ + 'xdg-output-unstable-v1.xml', +] + +protocol_src = [] +foreach xml : protocols + protocol_src += wayland_scanner_code.process(xml) + protocol_src += wayland_scanner_client.process(xml) +endforeach diff --git a/protocol/xdg-output-unstable-v1.xml b/protocol/xdg-output-unstable-v1.xml new file mode 100644 index 0000000..a7306e4 --- /dev/null +++ b/protocol/xdg-output-unstable-v1.xml @@ -0,0 +1,222 @@ +<?xml version="1.0" encoding="UTF-8"?> +<protocol name="xdg_output_unstable_v1"> + + <copyright> + Copyright © 2017 Red Hat Inc. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + </copyright> + + <description summary="Protocol to describe output regions"> + This protocol aims at describing outputs in a way which is more in line + with the concept of an output on desktop oriented systems. + + Some information are more specific to the concept of an output for + a desktop oriented system and may not make sense in other applications, + such as IVI systems for example. + + Typically, the global compositor space on a desktop system is made of + a contiguous or overlapping set of rectangular regions. + + The logical_position and logical_size events defined in this protocol + might provide information identical to their counterparts already + available from wl_output, in which case the information provided by this + protocol should be preferred to their equivalent in wl_output. The goal is + to move the desktop specific concepts (such as output location within the + global compositor space, etc.) out of the core wl_output protocol. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible + changes may be added together with the corresponding interface + version bump. + Backward incompatible changes are done by bumping the version + number in the protocol and interface names and resetting the + interface version. Once the protocol is to be declared stable, + the 'z' prefix and the version number in the protocol and + interface names are removed and the interface version number is + reset. + </description> + + <interface name="zxdg_output_manager_v1" version="3"> + <description summary="manage xdg_output objects"> + A global factory interface for xdg_output objects. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the xdg_output_manager object"> + Using this request a client can tell the server that it is not + going to use the xdg_output_manager object anymore. + + Any objects already created through this instance are not affected. + </description> + </request> + + <request name="get_xdg_output"> + <description summary="create an xdg output from a wl_output"> + This creates a new xdg_output object for the given wl_output. + </description> + <arg name="id" type="new_id" interface="zxdg_output_v1"/> + <arg name="output" type="object" interface="wl_output"/> + </request> + </interface> + + <interface name="zxdg_output_v1" version="3"> + <description summary="compositor logical output region"> + An xdg_output describes part of the compositor geometry. + + This typically corresponds to a monitor that displays part of the + compositor space. + + For objects version 3 onwards, after all xdg_output properties have been + sent (when the object is created and when properties are updated), a + wl_output.done event is sent. This allows changes to the output + properties to be seen as atomic, even if they happen via multiple events. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the xdg_output object"> + Using this request a client can tell the server that it is not + going to use the xdg_output object anymore. + </description> + </request> + + <event name="logical_position"> + <description summary="position of the output within the global compositor space"> + The position event describes the location of the wl_output within + the global compositor space. + + The logical_position event is sent after creating an xdg_output + (see xdg_output_manager.get_xdg_output) and whenever the location + of the output changes within the global compositor space. + </description> + <arg name="x" type="int" + summary="x position within the global compositor space"/> + <arg name="y" type="int" + summary="y position within the global compositor space"/> + </event> + + <event name="logical_size"> + <description summary="size of the output in the global compositor space"> + The logical_size event describes the size of the output in the + global compositor space. + + Most regular Wayland clients should not pay attention to the + logical size and would rather rely on xdg_shell interfaces. + + Some clients such as Xwayland, however, need this to configure + their surfaces in the global compositor space as the compositor + may apply a different scale from what is advertised by the output + scaling property (to achieve fractional scaling, for example). + + For example, for a wl_output mode 3840×2160 and a scale factor 2: + + - A compositor not scaling the monitor viewport in its compositing space + will advertise a logical size of 3840×2160, + + - A compositor scaling the monitor viewport with scale factor 2 will + advertise a logical size of 1920×1080, + + - A compositor scaling the monitor viewport using a fractional scale of + 1.5 will advertise a logical size of 2560×1440. + + For example, for a wl_output mode 1920×1080 and a 90 degree rotation, + the compositor will advertise a logical size of 1080x1920. + + The logical_size event is sent after creating an xdg_output + (see xdg_output_manager.get_xdg_output) and whenever the logical + size of the output changes, either as a result of a change in the + applied scale or because of a change in the corresponding output + mode(see wl_output.mode) or transform (see wl_output.transform). + </description> + <arg name="width" type="int" + summary="width in global compositor space"/> + <arg name="height" type="int" + summary="height in global compositor space"/> + </event> + + <event name="done" deprecated-since="3"> + <description summary="all information about the output have been sent"> + This event is sent after all other properties of an xdg_output + have been sent. + + This allows changes to the xdg_output properties to be seen as + atomic, even if they happen via multiple events. + + For objects version 3 onwards, this event is deprecated. Compositors + are not required to send it anymore and must send wl_output.done + instead. + </description> + </event> + + <!-- Version 2 additions --> + + <event name="name" since="2"> + <description summary="name of this output"> + Many compositors will assign names to their outputs, show them to the + user, allow them to be configured by name, etc. The client may wish to + know this name as well to offer the user similar behaviors. + + The naming convention is compositor defined, but limited to + alphanumeric characters and dashes (-). Each name is unique among all + wl_output globals, but if a wl_output global is destroyed the same name + may be reused later. The names will also remain consistent across + sessions with the same hardware and software configuration. + + Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. However, do + not assume that the name is a reflection of an underlying DRM + connector, X11 connection, etc. + + The name event is sent after creating an xdg_output (see + xdg_output_manager.get_xdg_output). This event is only sent once per + xdg_output, and the name does not change over the lifetime of the + wl_output global. + + This event is deprecated, instead clients should use wl_output.name. + Compositors must still support this event. + </description> + <arg name="name" type="string" summary="output name"/> + </event> + + <event name="description" since="2"> + <description summary="human-readable description of this output"> + Many compositors can produce human-readable descriptions of their + outputs. The client may wish to know this description as well, to + communicate the user for various purposes. + + The description is a UTF-8 string with no convention defined for its + contents. Examples might include 'Foocorp 11" Display' or 'Virtual X11 + output via :1'. + + The description event is sent after creating an xdg_output (see + xdg_output_manager.get_xdg_output) and whenever the description + changes. The description is optional, and may not be sent at all. + + For objects of version 2 and lower, this event is only sent once per + xdg_output, and the description does not change over the lifetime of + the wl_output global. + + This event is deprecated, instead clients should use + wl_output.description. Compositors must still support this event. + </description> + <arg name="description" type="string" summary="output description"/> + </event> + + </interface> +</protocol> diff --git a/src/AudioPlayer.cpp b/src/AudioPlayer.cpp new file mode 100644 index 0000000..cb6d1c7 --- /dev/null +++ b/src/AudioPlayer.cpp @@ -0,0 +1,86 @@ +#include "../include/AudioPlayer.hpp" + +#include <unistd.h> +#include <fcntl.h> +#include <stdio.h> +#include <string.h> + +#include <pulse/simple.h> +#include <pulse/error.h> + +#define BUFSIZE 4096 + +namespace gsr { + AudioPlayer::~AudioPlayer() { + if(thread.joinable()) { + stop_playing_audio = true; + thread.join(); + } + + if(audio_file_fd > 0) + close(audio_file_fd); + } + + bool AudioPlayer::play(const char *filepath) { + if(thread.joinable()) { + stop_playing_audio = true; + thread.join(); + } + + stop_playing_audio = false; + audio_file_fd = open(filepath, O_RDONLY); + if(audio_file_fd == -1) + return false; + + thread = std::thread([this]() { + pa_sample_spec ss; + ss.format = PA_SAMPLE_S16LE; + ss.rate = 48000; + ss.channels = 2; + + pa_simple *s = NULL; + int error; + + /* Create a new playback stream */ + if(!(s = pa_simple_new(NULL, "gsr-ui-audio-playback", PA_STREAM_PLAYBACK, NULL, "playback", &ss, NULL, NULL, &error))) { + fprintf(stderr, __FILE__": pa_simple_new() failed: %s\n", pa_strerror(error)); + goto finish; + } + + uint8_t buf[BUFSIZE]; + for(;;) { + ssize_t r; + + if(stop_playing_audio) + goto finish; + + if((r = read(audio_file_fd, buf, sizeof(buf))) <= 0) { + if(r == 0) /* EOF */ + break; + + fprintf(stderr, __FILE__": read() failed: %s\n", strerror(errno)); + goto finish; + } + + if(pa_simple_write(s, buf, (size_t) r, &error) < 0) { + fprintf(stderr, __FILE__": pa_simple_write() failed: %s\n", pa_strerror(error)); + goto finish; + } + } + + if(pa_simple_drain(s, &error) < 0) { + fprintf(stderr, __FILE__": pa_simple_drain() failed: %s\n", pa_strerror(error)); + goto finish; + } + + finish: + if(s) + pa_simple_free(s); + + close(audio_file_fd); + audio_file_fd = -1; + }); + + return true; + } +}
\ No newline at end of file diff --git a/src/Config.cpp b/src/Config.cpp index 4deaaf4..313cd38 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -1,41 +1,162 @@ #include "../include/Config.hpp" #include "../include/Utils.hpp" #include "../include/GsrInfo.hpp" +#include "../include/GlobalHotkeys/GlobalHotkeys.hpp" #include <variant> #include <limits.h> #include <inttypes.h> #include <libgen.h> +#include <string.h> +#include <assert.h> +#include <mglpp/window/Keyboard.hpp> #define FORMAT_I32 "%" PRIi32 #define FORMAT_I64 "%" PRIi64 #define FORMAT_U32 "%" PRIu32 -#define CONFIG_FILE_VERSION 1 - namespace gsr { + static const std::string_view add_audio_track_tag = "[add_audio_track]"; + + static std::vector<mgl::Keyboard::Key> hotkey_modifiers_to_mgl_keys(uint32_t modifiers) { + std::vector<mgl::Keyboard::Key> result; + if(modifiers & HOTKEY_MOD_LCTRL) + result.push_back(mgl::Keyboard::LControl); + if(modifiers & HOTKEY_MOD_LSHIFT) + result.push_back(mgl::Keyboard::LShift); + if(modifiers & HOTKEY_MOD_LALT) + result.push_back(mgl::Keyboard::LAlt); + if(modifiers & HOTKEY_MOD_LSUPER) + result.push_back(mgl::Keyboard::LSystem); + if(modifiers & HOTKEY_MOD_RCTRL) + result.push_back(mgl::Keyboard::RControl); + if(modifiers & HOTKEY_MOD_RSHIFT) + result.push_back(mgl::Keyboard::RShift); + if(modifiers & HOTKEY_MOD_RALT) + result.push_back(mgl::Keyboard::RAlt); + if(modifiers & HOTKEY_MOD_RSUPER) + result.push_back(mgl::Keyboard::RSystem); + return result; + } + + static void string_remove_all(std::string &str, const std::string &substr) { + size_t index = 0; + while(true) { + index = str.find(substr, index); + if(index == std::string::npos) + break; + str.erase(index, substr.size()); + } + } + + ReplayStartupMode replay_startup_string_to_type(const char *startup_mode_str) { + if(strcmp(startup_mode_str, "dont_turn_on_automatically") == 0) + return ReplayStartupMode::DONT_TURN_ON_AUTOMATICALLY; + else if(strcmp(startup_mode_str, "turn_on_at_system_startup") == 0) + return ReplayStartupMode::TURN_ON_AT_SYSTEM_STARTUP; + else if(strcmp(startup_mode_str, "turn_on_at_fullscreen") == 0) + return ReplayStartupMode::TURN_ON_AT_FULLSCREEN; + else if(strcmp(startup_mode_str, "turn_on_at_power_supply_connected") == 0) + return ReplayStartupMode::TURN_ON_AT_POWER_SUPPLY_CONNECTED; + else + return ReplayStartupMode::DONT_TURN_ON_AUTOMATICALLY; + } + + bool ConfigHotkey::operator==(const ConfigHotkey &other) const { + return key == other.key && modifiers == other.modifiers; + } + + bool ConfigHotkey::operator!=(const ConfigHotkey &other) const { + return !operator==(other); + } + + std::string ConfigHotkey::to_string(bool spaces, bool modifier_side) const { + std::string result; + + const std::vector<mgl::Keyboard::Key> modifier_keys = hotkey_modifiers_to_mgl_keys(modifiers); + std::string modifier_str; + for(const mgl::Keyboard::Key modifier_key : modifier_keys) { + if(!result.empty()) { + if(spaces) + result += " + "; + else + result += "+"; + } + + modifier_str = mgl::Keyboard::key_to_string(modifier_key); + if(!modifier_side) { + string_remove_all(modifier_str, "Left "); + string_remove_all(modifier_str, "Right "); + } + result += modifier_str; + } + + if(key != 0) { + if(!result.empty()) { + if(spaces) + result += " + "; + else + result += "+"; + } + result += mgl::Keyboard::key_to_string((mgl::Keyboard::Key)key); + } + + return result; + } + + bool AudioTrack::operator==(const AudioTrack &other) const { + return audio_inputs == other.audio_inputs && application_audio_invert == other.application_audio_invert; + } + + bool AudioTrack::operator!=(const AudioTrack &other) const { + return !operator==(other); + } + Config::Config(const SupportedCaptureOptions &capture_options) { - const std::string default_save_directory = get_videos_dir(); + const std::string default_videos_save_directory = get_videos_dir(); + const std::string default_pictures_save_directory = get_pictures_dir(); + + set_hotkeys_to_default(); streaming_config.record_options.video_quality = "custom"; - streaming_config.record_options.audio_tracks.push_back("default_output"); - streaming_config.record_options.video_bitrate = 15000; + streaming_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false}); + streaming_config.record_options.video_bitrate = 8000; - record_config.save_directory = default_save_directory; - record_config.record_options.audio_tracks.push_back("default_output"); - record_config.record_options.video_bitrate = 45000; + record_config.save_directory = default_videos_save_directory; + record_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false}); + record_config.record_options.video_bitrate = 40000; replay_config.record_options.video_quality = "custom"; - replay_config.save_directory = default_save_directory; - replay_config.record_options.audio_tracks.push_back("default_output"); - replay_config.record_options.video_bitrate = 45000; + replay_config.save_directory = default_videos_save_directory; + replay_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false}); + replay_config.record_options.video_bitrate = 40000; + + screenshot_config.save_directory = default_pictures_save_directory; if(!capture_options.monitors.empty()) { - streaming_config.record_options.record_area_option = capture_options.monitors.front().name; - record_config.record_options.record_area_option = capture_options.monitors.front().name; - replay_config.record_options.record_area_option = capture_options.monitors.front().name; + streaming_config.record_options.record_area_option = "focused_monitor"; + record_config.record_options.record_area_option = "focused_monitor"; + replay_config.record_options.record_area_option = "focused_monitor"; + screenshot_config.record_area_option = "focused_monitor"; } } + void Config::set_hotkeys_to_default() { + streaming_config.start_stop_hotkey = {mgl::Keyboard::F8, HOTKEY_MOD_LALT}; + + record_config.start_stop_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LALT}; + record_config.pause_unpause_hotkey = {mgl::Keyboard::F7, HOTKEY_MOD_LALT}; + + replay_config.start_stop_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT | HOTKEY_MOD_LSHIFT}; + replay_config.save_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT}; + replay_config.save_1_min_hotkey = {mgl::Keyboard::F11, HOTKEY_MOD_LALT}; + replay_config.save_10_min_hotkey = {mgl::Keyboard::F12, HOTKEY_MOD_LALT}; + + screenshot_config.take_screenshot_hotkey = {mgl::Keyboard::Printscreen, 0}; + screenshot_config.take_screenshot_region_hotkey = {mgl::Keyboard::Printscreen, HOTKEY_MOD_LCTRL}; + + main_config.show_hide_hotkey = {mgl::Keyboard::Z, HOTKEY_MOD_LALT}; + } + static std::optional<KeyValue> parse_key_value(std::string_view line) { const size_t space_index = line.find(' '); if(space_index == std::string_view::npos) @@ -43,13 +164,16 @@ namespace gsr { return KeyValue{line.substr(0, space_index), line.substr(space_index + 1)}; } - using ConfigValue = std::variant<bool*, std::string*, int32_t*, ConfigHotkey*, std::vector<std::string>*>; + using ConfigValue = std::variant<bool*, std::string*, int32_t*, ConfigHotkey*, std::vector<std::string>*, std::vector<AudioTrack>*>; static std::map<std::string_view, ConfigValue> get_config_options(Config &config) { return { {"main.config_file_version", &config.main_config.config_file_version}, {"main.software_encoding_warning_shown", &config.main_config.software_encoding_warning_shown}, + {"main.hotkeys_enable_option", &config.main_config.hotkeys_enable_option}, + {"main.joystick_hotkeys_enable_option", &config.main_config.joystick_hotkeys_enable_option}, {"main.tint_color", &config.main_config.tint_color}, + {"main.show_hide_hotkey", &config.main_config.show_hide_hotkey}, {"streaming.record_options.record_area_option", &config.streaming_config.record_options.record_area_option}, {"streaming.record_options.record_area_width", &config.streaming_config.record_options.record_area_width}, @@ -62,6 +186,7 @@ namespace gsr { {"streaming.record_options.application_audio_invert", &config.streaming_config.record_options.application_audio_invert}, {"streaming.record_options.change_video_resolution", &config.streaming_config.record_options.change_video_resolution}, {"streaming.record_options.audio_track", &config.streaming_config.record_options.audio_tracks}, + {"streaming.record_options.audio_track_item", &config.streaming_config.record_options.audio_tracks_list}, {"streaming.record_options.color_range", &config.streaming_config.record_options.color_range}, {"streaming.record_options.video_quality", &config.streaming_config.record_options.video_quality}, {"streaming.record_options.codec", &config.streaming_config.record_options.video_codec}, @@ -76,9 +201,10 @@ namespace gsr { {"streaming.service", &config.streaming_config.streaming_service}, {"streaming.youtube.key", &config.streaming_config.youtube.stream_key}, {"streaming.twitch.key", &config.streaming_config.twitch.stream_key}, + {"streaming.rumble.key", &config.streaming_config.rumble.stream_key}, {"streaming.custom.url", &config.streaming_config.custom.url}, {"streaming.custom.container", &config.streaming_config.custom.container}, - {"streaming.start_stop_recording_hotkey", &config.streaming_config.start_stop_recording_hotkey}, + {"streaming.start_stop_hotkey", &config.streaming_config.start_stop_hotkey}, {"record.record_options.record_area_option", &config.record_config.record_options.record_area_option}, {"record.record_options.record_area_width", &config.record_config.record_options.record_area_width}, @@ -91,6 +217,7 @@ namespace gsr { {"record.record_options.application_audio_invert", &config.record_config.record_options.application_audio_invert}, {"record.record_options.change_video_resolution", &config.record_config.record_options.change_video_resolution}, {"record.record_options.audio_track", &config.record_config.record_options.audio_tracks}, + {"record.record_options.audio_track_item", &config.record_config.record_options.audio_tracks_list}, {"record.record_options.color_range", &config.record_config.record_options.color_range}, {"record.record_options.video_quality", &config.record_config.record_options.video_quality}, {"record.record_options.codec", &config.record_config.record_options.video_codec}, @@ -103,10 +230,11 @@ namespace gsr { {"record.save_video_in_game_folder", &config.record_config.save_video_in_game_folder}, {"record.show_recording_started_notifications", &config.record_config.show_recording_started_notifications}, {"record.show_video_saved_notifications", &config.record_config.show_video_saved_notifications}, + {"record.show_video_paused_notifications", &config.record_config.show_video_paused_notifications}, {"record.save_directory", &config.record_config.save_directory}, {"record.container", &config.record_config.container}, - {"record.start_stop_recording_hotkey", &config.record_config.start_stop_recording_hotkey}, - {"record.pause_unpause_recording_hotkey", &config.record_config.pause_unpause_recording_hotkey}, + {"record.start_stop_hotkey", &config.record_config.start_stop_hotkey}, + {"record.pause_unpause_hotkey", &config.record_config.pause_unpause_hotkey}, {"replay.record_options.record_area_option", &config.replay_config.record_options.record_area_option}, {"replay.record_options.record_area_width", &config.replay_config.record_options.record_area_width}, @@ -119,6 +247,7 @@ namespace gsr { {"replay.record_options.application_audio_invert", &config.replay_config.record_options.application_audio_invert}, {"replay.record_options.change_video_resolution", &config.replay_config.record_options.change_video_resolution}, {"replay.record_options.audio_track", &config.replay_config.record_options.audio_tracks}, + {"replay.record_options.audio_track_item", &config.replay_config.record_options.audio_tracks_list}, {"replay.record_options.color_range", &config.replay_config.record_options.color_range}, {"replay.record_options.video_quality", &config.replay_config.record_options.video_quality}, {"replay.record_options.codec", &config.replay_config.record_options.video_codec}, @@ -130,17 +259,83 @@ namespace gsr { {"replay.record_options.restore_portal_session", &config.replay_config.record_options.restore_portal_session}, {"replay.turn_on_replay_automatically_mode", &config.replay_config.turn_on_replay_automatically_mode}, {"replay.save_video_in_game_folder", &config.replay_config.save_video_in_game_folder}, + {"replay.restart_replay_on_save", &config.replay_config.restart_replay_on_save}, {"replay.show_replay_started_notifications", &config.replay_config.show_replay_started_notifications}, {"replay.show_replay_stopped_notifications", &config.replay_config.show_replay_stopped_notifications}, {"replay.show_replay_saved_notifications", &config.replay_config.show_replay_saved_notifications}, {"replay.save_directory", &config.replay_config.save_directory}, {"replay.container", &config.replay_config.container}, {"replay.time", &config.replay_config.replay_time}, - {"replay.start_stop_recording_hotkey", &config.replay_config.start_stop_recording_hotkey}, - {"replay.save_recording_hotkey", &config.replay_config.save_recording_hotkey} + {"replay.replay_storage", &config.replay_config.replay_storage}, + {"replay.start_stop_hotkey", &config.replay_config.start_stop_hotkey}, + {"replay.save_hotkey", &config.replay_config.save_hotkey}, + {"replay.save_1_min_hotkey", &config.replay_config.save_1_min_hotkey}, + {"replay.save_10_min_hotkey", &config.replay_config.save_10_min_hotkey}, + + {"screenshot.record_area_option", &config.screenshot_config.record_area_option}, + {"screenshot.image_width", &config.screenshot_config.image_width}, + {"screenshot.image_height", &config.screenshot_config.image_height}, + {"screenshot.change_image_resolution", &config.screenshot_config.change_image_resolution}, + {"screenshot.image_quality", &config.screenshot_config.image_quality}, + {"screenshot.image_format", &config.screenshot_config.image_format}, + {"screenshot.record_cursor", &config.screenshot_config.record_cursor}, + {"screenshot.restore_portal_session", &config.screenshot_config.restore_portal_session}, + {"screenshot.save_screenshot_in_game_folder", &config.screenshot_config.save_screenshot_in_game_folder}, + {"screenshot.show_screenshot_saved_notifications", &config.screenshot_config.show_screenshot_saved_notifications}, + {"screenshot.save_directory", &config.screenshot_config.save_directory}, + {"screenshot.take_screenshot_hotkey", &config.screenshot_config.take_screenshot_hotkey}, + {"screenshot.take_screenshot_region_hotkey", &config.screenshot_config.take_screenshot_region_hotkey} }; } + bool Config::operator==(const Config &other) { + const auto config_options = get_config_options(*this); + const auto config_options_other = get_config_options(const_cast<Config&>(other)); + for(auto it : config_options) { + auto it_other = config_options_other.find(it.first); + if(it_other == config_options_other.end() || it_other->second.index() != it.second.index()) + return false; + + if(std::holds_alternative<bool*>(it.second)) { + if(*std::get<bool*>(it.second) != *std::get<bool*>(it_other->second)) + return false; + } else if(std::holds_alternative<std::string*>(it.second)) { + if(*std::get<std::string*>(it.second) != *std::get<std::string*>(it_other->second)) + return false; + } else if(std::holds_alternative<int32_t*>(it.second)) { + if(*std::get<int32_t*>(it.second) != *std::get<int32_t*>(it_other->second)) + return false; + } else if(std::holds_alternative<ConfigHotkey*>(it.second)) { + if(*std::get<ConfigHotkey*>(it.second) != *std::get<ConfigHotkey*>(it_other->second)) + return false; + } else if(std::holds_alternative<std::vector<std::string>*>(it.second)) { + if(*std::get<std::vector<std::string>*>(it.second) != *std::get<std::vector<std::string>*>(it_other->second)) + return false; + } else if(std::holds_alternative<std::vector<AudioTrack>*>(it.second)) { + if(*std::get<std::vector<AudioTrack>*>(it.second) != *std::get<std::vector<AudioTrack>*>(it_other->second)) + return false; + } else { + assert(false); + } + } + return true; + } + + bool Config::operator!=(const Config &other) { + return !operator==(other); + } + + static void populate_new_audio_track_from_old(RecordOptions &record_options) { + if(record_options.merge_audio_tracks) { + record_options.audio_tracks_list.push_back({std::move(record_options.audio_tracks), record_options.application_audio_invert}); + } else { + for(const std::string &audio_input : record_options.audio_tracks) { + record_options.audio_tracks_list.push_back({std::vector<std::string>{audio_input}, record_options.application_audio_invert}); + } + } + record_options.audio_tracks.clear(); + } + std::optional<Config> read_config(const SupportedCaptureOptions &capture_options) { std::optional<Config> config; @@ -152,10 +347,15 @@ namespace gsr { } config = Config(capture_options); + config->streaming_config.record_options.audio_tracks.clear(); config->record_config.record_options.audio_tracks.clear(); config->replay_config.record_options.audio_tracks.clear(); + config->streaming_config.record_options.audio_tracks_list.clear(); + config->record_config.record_options.audio_tracks_list.clear(); + config->replay_config.record_options.audio_tracks_list.clear(); + auto config_options = get_config_options(config.value()); string_split_char(file_content, '\n', [&](std::string_view line) { @@ -186,29 +386,53 @@ namespace gsr { } else if(std::holds_alternative<ConfigHotkey*>(it->second)) { std::string value_str(key_value->value); ConfigHotkey *config_hotkey = std::get<ConfigHotkey*>(it->second); - if(sscanf(value_str.c_str(), FORMAT_I64 " " FORMAT_U32, &config_hotkey->keysym, &config_hotkey->modifiers) != 2) { + if(sscanf(value_str.c_str(), FORMAT_I64 " " FORMAT_U32, &config_hotkey->key, &config_hotkey->modifiers) != 2) { fprintf(stderr, "Warning: Invalid config option value for %.*s\n", (int)key_value->key.size(), key_value->key.data()); - config_hotkey->keysym = 0; + config_hotkey->key = 0; config_hotkey->modifiers = 0; } } else if(std::holds_alternative<std::vector<std::string>*>(it->second)) { std::string array_value(key_value->value); std::get<std::vector<std::string>*>(it->second)->push_back(std::move(array_value)); + } else if(std::holds_alternative<std::vector<AudioTrack>*>(it->second)) { + const size_t space_index = key_value->value.find(' '); + if(space_index == std::string_view::npos) { + fprintf(stderr, "Warning: Invalid config option value for %.*s\n", (int)key_value->key.size(), key_value->key.data()); + return true; + } + + const bool application_audio_invert = key_value->value.substr(0, space_index) == "true"; + const std::string_view audio_input = key_value->value.substr(space_index + 1); + std::vector<AudioTrack> &audio_tracks = *std::get<std::vector<AudioTrack>*>(it->second); + + if(audio_input == add_audio_track_tag) { + audio_tracks.push_back({std::vector<std::string>{}, application_audio_invert}); + } else if(!audio_tracks.empty()) { + audio_tracks.back().application_audio_invert = application_audio_invert; + audio_tracks.back().audio_inputs.emplace_back(audio_input); + } + } else { + assert(false); } return true; }); - if(config->main_config.config_file_version != CONFIG_FILE_VERSION) { - fprintf(stderr, "Info: the config file is outdated, resetting it\n"); - config = std::nullopt; + if(config->main_config.config_file_version == 1) { + populate_new_audio_track_from_old(config->streaming_config.record_options); + populate_new_audio_track_from_old(config->record_config.record_options); + populate_new_audio_track_from_old(config->replay_config.record_options); } + config->streaming_config.record_options.audio_tracks.clear(); + config->record_config.record_options.audio_tracks.clear(); + config->replay_config.record_options.audio_tracks.clear(); + return config; } void save_config(Config &config) { - config.main_config.config_file_version = CONFIG_FILE_VERSION; + config.main_config.config_file_version = GSR_CONFIG_FILE_VERSION; const std::string config_path = get_config_dir() + "/config_ui"; @@ -237,12 +461,22 @@ namespace gsr { fprintf(file, "%.*s " FORMAT_I32 "\n", (int)it.first.size(), it.first.data(), *std::get<int32_t*>(it.second)); } else if(std::holds_alternative<ConfigHotkey*>(it.second)) { const ConfigHotkey *config_hotkey = std::get<ConfigHotkey*>(it.second); - fprintf(file, "%.*s " FORMAT_I64 " " FORMAT_U32 "\n", (int)it.first.size(), it.first.data(), config_hotkey->keysym, config_hotkey->modifiers); + fprintf(file, "%.*s " FORMAT_I64 " " FORMAT_U32 "\n", (int)it.first.size(), it.first.data(), config_hotkey->key, config_hotkey->modifiers); } else if(std::holds_alternative<std::vector<std::string>*>(it.second)) { - std::vector<std::string> *array = std::get<std::vector<std::string>*>(it.second); - for(const std::string &value : *array) { - fprintf(file, "%.*s %s\n", (int)it.first.size(), it.first.data(), value.c_str()); + std::vector<std::string> *audio_inputs = std::get<std::vector<std::string>*>(it.second); + for(const std::string &audio_input : *audio_inputs) { + fprintf(file, "%.*s %s\n", (int)it.first.size(), it.first.data(), audio_input.c_str()); + } + } else if(std::holds_alternative<std::vector<AudioTrack>*>(it.second)) { + std::vector<AudioTrack> *audio_tracks = std::get<std::vector<AudioTrack>*>(it.second); + for(const AudioTrack &audio_track : *audio_tracks) { + fprintf(file, "%.*s %s %.*s\n", (int)it.first.size(), it.first.data(), audio_track.application_audio_invert ? "true" : "false", (int)add_audio_track_tag.size(), add_audio_track_tag.data()); + for(const std::string &audio_input : audio_track.audio_inputs) { + fprintf(file, "%.*s %s %s\n", (int)it.first.size(), it.first.data(), audio_track.application_audio_invert ? "true" : "false", audio_input.c_str()); + } } + } else { + assert(false); } } diff --git a/src/CursorTracker/CursorTrackerWayland.cpp b/src/CursorTracker/CursorTrackerWayland.cpp new file mode 100644 index 0000000..7af86b4 --- /dev/null +++ b/src/CursorTracker/CursorTrackerWayland.cpp @@ -0,0 +1,538 @@ +#include "../../include/CursorTracker/CursorTrackerWayland.hpp" +#include <string.h> +#include <unistd.h> +#include <fcntl.h> +#include <xf86drm.h> +#include <xf86drmMode.h> +#include <wayland-client.h> +#include "xdg-output-unstable-v1-client-protocol.h" + +namespace gsr { + static const int MAX_CONNECTORS = 32; + static const uint32_t plane_property_all = 0xF; + + typedef enum { + PLANE_PROPERTY_CRTC_X = 1 << 0, + PLANE_PROPERTY_CRTC_Y = 1 << 1, + PLANE_PROPERTY_CRTC_ID = 1 << 2, + PLANE_PROPERTY_TYPE_CURSOR = 1 << 3, + } plane_property_mask; + + typedef struct { + uint64_t crtc_id; + mgl::vec2i size; + bool vrr_enabled; + } drm_connector; + + typedef struct { + drm_connector connectors[MAX_CONNECTORS]; + int num_connectors; + bool has_any_crtc_with_vrr_enabled; + } drm_connectors; + + /* Returns plane_property_mask */ + static uint32_t plane_get_properties(int drm_fd, uint32_t plane_id, int *crtc_x, int *crtc_y, int *crtc_id) { + *crtc_x = 0; + *crtc_y = 0; + *crtc_id = 0; + + uint32_t property_mask = 0; + + drmModeObjectPropertiesPtr props = drmModeObjectGetProperties(drm_fd, plane_id, DRM_MODE_OBJECT_PLANE); + if(!props) + return property_mask; + + for(uint32_t i = 0; i < props->count_props; ++i) { + drmModePropertyPtr prop = drmModeGetProperty(drm_fd, props->props[i]); + if(!prop) + continue; + + // SRC_* values are fixed 16.16 points + const uint32_t type = prop->flags & (DRM_MODE_PROP_LEGACY_TYPE | DRM_MODE_PROP_EXTENDED_TYPE); + if((type & DRM_MODE_PROP_SIGNED_RANGE) && strcmp(prop->name, "CRTC_X") == 0) { + *crtc_x = (int)props->prop_values[i]; + property_mask |= PLANE_PROPERTY_CRTC_X; + } else if((type & DRM_MODE_PROP_SIGNED_RANGE) && strcmp(prop->name, "CRTC_Y") == 0) { + *crtc_y = (int)props->prop_values[i]; + property_mask |= PLANE_PROPERTY_CRTC_Y; + } else if((type & DRM_MODE_PROP_OBJECT) && strcmp(prop->name, "CRTC_ID") == 0) { + *crtc_id = (int)props->prop_values[i]; + property_mask |= PLANE_PROPERTY_CRTC_ID; + } else if((type & DRM_MODE_PROP_ENUM) && strcmp(prop->name, "type") == 0) { + const uint64_t current_enum_value = props->prop_values[i]; + for(int j = 0; j < prop->count_enums; ++j) { + if(prop->enums[j].value == current_enum_value && strcmp(prop->enums[j].name, "Cursor") == 0) { + property_mask |= PLANE_PROPERTY_TYPE_CURSOR; + break; + } + } + } + + drmModeFreeProperty(prop); + } + + drmModeFreeObjectProperties(props); + return property_mask; + } + + static bool get_drm_property_by_name(int drm_fd, drmModeObjectPropertiesPtr props, const char *name, uint64_t *result) { + for(uint32_t i = 0; i < props->count_props; ++i) { + drmModePropertyPtr prop = drmModeGetProperty(drm_fd, props->props[i]); + if(!prop) + continue; + + if(strcmp(name, prop->name) == 0) { + *result = props->prop_values[i]; + drmModeFreeProperty(prop); + return true; + } + drmModeFreeProperty(prop); + } + return false; + } + + static bool connector_get_property_by_name(int drm_fd, drmModeConnectorPtr props, const char *name, uint64_t *result) { + drmModeObjectProperties properties; + properties.count_props = (uint32_t)props->count_props; + properties.props = props->props; + properties.prop_values = props->prop_values; + return get_drm_property_by_name(drm_fd, &properties, name, result); + } + + // Note: this monitor name logic is kept in sync with gpu screen recorder + static std::string get_monitor_name_from_crtc_id(int drm_fd, uint32_t crtc_id) { + std::string result; + drmModeResPtr resources = drmModeGetResources(drm_fd); + if(!resources) + return result; + + for(int i = 0; i < resources->count_connectors; ++i) { + uint64_t connector_crtc_id = 0; + drmModeConnectorPtr connector = drmModeGetConnectorCurrent(drm_fd, resources->connectors[i]); + if(!connector) + continue; + + const char *connection_name = drmModeGetConnectorTypeName(connector->connector_type); + if(!connection_name) + goto next; + + if(connector->connection != DRM_MODE_CONNECTED) + goto next; + + if(connector_get_property_by_name(drm_fd, connector, "CRTC_ID", &connector_crtc_id) && connector_crtc_id == crtc_id) { + result = connection_name; + result += "-"; + result += std::to_string(connector->connector_type_id); + drmModeFreeConnector(connector); + break; + } + + next: + drmModeFreeConnector(connector); + } + + drmModeFreeResources(resources); + return result; + } + + // Name is the crtc name. TODO: verify if this works on all wayland compositors + static const WaylandOutput* get_wayland_monitor_by_name(const std::vector<WaylandOutput> &monitors, const std::string &name) { + for(const WaylandOutput &monitor : monitors) { + if(monitor.name == name) + return &monitor; + } + return nullptr; + } + + static WaylandOutput* get_wayland_monitor_by_output(CursorTrackerWayland &cursor_tracker_wayland, struct wl_output *output) { + for(WaylandOutput &monitor : cursor_tracker_wayland.monitors) { + if(monitor.output == output) + return &monitor; + } + return nullptr; + } + + static void output_handle_geometry(void *data, struct wl_output *wl_output, + int32_t x, int32_t y, int32_t phys_width, int32_t phys_height, + int32_t subpixel, const char *make, const char *model, + int32_t transform) { + (void)wl_output; + (void)phys_width; + (void)phys_height; + (void)subpixel; + (void)make; + (void)model; + CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data; + WaylandOutput *monitor = get_wayland_monitor_by_output(*cursor_tracker_wayland, wl_output); + if(!monitor) + return; + + monitor->pos.x = x; + monitor->pos.y = y; + monitor->transform = transform; + } + + static void output_handle_mode(void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { + (void)wl_output; + (void)flags; + (void)refresh; + CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data; + WaylandOutput *monitor = get_wayland_monitor_by_output(*cursor_tracker_wayland, wl_output); + if(!monitor) + return; + + monitor->size.x = width; + monitor->size.y = height; + } + + static void output_handle_done(void *data, struct wl_output *wl_output) { + (void)data; + (void)wl_output; + } + + static void output_handle_scale(void* data, struct wl_output *wl_output, int32_t factor) { + (void)data; + (void)wl_output; + (void)factor; + } + + static void output_handle_name(void *data, struct wl_output *wl_output, const char *name) { + (void)wl_output; + CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data; + WaylandOutput *monitor = get_wayland_monitor_by_output(*cursor_tracker_wayland, wl_output); + if(!monitor) + return; + + monitor->name = name; + } + + static void output_handle_description(void *data, struct wl_output *wl_output, const char *description) { + (void)data; + (void)wl_output; + (void)description; + } + + static const struct wl_output_listener output_listener = { + output_handle_geometry, + output_handle_mode, + output_handle_done, + output_handle_scale, + output_handle_name, + output_handle_description, + }; + + static void registry_add_object(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { + (void)version; + CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data; + if(strcmp(interface, wl_output_interface.name) == 0) { + if(version < 4) { + fprintf(stderr, "Warning: wl output interface version is < 4, expected >= 4\n"); + return; + } + + struct wl_output *output = (struct wl_output*)wl_registry_bind(registry, name, &wl_output_interface, 4); + cursor_tracker_wayland->monitors.push_back( + WaylandOutput{ + name, + output, + nullptr, + mgl::vec2i{0, 0}, + mgl::vec2i{0, 0}, + 0, + "" + }); + wl_output_add_listener(output, &output_listener, cursor_tracker_wayland); + } else if(strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) { + if(version < 1) { + fprintf(stderr, "Warning: xdg output interface version is < 1, expected >= 1\n"); + return; + } + + if(cursor_tracker_wayland->xdg_output_manager) { + zxdg_output_manager_v1_destroy(cursor_tracker_wayland->xdg_output_manager); + cursor_tracker_wayland->xdg_output_manager = NULL; + } + cursor_tracker_wayland->xdg_output_manager = (struct zxdg_output_manager_v1*)wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, 1); + } + } + + static void registry_remove_object(void *data, struct wl_registry *registry, uint32_t name) { + (void)data; + (void)registry; + (void)name; + // TODO: Remove output + } + + static struct wl_registry_listener registry_listener = { + registry_add_object, + registry_remove_object, + }; + + static void xdg_output_logical_position(void *data, struct zxdg_output_v1 *zxdg_output_v1, int32_t x, int32_t y) { + (void)zxdg_output_v1; + WaylandOutput *monitor = (WaylandOutput*)data; + monitor->pos.x = x; + monitor->pos.y = y; + } + + static void xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, int32_t width, int32_t height) { + (void)data; + (void)xdg_output; + (void)width; + (void)height; + } + + static void xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) { + (void)data; + (void)xdg_output; + } + + static void xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, const char *name) { + (void)data; + (void)xdg_output; + (void)name; + } + + static void xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, const char *description) { + (void)data; + (void)xdg_output; + (void)description; + } + + static const struct zxdg_output_v1_listener xdg_output_listener = { + xdg_output_logical_position, + xdg_output_handle_logical_size, + xdg_output_handle_done, + xdg_output_handle_name, + xdg_output_handle_description, + }; + + /* Returns nullptr if not found */ + static drm_connector* get_drm_connector_by_crtc_id(drm_connectors *connectors, uint32_t crtc_id) { + for(int i = 0; i < connectors->num_connectors; ++i) { + if(connectors->connectors[i].crtc_id == crtc_id) + return &connectors->connectors[i]; + } + return nullptr; + } + + static void get_drm_connectors(int drm_fd, drm_connectors *drm_connectors) { + drm_connectors->num_connectors = 0; + drm_connectors->has_any_crtc_with_vrr_enabled = false; + + drmModeResPtr resources = drmModeGetResources(drm_fd); + if(!resources) + return; + + for(int i = 0; i < resources->count_connectors && drm_connectors->num_connectors < MAX_CONNECTORS; ++i) { + drmModeConnectorPtr connector = nullptr; + drmModeCrtcPtr crtc = nullptr; + + connector = drmModeGetConnectorCurrent(drm_fd, resources->connectors[i]); + if(!connector) + continue; + + uint64_t crtc_id = 0; + connector_get_property_by_name(drm_fd, connector, "CRTC_ID", &crtc_id); + if(crtc_id == 0) + goto next_connector; + + crtc = drmModeGetCrtc(drm_fd, crtc_id); + if(!crtc) + goto next_connector; + + drm_connectors->connectors[drm_connectors->num_connectors].crtc_id = crtc_id; + drm_connectors->connectors[drm_connectors->num_connectors].size = mgl::vec2i{(int)crtc->width, (int)crtc->height}; + drm_connectors->connectors[drm_connectors->num_connectors].vrr_enabled = false; + ++drm_connectors->num_connectors; + + next_connector: + if(crtc) + drmModeFreeCrtc(crtc); + + if(connector) + drmModeFreeConnector(connector); + } + + for(int i = 0; i < resources->count_crtcs; ++i) { + drmModeCrtcPtr crtc = nullptr; + drmModeObjectPropertiesPtr properties = nullptr; + uint64_t vrr_enabled = 0; + drm_connector *connector = nullptr; + + crtc = drmModeGetCrtc(drm_fd, resources->crtcs[i]); + if(!crtc) + continue; + + properties = drmModeObjectGetProperties(drm_fd, crtc->crtc_id, DRM_MODE_OBJECT_CRTC); + if(!properties) + goto next_crtc; + + if(!get_drm_property_by_name(drm_fd, properties, "VRR_ENABLED", &vrr_enabled)) + goto next_crtc; + + connector = get_drm_connector_by_crtc_id(drm_connectors, crtc->crtc_id); + if(!connector) + goto next_crtc; + + if(vrr_enabled) { + connector->vrr_enabled = true; + drm_connectors->has_any_crtc_with_vrr_enabled = true; + } + + next_crtc: + if(properties) + drmModeFreeObjectProperties(properties); + + if(crtc) + drmModeFreeCrtc(crtc); + } + + drmModeFreeResources(resources); + } + + CursorTrackerWayland::CursorTrackerWayland(const char *card_path) { + drm_fd = open(card_path, O_RDONLY); + if(drm_fd <= 0) { + fprintf(stderr, "Error: CursorTrackerWayland: failed to open %s\n", card_path); + return; + } + + drmSetClientCap(drm_fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1); + drmSetClientCap(drm_fd, DRM_CLIENT_CAP_ATOMIC, 1); + } + + CursorTrackerWayland::~CursorTrackerWayland() { + if(drm_fd > 0) + close(drm_fd); + } + + void CursorTrackerWayland::update() { + if(drm_fd <= 0) + return; + + drm_connectors connectors; + connectors.num_connectors = 0; + connectors.has_any_crtc_with_vrr_enabled = false; + get_drm_connectors(drm_fd, &connectors); + + drmModePlaneResPtr planes = drmModeGetPlaneResources(drm_fd); + if(!planes) + return; + + bool found_cursor = false; + for(uint32_t i = 0; i < planes->count_planes; ++i) { + drmModePlanePtr plane = nullptr; + const drm_connector *connector = nullptr; + int crtc_x = 0; + int crtc_y = 0; + int crtc_id = 0; + uint32_t property_mask = 0; + + plane = drmModeGetPlane(drm_fd, planes->planes[i]); + if(!plane) + goto next; + + if(!plane->fb_id) + goto next; + + property_mask = plane_get_properties(drm_fd, planes->planes[i], &crtc_x, &crtc_y, &crtc_id); + if(property_mask != plane_property_all || crtc_id <= 0) + goto next; + + connector = get_drm_connector_by_crtc_id(&connectors, crtc_id); + if(!connector) + goto next; + + if(crtc_x >= 0 && crtc_x <= connector->size.x && crtc_y >= 0 && crtc_y <= connector->size.y) { + latest_cursor_position.x = crtc_x; + latest_cursor_position.y = crtc_y; + latest_crtc_id = crtc_id; + found_cursor = true; + drmModeFreePlane(plane); + break; + } + + next: + drmModeFreePlane(plane); + } + + // On kde plasma wayland (and possibly other wayland compositors) it uses a software cursor only for the monitors with vrr enabled. + // In that case we cant know the cursor location and we instead want to fallback to getting focused monitor by using the hack of creating a window and getting the position. + if(!found_cursor && latest_crtc_id > 0 && connectors.has_any_crtc_with_vrr_enabled) + latest_crtc_id = -1; + + drmModeFreePlaneResources(planes); + } + + void CursorTrackerWayland::set_monitor_outputs_from_xdg_output(struct wl_display *dpy) { + if(!xdg_output_manager) { + fprintf(stderr, "Warning: CursorTrackerWayland::set_monitor_outputs_from_xdg_output: zxdg_output_manager not found. Registered monitor positions might be incorrect\n"); + return; + } + + for(WaylandOutput &monitor : monitors) { + monitor.xdg_output = zxdg_output_manager_v1_get_xdg_output(xdg_output_manager, monitor.output); + zxdg_output_v1_add_listener(monitor.xdg_output, &xdg_output_listener, &monitor); + } + + // Fetch xdg_output + wl_display_roundtrip(dpy); + } + + std::optional<CursorInfo> CursorTrackerWayland::get_latest_cursor_info() { + if(drm_fd <= 0 || latest_crtc_id == -1) + return std::nullopt; + + std::string monitor_name = get_monitor_name_from_crtc_id(drm_fd, latest_crtc_id); + if(monitor_name.empty()) + return std::nullopt; + + struct wl_display *dpy = wl_display_connect(nullptr); + if(!dpy) { + fprintf(stderr, "Error: CursorTrackerWayland::get_latest_cursor_info: failed to connect to the wayland server\n"); + return std::nullopt; + } + + monitors.clear(); + struct wl_registry *registry = wl_display_get_registry(dpy); + wl_registry_add_listener(registry, ®istry_listener, this); + + // Fetch globals + wl_display_roundtrip(dpy); + + // Fetch wl_output + wl_display_roundtrip(dpy); + + set_monitor_outputs_from_xdg_output(dpy); + + mgl::vec2i cursor_position = latest_cursor_position; + const WaylandOutput *wayland_monitor = get_wayland_monitor_by_name(monitors, monitor_name); + if(!wayland_monitor) + return std::nullopt; + + cursor_position = wayland_monitor->pos + latest_cursor_position; + for(WaylandOutput &monitor : monitors) { + if(monitor.output) { + wl_output_destroy(monitor.output); + monitor.output = nullptr; + } + + if(monitor.xdg_output) { + zxdg_output_v1_destroy(monitor.xdg_output); + monitor.xdg_output = nullptr; + } + } + monitors.clear(); + + if(xdg_output_manager) { + zxdg_output_manager_v1_destroy(xdg_output_manager); + xdg_output_manager = nullptr; + } + + wl_registry_destroy(registry); + wl_display_disconnect(dpy); + + return CursorInfo{ cursor_position, std::move(monitor_name) }; + } +}
\ No newline at end of file diff --git a/src/CursorTracker/CursorTrackerX11.cpp b/src/CursorTracker/CursorTrackerX11.cpp new file mode 100644 index 0000000..7c98f4d --- /dev/null +++ b/src/CursorTracker/CursorTrackerX11.cpp @@ -0,0 +1,29 @@ +#include "../../include/CursorTracker/CursorTrackerX11.hpp" +#include "../../include/WindowUtils.hpp" + +namespace gsr { + CursorTrackerX11::CursorTrackerX11(Display *dpy) : dpy(dpy) { + + } + + std::optional<CursorInfo> CursorTrackerX11::get_latest_cursor_info() { + Window window = None; + const auto cursor_pos = get_cursor_position(dpy, &window); + const auto monitors = get_monitors(dpy); + std::string monitor_name; + + for(const auto &monitor : monitors) { + if(cursor_pos.x >= monitor.position.x && cursor_pos.x <= monitor.position.x + monitor.size.x + && cursor_pos.y >= monitor.position.y && cursor_pos.y <= monitor.position.y + monitor.size.y) + { + monitor_name = monitor.name; + break; + } + } + + if(monitor_name.empty()) + return std::nullopt; + + return CursorInfo{ cursor_pos, std::move(monitor_name) }; + } +}
\ No newline at end of file diff --git a/src/GlobalHotkeys/GlobalHotkeysJoystick.cpp b/src/GlobalHotkeys/GlobalHotkeysJoystick.cpp new file mode 100644 index 0000000..5969438 --- /dev/null +++ b/src/GlobalHotkeys/GlobalHotkeysJoystick.cpp @@ -0,0 +1,392 @@ +#include "../../include/GlobalHotkeys/GlobalHotkeysJoystick.hpp" +#include <string.h> +#include <errno.h> +#include <fcntl.h> +#include <unistd.h> +#include <sys/eventfd.h> + +namespace gsr { + static constexpr int button_pressed = 1; + static constexpr int cross_button = 0; + static constexpr int triangle_button = 2; + static constexpr int options_button = 9; + static constexpr int playstation_button = 10; + static constexpr int l3_button = 11; + static constexpr int r3_button = 12; + static constexpr int axis_up_down = 7; + static constexpr int axis_left_right = 6; + + struct DeviceId { + uint16_t vendor; + uint16_t product; + }; + + static bool read_file_hex_number(const char *path, unsigned int *value) { + *value = 0; + FILE *f = fopen(path, "rb"); + if(!f) + return false; + + fscanf(f, "%x", value); + fclose(f); + return true; + } + + static DeviceId joystick_get_device_id(const char *path) { + DeviceId device_id; + device_id.vendor = 0; + device_id.product = 0; + + const char *js_path_id = nullptr; + const int len = strlen(path); + for(int i = len - 1; i >= 0; --i) { + if(path[i] == '/') { + js_path_id = path + i + 1; + break; + } + } + + if(!js_path_id) + return device_id; + + unsigned int vendor = 0; + unsigned int product = 0; + char path_buf[1024]; + + snprintf(path_buf, sizeof(path_buf), "/sys/class/input/%s/device/id/vendor", js_path_id); + if(!read_file_hex_number(path_buf, &vendor)) + return device_id; + + snprintf(path_buf, sizeof(path_buf), "/sys/class/input/%s/device/id/product", js_path_id); + if(!read_file_hex_number(path_buf, &product)) + return device_id; + + device_id.vendor = vendor; + device_id.product = product; + return device_id; + } + + static bool is_ps4_controller(DeviceId device_id) { + return device_id.vendor == 0x054C && (device_id.product == 0x09CC || device_id.product == 0x0BA0 || device_id.product == 0x05C4); + } + + static bool is_ps5_controller(DeviceId device_id) { + return device_id.vendor == 0x054C && (device_id.product == 0x0DF2 || device_id.product == 0x0CE6); + } + + static bool is_stadia_controller(DeviceId device_id) { + return device_id.vendor == 0x18D1 && (device_id.product == 0x9400); + } + + // Returns -1 on error + static int get_js_dev_input_id_from_filepath(const char *dev_input_filepath) { + if(strncmp(dev_input_filepath, "/dev/input/js", 13) != 0) + return -1; + + int dev_input_id = -1; + if(sscanf(dev_input_filepath + 13, "%d", &dev_input_id) == 1) + return dev_input_id; + return -1; + } + + GlobalHotkeysJoystick::~GlobalHotkeysJoystick() { + if(event_fd > 0) { + const uint64_t exit = 1; + write(event_fd, &exit, sizeof(exit)); + } + + if(read_thread.joinable()) + read_thread.join(); + + if(event_fd > 0) + close(event_fd); + + for(int i = 0; i < num_poll_fd; ++i) { + close(poll_fd[i].fd); + } + } + + bool GlobalHotkeysJoystick::start() { + if(num_poll_fd > 0) + return false; + + event_fd = eventfd(0, 0); + if(event_fd <= 0) + return false; + + event_index = num_poll_fd; + poll_fd[num_poll_fd] = { + event_fd, + POLLIN, + 0 + }; + extra_data[num_poll_fd] = { + -1 + }; + ++num_poll_fd; + + if(!hotplug.start()) { + fprintf(stderr, "Warning: failed to setup hotplugging\n"); + } else { + hotplug_poll_index = num_poll_fd; + poll_fd[num_poll_fd] = { + hotplug.steal_fd(), + POLLIN, + 0 + }; + extra_data[num_poll_fd] = { + -1 + }; + ++num_poll_fd; + } + + char dev_input_path[128]; + for(int i = 0; i < 8; ++i) { + snprintf(dev_input_path, sizeof(dev_input_path), "/dev/input/js%d", i); + add_device(dev_input_path, false); + } + + if(num_poll_fd == 0) + fprintf(stderr, "Info: no joysticks found, assuming they might be connected later\n"); + + read_thread = std::thread(&GlobalHotkeysJoystick::read_events, this); + return true; + } + + bool GlobalHotkeysJoystick::bind_action(const std::string &id, GlobalHotkeyCallback callback) { + if(num_poll_fd == 0) + return false; + return bound_actions_by_id.insert(std::make_pair(id, std::move(callback))).second; + } + + void GlobalHotkeysJoystick::poll_events() { + if(num_poll_fd == 0) + return; + + if(save_replay) { + save_replay = false; + auto it = bound_actions_by_id.find("save_replay"); + if(it != bound_actions_by_id.end()) + it->second("save_replay"); + } + + if(save_1_min_replay) { + save_1_min_replay = false; + auto it = bound_actions_by_id.find("save_1_min_replay"); + if(it != bound_actions_by_id.end()) + it->second("save_1_min_replay"); + } + + if(save_10_min_replay) { + save_10_min_replay = false; + auto it = bound_actions_by_id.find("save_10_min_replay"); + if(it != bound_actions_by_id.end()) + it->second("save_10_min_replay"); + } + + if(take_screenshot) { + take_screenshot = false; + auto it = bound_actions_by_id.find("take_screenshot"); + if(it != bound_actions_by_id.end()) + it->second("take_screenshot"); + } + + if(toggle_record) { + toggle_record = false; + auto it = bound_actions_by_id.find("toggle_record"); + if(it != bound_actions_by_id.end()) + it->second("toggle_record"); + } + + if(toggle_replay) { + toggle_replay = false; + auto it = bound_actions_by_id.find("toggle_replay"); + if(it != bound_actions_by_id.end()) + it->second("toggle_replay"); + } + + if(toggle_show) { + toggle_show = false; + auto it = bound_actions_by_id.find("toggle_show"); + if(it != bound_actions_by_id.end()) + it->second("toggle_show"); + } + } + + void GlobalHotkeysJoystick::read_events() { + js_event event; + while(poll(poll_fd, num_poll_fd, -1) > 0) { + for(int i = 0; i < num_poll_fd; ++i) { + if(poll_fd[i].revents & (POLLHUP|POLLERR|POLLNVAL)) { + if(i == event_index) + goto done; + + if(remove_poll_fd(i)) + --i; // This item was removed so we want to repeat the same index to continue to the next item + + continue; + } + + if(!(poll_fd[i].revents & POLLIN)) + continue; + + if(i == event_index) { + goto done; + } else if(i == hotplug_poll_index) { + hotplug.process_event_data(poll_fd[i].fd, [&](HotplugAction hotplug_action, const char *devname) { + char dev_input_filepath[1024]; + snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/%s", devname); + switch(hotplug_action) { + case HotplugAction::ADD: { + // Cant open the /dev/input device immediately or it fails. + // TODO: Remove this hack when a better solution is found. + usleep(50 * 1000); + add_device(dev_input_filepath); + break; + } + case HotplugAction::REMOVE: { + if(remove_device(dev_input_filepath)) + --i; // This item was removed so we want to repeat the same index to continue to the next item + break; + } + } + }); + } else { + process_js_event(poll_fd[i].fd, event); + } + } + } + + done: + ; + } + + void GlobalHotkeysJoystick::process_js_event(int fd, js_event &event) { + if(read(fd, &event, sizeof(event)) != sizeof(event)) + return; + + if((event.type & JS_EVENT_BUTTON) == JS_EVENT_BUTTON) { + switch(event.number) { + case playstation_button: { + // Workaround weird steam input (in-game) behavior where steam triggers playstation button + options when pressing both l3 and r3 at the same time + playstation_button_pressed = (event.value == button_pressed) && !l3_button_pressed && !r3_button_pressed; + break; + } + case options_button: { + if(playstation_button_pressed && event.value == button_pressed) + toggle_show = true; + break; + } + case cross_button: { + if(playstation_button_pressed && event.value == button_pressed) + save_1_min_replay = true; + break; + } + case triangle_button: { + if(playstation_button_pressed && event.value == button_pressed) + save_10_min_replay = true; + break; + } + case l3_button: { + l3_button_pressed = event.value == button_pressed; + break; + } + case r3_button: { + r3_button_pressed = event.value == button_pressed; + break; + } + } + } else if((event.type & JS_EVENT_AXIS) == JS_EVENT_AXIS && playstation_button_pressed) { + const int trigger_threshold = 16383; + const bool prev_up_pressed = up_pressed; + const bool prev_down_pressed = down_pressed; + const bool prev_left_pressed = left_pressed; + const bool prev_right_pressed = right_pressed; + + if(event.number == axis_up_down) { + up_pressed = event.value <= -trigger_threshold; + down_pressed = event.value >= trigger_threshold; + } else if(event.number == axis_left_right) { + left_pressed = event.value <= -trigger_threshold; + right_pressed = event.value >= trigger_threshold; + } + + if(up_pressed && !prev_up_pressed) + take_screenshot = true; + else if(down_pressed && !prev_down_pressed) + save_replay = true; + else if(left_pressed && !prev_left_pressed) + toggle_record = true; + else if(right_pressed && !prev_right_pressed) + toggle_replay = true; + } + } + + bool GlobalHotkeysJoystick::add_device(const char *dev_input_filepath, bool print_error) { + if(num_poll_fd >= max_js_poll_fd) { + fprintf(stderr, "Warning: failed to add joystick device %s, too many joysticks have been added\n", dev_input_filepath); + return false; + } + + const int dev_input_id = get_js_dev_input_id_from_filepath(dev_input_filepath); + if(dev_input_id == -1) + return false; + + const int fd = open(dev_input_filepath, O_RDONLY); + if(fd <= 0) { + if(print_error) + fprintf(stderr, "Error: failed to add joystick %s, error: %s\n", dev_input_filepath, strerror(errno)); + return false; + } + + poll_fd[num_poll_fd] = { + fd, + POLLIN, + 0 + }; + + extra_data[num_poll_fd] = { + dev_input_id + }; + + //const DeviceId device_id = joystick_get_device_id(dev_input_filepath); + + ++num_poll_fd; + fprintf(stderr, "Info: added joystick: %s\n", dev_input_filepath); + return true; + } + + bool GlobalHotkeysJoystick::remove_device(const char *dev_input_filepath) { + const int dev_input_id = get_js_dev_input_id_from_filepath(dev_input_filepath); + if(dev_input_id == -1) + return false; + + const int poll_fd_index = get_poll_fd_index_by_dev_input_id(dev_input_id); + if(poll_fd_index == -1) + return false; + + fprintf(stderr, "Info: removed joystick: %s\n", dev_input_filepath); + return remove_poll_fd(poll_fd_index); + } + + bool GlobalHotkeysJoystick::remove_poll_fd(int index) { + if(index < 0 || index >= num_poll_fd) + return false; + + close(poll_fd[index].fd); + for(int i = index + 1; i < num_poll_fd; ++i) { + poll_fd[i - 1] = poll_fd[i]; + extra_data[i - 1] = extra_data[i]; + } + --num_poll_fd; + return true; + } + + int GlobalHotkeysJoystick::get_poll_fd_index_by_dev_input_id(int dev_input_id) const { + for(int i = 0; i < num_poll_fd; ++i) { + if(dev_input_id == extra_data[i].dev_input_id) + return i; + } + return -1; + } +} diff --git a/src/GlobalHotkeys/GlobalHotkeysLinux.cpp b/src/GlobalHotkeys/GlobalHotkeysLinux.cpp new file mode 100644 index 0000000..a56bbc6 --- /dev/null +++ b/src/GlobalHotkeys/GlobalHotkeysLinux.cpp @@ -0,0 +1,275 @@ +#include "../../include/GlobalHotkeys/GlobalHotkeysLinux.hpp" +#include <sys/wait.h> +#include <fcntl.h> +#include <limits.h> +#include <string.h> + +extern "C" { +#include <mgl/mgl.h> +} +#include <X11/Xlib.h> +#include <X11/keysym.h> +#include <linux/input-event-codes.h> + +#define PIPE_READ 0 +#define PIPE_WRITE 1 + +namespace gsr { + static const char* grab_type_to_arg(GlobalHotkeysLinux::GrabType grab_type) { + switch(grab_type) { + case GlobalHotkeysLinux::GrabType::ALL: return "--all"; + case GlobalHotkeysLinux::GrabType::VIRTUAL: return "--virtual"; + } + return "--all"; + } + + static inline uint8_t x11_keycode_to_linux_keycode(uint8_t code) { + return code - 8; + } + + static std::vector<uint8_t> modifiers_to_linux_keys(uint32_t modifiers) { + std::vector<uint8_t> result; + if(modifiers & HOTKEY_MOD_LSHIFT) + result.push_back(KEY_LEFTSHIFT); + if(modifiers & HOTKEY_MOD_RSHIFT) + result.push_back(KEY_RIGHTSHIFT); + if(modifiers & HOTKEY_MOD_LCTRL) + result.push_back(KEY_LEFTCTRL); + if(modifiers & HOTKEY_MOD_RCTRL) + result.push_back(KEY_RIGHTCTRL); + if(modifiers & HOTKEY_MOD_LALT) + result.push_back(KEY_LEFTALT); + if(modifiers & HOTKEY_MOD_RALT) + result.push_back(KEY_RIGHTALT); + if(modifiers & HOTKEY_MOD_LSUPER) + result.push_back(KEY_LEFTMETA); + if(modifiers & HOTKEY_MOD_RSUPER) + result.push_back(KEY_RIGHTMETA); + return result; + } + + static std::string linux_keys_to_command_string(const uint8_t *keys, size_t size) { + std::string result; + for(size_t i = 0; i < size; ++i) { + if(!result.empty()) + result += "+"; + result += std::to_string(keys[i]); + } + return result; + } + + static bool x11_key_is_alpha_numerical(KeySym keysym) { + return (keysym >= XK_A && keysym <= XK_Z) || (keysym >= XK_a && keysym <= XK_z) || (keysym >= XK_0 && keysym <= XK_9); + } + + GlobalHotkeysLinux::GlobalHotkeysLinux(GrabType grab_type) : grab_type(grab_type) { + for(int i = 0; i < 2; ++i) { + read_pipes[i] = -1; + write_pipes[i] = -1; + } + } + + GlobalHotkeysLinux::~GlobalHotkeysLinux() { + if(write_pipes[PIPE_WRITE] > 0) { + char command[32]; + const int command_size = snprintf(command, sizeof(command), "exit\n"); + if(write(write_pipes[PIPE_WRITE], command, command_size) != command_size) { + fprintf(stderr, "Error: GlobalHotkeysLinux::~GlobalHotkeysLinux: failed to write command to gsr-global-hotkeys, error: %s\n", strerror(errno)); + close_fds(); + } + } else { + close_fds(); + } + + if(process_id > 0) { + int status; + waitpid(process_id, &status, 0); + } + + close_fds(); + } + + void GlobalHotkeysLinux::close_fds() { + for(int i = 0; i < 2; ++i) { + if(read_pipes[i] > 0) { + close(read_pipes[i]); + read_pipes[i] = -1; + } + + if(write_pipes[i] > 0) { + close(write_pipes[i]); + write_pipes[i] = -1; + } + } + + if(read_file) { + fclose(read_file); + read_file = nullptr; + } + } + + bool GlobalHotkeysLinux::start() { + const char *grab_type_arg = grab_type_to_arg(grab_type); + const bool inside_flatpak = getenv("FLATPAK_ID") != NULL; + const char *user_homepath = getenv("HOME"); + if(!user_homepath) + user_homepath = "/tmp"; + + if(process_id > 0) + return false; + + if(pipe(read_pipes) == -1) + return false; + + if(pipe(write_pipes) == -1) { + for(int i = 0; i < 2; ++i) { + close(read_pipes[i]); + read_pipes[i] = -1; + } + return false; + } + + const pid_t pid = vfork(); + if(pid == -1) { + perror("Failed to vfork"); + for(int i = 0; i < 2; ++i) { + close(read_pipes[i]); + close(write_pipes[i]); + read_pipes[i] = -1; + write_pipes[i] = -1; + } + return false; + } else if(pid == 0) { /* child */ + dup2(read_pipes[PIPE_WRITE], STDOUT_FILENO); + for(int i = 0; i < 2; ++i) { + close(read_pipes[i]); + } + + dup2(write_pipes[PIPE_READ], STDIN_FILENO); + for(int i = 0; i < 2; ++i) { + close(write_pipes[i]); + } + + if(inside_flatpak) { + const char *args[] = { "flatpak-spawn", "--host", "/var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/bin/kms-server-proxy", "launch-gsr-global-hotkeys", user_homepath, grab_type_arg, nullptr }; + execvp(args[0], (char* const*)args); + } else { + const char *args[] = { "gsr-global-hotkeys", grab_type_arg, nullptr }; + execvp(args[0], (char* const*)args); + } + + perror("gsr-global-hotkeys"); + _exit(127); + } else { /* parent */ + process_id = pid; + + close(read_pipes[PIPE_WRITE]); + read_pipes[PIPE_WRITE] = -1; + + close(write_pipes[PIPE_READ]); + write_pipes[PIPE_READ] = -1; + + fcntl(read_pipes[PIPE_READ], F_SETFL, fcntl(read_pipes[PIPE_READ], F_GETFL) | O_NONBLOCK); + read_file = fdopen(read_pipes[PIPE_READ], "r"); + if(read_file) + read_pipes[PIPE_READ] = -1; + else + fprintf(stderr, "fdopen failed for read, error: %s\n", strerror(errno)); + } + + return true; + } + + bool GlobalHotkeysLinux::bind_key_press(Hotkey hotkey, const std::string &id, GlobalHotkeyCallback callback) { + if(process_id <= 0) + return false; + + if(bound_actions_by_id.find(id) != bound_actions_by_id.end()) + return false; + + if(id.find(' ') != std::string::npos || id.find('\n') != std::string::npos) { + fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: id \"%s\" contains either space or newline\n", id.c_str()); + return false; + } + + if(hotkey.key == 0) { + //fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: hotkey requires a key\n"); + return false; + } + + if(hotkey.modifiers == 0 && x11_key_is_alpha_numerical(hotkey.key)) { + //fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: hotkey requires a modifier\n"); + return false; + } + + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + const uint8_t keycode = x11_keycode_to_linux_keycode(XKeysymToKeycode(display, hotkey.key)); + const std::vector<uint8_t> modifiers = modifiers_to_linux_keys(hotkey.modifiers); + const std::string modifiers_command = linux_keys_to_command_string(modifiers.data(), modifiers.size()); + + char command[256]; + int command_size = 0; + if(modifiers_command.empty()) + command_size = snprintf(command, sizeof(command), "bind %s %d\n", id.c_str(), (int)keycode); + else + command_size = snprintf(command, sizeof(command), "bind %s %d+%s\n", id.c_str(), (int)keycode, modifiers_command.c_str()); + + if(write(write_pipes[PIPE_WRITE], command, command_size) != command_size) { + fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: failed to write command to gsr-global-hotkeys, error: %s\n", strerror(errno)); + return false; + } + + bound_actions_by_id[id] = std::move(callback); + return true; + } + + void GlobalHotkeysLinux::unbind_all_keys() { + if(process_id <= 0) + return; + + if(bound_actions_by_id.empty()) + return; + + char command[32]; + const int command_size = snprintf(command, sizeof(command), "unbind_all\n"); + if(write(write_pipes[PIPE_WRITE], command, command_size) != command_size) { + fprintf(stderr, "Error: GlobalHotkeysLinux::unbind_all_keys: failed to write command to gsr-global-hotkeys, error: %s\n", strerror(errno)); + } + bound_actions_by_id.clear(); + } + + void GlobalHotkeysLinux::poll_events() { + if(process_id <= 0) { + //fprintf(stderr, "error: GlobalHotkeysLinux::poll_events failed, process has not been started yet. Use GlobalHotkeysLinux::start to start the process first\n"); + return; + } + + if(!read_file) { + //fprintf(stderr, "error: GlobalHotkeysLinux::poll_events failed, read file hasn't opened\n"); + return; + } + + std::string action; + char buffer[256]; + while(true) { + char *line = fgets(buffer, sizeof(buffer), read_file); + if(!line) + break; + + int line_len = strlen(line); + if(line_len == 0) + continue; + + if(line[line_len - 1] == '\n') { + line[line_len - 1] = '\0'; + --line_len; + } + + action = line; + auto it = bound_actions_by_id.find(action); + if(it != bound_actions_by_id.end()) + it->second(action); + } + } +} diff --git a/src/GlobalHotkeysX11.cpp b/src/GlobalHotkeys/GlobalHotkeysX11.cpp index 2943397..bc79ce8 100644 --- a/src/GlobalHotkeysX11.cpp +++ b/src/GlobalHotkeys/GlobalHotkeysX11.cpp @@ -1,4 +1,4 @@ -#include "../include/GlobalHotkeysX11.hpp" +#include "../../include/GlobalHotkeys/GlobalHotkeysX11.hpp" #include <X11/keysym.h> #include <mglpp/window/Event.hpp> #include <assert.h> @@ -50,6 +50,27 @@ namespace gsr { return mask; } + static uint32_t modifiers_to_x11_modifiers(uint32_t modifiers) { + uint32_t result = 0; + if(modifiers & HOTKEY_MOD_LSHIFT) + result |= ShiftMask; + if(modifiers & HOTKEY_MOD_RSHIFT) + result |= ShiftMask; + if(modifiers & HOTKEY_MOD_LCTRL) + result |= ControlMask; + if(modifiers & HOTKEY_MOD_RCTRL) + result |= ControlMask; + if(modifiers & HOTKEY_MOD_LALT) + result |= Mod1Mask; + if(modifiers & HOTKEY_MOD_RALT) + result |= Mod5Mask; + if(modifiers & HOTKEY_MOD_LSUPER) + result |= Mod4Mask; + if(modifiers & HOTKEY_MOD_RSUPER) + result |= Mod4Mask; + return result; + } + GlobalHotkeysX11::GlobalHotkeysX11() { dpy = XOpenDisplay(NULL); if(!dpy) @@ -74,16 +95,17 @@ namespace gsr { x_failed = false; XErrorHandler prev_xerror = XSetErrorHandler(xerror_grab_error); + const uint32_t modifiers_x11 = modifiers_to_x11_modifiers(hotkey.modifiers); unsigned int numlock_mask = x11_get_numlock_mask(dpy); unsigned int modifiers[] = { 0, LockMask, numlock_mask, numlock_mask|LockMask }; for(int i = 0; i < 4; ++i) { - XGrabKey(dpy, XKeysymToKeycode(dpy, hotkey.key), hotkey.modifiers | modifiers[i], DefaultRootWindow(dpy), False, GrabModeAsync, GrabModeAsync); + XGrabKey(dpy, XKeysymToKeycode(dpy, hotkey.key), modifiers_x11 | modifiers[i], DefaultRootWindow(dpy), False, GrabModeAsync, GrabModeAsync); } XSync(dpy, False); if(x_failed) { for(int i = 0; i < 4; ++i) { - XUngrabKey(dpy, XKeysymToKeycode(dpy, hotkey.key), hotkey.modifiers | modifiers[i], DefaultRootWindow(dpy)); + XUngrabKey(dpy, XKeysymToKeycode(dpy, hotkey.key), modifiers_x11 | modifiers[i], DefaultRootWindow(dpy)); } XSync(dpy, False); XSetErrorHandler(prev_xerror); @@ -106,10 +128,11 @@ namespace gsr { x_failed = false; XErrorHandler prev_xerror = XSetErrorHandler(xerror_grab_error); + const uint32_t modifiers_x11 = modifiers_to_x11_modifiers(it->second.hotkey.modifiers); unsigned int numlock_mask = x11_get_numlock_mask(dpy); unsigned int modifiers[] = { 0, LockMask, numlock_mask, numlock_mask|LockMask }; for(int i = 0; i < 4; ++i) { - XUngrabKey(dpy, XKeysymToKeycode(dpy, it->second.hotkey.key), it->second.hotkey.modifiers | modifiers[i], DefaultRootWindow(dpy)); + XUngrabKey(dpy, XKeysymToKeycode(dpy, it->second.hotkey.key), modifiers_x11 | modifiers[i], DefaultRootWindow(dpy)); } XSync(dpy, False); @@ -127,8 +150,9 @@ namespace gsr { unsigned int numlock_mask = x11_get_numlock_mask(dpy); unsigned int modifiers[] = { 0, LockMask, numlock_mask, numlock_mask|LockMask }; for(auto it = bound_keys_by_id.begin(); it != bound_keys_by_id.end();) { + const uint32_t modifiers_x11 = modifiers_to_x11_modifiers(it->second.hotkey.modifiers); for(int i = 0; i < 4; ++i) { - XUngrabKey(dpy, XKeysymToKeycode(dpy, it->second.hotkey.key), it->second.hotkey.modifiers | modifiers[i], DefaultRootWindow(dpy)); + XUngrabKey(dpy, XKeysymToKeycode(dpy, it->second.hotkey.key), modifiers_x11 | modifiers[i], DefaultRootWindow(dpy)); } } bound_keys_by_id.clear(); @@ -138,11 +162,14 @@ namespace gsr { } void GlobalHotkeysX11::poll_events() { + if(!dpy) + return; + while(XPending(dpy)) { XNextEvent(dpy, &xev); if(xev.type == KeyPress) { const KeySym key_sym = XLookupKeysym(&xev.xkey, 0); - call_hotkey_callback({ key_sym, xev.xkey.state }); + call_hotkey_callback({ (uint32_t)key_sym, xev.xkey.state }); } } } @@ -154,7 +181,7 @@ namespace gsr { // Note: not all keys are mapped in mgl_key_to_key_sym. If more hotkeys are added or changed then add the key mapping there const KeySym key_sym = mgl_key_to_key_sym(event.key.code); const uint32_t modifiers = mgl_key_modifiers_to_x11_modifier_mask(event.key); - return !call_hotkey_callback(Hotkey{key_sym, modifiers}); + return !call_hotkey_callback(Hotkey{(uint32_t)key_sym, modifiers}); } static unsigned int key_state_without_locks(unsigned int key_state) { @@ -162,8 +189,9 @@ namespace gsr { } bool GlobalHotkeysX11::call_hotkey_callback(Hotkey hotkey) const { + const uint32_t modifiers_x11 = modifiers_to_x11_modifiers(hotkey.modifiers); for(const auto &[key, val] : bound_keys_by_id) { - if(val.hotkey.key == hotkey.key && key_state_without_locks(val.hotkey.modifiers) == key_state_without_locks(hotkey.modifiers)) { + if(val.hotkey.key == hotkey.key && key_state_without_locks(modifiers_to_x11_modifiers(val.hotkey.modifiers)) == key_state_without_locks(modifiers_x11)) { val.callback(key); return true; } diff --git a/src/GlobalHotkeysLinux.cpp b/src/GlobalHotkeysLinux.cpp deleted file mode 100644 index b0e8e52..0000000 --- a/src/GlobalHotkeysLinux.cpp +++ /dev/null @@ -1,110 +0,0 @@ -#include "../include/GlobalHotkeysLinux.hpp" -#include <signal.h> -#include <sys/wait.h> -#include <fcntl.h> -#include <string.h> - -#define PIPE_READ 0 -#define PIPE_WRITE 1 - -namespace gsr { - GlobalHotkeysLinux::GlobalHotkeysLinux() { - for(int i = 0; i < 2; ++i) { - pipes[i] = -1; - } - } - - GlobalHotkeysLinux::~GlobalHotkeysLinux() { - for(int i = 0; i < 2; ++i) { - if(pipes[i] > 0) - close(pipes[i]); - } - - if(read_file) - fclose(read_file); - - if(process_id > 0) { - kill(process_id, SIGKILL); - int status; - waitpid(process_id, &status, 0); - } - } - - bool GlobalHotkeysLinux::start() { - if(process_id > 0) - return false; - - if(pipe(pipes) == -1) - return false; - - const pid_t pid = vfork(); - if(pid == -1) { - perror("Failed to vfork"); - for(int i = 0; i < 2; ++i) { - close(pipes[i]); - pipes[i] = -1; - } - return false; - } else if(pid == 0) { /* child */ - dup2(pipes[PIPE_WRITE], STDOUT_FILENO); - for(int i = 0; i < 2; ++i) { - close(pipes[i]); - } - - const char *args[] = { "gsr-global-hotkeys", NULL }; - execvp(args[0], (char* const*)args); - perror("execvp"); - _exit(127); - } else { /* parent */ - process_id = pid; - close(pipes[PIPE_WRITE]); - pipes[PIPE_WRITE] = -1; - - const int fdl = fcntl(pipes[PIPE_READ], F_GETFL); - fcntl(pipes[PIPE_READ], F_SETFL, fdl | O_NONBLOCK); - - read_file = fdopen(pipes[PIPE_READ], "r"); - if(read_file) - pipes[PIPE_READ] = -1; - else - fprintf(stderr, "fdopen failed, error: %s\n", strerror(errno)); - } - - return true; - } - - bool GlobalHotkeysLinux::bind_action(const std::string &id, GlobalHotkeyCallback callback) { - return bound_actions_by_id.insert(std::make_pair(id, callback)).second; - } - - void GlobalHotkeysLinux::poll_events() { - if(process_id <= 0) { - fprintf(stderr, "error: GlobalHotkeysLinux::poll_events failed, process has not been started yet. Use GlobalHotkeysLinux::start to start the process first\n"); - return; - } - - if(!read_file) { - fprintf(stderr, "error: GlobalHotkeysLinux::poll_events failed, read file hasn't opened\n"); - return; - } - - char buffer[256]; - while(true) { - char *line = fgets(buffer, sizeof(buffer), read_file); - if(!line) - break; - - const int line_len = strlen(line); - if(line_len == 0) - continue; - - if(line[line_len - 1] == '\n') - line[line_len - 1] = '\0'; - - const std::string action = line; - auto it = bound_actions_by_id.find(action); - if(it != bound_actions_by_id.end()) - it->second(action); - } - } -} diff --git a/src/GsrInfo.cpp b/src/GsrInfo.cpp index c35ccfb..d7212d7 100644 --- a/src/GsrInfo.cpp +++ b/src/GsrInfo.cpp @@ -6,6 +6,93 @@ #include <string.h> namespace gsr { + bool GsrVersion::operator>(const GsrVersion &other) const { + return major > other.major || (major == other.major && minor > other.minor) || (major == other.major && minor == other.minor && patch > other.patch); + } + + bool GsrVersion::operator>=(const GsrVersion &other) const { + return major > other.major || (major == other.major && minor > other.minor) || (major == other.major && minor == other.minor && patch >= other.patch); + } + + bool GsrVersion::operator<(const GsrVersion &other) const { + return !operator>=(other); + } + + bool GsrVersion::operator<=(const GsrVersion &other) const { + return !operator>(other); + } + + bool GsrVersion::operator==(const GsrVersion &other) const { + return major == other.major && minor == other.minor && patch == other.patch; + } + + bool GsrVersion::operator!=(const GsrVersion &other) const { + return !operator==(other); + } + + std::string GsrVersion::to_string() const { + std::string result; + if(major == 0 && minor == 0 && patch == 0) + result = "Unknown"; + else + result = std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(patch); + return result; + } + + /* Returns -1 on error */ + static int parse_u8(const char *str, int size) { + if(size <= 0) + return -1; + + int result = 0; + for(int i = 0; i < size; ++i) { + char c = str[i]; + if(c >= '0' && c <= '9') { + result = result * 10 + (c - '0'); + if(result > 255) + return -1; + } else { + return -1; + } + } + return result; + } + + static GsrVersion parse_gsr_version(const std::string_view str) { + GsrVersion result; + uint8_t numbers[3]; + int number_index = 0; + + size_t index = 0; + while(true) { + size_t next_index = str.find('.', index); + if(next_index == std::string::npos) + next_index = str.size(); + + const int number = parse_u8(str.data() + index, next_index - index); + if(number == -1) { + fprintf(stderr, "Error: gpu-screen-recorder --info contains invalid gsr version: %.*s\n", (int)str.size(), str.data()); + return {0, 0, 0}; + } + + if(number_index >= 3) { + fprintf(stderr, "Error: gpu-screen-recorder --info contains invalid gsr version: %.*s\n", (int)str.size(), str.data()); + return {0, 0, 0}; + } + + numbers[number_index] = number; + ++number_index; + index = next_index + 1; + if(next_index == str.size()) + break; + } + + result.major = numbers[0]; + result.minor = numbers[1]; + result.patch = numbers[2]; + return result; + } + static std::optional<KeyValue> parse_key_value(std::string_view line) { const size_t space_index = line.find('|'); if(space_index == std::string_view::npos) @@ -25,6 +112,8 @@ namespace gsr { gsr_info->system_info.display_server = DisplayServer::WAYLAND; } else if(key_value->key == "supports_app_audio") { gsr_info->system_info.supports_app_audio = key_value->value == "yes"; + } else if(key_value->key == "gsr_version") { + gsr_info->system_info.gsr_version = parse_gsr_version(key_value->value); } } @@ -40,6 +129,8 @@ namespace gsr { gsr_info->gpu_info.vendor = GpuVendor::INTEL; else if(key_value->value == "nvidia") gsr_info->gpu_info.vendor = GpuVendor::NVIDIA; + else if(key_value->value == "broadcom") + gsr_info->gpu_info.vendor = GpuVendor::BROADCOM; } else if(key_value->key == "card_path") { gsr_info->gpu_info.card_path = key_value->value; } @@ -68,19 +159,22 @@ namespace gsr { gsr_info->supported_video_codecs.vp9 = true; } + static void parse_image_formats_line(GsrInfo *gsr_info, std::string_view line) { + if(line == "jpeg") + gsr_info->supported_image_formats.jpeg = true; + else if(line == "png") + gsr_info->supported_image_formats.png = true; + } + enum class GsrInfoSection { UNKNOWN, SYSTEM_INFO, GPU_INFO, VIDEO_CODECS, + IMAGE_FORMATS, CAPTURE_OPTIONS }; - static bool starts_with(std::string_view str, const char *substr) { - size_t len = strlen(substr); - return str.size() >= len && memcmp(str.data(), substr, len) == 0; - } - GsrInfoExitStatus get_gpu_screen_recorder_info(GsrInfo *gsr_info) { *gsr_info = GsrInfo{}; @@ -105,6 +199,8 @@ namespace gsr { section = GsrInfoSection::GPU_INFO; else if(section_name == "video_codecs") section = GsrInfoSection::VIDEO_CODECS; + else if(section_name == "image_formats") + section = GsrInfoSection::IMAGE_FORMATS; else if(section_name == "capture_options") section = GsrInfoSection::CAPTURE_OPTIONS; else @@ -128,6 +224,10 @@ namespace gsr { parse_video_codecs_line(gsr_info, line); break; } + case GsrInfoSection::IMAGE_FORMATS: { + parse_image_formats_line(gsr_info, line); + break; + } case GsrInfoSection::CAPTURE_OPTIONS: { // Intentionally ignore, get capture options with get_supported_capture_options instead break; @@ -155,7 +255,7 @@ namespace gsr { std::string stdout_str; const char *args[] = { "gpu-screen-recorder", "--list-audio-devices", nullptr }; - if(exec_program_get_stdout(args, stdout_str) != 0) { + if(exec_program_get_stdout(args, stdout_str, false) != 0) { fprintf(stderr, "error: 'gpu-screen-recorder --list-audio-devices' failed\n"); return audio_devices; } @@ -207,10 +307,10 @@ namespace gsr { static void parse_capture_options_line(SupportedCaptureOptions &capture_options, std::string_view line) { if(line == "window") capture_options.window = true; + else if(line == "region") + capture_options.region = true; else if(line == "focused") capture_options.focused = true; - else if(line == "screen") - capture_options.screen = true; else if(line == "portal") capture_options.portal = true; else { @@ -222,10 +322,11 @@ namespace gsr { static const char* gpu_vendor_to_string(GpuVendor vendor) { switch(vendor) { - case GpuVendor::UNKNOWN: return "unknown"; - case GpuVendor::AMD: return "amd"; - case GpuVendor::INTEL: return "intel"; - case GpuVendor::NVIDIA: return "nvidia"; + case GpuVendor::UNKNOWN: return "unknown"; + case GpuVendor::AMD: return "amd"; + case GpuVendor::INTEL: return "intel"; + case GpuVendor::NVIDIA: return "nvidia"; + case GpuVendor::BROADCOM: return "broadcom"; } return "unknown"; } diff --git a/src/Hotplug.cpp b/src/Hotplug.cpp new file mode 100644 index 0000000..0f5155c --- /dev/null +++ b/src/Hotplug.cpp @@ -0,0 +1,82 @@ +#include "../include/Hotplug.hpp" + +#include <string.h> +#include <unistd.h> +#include <sys/socket.h> +#include <linux/types.h> +#include <linux/netlink.h> + +namespace gsr { + Hotplug::~Hotplug() { + if(fd > 0) + close(fd); + } + + bool Hotplug::start() { + if(started) + return false; + + struct sockaddr_nl nls = { + AF_NETLINK, + 0, + (unsigned int)getpid(), + (unsigned int)-1 + }; + + fd = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT); + if(fd == -1) + return false; /* Not root user */ + + if(bind(fd, (const struct sockaddr*)&nls, sizeof(struct sockaddr_nl))) { + close(fd); + fd = -1; + return false; + } + + started = true; + return true; + } + + int Hotplug::steal_fd() { + const int val = fd; + fd = -1; + return val; + } + + void Hotplug::process_event_data(int fd, const HotplugEventCallback &callback) { + const int bytes_read = read(fd, event_data, sizeof(event_data) - 1); + if(bytes_read <= 0) + return; + event_data[bytes_read] = '\0'; + + /* Hotplug data ends with a newline and a null terminator */ + int data_index = 0; + while(data_index < bytes_read) { + parse_netlink_data(event_data + data_index, callback); + data_index += strlen(event_data + data_index) + 1; /* Skip null terminator as well */ + } + } + + /* TODO: This assumes SUBSYSTEM= is output before DEVNAME=, is that always true? */ + void Hotplug::parse_netlink_data(const char *line, const HotplugEventCallback &callback) { + const char *at_symbol = strchr(line, '@'); + if(at_symbol) { + event_is_add = strncmp(line, "add@", 4) == 0; + event_is_remove = strncmp(line, "remove@", 7) == 0; + subsystem_is_input = false; + } else if(event_is_add || event_is_remove) { + if(strcmp(line, "SUBSYSTEM=input") == 0) + subsystem_is_input = true; + + if(subsystem_is_input && strncmp(line, "DEVNAME=", 8) == 0) { + if(event_is_add) + callback(HotplugAction::ADD, line+8); + else if(event_is_remove) + callback(HotplugAction::REMOVE, line+8); + + event_is_add = false; + event_is_remove = false; + } + } + } +} diff --git a/src/Overlay.cpp b/src/Overlay.cpp index d306910..794ef92 100644 --- a/src/Overlay.cpp +++ b/src/Overlay.cpp @@ -7,19 +7,26 @@ #include "../include/gui/DropdownButton.hpp" #include "../include/gui/CustomRendererWidget.hpp" #include "../include/gui/SettingsPage.hpp" +#include "../include/gui/ScreenshotSettingsPage.hpp" #include "../include/gui/GlobalSettingsPage.hpp" #include "../include/gui/Utils.hpp" #include "../include/gui/PageStack.hpp" -#include "../include/gui/GsrPage.hpp" #include "../include/WindowUtils.hpp" -#include "../include/GlobalHotkeys.hpp" +#include "../include/GlobalHotkeys/GlobalHotkeys.hpp" +#include "../include/GlobalHotkeys/GlobalHotkeysLinux.hpp" +#include "../include/CursorTracker/CursorTrackerX11.hpp" +#include "../include/CursorTracker/CursorTrackerWayland.hpp" #include <string.h> #include <assert.h> #include <sys/wait.h> #include <limits.h> #include <fcntl.h> +#include <poll.h> +#include <malloc.h> #include <stdexcept> +#include <algorithm> +#include <inttypes.h> #include <X11/Xlib.h> #include <X11/Xutil.h> @@ -27,9 +34,11 @@ #include <X11/cursorfont.h> #include <X11/extensions/Xfixes.h> #include <X11/extensions/XInput2.h> -#include <X11/extensions/shape.h> +#include <X11/extensions/shapeconst.h> +#include <X11/Xcursor/Xcursor.h> #include <mglpp/system/Rect.hpp> #include <mglpp/window/Event.hpp> +#include <mglpp/system/Utf8.hpp> extern "C" { #include <mgl/mgl.h> @@ -38,11 +47,11 @@ extern "C" { namespace gsr { static const mgl::Color bg_color(0, 0, 0, 100); static const double force_window_on_top_timeout_seconds = 1.0; - static const double replay_status_update_check_timeout_seconds = 1.0; - - static bool is_focused_application_wayland(Display *dpy) { - return get_focused_window(dpy, WindowCaptureType::FOCUSED) == 0; - } + static const double replay_status_update_check_timeout_seconds = 1.5; + static const double replay_saving_notification_timeout_seconds = 0.5; + static const double notification_timeout_seconds = 2.5; + static const double notification_error_timeout_seconds = 5.0; + static const double cursor_tracker_update_timeout_sec = 0.1; static mgl::Texture texture_from_ximage(XImage *img) { uint8_t *texture_data = (uint8_t*)malloc(img->width * img->height * 3); @@ -69,17 +78,17 @@ namespace gsr { return texture; } - static bool texture_from_x11_cursor(XFixesCursorImage *x11_cursor_image, bool *visible, mgl::vec2i *hotspot, mgl::Texture &texture) { + static bool texture_from_x11_cursor(XcursorImage *x11_cursor_image, bool *visible, mgl::vec2i *hotspot, mgl::Texture &texture) { uint8_t *cursor_data = NULL; uint8_t *out = NULL; - const unsigned long *pixels = NULL; + const unsigned int *pixels = NULL; *visible = false; if(!x11_cursor_image) - goto err; + return false; if(!x11_cursor_image->pixels) - goto err; + return false; hotspot->x = x11_cursor_image->xhot; hotspot->y = x11_cursor_image->yhot; @@ -87,12 +96,12 @@ namespace gsr { pixels = x11_cursor_image->pixels; cursor_data = (uint8_t*)malloc((int)x11_cursor_image->width * (int)x11_cursor_image->height * 4); if(!cursor_data) - goto err; + return false; out = cursor_data; /* Un-premultiply alpha */ - for(int y = 0; y < x11_cursor_image->height; ++y) { - for(int x = 0; x < x11_cursor_image->width; ++x) { + for(uint32_t y = 0; y < x11_cursor_image->height; ++y) { + for(uint32_t x = 0; x < x11_cursor_image->width; ++x) { uint32_t pixel = *pixels++; uint8_t *in = (uint8_t*)&pixel; uint8_t alpha = in[3]; @@ -113,13 +122,7 @@ namespace gsr { texture.load_from_memory(cursor_data, x11_cursor_image->width, x11_cursor_image->height, MGL_IMAGE_FORMAT_RGBA); free(cursor_data); - XFree(x11_cursor_image); return true; - - err: - if(x11_cursor_image) - XFree(x11_cursor_image); - return false; } static char hex_value_to_str(uint8_t v) { @@ -172,7 +175,7 @@ namespace gsr { return std::abs(a - b) <= difference; } - static bool is_window_fullscreen_on_monitor(Display *display, Window window, const mgl_monitor *monitor) { + static bool is_window_fullscreen_on_monitor(Display *display, Window window, const Monitor &monitor) { if(!window) return false; @@ -181,8 +184,8 @@ namespace gsr { return false; const int margin = 2; - return diff_int(geometry.x, monitor->pos.x, margin) && diff_int(geometry.y, monitor->pos.y, margin) - && diff_int(geometry.width, monitor->size.x, margin) && diff_int(geometry.height, monitor->size.y, margin); + return diff_int(geometry.x, monitor.position.x, margin) && diff_int(geometry.y, monitor.position.y, margin) + && diff_int(geometry.width, monitor.size.x, margin) && diff_int(geometry.height, monitor.size.y, margin); } /*static bool is_window_fullscreen_on_monitor(Display *display, Window window, const mgl_monitor *monitors, int num_monitors) { @@ -206,146 +209,20 @@ namespace gsr { return false; }*/ - static bool window_is_fullscreen(Display *display, Window window) { - const Atom wm_state_atom = XInternAtom(display, "_NET_WM_STATE", False); - const Atom wm_state_fullscreen_atom = XInternAtom(display, "_NET_WM_STATE_FULLSCREEN", False); - - Atom type = None; - int format = 0; - unsigned long num_items = 0; - unsigned long bytes_after = 0; - unsigned char *properties = nullptr; - if(XGetWindowProperty(display, window, wm_state_atom, 0, 1024, False, XA_ATOM, &type, &format, &num_items, &bytes_after, &properties) < Success) { - fprintf(stderr, "Failed to get window wm state property\n"); - return false; + static const Monitor* find_monitor_at_position(const std::vector<Monitor> &monitors, mgl::vec2i pos) { + for(const Monitor &monitor : monitors) { + if(mgl::IntRect(monitor.position, monitor.size).contains(pos)) + return &monitor; } - - if(!properties) - return false; - - bool is_fullscreen = false; - Atom *atoms = (Atom*)properties; - for(unsigned long i = 0; i < num_items; ++i) { - if(atoms[i] == wm_state_fullscreen_atom) { - is_fullscreen = true; - break; - } - } - - XFree(properties); - return is_fullscreen; - } - - static void set_focused_window(Display *dpy, Window window) { - XSetInputFocus(dpy, window, RevertToPointerRoot, CurrentTime); - - const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False); - XChangeProperty(dpy, DefaultRootWindow(dpy), net_active_window_atom, XA_WINDOW, 32, PropModeReplace, (const unsigned char*)&window, 1); - - XFlush(dpy); - } - - #define _NET_WM_STATE_REMOVE 0 - #define _NET_WM_STATE_ADD 1 - #define _NET_WM_STATE_TOGGLE 2 - - static Bool set_window_wm_state(Display *dpy, Window window, Atom atom) { - const Atom net_wm_state_atom = XInternAtom(dpy, "_NET_WM_STATE", False); - - XClientMessageEvent xclient; - memset(&xclient, 0, sizeof(xclient)); - - xclient.type = ClientMessage; - xclient.window = window; - xclient.message_type = net_wm_state_atom; - xclient.format = 32; - xclient.data.l[0] = _NET_WM_STATE_ADD; - xclient.data.l[1] = atom; - xclient.data.l[2] = 0; - xclient.data.l[3] = 0; - xclient.data.l[4] = 0; - - XSendEvent(dpy, DefaultRootWindow(dpy), False, SubstructureRedirectMask | SubstructureNotifyMask, (XEvent*)&xclient); - XFlush(dpy); - return True; - } - - static void make_window_click_through(Display *display, Window window) { - XRectangle rect; - memset(&rect, 0, sizeof(rect)); - XserverRegion region = XFixesCreateRegion(display, &rect, 1); - XFixesSetWindowShapeRegion(display, window, ShapeInput, 0, 0, region); - XFixesDestroyRegion(display, region); - } - - static Bool make_window_sticky(Display *dpy, Window window) { - return set_window_wm_state(dpy, window, XInternAtom(dpy, "_NET_WM_STATE_STICKY", False)); - } - - static Bool hide_window_from_taskbar(Display *dpy, Window window) { - return set_window_wm_state(dpy, window, XInternAtom(dpy, "_NET_WM_STATE_SKIP_TASKBAR", False)); - } - - // Returns the first monitor if not found. Assumes there is at least one monitor connected. - static const mgl_monitor* find_monitor_at_position(mgl::Window &window, mgl::vec2i pos) { - const mgl_window *win = window.internal_window(); - assert(win->num_monitors > 0); - for(int i = 0; i < win->num_monitors; ++i) { - const mgl_monitor *mon = &win->monitors[i]; - if(mgl::IntRect({ mon->pos.x, mon->pos.y }, { mon->size.x, mon->size.y }).contains(pos)) - return mon; - } - return &win->monitors[0]; + return nullptr; } - static mgl::vec2i create_window_get_center_position(Display *display) { - XSetWindowAttributes window_attr; - window_attr.event_mask = StructureNotifyMask; - window_attr.background_pixel = 0; - const Window window = XCreateWindow(display, DefaultRootWindow(display), 0, 0, 16, 16, 0, CopyFromParent, InputOutput, CopyFromParent, CWBackPixel | CWEventMask, &window_attr); - if(!window) - return {0, 0}; - - const Atom net_wm_window_type_atom = XInternAtom(display, "_NET_WM_WINDOW_TYPE", False); - const Atom net_wm_window_type_notification_atom = XInternAtom(display, "_NET_WM_WINDOW_TYPE_NOTIFICATION", False); - const Atom net_wm_window_type_utility = XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", False); - const Atom net_wm_window_opacity = XInternAtom(display, "_NET_WM_WINDOW_OPACITY", False); - - const Atom window_type_atoms[2] = { - net_wm_window_type_notification_atom, - net_wm_window_type_utility - }; - XChangeProperty(display, window, net_wm_window_type_atom, XA_ATOM, 32, PropModeReplace, (const unsigned char*)window_type_atoms, 2L); - - const double alpha = 0.0; - const unsigned long opacity = (unsigned long)(0xFFFFFFFFul * alpha); - XChangeProperty(display, window, net_wm_window_opacity, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&opacity, 1L); - - XMapWindow(display, window); - XFlush(display); - - mgl::vec2i window_pos; - XEvent xev; - while(true) { - XNextEvent(display, &xev); - if(xev.type == ConfigureNotify) { - window_pos.x = xev.xconfigure.x + xev.xconfigure.width / 2; - window_pos.y = xev.xconfigure.y + xev.xconfigure.height / 2; - break; - } + static const Monitor* find_monitor_by_name(const std::vector<Monitor> &monitors, const std::string &name) { + for(const Monitor &monitor : monitors) { + if(monitor.name == name) + return &monitor; } - - XDestroyWindow(display, window); - XFlush(display); - - return window_pos; - } - - static bool is_compositor_running(Display *dpy, int screen) { - char prop_name[20]; - snprintf(prop_name, sizeof(prop_name), "_NET_WM_CM_S%d", screen); - Atom prop_atom = XInternAtom(dpy, prop_name, False); - return XGetSelectionOwner(dpy, prop_atom) != None; + return nullptr; } static std::string get_power_supply_online_filepath() { @@ -396,6 +273,171 @@ namespace gsr { return true; } + static bool is_hyprland_waybar_running_as_dock() { + const char *args[] = { "hyprctl", "layers", nullptr }; + std::string stdout_str; + if(exec_program_on_host_get_stdout(args, stdout_str) != 0) + return false; + + int waybar_layer_level = -1; + int current_layer_level = 0; + string_split_char(stdout_str, '\n', [&](const std::string_view line) { + if(line.find("Layer level 0") != std::string_view::npos) + current_layer_level = 0; + else if(line.find("Layer level 1") != std::string_view::npos) + current_layer_level = 1; + else if(line.find("Layer level 2") != std::string_view::npos) + current_layer_level = 2; + else if(line.find("Layer level 3") != std::string_view::npos) + current_layer_level = 3; + else if(line.find("namespace: waybar") != std::string_view::npos) { + waybar_layer_level = current_layer_level; + return false; + } + return true; + }); + + return waybar_layer_level >= 0 && waybar_layer_level <= 1; + } + + static Hotkey config_hotkey_to_hotkey(ConfigHotkey config_hotkey) { + return { + (uint32_t)mgl::Keyboard::key_to_x11_keysym((mgl::Keyboard::Key)config_hotkey.key), + config_hotkey.modifiers + }; + } + + static void bind_linux_hotkeys(GlobalHotkeysLinux *global_hotkeys, Overlay *overlay) { + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().main_config.show_hide_hotkey), + "toggle_show", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_show(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().record_config.start_stop_hotkey), + "record", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_record(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().record_config.pause_unpause_hotkey), + "pause", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_pause(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().streaming_config.start_stop_hotkey), + "stream", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_stream(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().replay_config.start_stop_hotkey), + "replay_start", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_replay(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().replay_config.save_hotkey), + "replay_save", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->save_replay(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().replay_config.save_1_min_hotkey), + "replay_save_1_min", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->save_replay_1_min(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().replay_config.save_10_min_hotkey), + "replay_save_10_min", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->save_replay_10_min(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().screenshot_config.take_screenshot_hotkey), + "take_screenshot", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->take_screenshot(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().screenshot_config.take_screenshot_region_hotkey), + "take_screenshot_region", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->take_screenshot_region(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(ConfigHotkey{ mgl::Keyboard::Key::Escape, HOTKEY_MOD_LCTRL | HOTKEY_MOD_LSHIFT | HOTKEY_MOD_LALT }), + "exit", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->go_back_to_old_ui(); + }); + } + + static std::unique_ptr<GlobalHotkeysLinux> register_linux_hotkeys(Overlay *overlay, GlobalHotkeysLinux::GrabType grab_type) { + auto global_hotkeys = std::make_unique<GlobalHotkeysLinux>(grab_type); + if(!global_hotkeys->start()) + fprintf(stderr, "error: failed to start global hotkeys\n"); + + bind_linux_hotkeys(global_hotkeys.get(), overlay); + return global_hotkeys; + } + + static std::unique_ptr<GlobalHotkeysJoystick> register_joystick_hotkeys(Overlay *overlay) { + auto global_hotkeys_js = std::make_unique<GlobalHotkeysJoystick>(); + if(!global_hotkeys_js->start()) + fprintf(stderr, "Warning: failed to start joystick hotkeys\n"); + + global_hotkeys_js->bind_action("toggle_show", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_show(); + }); + + global_hotkeys_js->bind_action("save_replay", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->save_replay(); + }); + + global_hotkeys_js->bind_action("save_1_min_replay", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->save_replay_1_min(); + }); + + global_hotkeys_js->bind_action("save_10_min_replay", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->save_replay_10_min(); + }); + + global_hotkeys_js->bind_action("take_screenshot", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->take_screenshot(); + }); + + global_hotkeys_js->bind_action("toggle_record", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_record(); + }); + + global_hotkeys_js->bind_action("toggle_replay", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_replay(); + }); + + return global_hotkeys_js; + } + Overlay::Overlay(std::string resources_path, GsrInfo gsr_info, SupportedCaptureOptions capture_options, egl_functions egl_funcs) : resources_path(std::move(resources_path)), gsr_info(std::move(gsr_info)), @@ -405,9 +447,6 @@ namespace gsr { top_bar_background({0.0f, 0.0f}), close_button_widget({0.0f, 0.0f}) { - // TODO: - //xi_setup(); - key_bindings[0].key_event.code = mgl::Keyboard::Escape; key_bindings[0].key_event.alt = false; key_bindings[0].key_event.control = false; @@ -426,9 +465,26 @@ namespace gsr { init_color_theme(config, this->gsr_info); power_supply_online_filepath = get_power_supply_online_filepath(); + replay_startup_mode = replay_startup_string_to_type(config.replay_config.turn_on_replay_automatically_mode.c_str()); + + if(config.main_config.hotkeys_enable_option == "enable_hotkeys") + global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::ALL); + else if(config.main_config.hotkeys_enable_option == "enable_hotkeys_virtual_devices") + global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::VIRTUAL); + + if(config.main_config.joystick_hotkeys_enable_option == "enable_hotkeys") + global_hotkeys_js = register_joystick_hotkeys(this); - if(config.replay_config.turn_on_replay_automatically_mode == "turn_on_at_system_startup") - on_press_start_replay(true); + x11_mapping_display = XOpenDisplay(nullptr); + if(x11_mapping_display) + XKeysymToKeycode(x11_mapping_display, XK_F1); // If we dont call we will never get a MappingNotify + else + fprintf(stderr, "Warning: XOpenDisplay failed to mapping notify\n"); + + if(this->gsr_info.system_info.display_server == DisplayServer::X11) + cursor_tracker = std::make_unique<CursorTrackerX11>((Display*)mgl_get_context()->connection); + else if(this->gsr_info.system_info.display_server == DisplayServer::WAYLAND && !this->gsr_info.gpu_info.card_path.empty()) + cursor_tracker = std::make_unique<CursorTrackerWayland>(this->gsr_info.gpu_info.card_path.c_str()); } Overlay::~Overlay() { @@ -444,8 +500,6 @@ namespace gsr { notification_process = -1; } - close_gpu_screen_recorder_output(); - if(gpu_screen_recorder_process > 0) { kill(gpu_screen_recorder_process, SIGINT); int status; @@ -456,10 +510,21 @@ namespace gsr { gpu_screen_recorder_process = -1; } - free(xi_input_xev); - free(xi_output_xev); - if(xi_display) - XCloseDisplay(xi_display); + if(gpu_screen_recorder_screenshot_process > 0) { + kill(gpu_screen_recorder_screenshot_process, SIGINT); + int status; + if(waitpid(gpu_screen_recorder_screenshot_process, &status, 0) == -1) { + perror("waitpid failed"); + /* Ignore... */ + } + gpu_screen_recorder_screenshot_process = -1; + } + + close_gpu_screen_recorder_output(); + deinit_color_theme(); + + if(x11_mapping_display) + XCloseDisplay(x11_mapping_display); } void Overlay::xi_setup() { @@ -483,6 +548,7 @@ namespace gsr { unsigned char mask[XIMaskLen(XI_LASTEVENT)]; memset(mask, 0, sizeof(mask)); XISetMask(mask, XI_Motion); + //XISetMask(mask, XI_RawMotion); XISetMask(mask, XI_ButtonPress); XISetMask(mask, XI_ButtonRelease); XISetMask(mask, XI_KeyPress); @@ -539,10 +605,10 @@ namespace gsr { memset(xi_output_xev, 0, sizeof(*xi_output_xev)); xi_output_xev->type = MotionNotify; xi_output_xev->xmotion.display = display; - xi_output_xev->xmotion.window = window->get_system_handle(); - xi_output_xev->xmotion.subwindow = window->get_system_handle(); - xi_output_xev->xmotion.x = de->event_x; - xi_output_xev->xmotion.y = de->event_y; + xi_output_xev->xmotion.window = (Window)window->get_system_handle(); + xi_output_xev->xmotion.subwindow = (Window)window->get_system_handle(); + xi_output_xev->xmotion.x = de->root_x - window_pos.x; + xi_output_xev->xmotion.y = de->root_y - window_pos.y; xi_output_xev->xmotion.x_root = de->root_x; xi_output_xev->xmotion.y_root = de->root_y; //xi_output_xev->xmotion.state = // modifiers // TODO: @@ -552,10 +618,10 @@ namespace gsr { memset(xi_output_xev, 0, sizeof(*xi_output_xev)); xi_output_xev->type = cookie->evtype == XI_ButtonPress ? ButtonPress : ButtonRelease; xi_output_xev->xbutton.display = display; - xi_output_xev->xbutton.window = window->get_system_handle(); - xi_output_xev->xbutton.subwindow = window->get_system_handle(); - xi_output_xev->xbutton.x = de->event_x; - xi_output_xev->xbutton.y = de->event_y; + xi_output_xev->xbutton.window = (Window)window->get_system_handle(); + xi_output_xev->xbutton.subwindow = (Window)window->get_system_handle(); + xi_output_xev->xbutton.x = de->root_x - window_pos.x; + xi_output_xev->xbutton.y = de->root_y - window_pos.y; xi_output_xev->xbutton.x_root = de->root_x; xi_output_xev->xbutton.y_root = de->root_y; //xi_output_xev->xbutton.state = // modifiers // TODO: @@ -566,10 +632,10 @@ namespace gsr { memset(xi_output_xev, 0, sizeof(*xi_output_xev)); xi_output_xev->type = cookie->evtype == XI_KeyPress ? KeyPress : KeyRelease; xi_output_xev->xkey.display = display; - xi_output_xev->xkey.window = window->get_system_handle(); - xi_output_xev->xkey.subwindow = window->get_system_handle(); - xi_output_xev->xkey.x = de->event_x; - xi_output_xev->xkey.y = de->event_y; + xi_output_xev->xkey.window = (Window)window->get_system_handle(); + xi_output_xev->xkey.subwindow = (Window)window->get_system_handle(); + xi_output_xev->xkey.x = de->root_x - window_pos.x; + xi_output_xev->xkey.y = de->root_y - window_pos.y; xi_output_xev->xkey.x_root = de->root_x; xi_output_xev->xkey.y_root = de->root_y; xi_output_xev->xkey.state = de->mods.effective; @@ -601,7 +667,61 @@ namespace gsr { } } - void Overlay::handle_events(gsr::GlobalHotkeys *global_hotkeys) { + void Overlay::handle_keyboard_mapping_event() { + if(!x11_mapping_display) + return; + + bool mapping_updated = false; + while(XPending(x11_mapping_display)) { + XNextEvent(x11_mapping_display, &x11_mapping_xev); + if(x11_mapping_xev.type == MappingNotify) { + XRefreshKeyboardMapping(&x11_mapping_xev.xmapping); + mapping_updated = true; + } + } + + if(mapping_updated) + rebind_all_keyboard_hotkeys(); + } + + void Overlay::handle_events() { + if(global_hotkeys) + global_hotkeys->poll_events(); + + if(global_hotkeys_js) + global_hotkeys_js->poll_events(); + + if(cursor_tracker_update_clock.get_elapsed_time_seconds() >= cursor_tracker_update_timeout_sec) { + cursor_tracker_update_clock.restart(); + if(cursor_tracker) + cursor_tracker->update(); + } + + handle_keyboard_mapping_event(); + region_selector.poll_events(); + if(region_selector.take_canceled()) { + on_region_selected = nullptr; + } else if(region_selector.take_selection() && on_region_selected) { + on_region_selected(); + on_region_selected = nullptr; + } + + window_selector.poll_events(); + if(window_selector.take_canceled()) { + on_window_selected = nullptr; + } else if(window_selector.take_selection() && on_window_selected) { + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + + const Window selected_window = window_selector.get_selection(); + if(selected_window && selected_window != DefaultRootWindow(display)) { + on_window_selected(); + } else { + show_notification("No window selected", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE); + } + on_window_selected = nullptr; + } + if(!visible || !window) return; @@ -630,11 +750,37 @@ namespace gsr { } bool Overlay::draw() { + remove_widgets_to_be_removed(); + update_notification_process_status(); - update_gsr_replay_save(); + process_gsr_output(); update_gsr_process_status(); + update_gsr_screenshot_process_status(); replay_status_update_status(); + if(start_region_capture) { + start_region_capture = false; + hide(); + if(!region_selector.start(get_color_theme().tint_color)) { + show_notification("Failed to start region capture", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE); + on_region_selected = nullptr; + } + } + + if(start_window_capture) { + start_window_capture = false; + hide(); + if(!window_selector.start(get_color_theme().tint_color)) { + show_notification("Failed to start window capture", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE); + on_window_selected = nullptr; + } + } + + if(region_selector.is_started() || window_selector.is_started()) { + usleep(5 * 1000); // 5 ms + return true; + } + if(!visible) return false; @@ -650,45 +796,43 @@ namespace gsr { //force_window_on_top(); - window->clear(bg_color); + const bool draw_ui = show_overlay_clock.get_elapsed_time_seconds() >= show_overlay_timeout_seconds; - if(window_texture_sprite.get_texture() && window_texture.texture_id) { - window->draw(window_texture_sprite); - window->draw(bg_screenshot_overlay); - } else if(screenshot_texture.is_valid()) { - window->draw(screenshot_sprite); - window->draw(bg_screenshot_overlay); - } + window->clear(draw_ui ? bg_color : mgl::Color(0, 0, 0, 0)); - window->draw(top_bar_background); - window->draw(top_bar_text); - window->draw(logo_sprite); + if(draw_ui) { + if(window_texture_sprite.get_texture() && window_texture.texture_id) { + window->draw(window_texture_sprite); + window->draw(bg_screenshot_overlay); + } else if(screenshot_texture.is_valid()) { + window->draw(screenshot_sprite); + window->draw(bg_screenshot_overlay); + } - close_button_widget.draw(*window, mgl::vec2f(0.0f, 0.0f)); - page_stack.draw(*window, mgl::vec2f(0.0f, 0.0f)); + window->draw(top_bar_background); + window->draw(top_bar_text); + window->draw(logo_sprite); - if(cursor_texture.is_valid()) { - if(!cursor_drawn) { - cursor_drawn = true; - XFixesHideCursor(xi_display, DefaultRootWindow(xi_display)); - XFlush(xi_display); + close_button_widget.draw(*window, mgl::vec2f(0.0f, 0.0f)); + page_stack.draw(*window, mgl::vec2f(0.0f, 0.0f)); + + if(cursor_texture.is_valid()) { + cursor_sprite.set_position((window->get_mouse_position() - cursor_hotspot).to_vec2f()); + window->draw(cursor_sprite); } - cursor_sprite.set_position((window->get_mouse_position() - cursor_hotspot).to_vec2f()); - window->draw(cursor_sprite); + if(!drawn_first_frame) { + drawn_first_frame = true; + mgl::Event event; + event.type = mgl::Event::MouseMoved; + event.mouse_move.x = window->get_mouse_position().x; + event.mouse_move.y = window->get_mouse_position().y; + on_event(event); + } } window->display(); - if(!drawn_first_frame) { - drawn_first_frame = true; - mgl::Event event; - event.type = mgl::Event::MouseMoved; - event.mouse_move.x = window->get_mouse_position().x; - event.mouse_move.y = window->get_mouse_position().y; - on_event(event); - } - return true; } @@ -697,13 +841,13 @@ namespace gsr { // There should be a debug mode to not use these mgl_context *context = mgl_get_context(); Display *display = (Display*)context->connection; - XGrabPointer(display, window->get_system_handle(), True, + XGrabPointer(display, (Window)window->get_system_handle(), True, ButtonPressMask | ButtonReleaseMask | PointerMotionMask | Button1MotionMask | Button2MotionMask | Button3MotionMask | Button4MotionMask | Button5MotionMask | ButtonMotionMask, GrabModeAsync, GrabModeAsync, None, default_cursor, CurrentTime); // TODO: This breaks global hotkeys (when using x11 global hotkeys) - XGrabKeyboard(display, window->get_system_handle(), True, GrabModeAsync, GrabModeAsync, CurrentTime); + XGrabKeyboard(display, (Window)window->get_system_handle(), True, GrabModeAsync, GrabModeAsync, CurrentTime); XFlush(display); } @@ -711,66 +855,64 @@ namespace gsr { if(!xi_display) return; - XFixesShowCursor(xi_display, DefaultRootWindow(xi_display)); + XFixesHideCursor(xi_display, DefaultRootWindow(xi_display)); XFlush(xi_display); - bool cursor_visible = false; - texture_from_x11_cursor(XFixesGetCursorImage(xi_display), &cursor_visible, &cursor_hotspot, cursor_texture); - if(cursor_texture.is_valid()) - cursor_sprite.set_texture(&cursor_texture); - } - - void Overlay::xi_grab_all_devices() { - if(!xi_display) - return; + // TODO: XCURSOR_SIZE and XCURSOR_THEME environment variables + const char *cursor_theme = XcursorGetTheme(xi_display); + if(!cursor_theme) { + //fprintf(stderr, "Warning: failed to get cursor theme, using \"default\" theme instead\n"); + cursor_theme = "default"; + } - int num_devices = 0; - XIDeviceInfo *info = XIQueryDevice(xi_display, XIAllDevices, &num_devices); - if(!info) - return; + int cursor_size = XcursorGetDefaultSize(xi_display); + if(cursor_size <= 1) + cursor_size = 24; - for (int i = 0; i < num_devices; ++i) { - const XIDeviceInfo *dev = &info[i]; - XIEventMask masks[1]; - unsigned char mask0[XIMaskLen(XI_LASTEVENT)]; - memset(mask0, 0, sizeof(mask0)); - XISetMask(mask0, XI_Motion); - XISetMask(mask0, XI_ButtonPress); - XISetMask(mask0, XI_ButtonRelease); - XISetMask(mask0, XI_KeyPress); - XISetMask(mask0, XI_KeyRelease); - masks[0].deviceid = dev->deviceid; - masks[0].mask_len = sizeof(mask0); - masks[0].mask = mask0; - XIGrabDevice(xi_display, dev->deviceid, window->get_system_handle(), CurrentTime, None, XIGrabModeAsync, XIGrabModeAsync, XIOwnerEvents, masks); + XcursorImage *cursor_image = nullptr; + for(int cursor_size_test : {cursor_size, 24}) { + for(const char *cursor_theme_test : {cursor_theme, "default", "Adwaita"}) { + for(unsigned int shape : {XC_left_ptr, XC_arrow}) { + cursor_image = XcursorShapeLoadImage(shape, cursor_theme_test, cursor_size_test); + if(cursor_image) + goto done; + } + } } - XFlush(xi_display); - XIFreeDeviceInfo(info); - } - - void Overlay::xi_warp_pointer(mgl::vec2i position) { - if(!xi_display) - return; + done: + if(!cursor_image) { + fprintf(stderr, "Error: failed to get cursor, loading bundled default cursor instead\n"); + const std::string default_cursor_path = resources_path + "images/default.cur"; + for(int cursor_size_test : {cursor_size, 24}) { + cursor_image = XcursorFilenameLoadImage(default_cursor_path.c_str(), cursor_size_test); + if(cursor_image) + break; + } + } - int num_devices = 0; - XIDeviceInfo *info = XIQueryDevice(xi_display, XIAllDevices, &num_devices); - if(!info) + if(!cursor_image) { + fprintf(stderr, "Error: failed to get cursor\n"); + XFixesShowCursor(xi_display, DefaultRootWindow(xi_display)); + XFlush(xi_display); return; - - for (int i = 0; i < num_devices; ++i) { - const XIDeviceInfo *dev = &info[i]; - XIWarpPointer(xi_display, dev->deviceid, DefaultRootWindow(xi_display), DefaultRootWindow(xi_display), 0, 0, 0, 0, position.x, position.y); } - XFlush(xi_display); - XIFreeDeviceInfo(info); + bool cursor_visible = false; + texture_from_x11_cursor(cursor_image, &cursor_visible, &cursor_hotspot, cursor_texture); + if(cursor_texture.is_valid()) + cursor_sprite.set_texture(&cursor_texture); + + XcursorImageDestroy(cursor_image); } void Overlay::show() { if(visible) return; + if(region_selector.is_started() || window_selector.is_started()) + return; + drawn_first_frame = false; window.reset(); window = std::make_unique<mgl::Window>(); @@ -779,62 +921,184 @@ namespace gsr { mgl_context *context = mgl_get_context(); Display *display = (Display*)context->connection; + const std::vector<Monitor> monitors = get_monitors(display); + if(monitors.empty()) { + fprintf(stderr, "gsr warning: no monitors found, not showing overlay\n"); + window.reset(); + return; + } + + const std::string wm_name = get_window_manager_name(display); + const bool is_kwin = wm_name == "KWin"; + const bool is_wlroots = wm_name.find("wlroots") != std::string::npos; + const bool is_hyprland = wm_name.find("Hyprland") != std::string::npos; + const bool hyprland_waybar_is_dock = is_hyprland && is_hyprland_waybar_running_as_dock(); + + std::optional<CursorInfo> cursor_info; + if(cursor_tracker) { + cursor_tracker->update(); + cursor_info = cursor_tracker->get_latest_cursor_info(); + } + + // The cursor position is wrong on wayland if an x11 window is not focused. On wayland we instead create a window and get the position where the wayland compositor puts it + Window x11_cursor_window = None; + mgl::vec2i cursor_position = get_cursor_position(display, &x11_cursor_window); + const Monitor *focused_monitor = nullptr; + if(cursor_info) { + focused_monitor = find_monitor_by_name(monitors, cursor_info->monitor_name); + if(!focused_monitor) + focused_monitor = &monitors.front(); + cursor_position = cursor_info->position; + } else { + const mgl::vec2i monitor_position_query_value = (x11_cursor_window || gsr_info.system_info.display_server != DisplayServer::WAYLAND) ? cursor_position : create_window_get_center_position(display); + focused_monitor = find_monitor_at_position(monitors, monitor_position_query_value); + if(!focused_monitor) + focused_monitor = &monitors.front(); + } + // Wayland doesn't allow XGrabPointer/XGrabKeyboard when a wayland application is focused. // If the focused window is a wayland application then don't use override redirect and instead create // a fullscreen window for the ui. - const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || !is_focused_application_wayland(display); + // TODO: (x11_cursor_window && is_window_fullscreen_on_monitor(display, x11_cursor_window, *focused_monitor)) + const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || x11_cursor_window || is_wlroots || is_hyprland; - mgl::vec2i window_size = { 1280, 720 }; - mgl::vec2i window_pos = { 0, 0 }; + if(prevent_game_minimizing) { + window_pos = focused_monitor->position; + window_size = focused_monitor->size; + } else { + window_pos = {0, 0}; + window_size = focused_monitor->size / 2; + } mgl::Window::CreateParams window_create_params; window_create_params.size = window_size; - window_create_params.min_size = window_size; - window_create_params.max_size = window_size; - window_create_params.position = window_pos; - window_create_params.hidden = true; + if(prevent_game_minimizing) { + window_create_params.min_size = window_size; + window_create_params.max_size = window_size; + } + window_create_params.position = focused_monitor->position + focused_monitor->size / 2 - window_size / 2; + window_create_params.hidden = prevent_game_minimizing; window_create_params.override_redirect = prevent_game_minimizing; - window_create_params.background_color = bg_color; + window_create_params.background_color = mgl::Color(0, 0, 0, 0); window_create_params.support_alpha = true; - window_create_params.window_type = MGL_WINDOW_TYPE_NORMAL; - window_create_params.render_api = MGL_RENDER_API_EGL; - - if(!window->create("gsr ui", window_create_params)) + window_create_params.hide_decorations = true; + // MGL_WINDOW_TYPE_DIALOG is needed for kde plasma wayland in some cases, otherwise the window will pop up on another activity + // or may not be visible at all + window_create_params.window_type = (is_kwin && gsr_info.system_info.display_server == DisplayServer::WAYLAND) ? MGL_WINDOW_TYPE_DIALOG : MGL_WINDOW_TYPE_NORMAL; + // Nvidia + Wayland + Egl doesn't work on some systems properly and it instead falls back to software rendering. + // Use Glx on Wayland to workaround this issue. This is fine since Egl is only needed for x11 to reliably get the texture of the fullscreen window on Nvidia + // when a compositor isn't running. + window_create_params.graphics_api = gsr_info.system_info.display_server == DisplayServer::WAYLAND ? MGL_GRAPHICS_API_GLX : MGL_GRAPHICS_API_EGL; + + if(!window->create("gsr ui", window_create_params)) { fprintf(stderr, "error: failed to create window\n"); + window.reset(); + return; + } + + //window->set_low_latency(true); unsigned char data = 2; // Prefer being composed to allow transparency - XChangeProperty(display, window->get_system_handle(), XInternAtom(display, "_NET_WM_BYPASS_COMPOSITOR", False), XA_CARDINAL, 32, PropModeReplace, &data, 1); + XChangeProperty(display, (Window)window->get_system_handle(), XInternAtom(display, "_NET_WM_BYPASS_COMPOSITOR", False), XA_CARDINAL, 32, PropModeReplace, &data, 1); data = 1; - XChangeProperty(display, window->get_system_handle(), XInternAtom(display, "GAMESCOPE_EXTERNAL_OVERLAY", False), XA_CARDINAL, 32, PropModeReplace, &data, 1); + XChangeProperty(display, (Window)window->get_system_handle(), XInternAtom(display, "GAMESCOPE_EXTERNAL_OVERLAY", False), XA_CARDINAL, 32, PropModeReplace, &data, 1); + const auto original_window_size = window_size; + window_pos = focused_monitor->position; + window_size = focused_monitor->size; if(!init_theme(resources_path)) { fprintf(stderr, "Error: failed to load theme\n"); - exit(1); + window.reset(); + return; } + get_theme().set_window_size(window_size); + + if(prevent_game_minimizing) { + window->set_size(window_size); + window->set_size_limits(window_size, window_size); + } + window->set_position(focused_monitor->position + focused_monitor->size / 2 - original_window_size / 2); mgl_window *win = window->internal_window(); - if(win->num_monitors == 0) { - fprintf(stderr, "gsr warning: no monitors found, not showing overlay\n"); - return; + win->cursor_position.x = cursor_position.x - window_pos.x; + win->cursor_position.y = cursor_position.y - window_pos.y; + + update_compositor_texture(*focused_monitor); + + create_frontpage_ui_components(); + + // The focused application can be an xwayland application but the cursor can hover over a wayland application. + // This is even the case when hovering over the titlebar of the xwayland application. + const bool fake_cursor = is_wlroots ? x11_cursor_window != None : prevent_game_minimizing; + if(fake_cursor) + xi_setup(); + + //window->set_fullscreen(true); + if(gsr_info.system_info.display_server == DisplayServer::X11) + make_window_click_through(display, (Window)window->get_system_handle()); + + window->set_visible(true); + + make_window_sticky(display, (Window)window->get_system_handle()); + hide_window_from_taskbar(display, (Window)window->get_system_handle()); + + if(default_cursor) { + XFreeCursor(display, default_cursor); + default_cursor = 0; } + default_cursor = XCreateFontCursor(display, XC_left_ptr); + XFlush(display); - // The cursor position is wrong on wayland if an x11 window is not focused. On wayland we instead create a window and get the position where the wayland compositor puts it - const mgl::vec2i monitor_position_query_value = gsr_info.system_info.display_server == DisplayServer::WAYLAND ? create_window_get_center_position(display) : mgl::vec2i(win->cursor_position.x, win->cursor_position.y); - const mgl_monitor *focused_monitor = find_monitor_at_position(*window, monitor_position_query_value); - window_pos = {focused_monitor->pos.x, focused_monitor->pos.y}; - window_size = {focused_monitor->size.x, focused_monitor->size.y}; - get_theme().set_window_size(window_size); + grab_mouse_and_keyboard(); + + // The real cursor doesn't move when all devices are grabbed, so we create our own cursor and diplay that while grabbed + cursor_hotspot = {0, 0}; + xi_setup_fake_cursor(); + if(cursor_info && gsr_info.system_info.display_server == DisplayServer::WAYLAND) { + win->cursor_position.x += cursor_hotspot.x; + win->cursor_position.y += cursor_hotspot.y; + } - window->set_size(window_size); - window->set_size_limits(window_size, window_size); - window->set_position(window_pos); + // We want to grab all devices to prevent any other application below the UI from receiving events. + // Owlboy seems to use xi events and XGrabPointer doesn't prevent owlboy from receiving events. + xi_grab_all_mouse_devices(xi_display); - update_compositor_texture(focused_monitor); + if(!is_wlroots && !hyprland_waybar_is_dock) + window->set_fullscreen(true); - top_bar_text = mgl::Text("GPU Screen Recorder", get_theme().top_bar_font); - logo_sprite = mgl::Sprite(&get_theme().logo_texture); + visible = true; + + if(gpu_screen_recorder_process > 0) { + switch(recording_status) { + case RecordingStatus::NONE: + break; + case RecordingStatus::REPLAY: + update_ui_replay_started(); + break; + case RecordingStatus::RECORD: + update_ui_recording_started(); + break; + case RecordingStatus::STREAM: + update_ui_streaming_started(); + break; + } + } + + if(paused) + update_ui_recording_paused(); + + if(replay_recording) + update_ui_recording_started(); + + // Wayland compositors have retarded fullscreen animations that we cant disable in a proper way + // without messing up window position. + show_overlay_timeout_seconds = prevent_game_minimizing ? 0.0 : 0.15; + show_overlay_clock.restart(); + draw(); + } + void Overlay::create_frontpage_ui_components() { bg_screenshot_overlay = mgl::Rectangle(mgl::vec2f(get_theme().window_width, get_theme().window_height)); top_bar_background = mgl::Rectangle(mgl::vec2f(get_theme().window_width, get_theme().window_height*0.06f).floor()); top_bar_text = mgl::Text("GPU Screen Recorder", get_theme().top_bar_font); @@ -872,57 +1136,87 @@ namespace gsr { auto button = std::make_unique<DropdownButton>(&get_theme().title_font, &get_theme().body_font, "Instant Replay", "Off", &get_theme().replay_button_texture, mgl::vec2f(button_width, button_height)); replay_dropdown_button_ptr = button.get(); - button->add_item("Turn on", "start", "Alt+Shift+F10"); - button->add_item("Save", "save", "Alt+F10"); + button->add_item("Turn on", "start", config.replay_config.start_stop_hotkey.to_string(false, false)); + button->add_item("Save", "save", config.replay_config.save_hotkey.to_string(false, false)); + if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) { + button->add_item("Save 1 min", "save_1_min", config.replay_config.save_1_min_hotkey.to_string(false, false)); + button->add_item("Save 10 min", "save_10_min", config.replay_config.save_10_min_hotkey.to_string(false, false)); + } button->add_item("Settings", "settings"); button->set_item_icon("start", &get_theme().play_texture); button->set_item_icon("save", &get_theme().save_texture); + button->set_item_icon("save_1_min", &get_theme().save_texture); + button->set_item_icon("save_10_min", &get_theme().save_texture); + button->set_item_icon("settings", &get_theme().settings_extra_small_texture); button->on_click = [this](const std::string &id) { if(id == "settings") { auto replay_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::REPLAY, &gsr_info, config, &page_stack); + replay_settings_page->on_config_changed = [this]() { + replay_startup_mode = replay_startup_string_to_type(config.replay_config.turn_on_replay_automatically_mode.c_str()); + if(recording_status == RecordingStatus::REPLAY) + show_notification("Replay settings have been modified.\nYou may need to restart replay to apply the changes.", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); + }; page_stack.push(std::move(replay_settings_page)); } else if(id == "save") { on_press_save_replay(); + } else if(id == "save_1_min") { + on_press_save_replay_1_min_replay(); + } else if(id == "save_10_min") { + on_press_save_replay_10_min_replay(); } else if(id == "start") { - on_press_start_replay(false); + on_press_start_replay(false, false); } }; + button->set_item_enabled("save", false); + button->set_item_enabled("save_1_min", false); + button->set_item_enabled("save_10_min", false); main_buttons_list->add_widget(std::move(button)); } { auto button = std::make_unique<DropdownButton>(&get_theme().title_font, &get_theme().body_font, "Record", "Not recording", &get_theme().record_button_texture, mgl::vec2f(button_width, button_height)); record_dropdown_button_ptr = button.get(); - button->add_item("Start", "start", "Alt+F9"); - button->add_item("Pause", "pause", "Alt+F7"); + button->add_item("Start", "start", config.record_config.start_stop_hotkey.to_string(false, false)); + button->add_item("Pause", "pause", config.record_config.pause_unpause_hotkey.to_string(false, false)); button->add_item("Settings", "settings"); button->set_item_icon("start", &get_theme().play_texture); button->set_item_icon("pause", &get_theme().pause_texture); + button->set_item_icon("settings", &get_theme().settings_extra_small_texture); button->on_click = [this](const std::string &id) { if(id == "settings") { auto record_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::RECORD, &gsr_info, config, &page_stack); + record_settings_page->on_config_changed = [this]() { + if(recording_status == RecordingStatus::RECORD) + show_notification("Recording settings have been modified.\nYou may need to restart recording to apply the changes.", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD); + }; page_stack.push(std::move(record_settings_page)); } else if(id == "pause") { toggle_pause(); } else if(id == "start") { - on_press_start_record(); + on_press_start_record(false); } }; + button->set_item_enabled("pause", false); main_buttons_list->add_widget(std::move(button)); } { auto button = std::make_unique<DropdownButton>(&get_theme().title_font, &get_theme().body_font, "Livestream", "Not streaming", &get_theme().stream_button_texture, mgl::vec2f(button_width, button_height)); stream_dropdown_button_ptr = button.get(); - button->add_item("Start", "start", "Alt+F8"); + button->add_item("Start", "start", config.streaming_config.start_stop_hotkey.to_string(false, false)); button->add_item("Settings", "settings"); button->set_item_icon("start", &get_theme().play_texture); + button->set_item_icon("settings", &get_theme().settings_extra_small_texture); button->on_click = [this](const std::string &id) { if(id == "settings") { auto stream_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::STREAM, &gsr_info, config, &page_stack); + stream_settings_page->on_config_changed = [this]() { + if(recording_status == RecordingStatus::STREAM) + show_notification("Streaming settings have been modified.\nYou may need to restart streaming to apply the changes.", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM); + }; page_stack.push(std::move(stream_settings_page)); } else if(id == "start") { - on_press_start_stream(); + on_press_start_stream(false); } }; main_buttons_list->add_widget(std::move(button)); @@ -934,18 +1228,84 @@ namespace gsr { { const mgl::vec2f main_buttons_size = main_buttons_list_ptr->get_size(); - const int settings_button_size = main_buttons_size.y * 0.2f; + const int settings_button_size = main_buttons_size.y * 0.33f; auto button = std::make_unique<Button>(&get_theme().title_font, "", mgl::vec2f(settings_button_size, settings_button_size), mgl::Color(0, 0, 0, 180)); button->set_position((main_buttons_list_ptr->get_position() + main_buttons_size - mgl::vec2f(0.0f, settings_button_size) + mgl::vec2f(settings_button_size * 0.333f, 0.0f)).floor()); button->set_bg_hover_color(mgl::Color(0, 0, 0, 255)); button->set_icon(&get_theme().settings_small_texture); button->on_click = [&]() { - auto settings_page = std::make_unique<GlobalSettingsPage>(&gsr_info, config, &page_stack); + auto settings_page = std::make_unique<GlobalSettingsPage>(this, &gsr_info, config, &page_stack); + + settings_page->on_startup_changed = [&](bool enable, int exit_status) { + if(exit_status == 0) + return; + + if(exit_status == 127) { + if(enable) + show_notification("Failed to add GPU Screen Recorder to system startup.\nThis option only works on systems that use systemd.\nYou have to manually add \"gsr-ui\" to system startup on systems that uses another init system.", 7.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE); + } else { + if(enable) + show_notification("Failed to add GPU Screen Recorder to system startup", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE); + else + show_notification("Failed to remove GPU Screen Recorder from system startup", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE); + } + }; + + settings_page->on_click_exit_program_button = [this](const char *reason) { + do_exit = true; + exit_reason = reason; + }; + + settings_page->on_keyboard_hotkey_changed = [this](const char *hotkey_option) { + global_hotkeys.reset(); + if(strcmp(hotkey_option, "enable_hotkeys") == 0) + global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::ALL); + else if(strcmp(hotkey_option, "enable_hotkeys_virtual_devices") == 0) + global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::VIRTUAL); + else if(strcmp(hotkey_option, "disable_hotkeys") == 0) + global_hotkeys.reset(); + }; + + settings_page->on_joystick_hotkey_changed = [this](const char *hotkey_option) { + global_hotkeys_js.reset(); + if(strcmp(hotkey_option, "enable_hotkeys") == 0) + global_hotkeys_js = register_joystick_hotkeys(this); + else if(strcmp(hotkey_option, "disable_hotkeys") == 0) + global_hotkeys_js.reset(); + }; + + settings_page->on_page_closed = [this]() { + replay_dropdown_button_ptr->set_item_description("start", config.replay_config.start_stop_hotkey.to_string(false, false)); + replay_dropdown_button_ptr->set_item_description("save", config.replay_config.save_hotkey.to_string(false, false)); + replay_dropdown_button_ptr->set_item_description("save_1_min", config.replay_config.save_1_min_hotkey.to_string(false, false)); + replay_dropdown_button_ptr->set_item_description("save_10_min", config.replay_config.save_10_min_hotkey.to_string(false, false)); + + record_dropdown_button_ptr->set_item_description("start", config.record_config.start_stop_hotkey.to_string(false, false)); + record_dropdown_button_ptr->set_item_description("pause", config.record_config.pause_unpause_hotkey.to_string(false, false)); + + stream_dropdown_button_ptr->set_item_description("start", config.streaming_config.start_stop_hotkey.to_string(false, false)); + }; + page_stack.push(std::move(settings_page)); }; front_page_ptr->add_widget(std::move(button)); } + { + const mgl::vec2f main_buttons_size = main_buttons_list_ptr->get_size(); + const int settings_button_size = main_buttons_size.y * 0.33f; + auto button = std::make_unique<Button>(&get_theme().title_font, "", mgl::vec2f(settings_button_size, settings_button_size), mgl::Color(0, 0, 0, 180)); + button->set_position((main_buttons_list_ptr->get_position() + main_buttons_size - mgl::vec2f(0.0f, settings_button_size*2) + mgl::vec2f(settings_button_size * 0.333f, 0.0f)).floor()); + button->set_bg_hover_color(mgl::Color(0, 0, 0, 255)); + button->set_icon(&get_theme().screenshot_texture); + button->set_icon_padding_scale(1.2f); + button->on_click = [&]() { + auto screenshot_settings_page = std::make_unique<ScreenshotSettingsPage>(&gsr_info, config, &page_stack); + page_stack.push(std::move(screenshot_settings_page)); + }; + front_page_ptr->add_widget(std::move(button)); + } + close_button_widget.draw_handler = [&](mgl::Window &window, mgl::vec2f pos, mgl::vec2f size) { const int border_size = std::max(1.0f, 0.0015f * get_theme().window_height); const float padding_size = std::max(1.0f, 0.003f * get_theme().window_height); @@ -972,59 +1332,6 @@ namespace gsr { } return true; }; - - //window->set_fullscreen(true); - if(gsr_info.system_info.display_server == DisplayServer::X11) - make_window_click_through(display, window->get_system_handle()); - - window->set_visible(true); - - make_window_sticky(display, window->get_system_handle()); - hide_window_from_taskbar(display, window->get_system_handle()); - - if(default_cursor) { - XFreeCursor(display, default_cursor); - default_cursor = 0; - } - default_cursor = XCreateFontCursor(display, XC_arrow); - XFlush(display); - - grab_mouse_and_keyboard(); - - // The real cursor doesn't move when all devices are grabbed, so we create our own cursor and diplay that while grabbed - xi_setup_fake_cursor(); - - // We want to grab all devices to prevent any other application below the UI from receiving events. - // Owlboy seems to use xi events and XGrabPointer doesn't prevent owlboy from receiving events. - xi_grab_all_devices(); - - // if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { - // set_focused_window(display, window->get_system_handle()); - // XFlush(display); - // } - - window->set_fullscreen(true); - - visible = true; - - if(gpu_screen_recorder_process > 0) { - switch(recording_status) { - case RecordingStatus::NONE: - break; - case RecordingStatus::REPLAY: - update_ui_replay_started(); - break; - case RecordingStatus::RECORD: - update_ui_recording_started(); - break; - case RecordingStatus::STREAM: - update_ui_streaming_started(); - break; - } - } - - if(paused) - update_ui_recording_paused(); } void Overlay::hide() { @@ -1037,6 +1344,7 @@ namespace gsr { while(!page_stack.empty()) { page_stack.pop(); } + remove_widgets_to_be_removed(); if(default_cursor) { XFreeCursor(display, default_cursor); @@ -1048,10 +1356,6 @@ namespace gsr { XFlush(display); if(xi_display) { - // TODO: Only show cursor if it wasn't hidden before the ui was shown - cursor_drawn = false; - //XFixesShowCursor(xi_display, DefaultRootWindow(xi_display)); - //XFlush(xi_display); cursor_texture.clear(); cursor_sprite.set_texture(nullptr); } @@ -1063,30 +1367,60 @@ namespace gsr { visible = false; drawn_first_frame = false; + start_region_capture = false; + start_window_capture = false; - if(window) { - const mgl::vec2i new_cursor_position = mgl::vec2i(window->internal_window()->pos.x, window->internal_window()->pos.y) + window->get_mouse_position(); - window->set_visible(false); - window.reset(); + if(xi_input_xev) { + free(xi_input_xev); + xi_input_xev = nullptr; + } - if(xi_display) { - XFlush(display); + if(xi_output_xev) { + free(xi_output_xev); + xi_output_xev = nullptr; + } + + if(xi_display) { + if(window) { + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + + const mgl::vec2i new_cursor_position = mgl::vec2i(window->internal_window()->pos.x, window->internal_window()->pos.y) + window->get_mouse_position(); XWarpPointer(display, DefaultRootWindow(display), DefaultRootWindow(display), 0, 0, 0, 0, new_cursor_position.x, new_cursor_position.y); + xi_warp_all_mouse_devices(xi_display, new_cursor_position); + XFlush(display); + + XFixesShowCursor(display, DefaultRootWindow(display)); XFlush(display); - //xi_warp_pointer(new_cursor_position); + } + + XCloseDisplay(xi_display); + xi_display = nullptr; + } - XFixesShowCursor(xi_display, DefaultRootWindow(xi_display)); - XFlush(xi_display); + if(window) { + if(show_overlay_timeout_seconds > 0.0001) { + window->clear(mgl::Color(0, 0, 0, 0)); + window->display(); + + mgl_context *context = mgl_get_context(); + context->gl.glFlush(); + context->gl.glFinish(); + usleep(50 * 1000); // EGL doesn't do an immediate flush for some reason } + + window->set_visible(false); + window.reset(); } deinit_theme(); + malloc_trim(0); } void Overlay::toggle_show() { if(visible) { //hide(); - // We dont want to hide immediately because hide is called in event callback, in which it destroys the window. + // We dont want to hide immediately because hide is called in mgl event callback, in which it destroys the mgl window. // Instead remove all pages and wait until next iteration to close the UI (which happens when there are no pages to render). while(!page_stack.empty()) { page_stack.pop(); @@ -1097,7 +1431,7 @@ namespace gsr { } void Overlay::toggle_record() { - on_press_start_record(); + on_press_start_record(false); } void Overlay::toggle_pause() { @@ -1106,10 +1440,12 @@ namespace gsr { if(paused) { update_ui_recording_unpaused(); - show_notification("Recording has been unpaused", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD); + if(config.record_config.show_video_paused_notifications) + show_notification("Recording has been unpaused", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD); } else { update_ui_recording_paused(); - show_notification("Recording has been paused", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD); + if(config.record_config.show_video_paused_notifications) + show_notification("Recording has been paused", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD); } kill(gpu_screen_recorder_process, SIGUSR2); @@ -1117,47 +1453,215 @@ namespace gsr { } void Overlay::toggle_stream() { - on_press_start_stream(); + on_press_start_stream(false); } void Overlay::toggle_replay() { - on_press_start_replay(false); + on_press_start_replay(false, false); } void Overlay::save_replay() { on_press_save_replay(); } + void Overlay::save_replay_1_min() { + on_press_save_replay_1_min_replay(); + } + + void Overlay::save_replay_10_min() { + on_press_save_replay_10_min_replay(); + } + + void Overlay::take_screenshot() { + on_press_take_screenshot(false, false); + } + + void Overlay::take_screenshot_region() { + on_press_take_screenshot(false, true); + } + static const char* notification_type_to_string(NotificationType notification_type) { switch(notification_type) { - case NotificationType::NONE: return nullptr; - case NotificationType::RECORD: return "record"; - case NotificationType::REPLAY: return "replay"; - case NotificationType::STREAM: return "stream"; + case NotificationType::NONE: return nullptr; + case NotificationType::RECORD: return "record"; + case NotificationType::REPLAY: return "replay"; + case NotificationType::STREAM: return "stream"; + case NotificationType::SCREENSHOT: return "screenshot"; } return nullptr; } - void Overlay::show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type) { + static void truncate_string(std::string &str, int max_length) { + int index = 0; + size_t byte_index = 0; + + while(index < max_length && byte_index < str.size()) { + uint32_t codepoint = 0; + size_t codepoint_length = 0; + mgl::utf8_decode((const unsigned char*)str.c_str() + byte_index, str.size() - byte_index, &codepoint, &codepoint_length); + if(codepoint_length == 0) + codepoint_length = 1; + + index += 1; + byte_index += codepoint_length; + } + + if(byte_index < str.size()) { + str.erase(byte_index); + str += "..."; + } + } + + static bool is_hex_num(char c) { + return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9'); + } + + static bool contains_non_hex_number(const char *str) { + bool hex_start = false; + size_t len = strlen(str); + if(len >= 2 && memcmp(str, "0x", 2) == 0) { + str += 2; + len -= 2; + hex_start = true; + } + + bool is_hex = false; + for(size_t i = 0; i < len; ++i) { + char c = str[i]; + if(c == '\0') + return false; + if(!is_hex_num(c)) + return true; + if((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) + is_hex = true; + } + + return is_hex && !hex_start; + } + + static bool is_number(const char *str) { + const char *p = str; + while(*p) { + char c = *p; + if(c < '0' || c > '9') + return false; + ++p; + } + return true; + } + + static bool is_capture_target_monitor(const char *capture_target) { + return strcmp(capture_target, "window") != 0 && strcmp(capture_target, "focused") != 0 && strcmp(capture_target, "region") != 0 && strcmp(capture_target, "portal") != 0 && contains_non_hex_number(capture_target); + } + + static std::string capture_target_get_notification_name(const char *capture_target) { + std::string result; + if(is_capture_target_monitor(capture_target)) { + result = "this monitor"; + } else if(is_number(capture_target)) { + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + + int64_t window_id = None; + sscanf(capture_target, "%" PRIi64, &window_id); + + const std::optional<std::string> window_title = get_window_title(display, window_id); + if(window_title) { + result = strip(window_title.value()); + truncate_string(result, 20); + result = "window \"" + result + "\""; + } else { + result = std::string("window ") + capture_target; + } + } else { + result = capture_target; + } + return result; + } + + static std::string get_valid_monitor_x11(const std::string &target_monitor_name, const std::vector<Monitor> &monitors) { + std::string target_monitor_name_clean = target_monitor_name; + if(starts_with(target_monitor_name_clean, "HDMI-A")) + target_monitor_name_clean.replace(0, 6, "HDMI"); + + for(const Monitor &monitor : monitors) { + std::string monitor_name_clean = monitor.name; + if(starts_with(monitor_name_clean, "HDMI-A")) + monitor_name_clean.replace(0, 6, "HDMI"); + + if(target_monitor_name_clean == monitor_name_clean) + return monitor.name; + } + + return ""; + } + + static std::string get_focused_monitor_by_cursor(CursorTracker *cursor_tracker, const GsrInfo &gsr_info, const std::vector<Monitor> &x11_monitors) { + std::optional<CursorInfo> cursor_info; + if(cursor_tracker) { + cursor_tracker->update(); + cursor_info = cursor_tracker->get_latest_cursor_info(); + } + + std::string focused_monitor_name; + if(cursor_info) { + focused_monitor_name = std::move(cursor_info->monitor_name); + } else { + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + + Window x11_cursor_window = None; + mgl::vec2i cursor_position = get_cursor_position(display, &x11_cursor_window); + + const mgl::vec2i monitor_position_query_value = (x11_cursor_window || gsr_info.system_info.display_server != DisplayServer::WAYLAND) ? cursor_position : create_window_get_center_position(display); + const Monitor *focused_monitor = find_monitor_at_position(x11_monitors, monitor_position_query_value); + if(focused_monitor) + focused_monitor_name = focused_monitor->name; + } + + return focused_monitor_name; + } + + void Overlay::show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type, const char *capture_target) { char timeout_seconds_str[32]; snprintf(timeout_seconds_str, sizeof(timeout_seconds_str), "%f", timeout_seconds); const std::string icon_color_str = color_to_hex_str(icon_color); const std::string bg_color_str = color_to_hex_str(bg_color); - const char *notification_args[12] = { + const char *notification_args[14] = { "gsr-notify", "--text", str, "--timeout", timeout_seconds_str, "--icon-color", icon_color_str.c_str(), "--bg-color", bg_color_str.c_str(), }; + int arg_index = 9; const char *notification_type_str = notification_type_to_string(notification_type); if(notification_type_str) { - notification_args[9] = "--icon"; - notification_args[10] = "record", - notification_args[11] = nullptr; - } else { - notification_args[9] = nullptr; + notification_args[arg_index++] = "--icon"; + notification_args[arg_index++] = notification_type_str; + } + + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + + std::string monitor_name; + const auto monitors = get_monitors(display); + + if(capture_target && is_capture_target_monitor(capture_target)) + monitor_name = capture_target; + else + monitor_name = get_focused_monitor_by_cursor(cursor_tracker.get(), gsr_info, monitors); + + monitor_name = get_valid_monitor_x11(monitor_name, monitors); + if(!monitor_name.empty()) { + notification_args[arg_index++] = "--monitor"; + notification_args[arg_index++] = monitor_name.c_str(); + } else if(!monitors.empty()) { + notification_args[arg_index++] = "--monitor"; + notification_args[arg_index++] = monitors.front().name.c_str(); } + notification_args[arg_index++] = nullptr; + if(notification_process > 0) { kill(notification_process, SIGKILL); int status = 0; @@ -1171,6 +1675,46 @@ namespace gsr { return visible; } + bool Overlay::should_exit(std::string &reason) const { + reason.clear(); + if(do_exit) + reason = exit_reason; + return do_exit; + } + + void Overlay::exit() { + do_exit = true; + } + + void Overlay::go_back_to_old_ui() { + const bool inside_flatpak = getenv("FLATPAK_ID") != NULL; + if(inside_flatpak) + exit_reason = "back-to-old-ui"; + else + exit_reason = "exit"; + + const char *args[] = { "systemctl", "disable", "--user", "gpu-screen-recorder-ui", nullptr }; + std::string stdout_str; + exec_program_on_host_get_stdout(args, stdout_str); + exit(); + } + + const Config& Overlay::get_config() const { + return config; + } + + void Overlay::unbind_all_keyboard_hotkeys() { + if(global_hotkeys) + global_hotkeys->unbind_all_keys(); + } + + void Overlay::rebind_all_keyboard_hotkeys() { + unbind_all_keyboard_hotkeys(); + // TODO: Check if type is GlobalHotkeysLinux + if(global_hotkeys) + bind_linux_hotkeys(static_cast<GlobalHotkeysLinux*>(global_hotkeys.get()), this); + } + void Overlay::update_notification_process_status() { if(notification_process <= 0) return; @@ -1216,7 +1760,10 @@ namespace gsr { Display *display = (Display*)context->connection; const std::string video_filename = filepath_get_filename(video_filepath); - std::string focused_window_name = get_focused_window_name(display, WindowCaptureType::FOCUSED); + const Window gsr_ui_window = window ? (Window)window->get_system_handle() : None; + std::string focused_window_name = get_window_name_at_cursor_position(display, gsr_ui_window); + if(focused_window_name.empty()) + focused_window_name = get_focused_window_name(display, WindowCaptureType::FOCUSED); if(focused_window_name.empty()) focused_window_name = "Game"; @@ -1228,37 +1775,112 @@ namespace gsr { const std::string new_video_filepath = video_directory + "/" + video_filename; rename(video_filepath, new_video_filepath.c_str()); - std::string text; + truncate_string(focused_window_name, 20); + const char *capture_target = nullptr; + char msg[512]; + switch(notification_type) { - case NotificationType::RECORD: - text = "Saved recording to '" + focused_window_name + "/" + video_filename + "'"; + case NotificationType::RECORD: { + if(!config.record_config.show_video_saved_notifications) + return; + + snprintf(msg, sizeof(msg), "Saved a recording of %s to \"%s\"", capture_target_get_notification_name(recording_capture_target.c_str()).c_str(), focused_window_name.c_str()); + capture_target = recording_capture_target.c_str(); + break; + } + case NotificationType::REPLAY: { + if(!config.replay_config.show_replay_saved_notifications) + return; + + char duration[32]; + if(replay_save_duration_min > 0) + snprintf(duration, sizeof(duration), " %d minute ", replay_save_duration_min); + else + snprintf(duration, sizeof(duration), " "); + + snprintf(msg, sizeof(msg), "Saved a%sreplay of %s to \"%s\"", duration, capture_target_get_notification_name(recording_capture_target.c_str()).c_str(), focused_window_name.c_str()); + capture_target = recording_capture_target.c_str(); break; - case NotificationType::REPLAY: - text = "Saved replay to '" + focused_window_name + "/" + video_filename + "'"; + } + case NotificationType::SCREENSHOT: { + if(!config.screenshot_config.show_screenshot_saved_notifications) + return; + + snprintf(msg, sizeof(msg), "Saved a screenshot of %s to \"%s\"", capture_target_get_notification_name(screenshot_capture_target.c_str()).c_str(), focused_window_name.c_str()); + capture_target = screenshot_capture_target.c_str(); break; + } case NotificationType::NONE: case NotificationType::STREAM: break; } - show_notification(text.c_str(), 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, notification_type); + show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, notification_type, capture_target); + } + + static NotificationType recording_status_to_notification_type(RecordingStatus recording_status) { + switch(recording_status) { + case RecordingStatus::NONE: return NotificationType::NONE; + case RecordingStatus::REPLAY: return NotificationType::REPLAY; + case RecordingStatus::RECORD: return NotificationType::RECORD; + case RecordingStatus::STREAM: return NotificationType::STREAM; + } + return NotificationType::NONE; + } + + void Overlay::on_replay_saved(const char *replay_saved_filepath) { + replay_save_show_notification = false; + if(config.replay_config.save_video_in_game_folder) { + save_video_in_current_game_directory(replay_saved_filepath, NotificationType::REPLAY); + } else if(config.replay_config.show_replay_saved_notifications) { + char duration[32]; + if(replay_save_duration_min > 0) + snprintf(duration, sizeof(duration), " %d minute ", replay_save_duration_min); + else + snprintf(duration, sizeof(duration), " "); + + char msg[512]; + snprintf(msg, sizeof(msg), "Saved a%sreplay of %s", duration, capture_target_get_notification_name(recording_capture_target.c_str()).c_str()); + show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY, recording_capture_target.c_str()); + } } - void Overlay::update_gsr_replay_save() { + void Overlay::process_gsr_output() { + if(replay_save_show_notification && replay_save_clock.get_elapsed_time_seconds() >= replay_saving_notification_timeout_seconds) { + replay_save_show_notification = false; + show_notification("Saving replay, this might take some time", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); + } + if(gpu_screen_recorder_process_output_file) { char buffer[1024]; - char *replay_saved_filepath = fgets(buffer, sizeof(buffer), gpu_screen_recorder_process_output_file); - if(!replay_saved_filepath || replay_saved_filepath[0] == '\0') + char *line = fgets(buffer, sizeof(buffer), gpu_screen_recorder_process_output_file); + if(!line || line[0] == '\0') return; - const int line_len = strlen(replay_saved_filepath); - if(replay_saved_filepath[line_len - 1] == '\n') - replay_saved_filepath[line_len - 1] = '\0'; + const int line_len = strlen(line); + if(line[line_len - 1] == '\n') + line[line_len - 1] = '\0'; - if(config.replay_config.save_video_in_game_folder) { - save_video_in_current_game_directory(replay_saved_filepath, NotificationType::REPLAY); - } else { - const std::string text = "Saved replay to '" + filepath_get_filename(replay_saved_filepath) + "'"; - show_notification(text.c_str(), 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); + if(starts_with({line, (size_t)line_len}, "Error: ")) { + show_notification(line + 7, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), recording_status_to_notification_type(recording_status)); + return; + } + + const std::string video_filepath = filepath_get_filename(line); + if(starts_with(video_filepath, "Video_")) { + on_stop_recording(0, line); + return; + } + + switch(recording_status) { + case RecordingStatus::NONE: + break; + case RecordingStatus::REPLAY: + on_replay_saved(line); + break; + case RecordingStatus::RECORD: + break; + case RecordingStatus::STREAM: + break; } } else if(gpu_screen_recorder_process_output_fd > 0) { char buffer[1024]; @@ -1266,6 +1888,35 @@ namespace gsr { } } + void Overlay::on_gsr_process_error(int exit_code, NotificationType notification_type) { + fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code); + if(exit_code == 50) { + show_notification("Desktop portal capture failed.\nEither you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture\nor it's incorrectly setup on your system", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type); + } else if(exit_code == 60) { + show_notification("Stopped capture because the user canceled the desktop portal", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type); + } else { + const char *prefix = ""; + switch(notification_type) { + case NotificationType::NONE: + case NotificationType::SCREENSHOT: + break; + case NotificationType::RECORD: + prefix = "Failed to start/save recording"; + break; + case NotificationType::REPLAY: + prefix = "Replay stopped because of an error"; + break; + case NotificationType::STREAM: + prefix = "Streaming stopped because of an error"; + break; + } + + char msg[256]; + snprintf(msg, sizeof(msg), "%s. Verify if settings are correct", prefix); + show_notification(msg, notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type); + } + } + void Overlay::update_gsr_process_status() { if(gpu_screen_recorder_process <= 0) return; @@ -1286,29 +1937,28 @@ namespace gsr { case RecordingStatus::NONE: break; case RecordingStatus::REPLAY: { + replay_save_duration_min = 0; update_ui_replay_stopped(); if(exit_code == 0) { if(config.replay_config.show_replay_stopped_notifications) - show_notification("Replay stopped", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); + show_notification("Replay stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); } else { - fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code); - show_notification("Replay stopped because of an error", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY); + on_gsr_process_error(exit_code, NotificationType::REPLAY); } break; } case RecordingStatus::RECORD: { update_ui_recording_stopped(); - on_stop_recording(exit_code); + on_stop_recording(exit_code, record_filepath); break; } case RecordingStatus::STREAM: { update_ui_streaming_stopped(); if(exit_code == 0) { if(config.streaming_config.show_streaming_stopped_notifications) - show_notification("Streaming has stopped", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM); + show_notification("Streaming has stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM); } else { - fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code); - show_notification("Streaming stopped because of an error", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM); + on_gsr_process_error(exit_code, NotificationType::STREAM); } break; } @@ -1318,6 +1968,60 @@ namespace gsr { recording_status = RecordingStatus::NONE; } + void Overlay::update_gsr_screenshot_process_status() { + if(gpu_screen_recorder_screenshot_process <= 0) + return; + + int status; + if(waitpid(gpu_screen_recorder_screenshot_process, &status, WNOHANG) == 0) { + // Still running + return; + } + + int exit_code = -1; + if(WIFEXITED(status)) + exit_code = WEXITSTATUS(status); + + if(exit_code == 0) { + if(config.screenshot_config.save_screenshot_in_game_folder) { + save_video_in_current_game_directory(screenshot_filepath.c_str(), NotificationType::SCREENSHOT); + } else if(config.screenshot_config.show_screenshot_saved_notifications) { + char msg[512]; + snprintf(msg, sizeof(msg), "Saved a screenshot of %s", capture_target_get_notification_name(screenshot_capture_target.c_str()).c_str()); + show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::SCREENSHOT, screenshot_capture_target.c_str()); + } + } else { + fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_screenshot_process, exit_code); + show_notification("Failed to take a screenshot. Verify if settings are correct", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::SCREENSHOT); + } + + gpu_screen_recorder_screenshot_process = -1; + } + + static bool are_all_audio_tracks_available_to_capture(const std::vector<AudioTrack> &audio_tracks) { + const auto audio_devices = get_audio_devices(); + for(const AudioTrack &audio_track : audio_tracks) { + for(const std::string &audio_input : audio_track.audio_inputs) { + std::string_view audio_track_name(audio_input.c_str()); + const bool is_app_audio = starts_with(audio_track_name, "app:"); + if(is_app_audio) + continue; + + if(starts_with(audio_track_name, "device:")) + audio_track_name.remove_prefix(7); + + auto it = std::find_if(audio_devices.begin(), audio_devices.end(), [&](const auto &audio_device) { + return audio_device.name == audio_track_name; + }); + if(it == audio_devices.end()) { + //fprintf(stderr, "Audio not ready\n"); + return false; + } + } + } + return true; + } + void Overlay::replay_status_update_status() { if(replay_status_update_clock.get_elapsed_time_seconds() < replay_status_update_check_timeout_seconds) return; @@ -1325,55 +2029,71 @@ namespace gsr { replay_status_update_clock.restart(); update_focused_fullscreen_status(); update_power_supply_status(); + update_system_startup_status(); } void Overlay::update_focused_fullscreen_status() { - if(config.replay_config.turn_on_replay_automatically_mode != "turn_on_at_fullscreen") + if(replay_startup_mode != ReplayStartupMode::TURN_ON_AT_FULLSCREEN) return; mgl_context *context = mgl_get_context(); Display *display = (Display*)context->connection; const Window focused_window = get_focused_window(display, WindowCaptureType::FOCUSED); - if(window && focused_window == window->get_system_handle()) + if(window && focused_window == (Window)window->get_system_handle()) return; const bool prev_focused_window_is_fullscreen = focused_window_is_fullscreen; focused_window_is_fullscreen = focused_window != 0 && window_is_fullscreen(display, focused_window); if(focused_window_is_fullscreen != prev_focused_window_is_fullscreen) { - if(recording_status == RecordingStatus::NONE && focused_window_is_fullscreen) - on_press_start_replay(false); - else if(recording_status == RecordingStatus::REPLAY && !focused_window_is_fullscreen) - on_press_start_replay(true); + if(recording_status == RecordingStatus::NONE && focused_window_is_fullscreen) { + if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks_list)) + on_press_start_replay(false, false); + } else if(recording_status == RecordingStatus::REPLAY && !focused_window_is_fullscreen) { + on_press_start_replay(true, false); + } } } + // TODO: Instead of checking power supply status periodically listen to power supply event void Overlay::update_power_supply_status() { - if(config.replay_config.turn_on_replay_automatically_mode != "turn_on_at_power_supply_connected") + if(replay_startup_mode != ReplayStartupMode::TURN_ON_AT_POWER_SUPPLY_CONNECTED) return; const bool prev_power_supply_status = power_supply_connected; power_supply_connected = power_supply_online_filepath.empty() || power_supply_is_connected(power_supply_online_filepath.c_str()); if(power_supply_connected != prev_power_supply_status) { - if(recording_status == RecordingStatus::NONE && power_supply_connected) - on_press_start_replay(false); - else if(recording_status == RecordingStatus::REPLAY && !power_supply_connected) - on_press_start_replay(false); + if(recording_status == RecordingStatus::NONE && power_supply_connected) { + if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks_list)) + on_press_start_replay(false, false); + } else if(recording_status == RecordingStatus::REPLAY && !power_supply_connected) { + on_press_start_replay(false, false); + } } } - void Overlay::on_stop_recording(int exit_code) { + void Overlay::update_system_startup_status() { + if(replay_startup_mode != ReplayStartupMode::TURN_ON_AT_SYSTEM_STARTUP || recording_status != RecordingStatus::NONE || !try_replay_startup) + return; + + if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks_list)) + on_press_start_replay(true, false); + } + + void Overlay::on_stop_recording(int exit_code, const std::string &video_filepath) { if(exit_code == 0) { if(config.record_config.save_video_in_game_folder) { - save_video_in_current_game_directory(record_filepath.c_str(), NotificationType::RECORD); - } else { - const std::string text = "Saved recording to '" + filepath_get_filename(record_filepath.c_str()) + "'"; - show_notification(text.c_str(), 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD); + save_video_in_current_game_directory(video_filepath.c_str(), NotificationType::RECORD); + } else if(config.record_config.show_video_saved_notifications) { + char msg[512]; + snprintf(msg, sizeof(msg), "Saved a recording of %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str()); + show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str()); } } else { - fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code); - show_notification("Failed to start/save recording", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD); + on_gsr_process_error(exit_code, NotificationType::RECORD); } + update_ui_recording_stopped(); + replay_recording = false; } void Overlay::update_ui_recording_paused() { @@ -1402,6 +2122,7 @@ namespace gsr { record_dropdown_button_ptr->set_activated(true); record_dropdown_button_ptr->set_description("Recording"); record_dropdown_button_ptr->set_item_icon("start", &get_theme().stop_texture); + record_dropdown_button_ptr->set_item_enabled("pause", recording_status == RecordingStatus::RECORD); } void Overlay::update_ui_recording_stopped() { @@ -1415,7 +2136,9 @@ namespace gsr { record_dropdown_button_ptr->set_item_label("pause", "Pause"); record_dropdown_button_ptr->set_item_icon("pause", &get_theme().pause_texture); + record_dropdown_button_ptr->set_item_enabled("pause", false); paused = false; + replay_recording = false; } void Overlay::update_ui_streaming_started() { @@ -1436,6 +2159,7 @@ namespace gsr { stream_dropdown_button_ptr->set_activated(false); stream_dropdown_button_ptr->set_description("Not streaming"); stream_dropdown_button_ptr->set_item_icon("start", &get_theme().play_texture); + update_ui_recording_stopped(); } void Overlay::update_ui_replay_started() { @@ -1446,6 +2170,9 @@ namespace gsr { replay_dropdown_button_ptr->set_activated(true); replay_dropdown_button_ptr->set_description("On"); replay_dropdown_button_ptr->set_item_icon("start", &get_theme().stop_texture); + replay_dropdown_button_ptr->set_item_enabled("save", true); + replay_dropdown_button_ptr->set_item_enabled("save_1_min", true); + replay_dropdown_button_ptr->set_item_enabled("save_10_min", true); } void Overlay::update_ui_replay_stopped() { @@ -1456,6 +2183,10 @@ namespace gsr { replay_dropdown_button_ptr->set_activated(false); replay_dropdown_button_ptr->set_description("Off"); replay_dropdown_button_ptr->set_item_icon("start", &get_theme().play_texture); + replay_dropdown_button_ptr->set_item_enabled("save", false); + replay_dropdown_button_ptr->set_item_enabled("save_1_min", false); + replay_dropdown_button_ptr->set_item_enabled("save_10_min", false); + update_ui_recording_stopped(); } static std::string get_date_str() { @@ -1477,38 +2208,58 @@ namespace gsr { return container; } - static bool starts_with(std::string_view str, const char *substr) { - size_t len = strlen(substr); - return str.size() >= len && memcmp(str.data(), substr, len) == 0; - } - - static std::vector<std::string> create_audio_tracks_real_names(const std::vector<std::string> &audio_tracks, bool application_audio_invert, const GsrInfo &gsr_info) { + static std::vector<std::string> create_audio_tracks_cli_args(const std::vector<AudioTrack> &audio_tracks, const GsrInfo &gsr_info) { std::vector<std::string> result; - for(const std::string &audio_track : audio_tracks) { - std::string audio_track_name = audio_track; - const bool is_app_audio = starts_with(audio_track_name, "app:"); - if(is_app_audio && !gsr_info.system_info.supports_app_audio) - continue; + result.reserve(audio_tracks.size()); + + for(const AudioTrack &audio_track : audio_tracks) { + std::string audio_track_merged; + int num_app_audio = 0; + + for(const std::string &audio_input_name : audio_track.audio_inputs) { + std::string new_audio_input_name = audio_input_name; + const bool is_app_audio = starts_with(new_audio_input_name, "app:"); + if(is_app_audio && !gsr_info.system_info.supports_app_audio) + continue; + + if(is_app_audio && audio_track.application_audio_invert) + new_audio_input_name.replace(0, 4, "app-inverse:"); + + if(is_app_audio) + ++num_app_audio; + + if(!audio_track_merged.empty()) + audio_track_merged += "|"; + + audio_track_merged += new_audio_input_name; + } - if(is_app_audio && application_audio_invert) - audio_track_name.replace(0, 4, "app-inverse:"); + if(num_app_audio == 0 && audio_track.application_audio_invert) { + if(!audio_track_merged.empty()) + audio_track_merged += "|"; - result.push_back(std::move(audio_track_name)); + audio_track_merged += "app-inverse:"; + } + + if(!audio_track_merged.empty()) + result.push_back(std::move(audio_track_merged)); } + return result; } - static std::string merge_audio_tracks(const std::vector<std::string> &audio_tracks) { - std::string result; - for(size_t i = 0; i < audio_tracks.size(); ++i) { - if(i > 0) - result += "|"; - result += audio_tracks[i]; + static void add_region_command(std::vector<const char*> &args, char *region_str, int region_str_size, const RegionSelector ®ion_selector) { + Region region = region_selector.get_selection(); + if(region.size.x <= 32 && region.size.y <= 32) { + region.size.x = 0; + region.size.y = 0; } - return result; + snprintf(region_str, region_str_size, "%dx%d+%d+%d", region.size.x, region.size.y, region.pos.x, region.pos.y); + args.push_back("-region"); + args.push_back(region_str); } - static void add_common_gpu_screen_recorder_args(std::vector<const char*> &args, const RecordOptions &record_options, const std::vector<std::string> &audio_tracks, const std::string &video_bitrate, const char *region, const std::string &audio_devices_merged) { + static void add_common_gpu_screen_recorder_args(std::vector<const char*> &args, const RecordOptions &record_options, const std::vector<std::string> &audio_tracks, const std::string &video_bitrate, const char *region, char *region_str, int region_str_size, const RegionSelector ®ion_selector) { if(record_options.video_quality == "custom") { args.push_back("-bm"); args.push_back("cbr"); @@ -1524,33 +2275,31 @@ namespace gsr { args.push_back(region); } - if(record_options.merge_audio_tracks) { - if(!audio_devices_merged.empty()) { - args.push_back("-a"); - args.push_back(audio_devices_merged.c_str()); - } - } else { - for(const std::string &audio_track : audio_tracks) { - args.push_back("-a"); - args.push_back(audio_track.c_str()); - } + for(const std::string &audio_track : audio_tracks) { + args.push_back("-a"); + args.push_back(audio_track.c_str()); } if(record_options.restore_portal_session) { args.push_back("-restore-portal-session"); args.push_back("yes"); } + + if(record_options.record_area_option == "region") + add_region_command(args, region_str, region_str_size, region_selector); } - static bool validate_capture_target(const GsrInfo &gsr_info, const std::string &capture_target) { - const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info); - // TODO: Also check x11 window when enabled (check if capture_target is a decminal/hex number) - if(capture_target == "focused" && !capture_options.focused) { - return false; - } else if(capture_target == "screen" && !capture_options.screen) { - return false; - } else if(capture_target == "portal" && !capture_options.portal) { - return false; + static bool validate_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options) { + if(capture_target == "window") { + return capture_options.window; + } else if(capture_target == "focused") { + return capture_options.focused; + } else if(capture_target == "region") { + return capture_options.region; + } else if(capture_target == "portal") { + return capture_options.portal; + } else if(capture_target == "focused_monitor") { + return !capture_options.monitors.empty(); } else { for(const GsrMonitor &monitor : capture_options.monitors) { if(capture_target == monitor.name) @@ -1560,30 +2309,156 @@ namespace gsr { } } + static std::string get_valid_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options) { + std::string capture_target_clean = capture_target; + if(starts_with(capture_target_clean, "HDMI-A")) + capture_target_clean.replace(0, 6, "HDMI"); + + for(const GsrMonitor &monitor : capture_options.monitors) { + std::string monitor_name_clean = monitor.name; + if(starts_with(monitor_name_clean, "HDMI-A")) + monitor_name_clean.replace(0, 6, "HDMI"); + + if(capture_target_clean == monitor_name_clean) + return monitor.name; + } + + return ""; + } + + std::string Overlay::get_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options) { + if(capture_target == "window") { + return std::to_string(window_selector.get_selection()); + } else if(capture_target == "focused_monitor") { + std::optional<CursorInfo> cursor_info; + if(cursor_tracker) { + cursor_tracker->update(); + cursor_info = cursor_tracker->get_latest_cursor_info(); + } + + std::string focused_monitor_name; + if(cursor_info) { + focused_monitor_name = std::move(cursor_info->monitor_name); + } else { + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + focused_monitor_name = get_focused_monitor_by_cursor(cursor_tracker.get(), gsr_info, get_monitors(display)); + } + + focused_monitor_name = get_valid_capture_target(focused_monitor_name, capture_options); + if(!focused_monitor_name.empty()) + return focused_monitor_name; + else if(!capture_options.monitors.empty()) + return capture_options.monitors.front().name; + else + return ""; + } else { + return capture_target; + } + } + + void Overlay::prepare_gsr_output_for_reading() { + if(gpu_screen_recorder_process_output_fd <= 0) + return; + + const int fdl = fcntl(gpu_screen_recorder_process_output_fd, F_GETFL); + fcntl(gpu_screen_recorder_process_output_fd, F_SETFL, fdl | O_NONBLOCK); + gpu_screen_recorder_process_output_file = fdopen(gpu_screen_recorder_process_output_fd, "r"); + if(gpu_screen_recorder_process_output_file) + gpu_screen_recorder_process_output_fd = -1; + } + void Overlay::on_press_save_replay() { if(recording_status != RecordingStatus::REPLAY || gpu_screen_recorder_process <= 0) return; + replay_save_duration_min = 0; + replay_save_show_notification = true; + replay_save_clock.restart(); kill(gpu_screen_recorder_process, SIGUSR1); } - void Overlay::on_press_start_replay(bool disable_notification) { + void Overlay::on_press_save_replay_1_min_replay() { + if(recording_status != RecordingStatus::REPLAY || gpu_screen_recorder_process <= 0) + return; + + replay_save_duration_min = 1; + replay_save_show_notification = true; + replay_save_clock.restart(); + kill(gpu_screen_recorder_process, SIGRTMIN+3); + } + + void Overlay::on_press_save_replay_10_min_replay() { + if(recording_status != RecordingStatus::REPLAY || gpu_screen_recorder_process <= 0) + return; + + replay_save_duration_min = 10; + replay_save_show_notification = true; + replay_save_clock.restart(); + kill(gpu_screen_recorder_process, SIGRTMIN+5); + } + + static const char* switch_video_codec_to_usable_hardware_encoder(const GsrInfo &gsr_info) { + if(gsr_info.supported_video_codecs.h264) + return "h264"; + else if(gsr_info.supported_video_codecs.hevc) + return "hevc"; + else if(gsr_info.supported_video_codecs.av1) + return "av1"; + else if(gsr_info.supported_video_codecs.vp8) + return "vp8"; + else if(gsr_info.supported_video_codecs.vp9) + return "vp9"; + return nullptr; + } + + static const char* change_container_if_codec_not_supported(const char *video_codec, const char *container) { + if(strcmp(video_codec, "vp8") == 0 || strcmp(video_codec, "vp9") == 0) { + if(strcmp(container, "webm") != 0 && strcmp(container, "matroska") != 0) { + fprintf(stderr, "Warning: container '%s' is not compatible with video codec '%s', using webm container instead\n", container, video_codec); + return "webm"; + } + } else if(strcmp(container, "webm") == 0) { + fprintf(stderr, "Warning: container webm is not compatible with video codec '%s', using mp4 container instead\n", video_codec); + return "mp4"; + } + return container; + } + + static void choose_video_codec_and_container_with_fallback(const GsrInfo &gsr_info, const char **video_codec, const char **container, const char **encoder) { + *encoder = "gpu"; + if(strcmp(*video_codec, "h264_software") == 0) { + *video_codec = "h264"; + *encoder = "cpu"; + } else if(strcmp(*video_codec, "auto") == 0) { + *video_codec = switch_video_codec_to_usable_hardware_encoder(gsr_info); + if(!*video_codec) { + *video_codec = "h264"; + *encoder = "cpu"; + } + } + *container = change_container_if_codec_not_supported(*video_codec, *container); + } + + bool Overlay::on_press_start_replay(bool disable_notification, bool finished_selection) { + if(region_selector.is_started() || window_selector.is_started()) + return false; + switch(recording_status) { case RecordingStatus::NONE: case RecordingStatus::REPLAY: break; case RecordingStatus::RECORD: - show_notification("Unable to start replay when recording.\nStop recording before starting replay.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD); - return; + show_notification("Unable to start replay when recording.\nStop recording before starting replay.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD); + return false; case RecordingStatus::STREAM: - show_notification("Unable to start replay when streaming.\nStop streaming before starting replay.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM); - return; + show_notification("Unable to start replay when streaming.\nStop streaming before starting replay.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM); + return false; } paused = false; - - // window->close(); - // usleep(1000 * 50); // 50 milliseconds + replay_save_show_notification = false; + try_replay_startup = false; close_gpu_screen_recorder_output(); @@ -1597,47 +2472,64 @@ namespace gsr { gpu_screen_recorder_process = -1; recording_status = RecordingStatus::NONE; + replay_save_duration_min = 0; update_ui_replay_stopped(); // TODO: Show this with a slight delay to make sure it doesn't show up in the video if(!disable_notification && config.replay_config.show_replay_stopped_notifications) - show_notification("Replay stopped", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); - return; + show_notification("Replay stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); + + return true; } - if(!validate_capture_target(gsr_info, config.replay_config.record_options.record_area_option)) { + const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info); + recording_capture_target = get_capture_target(config.replay_config.record_options.record_area_option, capture_options); + if(!validate_capture_target(recording_capture_target, capture_options)) { char err_msg[256]; - snprintf(err_msg, sizeof(err_msg), "Failed to start replay, capture target \"%s\" is invalid. Please change capture target in settings", config.replay_config.record_options.record_area_option.c_str()); - show_notification(err_msg, 3.0, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::REPLAY); - return; + snprintf(err_msg, sizeof(err_msg), "Failed to start replay, capture target \"%s\" is invalid.\nPlease change capture target in settings", recording_capture_target.c_str()); + show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY); + return false; + } + + if(config.replay_config.record_options.record_area_option == "region" && !finished_selection) { + start_region_capture = true; + on_region_selected = [disable_notification, this]() { + on_press_start_replay(disable_notification, true); + }; + return false; + } + + if(config.replay_config.record_options.record_area_option == "window" && !finished_selection) { + start_window_capture = true; + on_window_selected = [disable_notification, this]() { + on_press_start_replay(disable_notification, true); + }; + return false; } // TODO: Validate input, fallback to valid values const std::string fps = std::to_string(config.replay_config.record_options.fps); const std::string video_bitrate = std::to_string(config.replay_config.record_options.video_bitrate); const std::string output_directory = config.replay_config.save_directory; - const std::vector<std::string> audio_tracks = create_audio_tracks_real_names(config.replay_config.record_options.audio_tracks, config.replay_config.record_options.application_audio_invert, gsr_info); - const std::string audio_tracks_merged = merge_audio_tracks(audio_tracks); + const std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.replay_config.record_options.audio_tracks_list, gsr_info); const std::string framerate_mode = config.replay_config.record_options.framerate_mode == "auto" ? "vfr" : config.replay_config.record_options.framerate_mode; const std::string replay_time = std::to_string(config.replay_config.replay_time); + const char *container = config.replay_config.container.c_str(); const char *video_codec = config.replay_config.record_options.video_codec.c_str(); const char *encoder = "gpu"; - if(strcmp(video_codec, "h264_software") == 0) { - video_codec = "h264"; - encoder = "cpu"; - } + choose_video_codec_and_container_with_fallback(gsr_info, &video_codec, &container, &encoder); - char region[64]; - region[0] = '\0'; + char size[64]; + size[0] = '\0'; if(config.replay_config.record_options.record_area_option == "focused") - snprintf(region, sizeof(region), "%dx%d", (int)config.replay_config.record_options.record_area_width, (int)config.replay_config.record_options.record_area_height); + snprintf(size, sizeof(size), "%dx%d", (int)config.replay_config.record_options.record_area_width, (int)config.replay_config.record_options.record_area_height); if(config.replay_config.record_options.record_area_option != "focused" && config.replay_config.record_options.change_video_resolution) - snprintf(region, sizeof(region), "%dx%d", (int)config.replay_config.record_options.video_width, (int)config.replay_config.record_options.video_height); + snprintf(size, sizeof(size), "%dx%d", (int)config.replay_config.record_options.video_width, (int)config.replay_config.record_options.video_height); std::vector<const char*> args = { - "gpu-screen-recorder", "-w", config.replay_config.record_options.record_area_option.c_str(), - "-c", config.replay_config.container.c_str(), + "gpu-screen-recorder", "-w", recording_capture_target.c_str(), + "-c", container, "-ac", config.replay_config.record_options.audio_codec.c_str(), "-cursor", config.replay_config.record_options.record_cursor ? "yes" : "no", "-cr", config.replay_config.record_options.color_range.c_str(), @@ -1650,23 +2542,36 @@ namespace gsr { "-o", output_directory.c_str() }; - add_common_gpu_screen_recorder_args(args, config.replay_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged); + if(config.replay_config.restart_replay_on_save && gsr_info.system_info.gsr_version >= GsrVersion{5, 0, 3}) { + args.push_back("-restart-replay-on-save"); + args.push_back("yes"); + } + + if(gsr_info.system_info.gsr_version >= GsrVersion{5, 5, 0}) { + args.push_back("-replay-storage"); + args.push_back(config.replay_config.replay_storage.c_str()); + } + + char region_str[128]; + add_common_gpu_screen_recorder_args(args, config.replay_config.record_options, audio_tracks, video_bitrate, size, region_str, sizeof(region_str), region_selector); + + if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) { + args.push_back("-ro"); + args.push_back(config.record_config.save_directory.c_str()); + } args.push_back(nullptr); gpu_screen_recorder_process = exec_program(args.data(), &gpu_screen_recorder_process_output_fd); if(gpu_screen_recorder_process == -1) { - // TODO: Show notification failed to start + show_notification("Failed to launch gpu-screen-recorder to start replay", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY); + return false; } else { recording_status = RecordingStatus::REPLAY; update_ui_replay_started(); } - const int fdl = fcntl(gpu_screen_recorder_process_output_fd, F_GETFL); - fcntl(gpu_screen_recorder_process_output_fd, F_SETFL, fdl | O_NONBLOCK); - gpu_screen_recorder_process_output_file = fdopen(gpu_screen_recorder_process_output_fd, "r"); - if(gpu_screen_recorder_process_output_file) - gpu_screen_recorder_process_output_fd = -1; + prepare_gsr_output_for_reading(); // TODO: Start recording after this notification has disappeared to make sure it doesn't show up in the video. // Make clear to the user that the recording starts after the notification is gone. @@ -1677,27 +2582,62 @@ namespace gsr { // TODO: Do not run this is a daemon. Instead get the pid and when launching another notification close the current notification // program and start another one. This can also be used to check when the notification has finished by checking with waitpid NOWAIT // to see when the program has exit. - if(!disable_notification && config.replay_config.show_replay_started_notifications) - show_notification("Replay has started", 3.0, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::REPLAY); + if(!disable_notification && config.replay_config.show_replay_started_notifications) { + char msg[256]; + snprintf(msg, sizeof(msg), "Started replaying %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str()); + show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::REPLAY, recording_capture_target.c_str()); + } + + return true; } - void Overlay::on_press_start_record() { + void Overlay::on_press_start_record(bool finished_selection) { + if(region_selector.is_started() || window_selector.is_started()) + return; + switch(recording_status) { case RecordingStatus::NONE: case RecordingStatus::RECORD: break; - case RecordingStatus::REPLAY: - show_notification("Unable to start recording when replay is turned on.\nTurn off replay before starting recording.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); + case RecordingStatus::REPLAY: { + if(gpu_screen_recorder_process <= 0) + return; + + if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) { + if(!replay_recording) { + if(config.record_config.show_recording_started_notifications) + show_notification("Started recording in the replay session", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD); + update_ui_recording_started(); + } + replay_recording = true; + kill(gpu_screen_recorder_process, SIGRTMIN); + } else { + show_notification("Unable to start recording when replay is turned on.\nTurn off replay before starting recording.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), get_color_theme().tint_color, NotificationType::REPLAY); + } return; - case RecordingStatus::STREAM: - show_notification("Unable to start recording when streaming.\nStop streaming before starting recording.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM); + } + case RecordingStatus::STREAM: { + if(gpu_screen_recorder_process <= 0) + return; + + if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) { + if(!replay_recording) { + if(config.record_config.show_recording_started_notifications) + show_notification("Started recording in the streaming session", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD); + update_ui_recording_started(); + } + replay_recording = true; + kill(gpu_screen_recorder_process, SIGRTMIN); + } else { + show_notification("Unable to start recording when streaming.\nStop streaming before starting recording.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), get_color_theme().tint_color, NotificationType::STREAM); + } return; + } } paused = false; - // window->close(); - // usleep(1000 * 50); // 50 milliseconds + close_gpu_screen_recorder_output(); if(gpu_screen_recorder_process > 0) { kill(gpu_screen_recorder_process, SIGINT); @@ -1709,7 +2649,7 @@ namespace gsr { int exit_code = -1; if(WIFEXITED(status)) exit_code = WEXITSTATUS(status); - on_stop_recording(exit_code); + on_stop_recording(exit_code, record_filepath); } gpu_screen_recorder_process = -1; @@ -1719,10 +2659,28 @@ namespace gsr { return; } - if(!validate_capture_target(gsr_info, config.record_config.record_options.record_area_option)) { + const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info); + recording_capture_target = get_capture_target(config.record_config.record_options.record_area_option, capture_options); + if(!validate_capture_target(config.record_config.record_options.record_area_option, capture_options)) { char err_msg[256]; - snprintf(err_msg, sizeof(err_msg), "Failed to start recording, capture target \"%s\" is invalid. Please change capture target in settings", config.record_config.record_options.record_area_option.c_str()); - show_notification(err_msg, 3.0, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::RECORD); + snprintf(err_msg, sizeof(err_msg), "Failed to start recording, capture target \"%s\" is invalid.\nPlease change capture target in settings", recording_capture_target.c_str()); + show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD); + return; + } + + if(config.record_config.record_options.record_area_option == "region" && !finished_selection) { + start_region_capture = true; + on_region_selected = [this]() { + on_press_start_record(true); + }; + return; + } + + if(config.record_config.record_options.record_area_option == "window" && !finished_selection) { + start_window_capture = true; + on_window_selected = [this]() { + on_press_start_record(true); + }; return; } @@ -1732,27 +2690,24 @@ namespace gsr { const std::string fps = std::to_string(config.record_config.record_options.fps); const std::string video_bitrate = std::to_string(config.record_config.record_options.video_bitrate); const std::string output_file = config.record_config.save_directory + "/Video_" + get_date_str() + "." + container_to_file_extension(config.record_config.container.c_str()); - const std::vector<std::string> audio_tracks = create_audio_tracks_real_names(config.record_config.record_options.audio_tracks, config.record_config.record_options.application_audio_invert, gsr_info); - const std::string audio_tracks_merged = merge_audio_tracks(audio_tracks); + const std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.record_config.record_options.audio_tracks_list, gsr_info); const std::string framerate_mode = config.record_config.record_options.framerate_mode == "auto" ? "vfr" : config.record_config.record_options.framerate_mode; + const char *container = config.record_config.container.c_str(); const char *video_codec = config.record_config.record_options.video_codec.c_str(); const char *encoder = "gpu"; - if(strcmp(video_codec, "h264_software") == 0) { - video_codec = "h264"; - encoder = "cpu"; - } + choose_video_codec_and_container_with_fallback(gsr_info, &video_codec, &container, &encoder); - char region[64]; - region[0] = '\0'; + char size[64]; + size[0] = '\0'; if(config.record_config.record_options.record_area_option == "focused") - snprintf(region, sizeof(region), "%dx%d", (int)config.record_config.record_options.record_area_width, (int)config.record_config.record_options.record_area_height); + snprintf(size, sizeof(size), "%dx%d", (int)config.record_config.record_options.record_area_width, (int)config.record_config.record_options.record_area_height); if(config.record_config.record_options.record_area_option != "focused" && config.record_config.record_options.change_video_resolution) - snprintf(region, sizeof(region), "%dx%d", (int)config.record_config.record_options.video_width, (int)config.record_config.record_options.video_height); + snprintf(size, sizeof(size), "%dx%d", (int)config.record_config.record_options.video_width, (int)config.record_config.record_options.video_height); std::vector<const char*> args = { - "gpu-screen-recorder", "-w", config.record_config.record_options.record_area_option.c_str(), - "-c", config.record_config.container.c_str(), + "gpu-screen-recorder", "-w", recording_capture_target.c_str(), + "-c", container, "-ac", config.record_config.record_options.audio_codec.c_str(), "-cursor", config.record_config.record_options.record_cursor ? "yes" : "no", "-cr", config.record_config.record_options.color_range.c_str(), @@ -1764,27 +2719,34 @@ namespace gsr { "-o", output_file.c_str() }; - add_common_gpu_screen_recorder_args(args, config.record_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged); + char region_str[128]; + add_common_gpu_screen_recorder_args(args, config.record_config.record_options, audio_tracks, video_bitrate, size, region_str, sizeof(region_str), region_selector); args.push_back(nullptr); record_filepath = output_file; - gpu_screen_recorder_process = exec_program(args.data(), nullptr); + gpu_screen_recorder_process = exec_program(args.data(), &gpu_screen_recorder_process_output_fd); if(gpu_screen_recorder_process == -1) { - // TODO: Show notification failed to start + show_notification("Failed to launch gpu-screen-recorder to start recording", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD); + return; } else { recording_status = RecordingStatus::RECORD; update_ui_recording_started(); } + prepare_gsr_output_for_reading(); + // TODO: Start recording after this notification has disappeared to make sure it doesn't show up in the video. // Make clear to the user that the recording starts after the notification is gone. // Maybe have the option in notification to show timer until its getting hidden, then the notification can say: // Starting recording in 3... // 2... // 1... - if(config.record_config.show_recording_started_notifications) - show_notification("Recording has started", 3.0, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD); + if(config.record_config.show_recording_started_notifications) { + char msg[256]; + snprintf(msg, sizeof(msg), "Started recording %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str()); + show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str()); + } } static std::string streaming_get_url(const Config &config) { @@ -1795,6 +2757,9 @@ namespace gsr { } else if(config.streaming_config.streaming_service == "youtube") { url += "rtmp://a.rtmp.youtube.com/live2/"; url += config.streaming_config.youtube.stream_key; + } else if(config.streaming_config.streaming_service == "rumble") { + url += "rtmp://rtmp.rumble.com/live/"; + url += config.streaming_config.rumble.stream_key; } else if(config.streaming_config.streaming_service == "custom") { url = config.streaming_config.custom.url; if(url.size() >= 7 && strncmp(url.c_str(), "rtmp://", 7) == 0) @@ -1819,23 +2784,25 @@ namespace gsr { return url; } - void Overlay::on_press_start_stream() { + void Overlay::on_press_start_stream(bool finished_selection) { + if(region_selector.is_started() || window_selector.is_started()) + return; + switch(recording_status) { case RecordingStatus::NONE: case RecordingStatus::STREAM: break; case RecordingStatus::REPLAY: - show_notification("Unable to start streaming when replay is turned on.\nTurn off replay before starting streaming.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); + show_notification("Unable to start streaming when replay is turned on.\nTurn off replay before starting streaming.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY); return; case RecordingStatus::RECORD: - show_notification("Unable to start streaming when recording.\nStop recording before starting streaming.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD); + show_notification("Unable to start streaming when recording.\nStop recording before starting streaming.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD); return; } paused = false; - // window->close(); - // usleep(1000 * 50); // 50 milliseconds + close_gpu_screen_recorder_output(); if(gpu_screen_recorder_process > 0) { kill(gpu_screen_recorder_process, SIGINT); @@ -1851,70 +2818,95 @@ namespace gsr { // TODO: Show this with a slight delay to make sure it doesn't show up in the video if(config.streaming_config.show_streaming_stopped_notifications) - show_notification("Streaming has stopped", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM); + show_notification("Streaming has stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM); return; } - if(!validate_capture_target(gsr_info, config.streaming_config.record_options.record_area_option)) { + const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info); + recording_capture_target = get_capture_target(config.streaming_config.record_options.record_area_option, capture_options); + if(!validate_capture_target(config.streaming_config.record_options.record_area_option, capture_options)) { char err_msg[256]; - snprintf(err_msg, sizeof(err_msg), "Failed to start streaming, capture target \"%s\" is invalid. Please change capture target in settings", config.streaming_config.record_options.record_area_option.c_str()); - show_notification(err_msg, 3.0, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::STREAM); + snprintf(err_msg, sizeof(err_msg), "Failed to start streaming, capture target \"%s\" is invalid.\nPlease change capture target in settings", recording_capture_target.c_str()); + show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM); + return; + } + + if(config.streaming_config.record_options.record_area_option == "region" && !finished_selection) { + start_region_capture = true; + on_region_selected = [this]() { + on_press_start_stream(true); + }; + return; + } + + if(config.streaming_config.record_options.record_area_option == "window" && !finished_selection) { + start_window_capture = true; + on_window_selected = [this]() { + on_press_start_stream(true); + }; return; } // TODO: Validate input, fallback to valid values const std::string fps = std::to_string(config.streaming_config.record_options.fps); const std::string video_bitrate = std::to_string(config.streaming_config.record_options.video_bitrate); - const std::vector<std::string> audio_tracks = create_audio_tracks_real_names(config.streaming_config.record_options.audio_tracks, config.streaming_config.record_options.application_audio_invert, gsr_info); - const std::string audio_tracks_merged = merge_audio_tracks(audio_tracks); + std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.streaming_config.record_options.audio_tracks_list, gsr_info); + // This isn't possible unless the user modified the config file manually, + // But we check it anyways as streaming on some sites can fail if there is more than one audio track + if(audio_tracks.size() > 1) + audio_tracks.resize(1); const std::string framerate_mode = config.streaming_config.record_options.framerate_mode == "auto" ? "vfr" : config.streaming_config.record_options.framerate_mode; + const char *container = "flv"; + if(config.streaming_config.streaming_service == "custom") + container = config.streaming_config.custom.container.c_str(); const char *video_codec = config.streaming_config.record_options.video_codec.c_str(); const char *encoder = "gpu"; - if(strcmp(video_codec, "h264_software") == 0) { - video_codec = "h264"; - encoder = "cpu"; - } - - std::string container = "flv"; - if(config.streaming_config.streaming_service == "custom") - container = config.streaming_config.custom.container; + choose_video_codec_and_container_with_fallback(gsr_info, &video_codec, &container, &encoder); const std::string url = streaming_get_url(config); - char region[64]; - region[0] = '\0'; + char size[64]; + size[0] = '\0'; if(config.record_config.record_options.record_area_option == "focused") - snprintf(region, sizeof(region), "%dx%d", (int)config.streaming_config.record_options.record_area_width, (int)config.streaming_config.record_options.record_area_height); + snprintf(size, sizeof(size), "%dx%d", (int)config.streaming_config.record_options.record_area_width, (int)config.streaming_config.record_options.record_area_height); if(config.record_config.record_options.record_area_option != "focused" && config.streaming_config.record_options.change_video_resolution) - snprintf(region, sizeof(region), "%dx%d", (int)config.streaming_config.record_options.video_width, (int)config.streaming_config.record_options.video_height); + snprintf(size, sizeof(size), "%dx%d", (int)config.streaming_config.record_options.video_width, (int)config.streaming_config.record_options.video_height); std::vector<const char*> args = { - "gpu-screen-recorder", "-w", config.streaming_config.record_options.record_area_option.c_str(), - "-c", container.c_str(), + "gpu-screen-recorder", "-w", recording_capture_target.c_str(), + "-c", container, "-ac", config.streaming_config.record_options.audio_codec.c_str(), "-cursor", config.streaming_config.record_options.record_cursor ? "yes" : "no", "-cr", config.streaming_config.record_options.color_range.c_str(), "-fm", framerate_mode.c_str(), "-encoder", encoder, "-f", fps.c_str(), - "-f", fps.c_str(), "-v", "no", "-o", url.c_str() }; - add_common_gpu_screen_recorder_args(args, config.streaming_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged); + char region_str[128]; + add_common_gpu_screen_recorder_args(args, config.streaming_config.record_options, audio_tracks, video_bitrate, size, region_str, sizeof(region_str), region_selector); + + if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) { + args.push_back("-ro"); + args.push_back(config.record_config.save_directory.c_str()); + } args.push_back(nullptr); - gpu_screen_recorder_process = exec_program(args.data(), nullptr); + gpu_screen_recorder_process = exec_program(args.data(), &gpu_screen_recorder_process_output_fd); if(gpu_screen_recorder_process == -1) { - // TODO: Show notification failed to start + show_notification("Failed to launch gpu-screen-recorder to start streaming", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM); + return; } else { recording_status = RecordingStatus::STREAM; update_ui_streaming_started(); } + prepare_gsr_output_for_reading(); + // TODO: Start recording after this notification has disappeared to make sure it doesn't show up in the video. // Make clear to the user that the recording starts after the notification is gone. // Maybe have the option in notification to show timer until its getting hidden, then the notification can say: @@ -1924,11 +2916,88 @@ namespace gsr { // TODO: Do not run this is a daemon. Instead get the pid and when launching another notification close the current notification // program and start another one. This can also be used to check when the notification has finished by checking with waitpid NOWAIT // to see when the program has exit. - if(config.streaming_config.show_streaming_started_notifications) - show_notification("Streaming has started", 3.0, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM); + if(config.streaming_config.show_streaming_started_notifications) { + char msg[256]; + snprintf(msg, sizeof(msg), "Started streaming %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str()); + show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM, recording_capture_target.c_str()); + } } - bool Overlay::update_compositor_texture(const mgl_monitor *monitor) { + void Overlay::on_press_take_screenshot(bool finished_selection, bool force_region_capture) { + if(region_selector.is_started() || window_selector.is_started()) + return; + + if(gpu_screen_recorder_screenshot_process > 0) { + fprintf(stderr, "Error: failed to take screenshot, another screenshot is currently being saved\n"); + return; + } + + const bool region_capture = config.screenshot_config.record_area_option == "region" || force_region_capture; + const char *record_area_option = region_capture ? "region" : config.screenshot_config.record_area_option.c_str(); + const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info); + screenshot_capture_target = get_capture_target(record_area_option, capture_options); + if(!validate_capture_target(record_area_option, capture_options)) { + char err_msg[256]; + snprintf(err_msg, sizeof(err_msg), "Failed to take a screenshot, capture target \"%s\" is invalid.\nPlease change capture target in settings", screenshot_capture_target.c_str()); + show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::SCREENSHOT); + return; + } + + if(region_capture && !finished_selection) { + start_region_capture = true; + on_region_selected = [this, force_region_capture]() { + usleep(200 * 1000); // Hack: wait 0.2 seconds before taking a screenshot to allow user to move cursor away. TODO: Remove this + on_press_take_screenshot(true, force_region_capture); + }; + return; + } + + if(config.screenshot_config.record_area_option == "window" && !finished_selection) { + start_window_capture = true; + on_window_selected = [this, force_region_capture]() { + on_press_take_screenshot(true, force_region_capture); + }; + return; + } + + // TODO: Validate input, fallback to valid values + const std::string output_file = config.screenshot_config.save_directory + "/Screenshot_" + get_date_str() + "." + config.screenshot_config.image_format; // TODO: Validate image format + + std::vector<const char*> args = { + "gpu-screen-recorder", "-w", screenshot_capture_target.c_str(), + "-cursor", config.screenshot_config.record_cursor ? "yes" : "no", + "-v", "no", + "-q", config.screenshot_config.image_quality.c_str(), + "-o", output_file.c_str() + }; + + char size[64]; + size[0] = '\0'; + if(config.screenshot_config.change_image_resolution) { + snprintf(size, sizeof(size), "%dx%d", (int)config.screenshot_config.image_width, (int)config.screenshot_config.image_height); + args.push_back("-s"); + args.push_back(size); + } + + if(config.screenshot_config.restore_portal_session) { + args.push_back("-restore-portal-session"); + args.push_back("yes"); + } + + char region_str[128]; + if(region_capture) + add_region_command(args, region_str, sizeof(region_str), region_selector); + + args.push_back(nullptr); + + screenshot_filepath = output_file; + gpu_screen_recorder_screenshot_process = exec_program(args.data(), nullptr); + if(gpu_screen_recorder_screenshot_process == -1) { + show_notification("Failed to launch gpu-screen-recorder to take a screenshot", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::SCREENSHOT); + } + } + + bool Overlay::update_compositor_texture(const Monitor &monitor) { window_texture_deinit(&window_texture); window_texture_sprite.set_texture(nullptr); screenshot_texture.clear(); @@ -1941,15 +3010,17 @@ namespace gsr { return false; bool window_texture_loaded = false; - const Window focused_window = get_focused_window(display, WindowCaptureType::FOCUSED); - if(is_window_fullscreen_on_monitor(display, focused_window, monitor) && focused_window) + Window focused_window = get_focused_window(display, WindowCaptureType::CURSOR); + if(!focused_window) + focused_window = get_focused_window(display, WindowCaptureType::FOCUSED); + if(focused_window && is_window_fullscreen_on_monitor(display, focused_window, monitor)) window_texture_loaded = window_texture_init(&window_texture, display, mgl_window_get_egl_display(window->internal_window()), focused_window, egl_funcs) == 0; if(window_texture_loaded && window_texture.texture_id) { window_texture_texture = mgl::Texture(window_texture.texture_id, MGL_TEXTURE_FORMAT_RGB); window_texture_sprite.set_texture(&window_texture_texture); } else { - XImage *img = XGetImage(display, DefaultRootWindow(display), monitor->pos.x, monitor->pos.y, monitor->size.x, monitor->size.y, AllPlanes, ZPixmap); + XImage *img = XGetImage(display, DefaultRootWindow(display), monitor.position.x, monitor.position.y, monitor.size.x, monitor.size.y, AllPlanes, ZPixmap); if(!img) fprintf(stderr, "Error: failed to take a screenshot\n"); @@ -1971,8 +3042,8 @@ namespace gsr { mgl_context *context = mgl_get_context(); Display *display = (Display*)context->connection; - XRaiseWindow(display, window->get_system_handle()); + XRaiseWindow(display, (Window)window->get_system_handle()); XFlush(display); } } -}
\ No newline at end of file +} diff --git a/src/Process.cpp b/src/Process.cpp index a8e5fb5..c02753a 100644 --- a/src/Process.cpp +++ b/src/Process.cpp @@ -22,14 +22,33 @@ namespace gsr { fprintf(stderr, "\n"); } - bool exec_program_daemonized(const char **args) { + static bool is_number(const char *str) { + for(int i = 0; str[i]; ++i) { + char c = str[i]; + if(c < '0' || c > '9') + return false; + } + return true; + } + + static int count_num_args(const char **args) { + int num_args = 0; + while(*args) { + ++num_args; + ++args; + } + return num_args; + } + + bool exec_program_daemonized(const char **args, bool debug) { /* 1 argument */ if(args[0] == nullptr) return false; - debug_print_args(args); + if(debug) + debug_print_args(args); - pid_t pid = vfork(); + const pid_t pid = vfork(); if(pid == -1) { perror("Failed to vfork"); return false; @@ -38,10 +57,10 @@ namespace gsr { signal(SIGHUP, SIG_IGN); // Daemonize child to make the parent the init process which will reap the zombie child - pid_t second_child = vfork(); + const pid_t second_child = vfork(); if(second_child == 0) { // child execvp(args[0], (char* const*)args); - perror("execvp"); + perror(args[0]); _exit(127); } else if(second_child != -1) { // TODO: @@ -54,7 +73,7 @@ namespace gsr { return true; } - pid_t exec_program(const char **args, int *read_fd) { + pid_t exec_program(const char **args, int *read_fd, bool debug) { if(read_fd) *read_fd = -1; @@ -66,9 +85,10 @@ namespace gsr { if(pipe(fds) == -1) return -1; - debug_print_args(args); + if(debug) + debug_print_args(args); - pid_t pid = vfork(); + const pid_t pid = vfork(); if(pid == -1) { close(fds[PIPE_READ]); close(fds[PIPE_WRITE]); @@ -80,7 +100,7 @@ namespace gsr { close(fds[PIPE_WRITE]); execvp(args[0], (char* const*)args); - perror("execvp"); + perror(args[0]); _exit(127); } else { /* parent */ close(fds[PIPE_WRITE]); @@ -92,10 +112,10 @@ namespace gsr { } } - int exec_program_get_stdout(const char **args, std::string &result) { + int exec_program_get_stdout(const char **args, std::string &result, bool debug) { result.clear(); int read_fd = -1; - pid_t process_id = exec_program(args, &read_fd); + const pid_t process_id = exec_program(args, &read_fd, debug); if(process_id == -1) return -1; @@ -110,8 +130,6 @@ namespace gsr { exit_status = -1; break; } - - buffer[bytes_read] = '\0'; result.append(buffer, bytes_read); } @@ -134,10 +152,45 @@ namespace gsr { return exit_status; } - bool read_cmdline_arg0(const char *filepath, char *output_buffer) { + int exec_program_on_host_get_stdout(const char **args, std::string &result, bool debug) { + if(count_num_args(args) > 64 - 3) { + fprintf(stderr, "Error: too many arguments when trying to launch \"%s\"\n", args[0]); + return -1; + } + + const bool inside_flatpak = getenv("FLATPAK_ID") != NULL; + if(inside_flatpak) { + // Assumes programs wont need more than 64 - 3 args + const char *modified_args[64] = { "flatpak-spawn", "--host", "--" }; + for(int i = 3; i < 64; ++i) { + const char *arg = args[i - 3]; + if(!arg) { + modified_args[i] = nullptr; + break; + } + modified_args[i] = arg; + } + return exec_program_get_stdout(modified_args, result, debug); + } else { + return exec_program_get_stdout(args, result, debug); + } + } + + static const char *get_basename(const char *path, int size) { + for(int i = size - 1; i >= 0; --i) { + if(path[i] == '/') + return path + i + 1; + } + return path; + } + + // |output_buffer| should be at least PATH_MAX in size + bool read_cmdline_arg0(const char *filepath, char *output_buffer, int output_buffer_size) { output_buffer[0] = '\0'; + const char *arg0_start = NULL; const char *arg0_end = NULL; + int arg0_size = 0; int fd = open(filepath, O_RDONLY); if(fd == -1) return false; @@ -147,17 +200,50 @@ namespace gsr { if(bytes_read == -1) goto err; - arg0_end = (const char*)memchr(buffer, '\0', bytes_read); + arg0_start = buffer; + arg0_end = (const char*)memchr(arg0_start, '\0', bytes_read); if(!arg0_end) goto err; - memcpy(output_buffer, buffer, arg0_end - buffer); - output_buffer[arg0_end - buffer] = '\0'; - close(fd); - return true; + arg0_start = get_basename(arg0_start, arg0_end - arg0_start); + arg0_size = arg0_end - arg0_start; + if(arg0_size + 1 <= output_buffer_size) { + memcpy(output_buffer, arg0_start, arg0_size); + output_buffer[arg0_size] = '\0'; + close(fd); + return true; + } err: close(fd); return false; } + + pid_t pidof(const char *process_name, pid_t ignore_pid) { + pid_t result = -1; + DIR *dir = opendir("/proc"); + if(!dir) + return -1; + + char cmdline_filepath[PATH_MAX]; + char arg0[PATH_MAX]; + + struct dirent *entry; + while((entry = readdir(dir)) != NULL) { + if(!is_number(entry->d_name)) + continue; + + snprintf(cmdline_filepath, sizeof(cmdline_filepath), "/proc/%s/cmdline", entry->d_name); + if(read_cmdline_arg0(cmdline_filepath, arg0, sizeof(arg0)) && strcmp(process_name, arg0) == 0) { + const pid_t pid = atoi(entry->d_name); + if(pid != ignore_pid) { + result = pid; + break; + } + } + } + + closedir(dir); + return result; + } }
\ No newline at end of file diff --git a/src/RegionSelector.cpp b/src/RegionSelector.cpp new file mode 100644 index 0000000..89a0209 --- /dev/null +++ b/src/RegionSelector.cpp @@ -0,0 +1,450 @@ +#include "../include/RegionSelector.hpp" + +#include <stdio.h> +#include <string.h> + +#include <X11/extensions/XInput2.h> +#include <X11/extensions/Xrandr.h> +#include <X11/extensions/shape.h> + +namespace gsr { + static const int cursor_window_size = 32; + static const int cursor_thickness = 5; + static const int region_border_size = 2; + + static bool xinput_is_supported(Display *dpy, int *xi_opcode) { + *xi_opcode = 0; + int query_event = 0; + int query_error = 0; + if(!XQueryExtension(dpy, "XInputExtension", xi_opcode, &query_event, &query_error)) { + fprintf(stderr, "error: RegionSelector: X Input extension not available\n"); + return false; + } + + int major = 2; + int minor = 1; + int retval = XIQueryVersion(dpy, &major, &minor); + if(retval != Success) { + fprintf(stderr, "error: RegionSelector: XInput 2.1 is not supported\n"); + return false; + } + + return true; + } + + static int max_int(int a, int b) { + return a >= b ? a : b; + } + + static void set_region_rectangle(Display *dpy, Window window, int x, int y, int width, int height, int border_size) { + if(width < 0) { + x += width; + width = abs(width); + } + + if(height < 0) { + y += height; + height = abs(height); + } + + XRectangle rectangles[] = { + { + (short)max_int(0, x), (short)max_int(0, y), + (unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height) + }, // Left + { + (short)max_int(0, x + width - border_size), (short)max_int(0, y), + (unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height) + }, // Right + { + (short)max_int(0, x + border_size), (short)max_int(0, y), + (unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size) + }, // Top + { + (short)max_int(0, x + border_size), (short)max_int(0, y + height - border_size), + (unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size) + }, // Bottom + }; + XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 4, ShapeSet, Unsorted); + XFlush(dpy); + } + + static void set_window_shape_cross(Display *dpy, Window window, int window_width, int window_height, int thickness) { + XRectangle rectangles[] = { + { + (short)(window_width / 2 - thickness / 2), (short)0, + (unsigned short)thickness, (unsigned short)window_height + }, // Vertical + { + (short)(0), (short)(window_height / 2 - thickness / 2), + (unsigned short)window_width, (unsigned short)thickness + }, // Horizontal + }; + XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 2, ShapeSet, Unsorted); + XFlush(dpy); + } + + static void draw_rectangle(Display *dpy, Window window, GC gc, int x, int y, int width, int height) { + if(width < 0) { + x += width; + width = abs(width); + } + + if(height < 0) { + y += height; + height = abs(height); + } + + XDrawRectangle(dpy, window, gc, x, y, width, height); + } + + static Window create_cursor_window(Display *dpy, int width, int height, XVisualInfo *vinfo, unsigned long background_pixel) { + XSetWindowAttributes window_attr; + window_attr.background_pixel = background_pixel; + window_attr.border_pixel = 0; + window_attr.override_redirect = true; + window_attr.event_mask = StructureNotifyMask | PointerMotionMask; + window_attr.colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), vinfo->visual, AllocNone); + const Window window = XCreateWindow(dpy, DefaultRootWindow(dpy), 0, 0, width, height, 0, vinfo->depth, InputOutput, vinfo->visual, CWBackPixel | CWBorderPixel | CWOverrideRedirect | CWEventMask | CWColormap, &window_attr); + if(window) { + set_window_size_not_resizable(dpy, window, width, height); + set_window_shape_cross(dpy, window, width, height, cursor_thickness); + make_window_click_through(dpy, window); + } + return window; + } + + static void draw_rectangle_around_selected_monitor(Display *dpy, Window window, GC region_gc, int region_border_size, bool is_wayland, const std::vector<Monitor> &monitors, mgl::vec2i cursor_pos) { + const Monitor *focused_monitor = nullptr; + for(const Monitor &monitor : monitors) { + if(cursor_pos.x >= monitor.position.x && cursor_pos.x <= monitor.position.x + monitor.size.x + && cursor_pos.y >= monitor.position.y && cursor_pos.y <= monitor.position.y + monitor.size.y) + { + focused_monitor = &monitor; + break; + } + } + + int x = 0; + int y = 0; + int width = 0; + int height = 0; + if(focused_monitor) { + x = focused_monitor->position.x; + y = focused_monitor->position.y; + width = focused_monitor->size.x; + height = focused_monitor->size.y; + } + + if(is_wayland) + draw_rectangle(dpy, window, region_gc, x, y, width, height); + else + set_region_rectangle(dpy, window, x, y, width, height, region_border_size); + } + + static void update_cursor_window(Display *dpy, Window window, Window cursor_window, bool is_wayland, int cursor_x, int cursor_y, int cursor_window_size, int thickness, GC cursor_gc) { + if(is_wayland) { + const int x = cursor_x - cursor_window_size / 2; + const int y = cursor_y - cursor_window_size / 2; + XFillRectangle(dpy, window, cursor_gc, x + cursor_window_size / 2 - thickness / 2 , y, thickness, cursor_window_size); + XFillRectangle(dpy, window, cursor_gc, x, y + cursor_window_size / 2 - thickness / 2, cursor_window_size, thickness); + } else if(cursor_window) { + XMoveWindow(dpy, cursor_window, cursor_x - cursor_window_size / 2, cursor_y - cursor_window_size / 2); + } + XFlush(dpy); + } + + static bool is_xwayland(Display *dpy) { + int opcode, event, error; + return XQueryExtension(dpy, "XWAYLAND", &opcode, &event, &error); + } + + static unsigned long mgl_color_to_x11_color(mgl::Color color) { + if(color.a == 0) + return 0; + return ((uint32_t)color.a << 24) | (((uint32_t)color.r * color.a / 0xFF) << 16) | (((uint32_t)color.g * color.a / 0xFF) << 8) | ((uint32_t)color.b * color.a / 0xFF); + } + + RegionSelector::RegionSelector() { + + } + + RegionSelector::~RegionSelector() { + stop(); + } + + bool RegionSelector::start(mgl::Color border_color) { + if(dpy) + return false; + + const unsigned long border_color_x11 = mgl_color_to_x11_color(border_color); + dpy = XOpenDisplay(nullptr); + if(!dpy) { + fprintf(stderr, "Error: RegionSelector::start: failed to connect to the X11 server\n"); + return false; + } + + xi_opcode = 0; + if(!xinput_is_supported(dpy, &xi_opcode)) { + fprintf(stderr, "Error: RegionSelector::start: xinput not supported on your system\n"); + stop(); + return false; + } + + is_wayland = is_xwayland(dpy); + monitors = get_monitors(dpy); + + Window x11_cursor_window = None; + cursor_pos = get_cursor_position(dpy, &x11_cursor_window); + region.pos = {0, 0}; + region.size = {0, 0}; + + XVisualInfo vinfo; + memset(&vinfo, 0, sizeof(vinfo)); + XMatchVisualInfo(dpy, DefaultScreen(dpy), 32, TrueColor, &vinfo); + region_window_colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), vinfo.visual, AllocNone); + + XSetWindowAttributes window_attr; + window_attr.background_pixel = is_wayland ? 0 : border_color_x11; + window_attr.border_pixel = 0; + window_attr.override_redirect = true; + window_attr.event_mask = StructureNotifyMask | PointerMotionMask | ButtonPressMask | ButtonReleaseMask; + window_attr.colormap = region_window_colormap; + + Screen *screen = XDefaultScreenOfDisplay(dpy); + region_window = XCreateWindow(dpy, DefaultRootWindow(dpy), 0, 0, XWidthOfScreen(screen), XHeightOfScreen(screen), 0, + vinfo.depth, InputOutput, vinfo.visual, CWBackPixel | CWBorderPixel | CWOverrideRedirect | CWEventMask | CWColormap, &window_attr); + if(!region_window) { + fprintf(stderr, "Error: RegionSelector::start: failed to create region window\n"); + stop(); + return false; + } + set_window_size_not_resizable(dpy, region_window, XWidthOfScreen(screen), XHeightOfScreen(screen)); + + if(!is_wayland) { + cursor_window = create_cursor_window(dpy, cursor_window_size, cursor_window_size, &vinfo, border_color_x11); + if(!cursor_window) + fprintf(stderr, "Warning: RegionSelector::start: failed to create cursor window\n"); + set_region_rectangle(dpy, region_window, 0, 0, 0, 0, 0); + } + + XGCValues region_gc_values; + memset(®ion_gc_values, 0, sizeof(region_gc_values)); + region_gc_values.foreground = border_color_x11; + region_gc_values.line_width = region_border_size; + region_gc_values.line_style = LineSolid; + region_gc = XCreateGC(dpy, region_window, GCForeground | GCLineWidth | GCLineStyle, ®ion_gc_values); + + XGCValues cursor_gc_values; + memset(&cursor_gc_values, 0, sizeof(cursor_gc_values)); + cursor_gc_values.foreground = border_color_x11; + cursor_gc_values.line_width = cursor_thickness; + cursor_gc_values.line_style = LineSolid; + cursor_gc = XCreateGC(dpy, region_window, GCForeground | GCLineWidth | GCLineStyle, &cursor_gc_values); + + if(!region_gc || !cursor_gc) { + fprintf(stderr, "Error: RegionSelector::start: failed to create gc\n"); + stop(); + return false; + } + + XMapWindow(dpy, region_window); + make_window_sticky(dpy, region_window); + hide_window_from_taskbar(dpy, region_window); + XFixesHideCursor(dpy, region_window); + XGrabPointer(dpy, DefaultRootWindow(dpy), True, ButtonPressMask | ButtonReleaseMask | ButtonMotionMask, GrabModeAsync, GrabModeAsync, None, None, CurrentTime); + XGrabKeyboard(dpy, DefaultRootWindow(dpy), True, GrabModeAsync, GrabModeAsync, CurrentTime); + xi_grab_all_mouse_devices(dpy); + XFlush(dpy); + + window_set_fullscreen(dpy, region_window, true); + + if(!is_wayland || x11_cursor_window) + update_cursor_window(dpy, region_window, cursor_window, is_wayland, cursor_pos.x, cursor_pos.y, cursor_window_size, cursor_thickness, cursor_gc); + + if(cursor_window) { + XMapWindow(dpy, cursor_window); + make_window_sticky(dpy, cursor_window); + hide_window_from_taskbar(dpy, cursor_window); + } + + draw_rectangle_around_selected_monitor(dpy, region_window, region_gc, region_border_size, is_wayland, monitors, cursor_pos); + + XFlush(dpy); + selected = false; + canceled = false; + return true; + } + + void RegionSelector::stop() { + if(!dpy) + return; + + XWarpPointer(dpy, DefaultRootWindow(dpy), DefaultRootWindow(dpy), 0, 0, 0, 0, cursor_pos.x, cursor_pos.y); + xi_warp_all_mouse_devices(dpy, cursor_pos); + XFixesShowCursor(dpy, region_window); + + XUngrabPointer(dpy, CurrentTime); + XUngrabKeyboard(dpy, CurrentTime); + xi_ungrab_all_mouse_devices(dpy); + XFlush(dpy); + + if(region_gc) { + XFreeGC(dpy, region_gc); + region_gc = nullptr; + } + + if(cursor_gc) { + XFreeGC(dpy, cursor_gc); + cursor_gc = nullptr; + } + + if(region_window_colormap) { + XFreeColormap(dpy, region_window_colormap); + region_window_colormap = 0; + } + + if(region_window) { + XDestroyWindow(dpy, region_window); + region_window = 0; + } + + XCloseDisplay(dpy); + dpy = nullptr; + selecting_region = false; + } + + bool RegionSelector::is_started() const { + return dpy != nullptr; + } + + bool RegionSelector::failed() const { + return !dpy; + } + + bool RegionSelector::poll_events() { + if(!dpy || selected) + return false; + + XEvent xev; + while(XPending(dpy)) { + XNextEvent(dpy, &xev); + + if(xev.type == KeyRelease && XKeycodeToKeysym(dpy, xev.xkey.keycode, 0) == XK_Escape) { + canceled = true; + selected = false; + stop(); + break; + } + + XGenericEventCookie *cookie = &xev.xcookie; + if(cookie->type != GenericEvent || cookie->extension != xi_opcode || !XGetEventData(dpy, cookie)) + continue; + + const XIDeviceEvent *de = (XIDeviceEvent*)cookie->data; + switch(cookie->evtype) { + case XI_ButtonPress: { + on_button_press(de); + break; + } + case XI_ButtonRelease: { + on_button_release(de); + break; + } + case XI_Motion: { + on_mouse_motion(de); + break; + } + } + XFreeEventData(dpy, cookie); + + if(selected) { + stop(); + break; + } + } + return true; + } + + bool RegionSelector::take_selection() { + const bool result = selected; + selected = false; + return result; + } + + bool RegionSelector::take_canceled() { + const bool result = canceled; + canceled = false; + return result; + } + + Region RegionSelector::get_selection() const { + return region; + } + + void RegionSelector::on_button_press(const void *de) { + const XIDeviceEvent *device_event = (XIDeviceEvent*)de; + if(device_event->detail != Button1) + return; + + region.pos = { (int)device_event->root_x, (int)device_event->root_y }; + selecting_region = true; + } + + void RegionSelector::on_button_release(const void *de) { + const XIDeviceEvent *device_event = (XIDeviceEvent*)de; + if(device_event->detail != Button1) + return; + + if(!selecting_region) + return; + + if(is_wayland) { + XClearWindow(dpy, region_window); + XFlush(dpy); + } else { + set_region_rectangle(dpy, region_window, 0, 0, 0, 0, 0); + } + selecting_region = false; + + cursor_pos = region.pos + region.size; + + if(region.size.x < 0) { + region.pos.x += region.size.x; + region.size.x = abs(region.size.x); + } + + if(region.size.y < 0) { + region.pos.y += region.size.y; + region.size.y = abs(region.size.y); + } + + if(region.size.x > 0) + region.size.x += 1; + + if(region.size.y > 0) + region.size.y += 1; + + selected = true; + } + + void RegionSelector::on_mouse_motion(const void *de) { + const XIDeviceEvent *device_event = (XIDeviceEvent*)de; + XClearWindow(dpy, region_window); + if(selecting_region) { + region.size.x = device_event->root_x - region.pos.x; + region.size.y = device_event->root_y - region.pos.y; + cursor_pos = region.pos + region.size; + + if(is_wayland) + draw_rectangle(dpy, region_window, region_gc, region.pos.x, region.pos.y, region.size.x, region.size.y); + else + set_region_rectangle(dpy, region_window, region.pos.x, region.pos.y, region.size.x, region.size.y, region_border_size); + } else { + cursor_pos = { (int)device_event->root_x, (int)device_event->root_y }; + draw_rectangle_around_selected_monitor(dpy, region_window, region_gc, region_border_size, is_wayland, monitors, cursor_pos); + } + update_cursor_window(dpy, region_window, cursor_window, is_wayland, cursor_pos.x, cursor_pos.y, cursor_window_size, cursor_thickness, cursor_gc); + XFlush(dpy); + } +}
\ No newline at end of file diff --git a/src/Rpc.cpp b/src/Rpc.cpp new file mode 100644 index 0000000..3eec98d --- /dev/null +++ b/src/Rpc.cpp @@ -0,0 +1,133 @@ +#include "../include/Rpc.hpp" +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <limits.h> +#include <string.h> +#include <errno.h> +#include <sys/stat.h> +#include <fcntl.h> + +namespace gsr { + static void get_runtime_filepath(char *buffer, size_t buffer_size, const char *filename) { + char dir[PATH_MAX]; + + const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); + if(runtime_dir) + snprintf(dir, sizeof(dir), "%s", runtime_dir); + else + snprintf(dir, sizeof(dir), "/run/user/%d", geteuid()); + + if(access(dir, F_OK) != 0) + snprintf(dir, sizeof(dir), "/tmp"); + + snprintf(buffer, buffer_size, "%s/%s", dir, filename); + } + + Rpc::~Rpc() { + if(fd > 0) + close(fd); + + if(file) + fclose(file); + + if(!fifo_filepath.empty()) + remove(fifo_filepath.c_str()); + } + + bool Rpc::create(const char *name) { + if(file) { + fprintf(stderr, "Error: Rpc::create: already created/opened\n"); + return false; + } + + char fifo_filepath_tmp[PATH_MAX]; + get_runtime_filepath(fifo_filepath_tmp, sizeof(fifo_filepath_tmp), name); + fifo_filepath = fifo_filepath_tmp; + remove(fifo_filepath.c_str()); + + if(mkfifo(fifo_filepath.c_str(), 0600) != 0) { + fprintf(stderr, "Error: mkfifo failed, error: %s, %s\n", strerror(errno), fifo_filepath.c_str()); + return false; + } + + if(!open_filepath(fifo_filepath.c_str())) { + remove(fifo_filepath.c_str()); + fifo_filepath.clear(); + return false; + } + + return true; + } + + bool Rpc::open(const char *name) { + if(file) { + fprintf(stderr, "Error: Rpc::open: already created/opened\n"); + return false; + } + + char fifo_filepath_tmp[PATH_MAX]; + get_runtime_filepath(fifo_filepath_tmp, sizeof(fifo_filepath_tmp), name); + return open_filepath(fifo_filepath_tmp); + } + + bool Rpc::open_filepath(const char *filepath) { + fd = ::open(filepath, O_RDWR | O_NONBLOCK); + if(fd <= 0) + return false; + + file = fdopen(fd, "r+"); + if(!file) { + close(fd); + fd = 0; + return false; + } + fd = 0; + return true; + } + + bool Rpc::write(const char *str, size_t size) { + if(!file) { + fprintf(stderr, "Error: Rpc::write: fifo not created/opened yet\n"); + return false; + } + + ssize_t offset = 0; + while(offset < (ssize_t)size) { + const ssize_t bytes_written = fwrite(str + offset, 1, size - offset, file); + fflush(file); + if(bytes_written > 0) + offset += bytes_written; + } + return true; + } + + void Rpc::poll() { + if(!file) { + //fprintf(stderr, "Error: Rpc::poll: fifo not created/opened yet\n"); + return; + } + + std::string name; + char line[1024]; + while(fgets(line, sizeof(line), file)) { + int line_len = strlen(line); + if(line_len == 0) + continue; + + if(line[line_len - 1] == '\n') { + line[line_len - 1] = '\0'; + --line_len; + } + + name = line; + auto it = handlers_by_name.find(name); + if(it != handlers_by_name.end()) + it->second(name); + } + } + + bool Rpc::add_handler(const std::string &name, RpcCallback callback) { + return handlers_by_name.insert(std::make_pair(name, std::move(callback))).second; + } +}
\ No newline at end of file diff --git a/src/Theme.cpp b/src/Theme.cpp index a6d1050..2bef3c8 100644 --- a/src/Theme.cpp +++ b/src/Theme.cpp @@ -10,10 +10,11 @@ namespace gsr { static mgl::Color gpu_vendor_to_color(GpuVendor vendor) { switch(vendor) { - case GpuVendor::UNKNOWN: return mgl::Color(221, 0, 49); - case GpuVendor::AMD: return mgl::Color(221, 0, 49); - case GpuVendor::INTEL: return mgl::Color(8, 109, 183); - case GpuVendor::NVIDIA: return mgl::Color(118, 185, 0); + case GpuVendor::UNKNOWN: return mgl::Color(221, 0, 49); + case GpuVendor::AMD: return mgl::Color(221, 0, 49); + case GpuVendor::INTEL: return mgl::Color(8, 109, 183); + case GpuVendor::NVIDIA: return mgl::Color(118, 185, 0); + case GpuVendor::BROADCOM: return mgl::Color(221, 0, 49); } return mgl::Color(221, 0, 49); } @@ -26,6 +27,8 @@ namespace gsr { vendor = GpuVendor::INTEL; else if(color_name == "nvidia") vendor = GpuVendor::NVIDIA; + else if(color_name == "broadcom") + vendor = GpuVendor::BROADCOM; return gpu_vendor_to_color(vendor); } @@ -60,52 +63,85 @@ namespace gsr { if(!theme->title_font_file.load((resources_path + "fonts/NotoSans-Bold.ttf").c_str(), mgl::MemoryMappedFile::LoadOptions{true, false})) goto error; - if(!theme->combobox_arrow_texture.load_from_file((resources_path + "images/combobox_arrow.png").c_str())) + if(!theme->combobox_arrow_texture.load_from_file((resources_path + "images/combobox_arrow.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; if(!theme->settings_texture.load_from_file((resources_path + "images/settings.png").c_str())) goto error; - if(!theme->settings_small_texture.load_from_file((resources_path + "images/settings_small.png").c_str())) + if(!theme->settings_small_texture.load_from_file((resources_path + "images/settings_small.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->folder_texture.load_from_file((resources_path + "images/folder.png").c_str())) + if(!theme->settings_extra_small_texture.load_from_file((resources_path + "images/settings_extra_small.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->up_arrow_texture.load_from_file((resources_path + "images/up_arrow.png").c_str())) + if(!theme->folder_texture.load_from_file((resources_path + "images/folder.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->replay_button_texture.load_from_file((resources_path + "images/replay.png").c_str())) + if(!theme->up_arrow_texture.load_from_file((resources_path + "images/up_arrow.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->record_button_texture.load_from_file((resources_path + "images/record.png").c_str())) + if(!theme->replay_button_texture.load_from_file((resources_path + "images/replay.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->stream_button_texture.load_from_file((resources_path + "images/stream.png").c_str())) + if(!theme->record_button_texture.load_from_file((resources_path + "images/record.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->close_texture.load_from_file((resources_path + "images/cross.png").c_str())) + if(!theme->stream_button_texture.load_from_file((resources_path + "images/stream.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->close_texture.load_from_file((resources_path + "images/cross.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; if(!theme->logo_texture.load_from_file((resources_path + "images/gpu_screen_recorder_logo.png").c_str())) goto error; - if(!theme->checkbox_circle_texture.load_from_file((resources_path + "images/checkbox_circle.png").c_str())) + if(!theme->checkbox_circle_texture.load_from_file((resources_path + "images/checkbox_circle.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->checkbox_background_texture.load_from_file((resources_path + "images/checkbox_background.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->play_texture.load_from_file((resources_path + "images/play.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->checkbox_background_texture.load_from_file((resources_path + "images/checkbox_background.png").c_str())) + if(!theme->stop_texture.load_from_file((resources_path + "images/stop.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->play_texture.load_from_file((resources_path + "images/play.png").c_str())) + if(!theme->pause_texture.load_from_file((resources_path + "images/pause.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->save_texture.load_from_file((resources_path + "images/save.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->screenshot_texture.load_from_file((resources_path + "images/screenshot.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->trash_texture.load_from_file((resources_path + "images/trash.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->ps4_home_texture.load_from_file((resources_path + "images/ps4_home.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->ps4_options_texture.load_from_file((resources_path + "images/ps4_options.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->ps4_dpad_up_texture.load_from_file((resources_path + "images/ps4_dpad_up.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->ps4_dpad_down_texture.load_from_file((resources_path + "images/ps4_dpad_down.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->ps4_dpad_left_texture.load_from_file((resources_path + "images/ps4_dpad_left.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->stop_texture.load_from_file((resources_path + "images/stop.png").c_str())) + if(!theme->ps4_dpad_right_texture.load_from_file((resources_path + "images/ps4_dpad_right.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->pause_texture.load_from_file((resources_path + "images/pause.png").c_str())) + if(!theme->ps4_cross_texture.load_from_file((resources_path + "images/ps4_cross.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->save_texture.load_from_file((resources_path + "images/save.png").c_str())) + if(!theme->ps4_triangle_texture.load_from_file((resources_path + "images/ps4_triangle.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; return true; diff --git a/src/Utils.cpp b/src/Utils.cpp index 6d45196..c36a64a 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -22,6 +22,38 @@ namespace gsr { } } + bool starts_with(std::string_view str, const char *substr) { + size_t len = strlen(substr); + return str.size() >= len && memcmp(str.data(), substr, len) == 0; + } + + bool ends_with(std::string_view str, const char *substr) { + size_t len = strlen(substr); + return str.size() >= len && memcmp(str.data() + str.size() - len, substr, len) == 0; + } + + std::string strip(const std::string &str) { + int start_index = 0; + int str_len = str.size(); + + for(int i = 0; i < str_len; ++i) { + if(str[i] != ' ') { + start_index += i; + str_len -= i; + break; + } + } + + for(int i = str_len - 1; i >= 0; --i) { + if(str[i] != ' ') { + str_len = i + 1; + break; + } + } + + return str.substr(start_index, str_len); + } + std::string get_home_dir() { const char *home_dir = getenv("HOME"); if(!home_dir) { @@ -114,6 +146,14 @@ namespace gsr { return xdg_videos_dir; } + std::string get_pictures_dir() { + auto xdg_vars = get_xdg_variables(); + std::string xdg_videos_dir = xdg_vars["XDG_PICTURES_DIR"]; + if(xdg_videos_dir.empty()) + xdg_videos_dir = get_home_dir() + "/Pictures"; + return xdg_videos_dir; + } + int create_directory_recursive(char *path) { int path_len = strlen(path); char *p = path; diff --git a/src/WindowSelector.cpp b/src/WindowSelector.cpp new file mode 100644 index 0000000..f04d600 --- /dev/null +++ b/src/WindowSelector.cpp @@ -0,0 +1,229 @@ +#include "../include/WindowSelector.hpp" +#include "../include/WindowUtils.hpp" + +#include <stdio.h> +#include <string.h> + +#include <X11/extensions/shape.h> +#include <X11/cursorfont.h> +#include <X11/keysym.h> + +namespace gsr { + static const int rectangle_border_size = 2; + + static int max_int(int a, int b) { + return a >= b ? a : b; + } + + static void set_region_rectangle(Display *dpy, Window window, int x, int y, int width, int height, int border_size) { + if(width < 0) { + x += width; + width = abs(width); + } + + if(height < 0) { + y += height; + height = abs(height); + } + + XRectangle rectangles[] = { + { + (short)max_int(0, x), (short)max_int(0, y), + (unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height) + }, // Left + { + (short)max_int(0, x + width - border_size), (short)max_int(0, y), + (unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height) + }, // Right + { + (short)max_int(0, x + border_size), (short)max_int(0, y), + (unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size) + }, // Top + { + (short)max_int(0, x + border_size), (short)max_int(0, y + height - border_size), + (unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size) + }, // Bottom + }; + XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 4, ShapeSet, Unsorted); + XFlush(dpy); + } + + static unsigned long mgl_color_to_x11_color(mgl::Color color) { + if(color.a == 0) + return 0; + return ((uint32_t)color.a << 24) | (((uint32_t)color.r * color.a / 0xFF) << 16) | (((uint32_t)color.g * color.a / 0xFF) << 8) | ((uint32_t)color.b * color.a / 0xFF); + } + + static Window get_cursor_window(Display *dpy) { + Window root_window = None; + Window window = None; + int dummy_i; + unsigned int dummy_u; + mgl::vec2i root_pos; + XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &root_pos.x, &root_pos.y, &dummy_i, &dummy_i, &dummy_u); + return window; + } + + static void get_window_geometry(Display *dpy, Window window, mgl::vec2i &pos, mgl::vec2i &size) { + Window root_window; + int x = 0; + int y = 0; + unsigned int w = 0; + unsigned int h = 0; + unsigned int dummy_border, dummy_depth; + XGetGeometry(dpy, window, &root_window, &x, &y, &w, &h, &dummy_border, &dummy_depth); + pos.x = x; + pos.y = y; + size.x = w; + size.y = h; + } + + WindowSelector::WindowSelector() { + + } + + WindowSelector::~WindowSelector() { + stop(); + } + + bool WindowSelector::start(mgl::Color border_color) { + if(dpy) + return false; + + const unsigned long border_color_x11 = mgl_color_to_x11_color(border_color); + dpy = XOpenDisplay(nullptr); + if(!dpy) { + fprintf(stderr, "Error: WindowSelector::start: failed to connect to the X11 server\n"); + return false; + } + + const Window cursor_window = get_cursor_window(dpy); + mgl::vec2i cursor_window_pos, cursor_window_size; + get_window_geometry(dpy, cursor_window, cursor_window_pos, cursor_window_size); + + XVisualInfo vinfo; + memset(&vinfo, 0, sizeof(vinfo)); + XMatchVisualInfo(dpy, DefaultScreen(dpy), 32, TrueColor, &vinfo); + border_window_colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), vinfo.visual, AllocNone); + + XSetWindowAttributes window_attr; + window_attr.background_pixel = border_color_x11; + window_attr.border_pixel = 0; + window_attr.override_redirect = true; + window_attr.event_mask = StructureNotifyMask | PointerMotionMask | ButtonPressMask | ButtonReleaseMask; + window_attr.colormap = border_window_colormap; + + Screen *screen = XDefaultScreenOfDisplay(dpy); + border_window = XCreateWindow(dpy, DefaultRootWindow(dpy), 0, 0, XWidthOfScreen(screen), XHeightOfScreen(screen), 0, + vinfo.depth, InputOutput, vinfo.visual, CWBackPixel | CWBorderPixel | CWOverrideRedirect | CWEventMask | CWColormap, &window_attr); + if(!border_window) { + fprintf(stderr, "Error: WindowSelector::start: failed to create region window\n"); + stop(); + return false; + } + set_window_size_not_resizable(dpy, border_window, XWidthOfScreen(screen), XHeightOfScreen(screen)); + if(cursor_window && cursor_window != DefaultRootWindow(dpy)) + set_region_rectangle(dpy, border_window, cursor_window_pos.x, cursor_window_pos.y, cursor_window_size.x, cursor_window_size.y, rectangle_border_size); + else + set_region_rectangle(dpy, border_window, 0, 0, 0, 0, 0); + make_window_click_through(dpy, border_window); + XMapWindow(dpy, border_window); + + crosshair_cursor = XCreateFontCursor(dpy, XC_crosshair); + XGrabPointer(dpy, DefaultRootWindow(dpy), True, PointerMotionMask | ButtonPressMask | ButtonReleaseMask | ButtonMotionMask, GrabModeAsync, GrabModeAsync, None, crosshair_cursor, CurrentTime); + XGrabKeyboard(dpy, DefaultRootWindow(dpy), True, GrabModeAsync, GrabModeAsync, CurrentTime); + XFlush(dpy); + + selected = false; + canceled = false; + selected_window = None; + return true; + } + + void WindowSelector::stop() { + if(!dpy) + return; + + XUngrabPointer(dpy, CurrentTime); + XUngrabKeyboard(dpy, CurrentTime); + + if(border_window_colormap) { + XFreeColormap(dpy, border_window_colormap); + border_window_colormap = 0; + } + + if(border_window) { + XDestroyWindow(dpy, border_window); + border_window = 0; + } + + if(crosshair_cursor) { + XFreeCursor(dpy, crosshair_cursor); + crosshair_cursor = None; + } + + XFlush(dpy); + XCloseDisplay(dpy); + dpy = nullptr; + } + + bool WindowSelector::is_started() const { + return dpy != nullptr; + } + + bool WindowSelector::failed() const { + return !dpy; + } + + bool WindowSelector::poll_events() { + if(!dpy || selected) + return false; + + XEvent xev; + while(XPending(dpy)) { + XNextEvent(dpy, &xev); + + if(xev.type == MotionNotify) { + const Window motion_window = xev.xmotion.subwindow; + mgl::vec2i motion_window_pos, motion_window_size; + get_window_geometry(dpy, motion_window, motion_window_pos, motion_window_size); + if(motion_window && motion_window != DefaultRootWindow(dpy)) + set_region_rectangle(dpy, border_window, motion_window_pos.x, motion_window_pos.y, motion_window_size.x, motion_window_size.y, rectangle_border_size); + else + set_region_rectangle(dpy, border_window, 0, 0, 0, 0, 0); + XFlush(dpy); + } else if(xev.type == ButtonRelease && xev.xbutton.button == Button1) { + selected_window = xev.xbutton.subwindow; + const Window clicked_window_real = window_get_target_window_child(dpy, selected_window); + if(clicked_window_real) + selected_window = clicked_window_real; + selected = true; + + stop(); + break; + } else if(xev.type == KeyRelease && XKeycodeToKeysym(dpy, xev.xkey.keycode, 0) == XK_Escape) { + canceled = true; + selected = false; + stop(); + break; + } + } + return true; + } + + bool WindowSelector::take_selection() { + const bool result = selected; + selected = false; + return result; + } + + bool WindowSelector::take_canceled() { + const bool result = canceled; + canceled = false; + return result; + } + + Window WindowSelector::get_selection() const { + return selected_window; + } +}
\ No newline at end of file diff --git a/src/WindowUtils.cpp b/src/WindowUtils.cpp index c033058..c6b278b 100644 --- a/src/WindowUtils.cpp +++ b/src/WindowUtils.cpp @@ -1,14 +1,48 @@ #include "../include/WindowUtils.hpp" +#include "../include/Utils.hpp" -#include <X11/Xlib.h> #include <X11/Xatom.h> #include <X11/Xutil.h> +#include <X11/extensions/XInput2.h> +#include <X11/extensions/Xfixes.h> +#include <X11/extensions/shapeconst.h> +#include <X11/extensions/Xrandr.h> + +#include <mglpp/system/Utf8.hpp> + +extern "C" { +#include <mgl/window/window.h> +} #include <stdbool.h> +#include <stdint.h> #include <stdio.h> #include <string.h> +#include <poll.h> + +#define MAX_PROPERTY_VALUE_LEN 4096 namespace gsr { + static unsigned char* window_get_property(Display *dpy, Window window, Atom property_type, const char *property_name, unsigned int *property_size) { + Atom ret_property_type = None; + int ret_format = 0; + unsigned long num_items = 0; + unsigned long num_remaining_bytes = 0; + unsigned char *data = nullptr; + const Atom atom = XInternAtom(dpy, property_name, False); + if(XGetWindowProperty(dpy, window, atom, 0, MAX_PROPERTY_VALUE_LEN / 4, False, property_type, &ret_property_type, &ret_format, &num_items, &num_remaining_bytes, &data) != Success || !data) { + return nullptr; + } + + if(ret_property_type != property_type) { + XFree(data); + return nullptr; + } + + *property_size = (ret_format / (32 / sizeof(long))) * num_items; + return data; + } + static bool window_has_atom(Display *dpy, Window window, Atom atom) { Atom type; unsigned long len, bytes_left; @@ -29,19 +63,66 @@ namespace gsr { return window_has_atom(dpy, window, net_wm_state_atom) || window_has_atom(dpy, window, wm_state_atom); } - static Window get_window_at_cursor_position(Display *dpy) { + Window window_get_target_window_child(Display *display, Window window) { + if(window == None) + return None; + + if(window_is_user_program(display, window)) + return window; + + Window root; + Window parent; + Window *children = nullptr; + unsigned int num_children = 0; + if(!XQueryTree(display, window, &root, &parent, &children, &num_children) || !children) + return None; + + Window found_window = None; + for(int i = num_children - 1; i >= 0; --i) { + if(children[i] && window_is_user_program(display, children[i])) { + found_window = children[i]; + goto finished; + } + } + + for(int i = num_children - 1; i >= 0; --i) { + if(children[i]) { + Window win = window_get_target_window_child(display, children[i]); + if(win) { + found_window = win; + goto finished; + } + } + } + + finished: + XFree(children); + return found_window; + } + + mgl::vec2i get_cursor_position(Display *dpy, Window *window) { Window root_window = None; - Window window = None; + *window = None; int dummy_i; unsigned int dummy_u; - int cursor_pos_x = 0; - int cursor_pos_y = 0; - XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &dummy_i, &dummy_i, &cursor_pos_x, &cursor_pos_y, &dummy_u); - return window; + mgl::vec2i root_pos; + XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, window, &root_pos.x, &root_pos.y, &dummy_i, &dummy_i, &dummy_u); + + const Window direct_window = *window; + *window = window_get_target_window_child(dpy, *window); + // HACK: Count some other x11 windows as having an x11 window focused. Some games seem to create an Input window and that gets focused. + if(!*window) { + XWindowAttributes attr; + memset(&attr, 0, sizeof(attr)); + XGetWindowAttributes(dpy, direct_window, &attr); + if(attr.c_class == InputOnly && !get_window_title(dpy, direct_window)) + *window = direct_window; + } + return root_pos; } Window get_focused_window(Display *dpy, WindowCaptureType cap_type) { - const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False); + //const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False); Window focused_window = None; if(cap_type == WindowCaptureType::FOCUSED) { @@ -52,10 +133,14 @@ namespace gsr { // unsigned char *data = NULL; // XGetWindowProperty(dpy, DefaultRootWindow(dpy), net_active_window_atom, 0, 1, False, XA_WINDOW, &type, &format, &num_items, &bytes_left, &data); - // fprintf(stderr, "focused window: %p\n", (void*)data); - // if(type == XA_WINDOW && num_items == 1 && data) - // return *(Window*)data; + // focused_window = *(Window*)data; + + // if(data) + // XFree(data); + + // if(focused_window) + // return focused_window; int revert_to = 0; XGetInputFocus(dpy, &focused_window, &revert_to); @@ -63,16 +148,36 @@ namespace gsr { return focused_window; } - focused_window = get_window_at_cursor_position(dpy); - if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window)) + get_cursor_position(dpy, &focused_window); + if(focused_window && focused_window != DefaultRootWindow(dpy)) return focused_window; return None; } - static char* get_window_title(Display *dpy, Window window) { + static std::string utf8_sanitize(const uint8_t *str, int size) { + const uint32_t zero_width_space_codepoint = 0x200b; // Some games such as the finals has zero-width space characters + std::string result; + for(int i = 0; i < size;) { + // Some games such as the finals has utf8-bom between each character, wtf? + if(i + 3 <= size && memcmp(str + i, "\xEF\xBB\xBF", 3) == 0) { + i += 3; + continue; + } + + uint32_t codepoint = 0; + size_t codepoint_length = 1; + if(mgl::utf8_decode(str + i, size - i, &codepoint, &codepoint_length) && codepoint != zero_width_space_codepoint) + result.append((const char*)str + i, codepoint_length); + i += codepoint_length; + } + return result; + } + + std::optional<std::string> get_window_title(Display *dpy, Window window) { + std::optional<std::string> result; const Atom net_wm_name_atom = XInternAtom(dpy, "_NET_WM_NAME", False); - const Atom wm_name_atom = XInternAtom(dpy, "_NET_WM_NAME", False); + const Atom wm_name_atom = XInternAtom(dpy, "WM_NAME", False); const Atom utf8_string_atom = XInternAtom(dpy, "UTF8_STRING", False); Atom type = None; @@ -82,8 +187,13 @@ namespace gsr { unsigned char *data = NULL; XGetWindowProperty(dpy, window, net_wm_name_atom, 0, 1024, False, utf8_string_atom, &type, &format, &num_items, &bytes_left, &data); - if(type == utf8_string_atom && format == 8 && data) - return (char*)data; + if(type == utf8_string_atom && format == 8 && data) { + result = utf8_sanitize(data, num_items); + goto done; + } + + if(data) + XFree(data); type = None; format = 0; @@ -92,37 +202,15 @@ namespace gsr { data = NULL; XGetWindowProperty(dpy, window, wm_name_atom, 0, 1024, False, 0, &type, &format, &num_items, &bytes_left, &data); - if((type == XA_STRING || type == utf8_string_atom) && data) - return (char*)data; - - return NULL; - } - - static const char* strip(const char *str, int *len) { - int str_len = strlen(str); - for(int i = 0; i < str_len; ++i) { - if(str[i] != ' ') { - str += i; - str_len -= i; - break; - } + if((type == XA_STRING || type == utf8_string_atom) && data) { + result = utf8_sanitize(data, num_items); + goto done; } - for(int i = str_len - 1; i >= 0; --i) { - if(str[i] != ' ') { - str_len = i + 1; - break; - } - } - - *len = str_len; - return str; - } - - static std::string string_string(const char *str) { - int len = 0; - str = strip(str, &len); - return std::string(str, len); + done: + if(data) + XFree(data); + return result; } std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type) { @@ -132,19 +220,467 @@ namespace gsr { return result; // Window title is not always ideal (for example for a browser), but for games its pretty much required - char *window_title = get_window_title(dpy, focused_window); + const std::optional<std::string> window_title = get_window_title(dpy, focused_window); if(window_title) { - result = string_string(window_title); + result = strip(window_title.value()); return result; } XClassHint class_hint = {nullptr, nullptr}; XGetClassHint(dpy, focused_window, &class_hint); - if(class_hint.res_class) { - result = string_string(class_hint.res_class); + if(class_hint.res_class) + result = strip(class_hint.res_class); + + if(class_hint.res_name) + XFree(class_hint.res_name); + + if(class_hint.res_class) + XFree(class_hint.res_class); + + return result; + } + + std::string get_window_name_at_position(Display *dpy, mgl::vec2i position, Window ignore_window) { + std::string result; + + Window root; + Window parent; + Window *children = nullptr; + unsigned int num_children = 0; + if(!XQueryTree(dpy, DefaultRootWindow(dpy), &root, &parent, &children, &num_children) || !children) return result; + + for(int i = (int)num_children - 1; i >= 0; --i) { + if(children[i] == ignore_window) + continue; + + XWindowAttributes attr; + memset(&attr, 0, sizeof(attr)); + XGetWindowAttributes(dpy, children[i], &attr); + if(attr.override_redirect || attr.c_class != InputOutput || attr.map_state != IsViewable) + continue; + + if(position.x >= attr.x && position.x <= attr.x + attr.width && position.y >= attr.y && position.y <= attr.y + attr.height) { + const Window real_window = window_get_target_window_child(dpy, children[i]); + if(!real_window || real_window == ignore_window) + continue; + + const std::optional<std::string> window_title = get_window_title(dpy, real_window); + if(window_title) + result = strip(window_title.value()); + + break; + } } + XFree(children); return result; } + + std::string get_window_name_at_cursor_position(Display *dpy, Window ignore_window) { + Window cursor_window; + const mgl::vec2i cursor_position = get_cursor_position(dpy, &cursor_window); + return get_window_name_at_position(dpy, cursor_position, ignore_window); + } + + void set_window_size_not_resizable(Display *dpy, Window window, int width, int height) { + XSizeHints *size_hints = XAllocSizeHints(); + if(size_hints) { + size_hints->width = width; + size_hints->height = height; + size_hints->min_width = width; + size_hints->min_height = height; + size_hints->max_width = width; + size_hints->max_height = height; + size_hints->flags = PSize | PMinSize | PMaxSize; + XSetWMNormalHints(dpy, window, size_hints); + XFree(size_hints); + } + } + + typedef struct { + unsigned long flags; + unsigned long functions; + unsigned long decorations; + long input_mode; + unsigned long status; + } MotifHints; + + #define MWM_HINTS_DECORATIONS 2 + + #define MWM_DECOR_NONE 0 + #define MWM_DECOR_ALL 1 + + static void window_set_decorations_visible(Display *display, Window window, bool visible) { + const Atom motif_wm_hints_atom = XInternAtom(display, "_MOTIF_WM_HINTS", False); + MotifHints motif_hints; + memset(&motif_hints, 0, sizeof(motif_hints)); + motif_hints.flags = MWM_HINTS_DECORATIONS; + motif_hints.decorations = visible ? MWM_DECOR_ALL : MWM_DECOR_NONE; + XChangeProperty(display, window, motif_wm_hints_atom, motif_wm_hints_atom, 32, PropModeReplace, (unsigned char*)&motif_hints, sizeof(motif_hints) / sizeof(long)); + } + + static bool create_window_get_center_position_kde(Display *display, mgl::vec2i &position) { + const int size = 1; + XSetWindowAttributes window_attr; + window_attr.event_mask = StructureNotifyMask; + window_attr.background_pixel = 0; + const Window window = XCreateWindow(display, DefaultRootWindow(display), 0, 0, size, size, 0, CopyFromParent, InputOutput, CopyFromParent, CWBackPixel | CWEventMask, &window_attr); + if(!window) + return false; + + const Atom net_wm_window_type_atom = XInternAtom(display, "_NET_WM_WINDOW_TYPE", False); + const Atom net_wm_window_type_notification_atom = XInternAtom(display, "_NET_WM_WINDOW_TYPE_NOTIFICATION", False); + const Atom net_wm_window_type_utility = XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", False); + const Atom net_wm_window_opacity = XInternAtom(display, "_NET_WM_WINDOW_OPACITY", False); + + const Atom window_type_atoms[2] = { + net_wm_window_type_notification_atom, + net_wm_window_type_utility + }; + XChangeProperty(display, window, net_wm_window_type_atom, XA_ATOM, 32, PropModeReplace, (const unsigned char*)window_type_atoms, 2L); + + const double alpha = 0.0; + const unsigned long opacity = (unsigned long)(0xFFFFFFFFul * alpha); + XChangeProperty(display, window, net_wm_window_opacity, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&opacity, 1L); + + window_set_decorations_visible(display, window, false); + set_window_size_not_resizable(display, window, size, size); + + XMapWindow(display, window); + XFlush(display); + + bool got_data = false; + const int x_fd = XConnectionNumber(display); + XEvent xev; + while(true) { + struct pollfd poll_fd; + poll_fd.fd = x_fd; + poll_fd.events = POLLIN; + poll_fd.revents = 0; + const int fds_ready = poll(&poll_fd, 1, 200); + if(fds_ready == 0) { + fprintf(stderr, "Error: timed out waiting for ConfigureNotify after XCreateWindow\n"); + break; + } else if(fds_ready == -1 || !(poll_fd.revents & POLLIN)) { + continue; + } + + while(XPending(display)) { + XNextEvent(display, &xev); + if(xev.type == ConfigureNotify && xev.xconfigure.window == window) { + got_data = xev.xconfigure.x > 0 && xev.xconfigure.y > 0; + position.x = xev.xconfigure.x + xev.xconfigure.width / 2; + position.y = xev.xconfigure.y + xev.xconfigure.height / 2; + goto done; + } + } + } + + done: + XDestroyWindow(display, window); + XFlush(display); + + return got_data; + } + + static bool create_window_get_center_position_gnome(Display *display, mgl::vec2i &position) { + const int size = 32; + XSetWindowAttributes window_attr; + window_attr.event_mask = StructureNotifyMask | ExposureMask; + window_attr.background_pixel = 0; + const Window window = XCreateWindow(display, DefaultRootWindow(display), 0, 0, size, size, 0, CopyFromParent, InputOutput, CopyFromParent, CWBackPixel | CWEventMask, &window_attr); + if(!window) + return false; + + const Atom net_wm_window_opacity = XInternAtom(display, "_NET_WM_WINDOW_OPACITY", False); + const double alpha = 0.0; + const unsigned long opacity = (unsigned long)(0xFFFFFFFFul * alpha); + XChangeProperty(display, window, net_wm_window_opacity, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&opacity, 1L); + + window_set_decorations_visible(display, window, false); + set_window_size_not_resizable(display, window, size, size); + + XMapWindow(display, window); + XFlush(display); + + bool got_data = false; + const int x_fd = XConnectionNumber(display); + XEvent xev; + while(true) { + struct pollfd poll_fd; + poll_fd.fd = x_fd; + poll_fd.events = POLLIN; + poll_fd.revents = 0; + const int fds_ready = poll(&poll_fd, 1, 200); + if(fds_ready == 0) { + fprintf(stderr, "Error: timed out waiting for MapNotify/ConfigureNotify after XCreateWindow\n"); + break; + } else if(fds_ready == -1 || !(poll_fd.revents & POLLIN)) { + continue; + } + + while(XPending(display)) { + XNextEvent(display, &xev); + if(xev.type == MapNotify && xev.xmap.window == window) { + int x = 0; + int y = 0; + Window w = None; + XTranslateCoordinates(display, window, DefaultRootWindow(display), 0, 0, &x, &y, &w); + + got_data = x > 0 && y > 0; + position.x = x + size / 2; + position.y = y + size / 2; + if(got_data) + goto done; + } else if(xev.type == ConfigureNotify && xev.xconfigure.window == window) { + got_data = xev.xconfigure.x > 0 && xev.xconfigure.y > 0; + position.x = xev.xconfigure.x + xev.xconfigure.width / 2; + position.y = xev.xconfigure.y + xev.xconfigure.height / 2; + if(got_data) + goto done; + } + } + } + + done: + XDestroyWindow(display, window); + XFlush(display); + + return got_data; + } + + mgl::vec2i create_window_get_center_position(Display *display) { + mgl::vec2i pos; + if(!create_window_get_center_position_kde(display, pos)) { + pos.x = 0; + pos.y = 0; + create_window_get_center_position_gnome(display, pos); + } + return pos; + } + + std::string get_window_manager_name(Display *display) { + std::string wm_name; + unsigned int property_size = 0; + Window window = None; + + unsigned char *net_supporting_wm_check = window_get_property(display, DefaultRootWindow(display), XA_WINDOW, "_NET_SUPPORTING_WM_CHECK", &property_size); + if(net_supporting_wm_check) { + if(property_size == 8) + window = *(Window*)net_supporting_wm_check; + XFree(net_supporting_wm_check); + } + + if(!window) { + unsigned char *win_supporting_wm_check = window_get_property(display, DefaultRootWindow(display), XA_WINDOW, "_WIN_SUPPORTING_WM_CHECK", &property_size); + if(win_supporting_wm_check) { + if(property_size == 8) + window = *(Window*)win_supporting_wm_check; + XFree(win_supporting_wm_check); + } + } + + if(!window) + return wm_name; + + const std::optional<std::string> window_title = get_window_title(display, window); + if(window_title) + wm_name = strip(window_title.value()); + + return wm_name; + } + + bool is_compositor_running(Display *dpy, int screen) { + char prop_name[20]; + snprintf(prop_name, sizeof(prop_name), "_NET_WM_CM_S%d", screen); + const Atom prop_atom = XInternAtom(dpy, prop_name, False); + return XGetSelectionOwner(dpy, prop_atom) != None; + } + + std::vector<Monitor> get_monitors(Display *dpy) { + std::vector<Monitor> monitors; + int nmonitors = 0; + XRRMonitorInfo *monitor_info = XRRGetMonitors(dpy, DefaultRootWindow(dpy), True, &nmonitors); + if(monitor_info) { + for(int i = 0; i < nmonitors; ++i) { + char *monitor_name = XGetAtomName(dpy, monitor_info[i].name); + if(!monitor_name) + continue; + + monitors.push_back({mgl::vec2i(monitor_info[i].x, monitor_info[i].y), mgl::vec2i(monitor_info[i].width, monitor_info[i].height), std::string(monitor_name)}); + XFree(monitor_name); + } + XRRFreeMonitors(monitor_info); + } + return monitors; + } + + static bool device_is_mouse(const XIDeviceInfo *dev) { + for(int i = 0; i < dev->num_classes; ++i) { + if(dev->classes[i]->type == XIMasterPointer || dev->classes[i]->type == XISlavePointer) + return true; + } + return false; + } + + static void xi_grab_all_mouse_devices(Display *dpy, bool grab) { + if(!dpy) + return; + + int num_devices = 0; + XIDeviceInfo *info = XIQueryDevice(dpy, XIAllDevices, &num_devices); + if(!info) + return; + + unsigned char mask[XIMaskLen(XI_LASTEVENT)]; + memset(mask, 0, sizeof(mask)); + XISetMask(mask, XI_Motion); + //XISetMask(mask, XI_RawMotion); + XISetMask(mask, XI_ButtonPress); + XISetMask(mask, XI_ButtonRelease); + XISetMask(mask, XI_KeyPress); + XISetMask(mask, XI_KeyRelease); + + for (int i = 0; i < num_devices; ++i) { + const XIDeviceInfo *dev = &info[i]; + if(!device_is_mouse(dev)) + continue; + + XIEventMask xi_masks; + xi_masks.deviceid = dev->deviceid; + xi_masks.mask_len = sizeof(mask); + xi_masks.mask = mask; + if(grab) + XIGrabDevice(dpy, dev->deviceid, DefaultRootWindow(dpy), CurrentTime, None, XIGrabModeAsync, XIGrabModeAsync, XIOwnerEvents, &xi_masks); + else + XIUngrabDevice(dpy, dev->deviceid, CurrentTime); + } + + XFlush(dpy); + XIFreeDeviceInfo(info); + } + + void xi_grab_all_mouse_devices(Display *dpy) { + xi_grab_all_mouse_devices(dpy, true); + } + + void xi_ungrab_all_mouse_devices(Display *dpy) { + xi_grab_all_mouse_devices(dpy, false); + } + + void xi_warp_all_mouse_devices(Display *dpy, mgl::vec2i position) { + if(!dpy) + return; + + int num_devices = 0; + XIDeviceInfo *info = XIQueryDevice(dpy, XIAllDevices, &num_devices); + if(!info) + return; + + for (int i = 0; i < num_devices; ++i) { + const XIDeviceInfo *dev = &info[i]; + if(!device_is_mouse(dev)) + continue; + + XIWarpPointer(dpy, dev->deviceid, DefaultRootWindow(dpy), DefaultRootWindow(dpy), 0, 0, 0, 0, position.x, position.y); + } + + XFlush(dpy); + XIFreeDeviceInfo(info); + } + + void window_set_fullscreen(Display *dpy, Window window, bool fullscreen) { + const Atom net_wm_state_atom = XInternAtom(dpy, "_NET_WM_STATE", False); + const Atom net_wm_state_fullscreen_atom = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", False); + + XEvent xev; + xev.type = ClientMessage; + xev.xclient.window = window; + xev.xclient.message_type = net_wm_state_atom; + xev.xclient.format = 32; + xev.xclient.data.l[0] = fullscreen ? 1 : 0; + xev.xclient.data.l[1] = net_wm_state_fullscreen_atom; + xev.xclient.data.l[2] = 0; + xev.xclient.data.l[3] = 1; + xev.xclient.data.l[4] = 0; + + if(!XSendEvent(dpy, DefaultRootWindow(dpy), 0, SubstructureRedirectMask | SubstructureNotifyMask, &xev)) { + fprintf(stderr, "mgl warning: failed to change window fullscreen state\n"); + return; + } + + XFlush(dpy); + } + + bool window_is_fullscreen(Display *display, Window window) { + const Atom wm_state_atom = XInternAtom(display, "_NET_WM_STATE", False); + const Atom wm_state_fullscreen_atom = XInternAtom(display, "_NET_WM_STATE_FULLSCREEN", False); + + Atom type = None; + int format = 0; + unsigned long num_items = 0; + unsigned long bytes_after = 0; + unsigned char *properties = nullptr; + if(XGetWindowProperty(display, window, wm_state_atom, 0, 1024, False, XA_ATOM, &type, &format, &num_items, &bytes_after, &properties) < Success) { + fprintf(stderr, "Failed to get window wm state property\n"); + return false; + } + + if(!properties) + return false; + + bool is_fullscreen = false; + Atom *atoms = (Atom*)properties; + for(unsigned long i = 0; i < num_items; ++i) { + if(atoms[i] == wm_state_fullscreen_atom) { + is_fullscreen = true; + break; + } + } + + XFree(properties); + return is_fullscreen; + } + + #define _NET_WM_STATE_REMOVE 0 + #define _NET_WM_STATE_ADD 1 + #define _NET_WM_STATE_TOGGLE 2 + + bool set_window_wm_state(Display *dpy, Window window, Atom atom) { + const Atom net_wm_state_atom = XInternAtom(dpy, "_NET_WM_STATE", False); + + XClientMessageEvent xclient; + memset(&xclient, 0, sizeof(xclient)); + + xclient.type = ClientMessage; + xclient.window = window; + xclient.message_type = net_wm_state_atom; + xclient.format = 32; + xclient.data.l[0] = _NET_WM_STATE_ADD; + xclient.data.l[1] = atom; + xclient.data.l[2] = 0; + xclient.data.l[3] = 0; + xclient.data.l[4] = 0; + + XSendEvent(dpy, DefaultRootWindow(dpy), False, SubstructureRedirectMask | SubstructureNotifyMask, (XEvent*)&xclient); + XFlush(dpy); + return true; + } + + void make_window_click_through(Display *display, Window window) { + XRectangle rect; + memset(&rect, 0, sizeof(rect)); + XserverRegion region = XFixesCreateRegion(display, &rect, 1); + XFixesSetWindowShapeRegion(display, window, ShapeInput, 0, 0, region); + XFixesDestroyRegion(display, region); + } + + bool make_window_sticky(Display *dpy, Window window) { + return set_window_wm_state(dpy, window, XInternAtom(dpy, "_NET_WM_STATE_STICKY", False)); + } + + bool hide_window_from_taskbar(Display *dpy, Window window) { + return set_window_wm_state(dpy, window, XInternAtom(dpy, "_NET_WM_STATE_SKIP_TASKBAR", False)); + } }
\ No newline at end of file diff --git a/src/gui/Button.cpp b/src/gui/Button.cpp index 54d1854..6e343c4 100644 --- a/src/gui/Button.cpp +++ b/src/gui/Button.cpp @@ -15,8 +15,8 @@ namespace gsr { // These are relative to the button size static const float padding_top_icon_scale = 0.25f; static const float padding_bottom_icon_scale = 0.25f; - static const float padding_left_icon_scale = 0.25f; - static const float padding_right_icon_scale = 0.25f; + //static const float padding_left_icon_scale = 0.25f; + static const float padding_right_icon_scale = 0.15f; Button::Button(mgl::Font *font, const char *text, mgl::vec2f size, mgl::Color bg_color) : size(size), bg_color(bg_color), bg_hover_color(bg_color), text(text, *font) @@ -53,13 +53,21 @@ namespace gsr { background.set_color(mouse_inside ? bg_hover_color : bg_color); window.draw(background); - text.set_position((draw_pos + item_size * 0.5f - text.get_bounds().size * 0.5f).floor()); - window.draw(text); - if(sprite.get_texture() && sprite.get_texture()->is_valid()) { scale_sprite_to_button_size(); - sprite.set_position((background.get_position() + background.get_size() * 0.5f - sprite.get_size() * 0.5f).floor()); + const int padding_left = padding_left_scale * get_theme().window_height; + if(text.get_string().empty()) // Center + sprite.set_position((background.get_position() + background.get_size() * 0.5f - sprite.get_size() * 0.5f).floor()); + else // Left + sprite.set_position((draw_pos + mgl::vec2f(padding_left, background.get_size().y * 0.5f - sprite.get_size().y * 0.5f)).floor()); window.draw(sprite); + + const int padding_icon_right = padding_right_icon_scale * get_button_height(); + text.set_position((sprite.get_position() + mgl::vec2f(sprite.get_size().x + padding_icon_right, sprite.get_size().y * 0.5f - text.get_bounds().size.y * 0.52f)).floor()); + window.draw(text); + } else { + text.set_position((draw_pos + item_size * 0.5f - text.get_bounds().size * 0.5f).floor()); + window.draw(text); } if(mouse_inside) { @@ -72,24 +80,35 @@ namespace gsr { if(!visible) return {0.0f, 0.0f}; - const int padding_top = padding_top_scale * get_theme().window_height; - const int padding_bottom = padding_bottom_scale * get_theme().window_height; const int padding_left = padding_left_scale * get_theme().window_height; const int padding_right = padding_right_scale * get_theme().window_height; const mgl::vec2f text_bounds = text.get_bounds().size; - mgl::vec2f s = size; - if(s.x < 0.0001f) - s.x = padding_left + text_bounds.x + padding_right; - if(s.y < 0.0001f) - s.y = padding_top + text_bounds.y + padding_bottom; - return s; + mgl::vec2f widget_size = size; + + if(widget_size.y < 0.0001f) + widget_size.y = get_button_height(); + + if(widget_size.x < 0.0001f) { + widget_size.x = padding_left + text_bounds.x + padding_right; + if(sprite.get_texture() && sprite.get_texture()->is_valid()) { + scale_sprite_to_button_size(); + const int padding_icon_right = text_bounds.x > 0.001f ? padding_right_icon_scale * widget_size.y : 0.0f; + widget_size.x += sprite.get_size().x + padding_icon_right; + } + } + + return widget_size; } void Button::set_border_scale(float scale) { border_scale = scale; } + void Button::set_icon_padding_scale(float scale) { + icon_padding_scale = scale; + } + void Button::set_bg_hover_color(mgl::Color color) { bg_hover_color = color; } @@ -110,13 +129,23 @@ namespace gsr { if(!sprite.get_texture() || !sprite.get_texture()->is_valid()) return; - const mgl::vec2f button_size = get_size(); - const int padding_icon_top = padding_top_icon_scale * button_size.y; - const int padding_icon_bottom = padding_bottom_icon_scale * button_size.y; - const int padding_icon_left = padding_left_icon_scale * button_size.y; - const int padding_icon_right = padding_right_icon_scale * button_size.y; + const float widget_height = get_button_height(); + + const int padding_icon_top = padding_top_icon_scale * icon_padding_scale * widget_height; + const int padding_icon_bottom = padding_bottom_icon_scale * icon_padding_scale * widget_height; + + const float desired_height = widget_height - (padding_icon_top + padding_icon_bottom); + sprite.set_height((int)desired_height); + } + + float Button::get_button_height() { + const int padding_top = padding_top_scale * get_theme().window_height; + const int padding_bottom = padding_bottom_scale * get_theme().window_height; + + float widget_height = size.y; + if(widget_height < 0.0001f) + widget_height = padding_top + text.get_bounds().size.y + padding_bottom; - const mgl::vec2f desired_size = button_size - mgl::vec2f(padding_icon_left + padding_icon_right, padding_icon_top + padding_icon_bottom); - sprite.set_size(scale_keep_aspect_ratio(sprite.get_texture()->get_size().to_vec2f(), desired_size).floor()); + return widget_height; } }
\ No newline at end of file diff --git a/src/gui/ComboBox.cpp b/src/gui/ComboBox.cpp index 62b2086..4287a53 100644 --- a/src/gui/ComboBox.cpp +++ b/src/gui/ComboBox.cpp @@ -26,16 +26,21 @@ namespace gsr { return true; if(event.type == mgl::Event::MouseButtonPressed && event.mouse_button.button == mgl::Mouse::Left) { + const int padding_top = padding_top_scale * get_theme().window_height; + const int padding_bottom = padding_bottom_scale * get_theme().window_height; + const mgl::vec2f mouse_pos = { (float)event.mouse_button.x, (float)event.mouse_button.y }; - const mgl::vec2f item_size = get_size(); + mgl::vec2f item_size = get_size(); if(show_dropdown) { for(size_t i = 0; i < items.size(); ++i) { Item &item = items[i]; + item_size.y = padding_top + item.text.get_bounds().size.y + padding_bottom; if(mgl::FloatRect(item.position, item_size).contains(mouse_pos)) { const size_t prev_selected_item = selected_item; selected_item = i; show_dropdown = false; + dirty = true; remove_widget_as_selected_in_parent(); if(selected_item != prev_selected_item && on_selection_changed) @@ -47,6 +52,7 @@ namespace gsr { } const mgl::vec2f draw_pos = position + offset; + item_size = get_size(); if(mgl::FloatRect(draw_pos, item_size).contains(mouse_pos)) { show_dropdown = !show_dropdown; if(show_dropdown) @@ -66,9 +72,10 @@ namespace gsr { if(!visible) return; + //const mgl::Scissor scissor = window.get_scissor(); update_if_dirty(); - const mgl::vec2f draw_pos = (position + offset).floor(); + //max_size.x = std::min((scissor.position.x + scissor.size.x) - draw_pos.x, max_size.x); if(show_dropdown) draw_selected(window, draw_pos); @@ -78,6 +85,8 @@ namespace gsr { void ComboBox::add_item(const std::string &text, const std::string &id) { items.push_back({mgl::Text(text, *font), id, {0.0f, 0.0f}}); + items.back().text.set_max_width(font->get_character_size() * 20); // TODO: Make a proper solution + //items.back().text.set_max_rows(1); dirty = true; } @@ -87,6 +96,7 @@ namespace gsr { if(item.id == id) { const size_t prev_selected_item = selected_item; selected_item = i; + dirty = true; if(trigger_event && (trigger_event_even_if_selection_not_changed || selected_item != prev_selected_item) && on_selection_changed) on_selection_changed(item.text.get_string(), item.id); @@ -107,13 +117,13 @@ namespace gsr { void ComboBox::draw_selected(mgl::Window &window, mgl::vec2f draw_pos) { const int padding_top = padding_top_scale * get_theme().window_height; + const int padding_bottom = padding_bottom_scale * get_theme().window_height; const int padding_left = padding_left_scale * get_theme().window_height; - mgl_scissor scissor; - mgl_window_get_scissor(window.internal_window(), &scissor); + const mgl::Scissor scissor = window.get_scissor(); const bool bottom_is_outside_scissor = draw_pos.y + max_size.y > scissor.position.y + scissor.size.y; - const mgl::vec2f item_size = get_size(); + mgl::vec2f item_size = get_size(); mgl::vec2f items_draw_pos = draw_pos + mgl::vec2f(0.0f, item_size.y); mgl::Rectangle background(draw_pos, item_size.floor()); @@ -137,6 +147,9 @@ namespace gsr { const mgl::vec2f mouse_pos = window.get_mouse_position().to_vec2f(); for(size_t i = 0; i < items.size(); ++i) { + Item &item = items[i]; + item_size.y = padding_top + item.text.get_bounds().size.y + padding_bottom; + if(!cursor_inside) { cursor_inside = mgl::FloatRect(items_draw_pos, item_size).contains(mouse_pos); if(cursor_inside) { @@ -146,7 +159,6 @@ namespace gsr { } } - Item &item = items[i]; item.text.set_position((items_draw_pos + mgl::vec2f(padding_left, padding_top)).floor()); window.draw(item.text); @@ -160,7 +172,7 @@ namespace gsr { const int padding_left = padding_left_scale * get_theme().window_height; const int padding_right = padding_right_scale * get_theme().window_height; - const mgl::vec2f item_size = get_size(); + mgl::vec2f item_size = get_size(); mgl::Rectangle background(draw_pos.floor(), item_size.floor()); background.set_color(mgl::Color(0, 0, 0, 120)); window.draw(background); @@ -197,11 +209,12 @@ namespace gsr { const int padding_left = padding_left_scale * get_theme().window_height; const int padding_right = padding_right_scale * get_theme().window_height; - max_size = { 0.0f, font->get_character_size() + (float)padding_top + (float)padding_bottom }; + Item *selected_item_ptr = (selected_item < items.size()) ? &items[selected_item] : nullptr; + max_size = { 0.0f, padding_top + padding_bottom + (selected_item_ptr ? selected_item_ptr->text.get_bounds().size.y : font->get_character_size()) }; for(Item &item : items) { const mgl::vec2f bounds = item.text.get_bounds().size; max_size.x = std::max(max_size.x, bounds.x + padding_left + padding_right); - max_size.y += bounds.y + padding_top + padding_bottom; + max_size.y += padding_top + bounds.y + padding_bottom; } if(max_size.x <= 0.001f) @@ -219,7 +232,8 @@ namespace gsr { const int padding_top = padding_top_scale * get_theme().window_height; const int padding_bottom = padding_bottom_scale * get_theme().window_height; - return { max_size.x, font->get_character_size() + (float)padding_top + (float)padding_bottom }; + Item *selected_item_ptr = (selected_item < items.size()) ? &items[selected_item] : nullptr; + return { max_size.x, padding_top + padding_bottom + (selected_item_ptr ? selected_item_ptr->text.get_bounds().size.y : font->get_character_size()) }; } float ComboBox::get_dropdown_arrow_height() const { diff --git a/src/gui/CustomRendererWidget.cpp b/src/gui/CustomRendererWidget.cpp index cfb113b..5b6c809 100644 --- a/src/gui/CustomRendererWidget.cpp +++ b/src/gui/CustomRendererWidget.cpp @@ -17,19 +17,11 @@ namespace gsr { const mgl::vec2f draw_pos = position + offset; - mgl_scissor prev_scissor; - mgl_window_get_scissor(window.internal_window(), &prev_scissor); - - mgl_scissor new_scissor = { - mgl_vec2i{(int)draw_pos.x, (int)draw_pos.y}, - mgl_vec2i{(int)size.x, (int)size.y} - }; - mgl_window_set_scissor(window.internal_window(), &new_scissor); - + const mgl::Scissor prev_scissor = window.get_scissor(); + window.set_scissor({draw_pos.to_vec2i(), size.to_vec2i()}); if(draw_handler) draw_handler(window, draw_pos, size); - - mgl_window_set_scissor(window.internal_window(), &prev_scissor); + window.set_scissor(prev_scissor); } mgl::vec2f CustomRendererWidget::get_size() { diff --git a/src/gui/DropdownButton.cpp b/src/gui/DropdownButton.cpp index 4a2ae3a..5d1cc38 100644 --- a/src/gui/DropdownButton.cpp +++ b/src/gui/DropdownButton.cpp @@ -20,7 +20,7 @@ namespace gsr { { if(icon_texture && icon_texture->is_valid()) { icon_sprite.set_texture(icon_texture); - icon_sprite.set_height((int)(size.y * 0.5f)); + icon_sprite.set_height((int)(size.y * 0.45f)); } this->description.set_color(mgl::Color(150, 150, 150)); } @@ -110,6 +110,14 @@ namespace gsr { window.draw(rect); } + if(activated) { + description.set_color(get_color_theme().tint_color); + icon_sprite.set_color(get_color_theme().tint_color); + } else { + description.set_color(mgl::Color(150, 150, 150)); + icon_sprite.set_color(mgl::Color(255, 255, 255)); + } + const int text_margin = size.y * 0.085; const auto title_bounds = title.get_bounds(); @@ -148,7 +156,7 @@ namespace gsr { window.draw(separator); } - if(mouse_inside_item == -1) { + if(mouse_inside_item == -1 && item.enabled) { const bool inside = mgl::FloatRect(item_position, item_size).contains({ (float)mouse_pos.x, (float)mouse_pos.y }); if(inside) { draw_rectangle_outline(window, item_position, item_size, get_color_theme().tint_color, border_size); @@ -161,16 +169,18 @@ namespace gsr { mgl::Sprite icon(item.icon_texture); icon.set_height((int)(item_size.y * 0.4f)); icon.set_position((item_position + mgl::vec2f(padding_left, item_size.y * 0.5f - icon.get_size().y * 0.5f)).floor()); + icon.set_color(item.enabled ? mgl::Color(255, 255, 255, 255) : mgl::Color(255, 255, 255, 80)); window.draw(icon); icon_offset = icon.get_size().x + icon_spacing; } item.text.set_position((item_position + mgl::vec2f(padding_left + icon_offset, item_size.y * 0.5f - text_bounds.size.y * 0.5f)).floor()); + item.text.set_color(item.enabled ? mgl::Color(255, 255, 255, 255) : mgl::Color(255, 255, 255, 80)); window.draw(item.text); const auto description_bounds = item.description_text.get_bounds(); item.description_text.set_position((item_position + mgl::vec2f(item_size.x - description_bounds.size.x - padding_right, item_size.y * 0.5f - description_bounds.size.y * 0.5f)).floor()); - item.description_text.set_color(mgl::Color(255, 255, 255, 120)); + item.description_text.set_color(item.enabled ? mgl::Color(255, 255, 255, 120) : mgl::Color(255, 255, 255, 40)); window.draw(item.description_text); item_position.y += item_size.y; @@ -179,6 +189,10 @@ namespace gsr { } void DropdownButton::add_item(const std::string &text, const std::string &id, const std::string &description) { + for(auto &item : items) { + if(item.id == id) + return; + } items.push_back({mgl::Text(text, *title_font), mgl::Text(description, *description_font), nullptr, id}); dirty = true; } @@ -201,6 +215,24 @@ namespace gsr { } } + void DropdownButton::set_item_description(const std::string &id, const std::string &new_description) { + for(auto &item : items) { + if(item.id == id) { + item.description_text.set_string(new_description); + return; + } + } + } + + void DropdownButton::set_item_enabled(const std::string &id, bool enabled) { + for(auto &item : items) { + if(item.id == id) { + item.enabled = enabled; + return; + } + } + } + void DropdownButton::set_description(std::string description_text) { description.set_string(std::move(description_text)); } @@ -210,14 +242,6 @@ namespace gsr { return; this->activated = activated; - - if(activated) { - description.set_color(get_color_theme().tint_color); - icon_sprite.set_color(get_color_theme().tint_color); - } else { - description.set_color(mgl::Color(150, 150, 150)); - icon_sprite.set_color(mgl::Color(255, 255, 255)); - } } void DropdownButton::update_if_dirty() { @@ -242,4 +266,4 @@ namespace gsr { update_if_dirty(); return size; } -}
\ No newline at end of file +} diff --git a/src/gui/FileChooser.cpp b/src/gui/FileChooser.cpp index 03a44c1..ceb8c94 100644 --- a/src/gui/FileChooser.cpp +++ b/src/gui/FileChooser.cpp @@ -65,8 +65,7 @@ namespace gsr { if(!visible) return; - mgl_scissor scissor; - mgl_window_get_scissor(window.internal_window(), &scissor); + const mgl::Scissor scissor = window.get_scissor(); const mgl::vec2f draw_pos = position + offset; const mgl::vec2f mouse_pos = window.get_mouse_position().to_vec2f(); @@ -96,7 +95,12 @@ namespace gsr { selected_item_background.set_color(get_color_theme().tint_color); window.draw(selected_item_background); } - if(!has_parent_with_selected_child_widget() && mouse_over_item == -1 && mgl::FloatRect(item_pos, item_size).contains(mouse_pos)) { + + if(!has_parent_with_selected_child_widget() && mouse_over_item == -1 && + mouse_pos.x >= scissor.position.x && mouse_pos.x <= scissor.position.x + scissor.size.x && + mouse_pos.y >= scissor.position.y && mouse_pos.y <= scissor.position.y + scissor.size.y && + mgl::FloatRect(item_pos, item_size).contains(mouse_pos)) + { // mgl::Rectangle selected_item_background(item_size.floor()); // selected_item_background.set_position(item_pos.floor()); // selected_item_background.set_color(mgl::Color(20, 20, 20, 150)); diff --git a/src/gui/GlobalSettingsPage.cpp b/src/gui/GlobalSettingsPage.cpp index 6c25663..6650c69 100644 --- a/src/gui/GlobalSettingsPage.cpp +++ b/src/gui/GlobalSettingsPage.cpp @@ -1,32 +1,86 @@ #include "../../include/gui/GlobalSettingsPage.hpp" +#include "../../include/Overlay.hpp" #include "../../include/Theme.hpp" +#include "../../include/Process.hpp" #include "../../include/gui/GsrPage.hpp" #include "../../include/gui/PageStack.hpp" #include "../../include/gui/ScrollablePage.hpp" #include "../../include/gui/Subsection.hpp" #include "../../include/gui/List.hpp" #include "../../include/gui/Label.hpp" +#include "../../include/gui/Image.hpp" #include "../../include/gui/RadioButton.hpp" +#include "../../include/gui/LineSeparator.hpp" +#include "../../include/gui/CustomRendererWidget.hpp" + +#include <assert.h> +#include <X11/Xlib.h> +extern "C" { +#include <mgl/mgl.h> +} +#include <mglpp/window/Window.hpp> +#include <mglpp/graphics/Rectangle.hpp> +#include <mglpp/graphics/Text.hpp> + +#ifndef GSR_UI_VERSION +#define GSR_UI_VERSION "Unknown" +#endif + +#ifndef GSR_FLATPAK_VERSION +#define GSR_FLATPAK_VERSION "Unknown" +#endif namespace gsr { static const char* gpu_vendor_to_color_name(GpuVendor vendor) { switch(vendor) { - case GpuVendor::UNKNOWN: return "amd"; - case GpuVendor::AMD: return "amd"; - case GpuVendor::INTEL: return "intel"; - case GpuVendor::NVIDIA: return "nvidia"; + case GpuVendor::UNKNOWN: return "amd"; + case GpuVendor::AMD: return "amd"; + case GpuVendor::INTEL: return "intel"; + case GpuVendor::NVIDIA: return "nvidia"; + case GpuVendor::BROADCOM: return "broadcom"; } return "amd"; } - GlobalSettingsPage::GlobalSettingsPage(const GsrInfo *gsr_info, Config &config, PageStack *page_stack) : + static const char* gpu_vendor_to_string(GpuVendor vendor) { + switch(vendor) { + case GpuVendor::UNKNOWN: return "Unknown"; + case GpuVendor::AMD: return "AMD"; + case GpuVendor::INTEL: return "Intel"; + case GpuVendor::NVIDIA: return "NVIDIA"; + case GpuVendor::BROADCOM: return "Broadcom"; + } + return "unknown"; + } + + static uint32_t mgl_modifier_to_hotkey_modifier(mgl::Keyboard::Key modifier_key) { + switch(modifier_key) { + case mgl::Keyboard::LControl: return HOTKEY_MOD_LCTRL; + case mgl::Keyboard::LShift: return HOTKEY_MOD_LSHIFT; + case mgl::Keyboard::LAlt: return HOTKEY_MOD_LALT; + case mgl::Keyboard::LSystem: return HOTKEY_MOD_LSUPER; + case mgl::Keyboard::RControl: return HOTKEY_MOD_RCTRL; + case mgl::Keyboard::RShift: return HOTKEY_MOD_RSHIFT; + case mgl::Keyboard::RAlt: return HOTKEY_MOD_RALT; + case mgl::Keyboard::RSystem: return HOTKEY_MOD_RSUPER; + default: return 0; + } + return 0; + } + + static bool key_is_alpha_numerical(mgl::Keyboard::Key key) { + return key >= mgl::Keyboard::A && key <= mgl::Keyboard::Num9; + } + + GlobalSettingsPage::GlobalSettingsPage(Overlay *overlay, const GsrInfo *gsr_info, Config &config, PageStack *page_stack) : StaticPage(mgl::vec2f(get_theme().window_width, get_theme().window_height).floor()), + overlay(overlay), config(config), gsr_info(gsr_info), page_stack(page_stack) { - auto content_page = std::make_unique<GsrPage>(); + auto content_page = std::make_unique<GsrPage>("Global", "Settings"); content_page->add_button("Back", "back", get_color_theme().page_bg_color); content_page->on_click = [page_stack](const std::string &id) { if(id == "back") @@ -37,16 +91,64 @@ namespace gsr { add_widgets(); load(); + + auto hotkey_overlay = std::make_unique<CustomRendererWidget>(get_size()); + hotkey_overlay->draw_handler = [this](mgl::Window &window, mgl::vec2f, mgl::vec2f) { + Button *configure_hotkey_button = configure_hotkey_get_button_by_active_type(); + if(!configure_hotkey_button) + return; + + mgl::Text title_text("Press a key combination to use for the hotkey \"" + hotkey_configure_action_name + "\":", get_theme().title_font); + mgl::Text hotkey_text(configure_hotkey_button->get_text(), get_theme().top_bar_font); + mgl::Text description_text("Alpha-numerical keys can't be used alone in hotkeys, they have to be used one or more of these keys: Alt, Ctrl, Shift and Super.\nPress Esc to cancel or Backspace to remove the hotkey.", get_theme().body_font); + const float text_max_width = std::max(title_text.get_bounds().size.x, std::max(hotkey_text.get_bounds().size.x, description_text.get_bounds().size.x)); + + const float padding_horizontal = int(get_theme().window_height * 0.01f); + const float padding_vertical = int(get_theme().window_height * 0.01f); + + const mgl::vec2f bg_size = mgl::vec2f(text_max_width + padding_horizontal*2.0f, get_theme().window_height * 0.13f).floor(); + mgl::Rectangle bg_rect(mgl::vec2f(get_theme().window_width*0.5f - bg_size.x*0.5f, get_theme().window_height*0.5f - bg_size.y*0.5f).floor(), bg_size); + bg_rect.set_color(get_color_theme().page_bg_color); + window.draw(bg_rect); + + const mgl::vec2f tint_size = mgl::vec2f(bg_size.x, 0.004f * get_theme().window_height).floor(); + mgl::Rectangle tint_rect(bg_rect.get_position() - mgl::vec2f(0.0f, tint_size.y), tint_size); + tint_rect.set_color(get_color_theme().tint_color); + window.draw(tint_rect); + + title_text.set_position(mgl::vec2f(bg_rect.get_position() + mgl::vec2f(bg_rect.get_size().x*0.5f - title_text.get_bounds().size.x*0.5f, padding_vertical)).floor()); + description_text.set_position(mgl::vec2f(bg_rect.get_position() + mgl::vec2f(bg_rect.get_size().x*0.5f - description_text.get_bounds().size.x*0.5f, bg_rect.get_size().y - description_text.get_bounds().size.y - padding_vertical)).floor()); + + window.draw(title_text); + + const float title_text_bottom = title_text.get_position().y + title_text.get_bounds().size.y; + hotkey_text.set_position( + mgl::vec2f( + bg_rect.get_position().x + bg_rect.get_size().x*0.5f - hotkey_text.get_bounds().size.x*0.5f, + title_text_bottom + (description_text.get_position().y - title_text_bottom) * 0.5f - hotkey_text.get_bounds().size.y*0.5f + ).floor()); + window.draw(hotkey_text); + + const float caret_padding_x = int(0.001f * get_theme().window_height); + const mgl::vec2f caret_size = mgl::vec2f(std::max(2.0f, 0.002f * get_theme().window_height), hotkey_text.get_bounds().size.y).floor(); + mgl::Rectangle caret_rect(hotkey_text.get_position() + mgl::vec2f(hotkey_text.get_bounds().size.x + caret_padding_x, hotkey_text.get_bounds().size.y*0.5f - caret_size.y*0.5f).floor(), caret_size); + window.draw(caret_rect); + + window.draw(description_text); + }; + hotkey_overlay->set_visible(false); + hotkey_overlay_ptr = hotkey_overlay.get(); + add_widget(std::move(hotkey_overlay)); } std::unique_ptr<Subsection> GlobalSettingsPage::create_appearance_subsection(ScrollablePage *parent_page) { auto list = std::make_unique<List>(List::Orientation::VERTICAL); - list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Tint color", get_color_theme().text_color)); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Accent color", get_color_theme().text_color)); auto tint_color_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL); tint_color_radio_button_ptr = tint_color_radio_button.get(); tint_color_radio_button->add_item("Red", "amd"); tint_color_radio_button->add_item("Green", "nvidia"); - tint_color_radio_button->add_item("blue", "intel"); + tint_color_radio_button->add_item("Blue", "intel"); tint_color_radio_button->on_selection_changed = [](const std::string&, const std::string &id) { if(id == "amd") get_color_theme().tint_color = mgl::Color(221, 0, 49); @@ -54,19 +156,346 @@ namespace gsr { get_color_theme().tint_color = mgl::Color(118, 185, 0); else if(id == "intel") get_color_theme().tint_color = mgl::Color(8, 109, 183); + return true; }; list->add_widget(std::move(tint_color_radio_button)); return std::make_unique<Subsection>("Appearance", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f)); } + std::unique_ptr<Subsection> GlobalSettingsPage::create_startup_subsection(ScrollablePage *parent_page) { + auto list = std::make_unique<List>(List::Orientation::VERTICAL); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Start program on system startup?", get_color_theme().text_color)); + auto startup_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL); + startup_radio_button_ptr = startup_radio_button.get(); + startup_radio_button->add_item("Yes", "start_on_system_startup"); + startup_radio_button->add_item("No", "dont_start_on_system_startup"); + startup_radio_button->on_selection_changed = [&](const std::string&, const std::string &id) { + bool enable = false; + if(id == "dont_start_on_system_startup") + enable = false; + else if(id == "start_on_system_startup") + enable = true; + else + return false; + + const char *args[] = { "systemctl", enable ? "enable" : "disable", "--user", "gpu-screen-recorder-ui", nullptr }; + std::string stdout_str; + const int exit_status = exec_program_on_host_get_stdout(args, stdout_str); + if(on_startup_changed) + on_startup_changed(enable, exit_status); + return exit_status == 0; + }; + list->add_widget(std::move(startup_radio_button)); + return std::make_unique<Subsection>("Startup", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f)); + } + + std::unique_ptr<RadioButton> GlobalSettingsPage::create_enable_keyboard_hotkeys_button() { + auto enable_hotkeys_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL); + enable_keyboard_hotkeys_radio_button_ptr = enable_hotkeys_radio_button.get(); + enable_hotkeys_radio_button->add_item("Yes", "enable_hotkeys"); + enable_hotkeys_radio_button->add_item("No", "disable_hotkeys"); + enable_hotkeys_radio_button->add_item("Only grab virtual devices (supports input remapping software)", "enable_hotkeys_virtual_devices"); + enable_hotkeys_radio_button->on_selection_changed = [&](const std::string&, const std::string &id) { + if(on_keyboard_hotkey_changed) + on_keyboard_hotkey_changed(id.c_str()); + return true; + }; + return enable_hotkeys_radio_button; + } + + std::unique_ptr<RadioButton> GlobalSettingsPage::create_enable_joystick_hotkeys_button() { + auto enable_hotkeys_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL); + enable_joystick_hotkeys_radio_button_ptr = enable_hotkeys_radio_button.get(); + enable_hotkeys_radio_button->add_item("Yes", "enable_hotkeys"); + enable_hotkeys_radio_button->add_item("No", "disable_hotkeys"); + enable_hotkeys_radio_button->on_selection_changed = [&](const std::string&, const std::string &id) { + if(on_joystick_hotkey_changed) + on_joystick_hotkey_changed(id.c_str()); + return true; + }; + return enable_hotkeys_radio_button; + } + + std::unique_ptr<List> GlobalSettingsPage::create_show_hide_hotkey_options() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Show/hide UI:", get_color_theme().text_color)); + auto show_hide_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + show_hide_button_ptr = show_hide_button.get(); + list->add_widget(std::move(show_hide_button)); + + show_hide_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::SHOW_HIDE); + }; + + return list; + } + + std::unique_ptr<List> GlobalSettingsPage::create_replay_hotkey_options() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Turn replay on/off:", get_color_theme().text_color)); + auto turn_replay_on_off_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + turn_replay_on_off_button_ptr = turn_replay_on_off_button.get(); + list->add_widget(std::move(turn_replay_on_off_button)); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Save replay:", get_color_theme().text_color)); + auto save_replay_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + save_replay_button_ptr = save_replay_button.get(); + list->add_widget(std::move(save_replay_button)); + + turn_replay_on_off_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::REPLAY_START_STOP); + }; + + save_replay_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::REPLAY_SAVE); + }; + + return list; + } + + std::unique_ptr<List> GlobalSettingsPage::create_replay_partial_save_hotkey_options() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Save 1 minute replay:", get_color_theme().text_color)); + auto save_replay_1_min_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + save_replay_1_min_button_ptr = save_replay_1_min_button.get(); + list->add_widget(std::move(save_replay_1_min_button)); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Save 10 minute replay:", get_color_theme().text_color)); + auto save_replay_10_min_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + save_replay_10_min_button_ptr = save_replay_10_min_button.get(); + list->add_widget(std::move(save_replay_10_min_button)); + + save_replay_1_min_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::REPLAY_SAVE_1_MIN); + }; + + save_replay_10_min_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::REPLAY_SAVE_10_MIN); + }; + + return list; + } + + std::unique_ptr<List> GlobalSettingsPage::create_record_hotkey_options() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Start/stop recording:", get_color_theme().text_color)); + auto start_stop_recording_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + start_stop_recording_button_ptr = start_stop_recording_button.get(); + list->add_widget(std::move(start_stop_recording_button)); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Pause/unpause recording:", get_color_theme().text_color)); + auto pause_unpause_recording_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + pause_unpause_recording_button_ptr = pause_unpause_recording_button.get(); + list->add_widget(std::move(pause_unpause_recording_button)); + + start_stop_recording_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::RECORD_START_STOP); + }; + + pause_unpause_recording_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE); + }; + + return list; + } + + std::unique_ptr<List> GlobalSettingsPage::create_stream_hotkey_options() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Start/stop streaming:", get_color_theme().text_color)); + auto start_stop_streaming_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + start_stop_streaming_button_ptr = start_stop_streaming_button.get(); + list->add_widget(std::move(start_stop_streaming_button)); + + start_stop_streaming_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::STREAM_START_STOP); + }; + + return list; + } + + std::unique_ptr<List> GlobalSettingsPage::create_screenshot_hotkey_options() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Take a screenshot:", get_color_theme().text_color)); + auto take_screenshot_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + take_screenshot_button_ptr = take_screenshot_button.get(); + list->add_widget(std::move(take_screenshot_button)); + + take_screenshot_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::TAKE_SCREENSHOT); + }; + + return list; + } + + std::unique_ptr<List> GlobalSettingsPage::create_screenshot_region_hotkey_options() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Take a screenshot of a region:", get_color_theme().text_color)); + auto take_screenshot_region_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + take_screenshot_region_button_ptr = take_screenshot_region_button.get(); + list->add_widget(std::move(take_screenshot_region_button)); + + take_screenshot_region_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::TAKE_SCREENSHOT_REGION); + }; + + return list; + } + + std::unique_ptr<List> GlobalSettingsPage::create_hotkey_control_buttons() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + + auto clear_hotkeys_button = std::make_unique<Button>(&get_theme().body_font, "Clear hotkeys", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + clear_hotkeys_button->on_click = [this] { + config.streaming_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0}; + config.record_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0}; + config.record_config.pause_unpause_hotkey = {mgl::Keyboard::Unknown, 0}; + config.replay_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0}; + config.replay_config.save_hotkey = {mgl::Keyboard::Unknown, 0}; + config.replay_config.save_1_min_hotkey = {mgl::Keyboard::Unknown, 0}; + config.replay_config.save_10_min_hotkey = {mgl::Keyboard::Unknown, 0}; + config.screenshot_config.take_screenshot_hotkey = {mgl::Keyboard::Unknown, 0}; + config.screenshot_config.take_screenshot_region_hotkey = {mgl::Keyboard::Unknown, 0}; + config.main_config.show_hide_hotkey = {mgl::Keyboard::Unknown, 0}; + load_hotkeys(); + overlay->rebind_all_keyboard_hotkeys(); + }; + list->add_widget(std::move(clear_hotkeys_button)); + + auto reset_hotkeys_button = std::make_unique<Button>(&get_theme().body_font, "Reset hotkeys to default", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + reset_hotkeys_button->on_click = [this] { + config.set_hotkeys_to_default(); + load_hotkeys(); + overlay->rebind_all_keyboard_hotkeys(); + }; + list->add_widget(std::move(reset_hotkeys_button)); + + return list; + } + + static std::unique_ptr<List> create_joystick_hotkey_text(mgl::Texture *image1, mgl::Texture *image2, float max_height, const char *suffix) { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Press", get_color_theme().text_color)); + list->add_widget(std::make_unique<Image>(image1, mgl::vec2f{max_height, 1000.0f}, Image::ScaleBehavior::SCALE)); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "and", get_color_theme().text_color)); + list->add_widget(std::make_unique<Image>(image2, mgl::vec2f{max_height, 1000.0f}, Image::ScaleBehavior::SCALE)); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, suffix, get_color_theme().text_color)); + return list; + } + + std::unique_ptr<Subsection> GlobalSettingsPage::create_keyboard_hotkey_subsection(ScrollablePage *parent_page) { + auto list = std::make_unique<List>(List::Orientation::VERTICAL); + List *list_ptr = list.get(); + auto subsection = std::make_unique<Subsection>("Keyboard hotkeys", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f)); + + list_ptr->add_widget(std::make_unique<Label>(&get_theme().body_font, "Enable keyboard hotkeys?", get_color_theme().text_color)); + list_ptr->add_widget(create_enable_keyboard_hotkeys_button()); + list_ptr->add_widget(std::make_unique<LineSeparator>(LineSeparator::Orientation::HORIZONTAL, subsection->get_inner_size().x)); + list_ptr->add_widget(create_show_hide_hotkey_options()); + list_ptr->add_widget(create_replay_hotkey_options()); + list_ptr->add_widget(create_replay_partial_save_hotkey_options()); + list_ptr->add_widget(create_record_hotkey_options()); + list_ptr->add_widget(create_stream_hotkey_options()); + list_ptr->add_widget(create_screenshot_hotkey_options()); + list_ptr->add_widget(create_screenshot_region_hotkey_options()); + list_ptr->add_widget(create_hotkey_control_buttons()); + return subsection; + } + + std::unique_ptr<Subsection> GlobalSettingsPage::create_controller_hotkey_subsection(ScrollablePage *parent_page) { + auto list = std::make_unique<List>(List::Orientation::VERTICAL); + List *list_ptr = list.get(); + auto subsection = std::make_unique<Subsection>("Controller hotkeys", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f)); + + list_ptr->add_widget(std::make_unique<Label>(&get_theme().body_font, "Enable controller hotkeys?", get_color_theme().text_color)); + list_ptr->add_widget(create_enable_joystick_hotkeys_button()); + list_ptr->add_widget(std::make_unique<LineSeparator>(LineSeparator::Orientation::HORIZONTAL, subsection->get_inner_size().x)); + list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_options_texture, get_theme().body_font.get_character_size(), "to show/hide the UI")); + list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_up_texture, get_theme().body_font.get_character_size(), "to take a screenshot")); + list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_down_texture, get_theme().body_font.get_character_size(), "to save a replay")); + list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_left_texture, get_theme().body_font.get_character_size(), "to start/stop recording")); + list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_right_texture, get_theme().body_font.get_character_size(), "to turn replay on/off")); + list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_cross_texture, get_theme().body_font.get_character_size(), "to save a 1 minute replay")); + list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_triangle_texture, get_theme().body_font.get_character_size(), "to save a 10 minute replay")); + return subsection; + } + + std::unique_ptr<Button> GlobalSettingsPage::create_exit_program_button() { + auto exit_program_button = std::make_unique<Button>(&get_theme().body_font, "Exit program", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + exit_program_button->on_click = [&]() { + if(on_click_exit_program_button) + on_click_exit_program_button("exit"); + }; + return exit_program_button; + } + + std::unique_ptr<Button> GlobalSettingsPage::create_go_back_to_old_ui_button() { + auto exit_program_button = std::make_unique<Button>(&get_theme().body_font, "Go back to the old UI", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + exit_program_button->on_click = [&]() { + if(on_click_exit_program_button) + on_click_exit_program_button("back-to-old-ui"); + }; + return exit_program_button; + } + + std::unique_ptr<Subsection> GlobalSettingsPage::create_application_options_subsection(ScrollablePage *parent_page) { + const bool inside_flatpak = getenv("FLATPAK_ID") != NULL; + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL); + list->add_widget(create_exit_program_button()); + if(inside_flatpak) + list->add_widget(create_go_back_to_old_ui_button()); + return std::make_unique<Subsection>("Application options", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f)); + } + + std::unique_ptr<Subsection> GlobalSettingsPage::create_application_info_subsection(ScrollablePage *parent_page) { + const bool inside_flatpak = getenv("FLATPAK_ID") != NULL; + auto list = std::make_unique<List>(List::Orientation::VERTICAL); + + char str[128]; + const std::string gsr_version = gsr_info->system_info.gsr_version.to_string(); + snprintf(str, sizeof(str), "GSR version: %s", gsr_version.c_str()); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color)); + + snprintf(str, sizeof(str), "GSR-UI version: %s", GSR_UI_VERSION); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color)); + + if(inside_flatpak) { + snprintf(str, sizeof(str), "Flatpak version: %s", GSR_FLATPAK_VERSION); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color)); + } + + snprintf(str, sizeof(str), "GPU vendor: %s", gpu_vendor_to_string(gsr_info->gpu_info.vendor)); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color)); + + return std::make_unique<Subsection>("Application info", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f)); + } + void GlobalSettingsPage::add_widgets() { auto scrollable_page = std::make_unique<ScrollablePage>(content_page_ptr->get_inner_size()); - scrollable_page->add_widget(create_appearance_subsection(scrollable_page.get())); + + auto settings_list = std::make_unique<List>(List::Orientation::VERTICAL); + settings_list->set_spacing(0.018f); + settings_list->add_widget(create_appearance_subsection(scrollable_page.get())); + settings_list->add_widget(create_startup_subsection(scrollable_page.get())); + settings_list->add_widget(create_keyboard_hotkey_subsection(scrollable_page.get())); + settings_list->add_widget(create_controller_hotkey_subsection(scrollable_page.get())); + settings_list->add_widget(create_application_options_subsection(scrollable_page.get())); + settings_list->add_widget(create_application_info_subsection(scrollable_page.get())); + scrollable_page->add_widget(std::move(settings_list)); + content_page_ptr->add_widget(std::move(scrollable_page)); } void GlobalSettingsPage::on_navigate_away_from_page() { save(); + if(on_page_closed) + on_page_closed(); } void GlobalSettingsPage::load() { @@ -74,10 +503,251 @@ namespace gsr { tint_color_radio_button_ptr->set_selected_item(gpu_vendor_to_color_name(gsr_info->gpu_info.vendor)); else tint_color_radio_button_ptr->set_selected_item(config.main_config.tint_color); + + const char *args[] = { "systemctl", "is-enabled", "--quiet", "--user", "gpu-screen-recorder-ui", nullptr }; + std::string stdout_str; + const int exit_status = exec_program_on_host_get_stdout(args, stdout_str); + startup_radio_button_ptr->set_selected_item(exit_status == 0 ? "start_on_system_startup" : "dont_start_on_system_startup", false, false); + + enable_keyboard_hotkeys_radio_button_ptr->set_selected_item(config.main_config.hotkeys_enable_option, false, false); + enable_joystick_hotkeys_radio_button_ptr->set_selected_item(config.main_config.joystick_hotkeys_enable_option, false, false); + + load_hotkeys(); + } + + void GlobalSettingsPage::load_hotkeys() { + turn_replay_on_off_button_ptr->set_text(config.replay_config.start_stop_hotkey.to_string()); + save_replay_button_ptr->set_text(config.replay_config.save_hotkey.to_string()); + save_replay_1_min_button_ptr->set_text(config.replay_config.save_1_min_hotkey.to_string()); + save_replay_10_min_button_ptr->set_text(config.replay_config.save_10_min_hotkey.to_string()); + + start_stop_recording_button_ptr->set_text(config.record_config.start_stop_hotkey.to_string()); + pause_unpause_recording_button_ptr->set_text(config.record_config.pause_unpause_hotkey.to_string()); + + start_stop_streaming_button_ptr->set_text(config.streaming_config.start_stop_hotkey.to_string()); + + take_screenshot_button_ptr->set_text(config.screenshot_config.take_screenshot_hotkey.to_string()); + take_screenshot_region_button_ptr->set_text(config.screenshot_config.take_screenshot_region_hotkey.to_string()); + + show_hide_button_ptr->set_text(config.main_config.show_hide_hotkey.to_string()); } void GlobalSettingsPage::save() { + configure_hotkey_cancel(); config.main_config.tint_color = tint_color_radio_button_ptr->get_selected_id(); + config.main_config.hotkeys_enable_option = enable_keyboard_hotkeys_radio_button_ptr->get_selected_id(); + config.main_config.joystick_hotkeys_enable_option = enable_joystick_hotkeys_radio_button_ptr->get_selected_id(); save_config(config); } -}
\ No newline at end of file + + bool GlobalSettingsPage::on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) { + if(!StaticPage::on_event(event, window, offset)) + return false; + + if(configure_hotkey_type == ConfigureHotkeyType::NONE) + return true; + + Button *configure_hotkey_button = configure_hotkey_get_button_by_active_type(); + if(!configure_hotkey_button) + return true; + + if(event.type == mgl::Event::KeyPressed) { + if(event.key.code == mgl::Keyboard::Escape) + return false; + + if(event.key.code == mgl::Keyboard::Backspace) { + configure_config_hotkey = {mgl::Keyboard::Unknown, 0}; + configure_hotkey_button->set_text(""); + configure_hotkey_stop_and_save(); + return false; + } + + if(mgl::Keyboard::key_is_modifier(event.key.code)) { + configure_config_hotkey.modifiers |= mgl_modifier_to_hotkey_modifier(event.key.code); + configure_hotkey_button->set_text(configure_config_hotkey.to_string()); + } else if(event.key.code != mgl::Keyboard::Unknown && (configure_config_hotkey.modifiers != 0 || !key_is_alpha_numerical(event.key.code))) { + configure_config_hotkey.key = event.key.code; + configure_hotkey_button->set_text(configure_config_hotkey.to_string()); + configure_hotkey_stop_and_save(); + } + + return false; + } else if(event.type == mgl::Event::KeyReleased) { + if(event.key.code == mgl::Keyboard::Escape) { + configure_hotkey_cancel(); + return false; + } + + if(mgl::Keyboard::key_is_modifier(event.key.code)) { + configure_config_hotkey.modifiers &= ~mgl_modifier_to_hotkey_modifier(event.key.code); + configure_hotkey_button->set_text(configure_config_hotkey.to_string()); + } + + return false; + } + + return true; + } + + Button* GlobalSettingsPage::configure_hotkey_get_button_by_active_type() { + switch(configure_hotkey_type) { + case ConfigureHotkeyType::NONE: + return nullptr; + case ConfigureHotkeyType::REPLAY_START_STOP: + return turn_replay_on_off_button_ptr; + case ConfigureHotkeyType::REPLAY_SAVE: + return save_replay_button_ptr; + case ConfigureHotkeyType::REPLAY_SAVE_1_MIN: + return save_replay_1_min_button_ptr; + case ConfigureHotkeyType::REPLAY_SAVE_10_MIN: + return save_replay_10_min_button_ptr; + case ConfigureHotkeyType::RECORD_START_STOP: + return start_stop_recording_button_ptr; + case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE: + return pause_unpause_recording_button_ptr; + case ConfigureHotkeyType::STREAM_START_STOP: + return start_stop_streaming_button_ptr; + case ConfigureHotkeyType::TAKE_SCREENSHOT: + return take_screenshot_button_ptr; + case ConfigureHotkeyType::TAKE_SCREENSHOT_REGION: + return take_screenshot_region_button_ptr; + case ConfigureHotkeyType::SHOW_HIDE: + return show_hide_button_ptr; + } + return nullptr; + } + + ConfigHotkey* GlobalSettingsPage::configure_hotkey_get_config_by_active_type() { + switch(configure_hotkey_type) { + case ConfigureHotkeyType::NONE: + return nullptr; + case ConfigureHotkeyType::REPLAY_START_STOP: + return &config.replay_config.start_stop_hotkey; + case ConfigureHotkeyType::REPLAY_SAVE: + return &config.replay_config.save_hotkey; + case ConfigureHotkeyType::REPLAY_SAVE_1_MIN: + return &config.replay_config.save_1_min_hotkey; + case ConfigureHotkeyType::REPLAY_SAVE_10_MIN: + return &config.replay_config.save_10_min_hotkey; + case ConfigureHotkeyType::RECORD_START_STOP: + return &config.record_config.start_stop_hotkey; + case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE: + return &config.record_config.pause_unpause_hotkey; + case ConfigureHotkeyType::STREAM_START_STOP: + return &config.streaming_config.start_stop_hotkey; + case ConfigureHotkeyType::TAKE_SCREENSHOT: + return &config.screenshot_config.take_screenshot_hotkey; + case ConfigureHotkeyType::TAKE_SCREENSHOT_REGION: + return &config.screenshot_config.take_screenshot_region_hotkey; + case ConfigureHotkeyType::SHOW_HIDE: + return &config.main_config.show_hide_hotkey; + } + return nullptr; + } + + void GlobalSettingsPage::for_each_config_hotkey(std::function<void(ConfigHotkey *config_hotkey)> callback) { + ConfigHotkey *config_hotkeys[] = { + &config.replay_config.start_stop_hotkey, + &config.replay_config.save_hotkey, + &config.record_config.start_stop_hotkey, + &config.record_config.pause_unpause_hotkey, + &config.streaming_config.start_stop_hotkey, + &config.screenshot_config.take_screenshot_hotkey, + &config.screenshot_config.take_screenshot_region_hotkey, + &config.main_config.show_hide_hotkey + }; + for(ConfigHotkey *config_hotkey : config_hotkeys) { + callback(config_hotkey); + } + } + + void GlobalSettingsPage::configure_hotkey_start(ConfigureHotkeyType hotkey_type) { + assert(hotkey_type != ConfigureHotkeyType::NONE); + configure_config_hotkey = {0, 0}; + configure_hotkey_type = hotkey_type; + + content_page_ptr->set_visible(false); + hotkey_overlay_ptr->set_visible(true); + overlay->unbind_all_keyboard_hotkeys(); + configure_hotkey_get_button_by_active_type()->set_text(""); + + switch(hotkey_type) { + case ConfigureHotkeyType::NONE: + hotkey_configure_action_name = ""; + break; + case ConfigureHotkeyType::REPLAY_START_STOP: + hotkey_configure_action_name = "Turn replay on/off"; + break; + case ConfigureHotkeyType::REPLAY_SAVE: + hotkey_configure_action_name = "Save replay"; + break; + case ConfigureHotkeyType::REPLAY_SAVE_1_MIN: + hotkey_configure_action_name = "Save 1 minute replay"; + break; + case ConfigureHotkeyType::REPLAY_SAVE_10_MIN: + hotkey_configure_action_name = "Save 10 minute replay"; + break; + case ConfigureHotkeyType::RECORD_START_STOP: + hotkey_configure_action_name = "Start/stop recording"; + break; + case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE: + hotkey_configure_action_name = "Pause/unpause recording"; + break; + case ConfigureHotkeyType::STREAM_START_STOP: + hotkey_configure_action_name = "Start/stop streaming"; + break; + case ConfigureHotkeyType::TAKE_SCREENSHOT: + hotkey_configure_action_name = "Take a screenshot"; + break; + case ConfigureHotkeyType::TAKE_SCREENSHOT_REGION: + hotkey_configure_action_name = "Take a screenshot of a region"; + break; + case ConfigureHotkeyType::SHOW_HIDE: + hotkey_configure_action_name = "Show/hide UI"; + break; + } + } + + void GlobalSettingsPage::configure_hotkey_cancel() { + Button *config_hotkey_button = configure_hotkey_get_button_by_active_type(); + ConfigHotkey *config_hotkey = configure_hotkey_get_config_by_active_type(); + if(config_hotkey_button && config_hotkey) + config_hotkey_button->set_text(config_hotkey->to_string()); + + configure_config_hotkey = {0, 0}; + configure_hotkey_type = ConfigureHotkeyType::NONE; + content_page_ptr->set_visible(true); + hotkey_overlay_ptr->set_visible(false); + overlay->rebind_all_keyboard_hotkeys(); + } + + void GlobalSettingsPage::configure_hotkey_stop_and_save() { + Button *config_hotkey_button = configure_hotkey_get_button_by_active_type(); + ConfigHotkey *config_hotkey = configure_hotkey_get_config_by_active_type(); + if(config_hotkey_button && config_hotkey) { + bool hotkey_used_by_another_action = false; + if(configure_config_hotkey.key != mgl::Keyboard::Unknown) { + for_each_config_hotkey([&](ConfigHotkey *config_hotkey_item) { + if(config_hotkey_item != config_hotkey && *config_hotkey_item == configure_config_hotkey) + hotkey_used_by_another_action = true; + }); + } + + if(hotkey_used_by_another_action) { + const std::string error_msg = "The hotkey \"" + configure_config_hotkey.to_string() + " is already used for something else"; + overlay->show_notification(error_msg.c_str(), 3.0, mgl::Color(255, 0, 0, 255), mgl::Color(255, 0, 0, 255), NotificationType::NONE); + config_hotkey_button->set_text(config_hotkey->to_string()); + configure_config_hotkey = {0, 0}; + return; + } + + *config_hotkey = configure_config_hotkey; + } + + configure_config_hotkey = {0, 0}; + configure_hotkey_type = ConfigureHotkeyType::NONE; + content_page_ptr->set_visible(true); + hotkey_overlay_ptr->set_visible(false); + overlay->rebind_all_keyboard_hotkeys(); + } +} diff --git a/src/gui/GsrPage.cpp b/src/gui/GsrPage.cpp index e6ee5fc..b4005f5 100644 --- a/src/gui/GsrPage.cpp +++ b/src/gui/GsrPage.cpp @@ -8,8 +8,9 @@ namespace gsr { static const float button_spacing_scale = 0.015f; - GsrPage::GsrPage() : - label_text("Settings", get_theme().title_font) + GsrPage::GsrPage(const char *top_text, const char *bottom_text) : + top_text(top_text, get_theme().title_font), + bottom_text(bottom_text, get_theme().title_font) { const float margin = 0.02f; set_margins(margin, margin, margin, margin); @@ -38,8 +39,9 @@ namespace gsr { // Process widgets by visibility (backwards) return widgets.for_each_reverse([selected_widget, &window, &event, content_page_position](std::unique_ptr<Widget> &widget) { - if(widget.get() != selected_widget) { - if(!widget->on_event(event, window, content_page_position)) + Widget *p = widget.get(); + if(p != selected_widget) { + if(!p->on_event(event, window, content_page_position)) return false; } return true; @@ -80,13 +82,17 @@ namespace gsr { window.draw(background); const int text_margin = background.get_size().y * 0.085; - label_text.set_position((background.get_position() + mgl::vec2f(background.get_size().x * 0.5f - label_text.get_bounds().size.x * 0.5f, text_margin)).floor()); - window.draw(label_text); + + top_text.set_position((background.get_position() + mgl::vec2f(background.get_size().x * 0.5f - top_text.get_bounds().size.x * 0.5f, text_margin)).floor()); + window.draw(top_text); mgl::Sprite icon(&get_theme().settings_texture); icon.set_height((int)(background.get_size().y * 0.5f)); icon.set_position((background.get_position() + background.get_size() * 0.5f - icon.get_size() * 0.5f).floor()); window.draw(icon); + + bottom_text.set_position((background.get_position() + mgl::vec2f(background.get_size().x * 0.5f - bottom_text.get_bounds().size.x * 0.5f, background.get_size().y - bottom_text.get_bounds().size.y - text_margin)).floor()); + window.draw(bottom_text); } void GsrPage::draw_buttons(mgl::Window &window, mgl::vec2f body_pos, mgl::vec2f body_size) { @@ -102,15 +108,8 @@ namespace gsr { void GsrPage::draw_children(mgl::Window &window, mgl::vec2f position) { Widget *selected_widget = selected_child_widget; - mgl_scissor prev_scissor; - mgl_window_get_scissor(window.internal_window(), &prev_scissor); - - const mgl::vec2f inner_size = get_inner_size(); - mgl_scissor new_scissor = { - mgl_vec2i{(int)position.x, (int)position.y}, - mgl_vec2i{(int)inner_size.x, (int)inner_size.y} - }; - mgl_window_set_scissor(window.internal_window(), &new_scissor); + const mgl::Scissor prev_scissor = window.get_scissor(); + window.set_scissor({position.to_vec2i(), get_inner_size().to_vec2i()}); for(size_t i = 0; i < widgets.size(); ++i) { auto &widget = widgets[i]; @@ -121,7 +120,7 @@ namespace gsr { if(selected_widget) selected_widget->draw(window, position); - mgl_window_set_scissor(window.internal_window(), &prev_scissor); + window.set_scissor(prev_scissor); } mgl::vec2f GsrPage::get_size() { diff --git a/src/gui/Image.cpp b/src/gui/Image.cpp new file mode 100644 index 0000000..b6cec9a --- /dev/null +++ b/src/gui/Image.cpp @@ -0,0 +1,39 @@ +#include "../../include/gui/Image.hpp" +#include "../../include/gui/Utils.hpp" + +#include <mglpp/window/Window.hpp> +#include <mglpp/graphics/Texture.hpp> + +namespace gsr { + Image::Image(mgl::Texture *texture, mgl::vec2f size, ScaleBehavior scale_behavior) : + sprite(texture), size(size), scale_behavior(scale_behavior) + { + + } + + bool Image::on_event(mgl::Event&, mgl::Window&, mgl::vec2f) { + return true; + } + + void Image::draw(mgl::Window &window, mgl::vec2f offset) { + if(!visible) + return; + + sprite.set_size(get_size()); + sprite.set_position((position + offset).floor()); + window.draw(sprite); + } + + mgl::vec2f Image::get_size() { + if(!visible || !sprite.get_texture()) + return {0.0f, 0.0f}; + + const mgl::vec2f sprite_size = sprite.get_texture()->get_size().to_vec2f(); + if(size.x < 0.001f && size.y < 0.001f) + return sprite_size; + else if(scale_behavior == ScaleBehavior::SCALE) + return scale_keep_aspect_ratio(sprite_size, size); + else + return clamp_keep_aspect_ratio(sprite_size, size); + } +}
\ No newline at end of file diff --git a/src/gui/List.cpp b/src/gui/List.cpp index 5294e36..57a6045 100644 --- a/src/gui/List.cpp +++ b/src/gui/List.cpp @@ -24,14 +24,23 @@ namespace gsr { // Process widgets by visibility (backwards) return widgets.for_each_reverse([selected_widget, &event, &window](std::unique_ptr<Widget> &widget) { // Ignore offset because widgets are positioned with offset in ::draw, this solution is simpler - if(widget.get() != selected_widget) { - if(!widget->on_event(event, window, mgl::vec2f(0.0f, 0.0f))) + Widget *p = widget.get(); + if(p != selected_widget) { + if(!p->on_event(event, window, mgl::vec2f(0.0f, 0.0f))) return false; } return true; }); } + List::~List() { + widgets.for_each([this](std::unique_ptr<Widget> &widget) { + if(widget->parent_widget == this) + widget->parent_widget = nullptr; + return true; + }, true); + } + void List::draw(mgl::Window &window, mgl::vec2f offset) { if(!visible) return; @@ -104,15 +113,6 @@ namespace gsr { selected_widget->draw(window, mgl::vec2f(0.0f, 0.0f)); } - // void List::remove_child_widget(Widget *widget) { - // for(auto it = widgets.begin(), end = widgets.end(); it != end; ++it) { - // if(it->get() == widget) { - // widgets.erase(it); - // return; - // } - // } - // } - void List::add_widget(std::unique_ptr<Widget> widget) { widget->parent_widget = this; widgets.push_back(std::move(widget)); @@ -122,6 +122,10 @@ namespace gsr { widgets.remove(widget); } + void List::replace_widget(Widget *widget, std::unique_ptr<Widget> new_widget) { + widgets.replace_item(widget, std::move(new_widget)); + } + void List::clear() { widgets.clear(); } @@ -137,6 +141,10 @@ namespace gsr { return nullptr; } + size_t List::get_num_children() const { + return widgets.size(); + } + void List::set_spacing(float spacing) { spacing_scale = spacing; } diff --git a/src/gui/Page.cpp b/src/gui/Page.cpp index ae13d82..5f21b71 100644 --- a/src/gui/Page.cpp +++ b/src/gui/Page.cpp @@ -1,14 +1,13 @@ #include "../../include/gui/Page.hpp" namespace gsr { - // void Page::remove_child_widget(Widget *widget) { - // for(auto it = widgets.begin(), end = widgets.end(); it != end; ++it) { - // if(it->get() == widget) { - // widgets.erase(it); - // return; - // } - // } - // } + Page::~Page() { + widgets.for_each([this](std::unique_ptr<Widget> &widget) { + if(widget->parent_widget == this) + widget->parent_widget = nullptr; + return true; + }, true); + } void Page::add_widget(std::unique_ptr<Widget> widget) { widget->parent_widget = this; diff --git a/src/gui/RadioButton.cpp b/src/gui/RadioButton.cpp index 061d811..bbb958a 100644 --- a/src/gui/RadioButton.cpp +++ b/src/gui/RadioButton.cpp @@ -35,12 +35,12 @@ namespace gsr { const bool mouse_inside = mgl::FloatRect(draw_pos, item_size).contains(mgl::vec2f(event.mouse_button.x, event.mouse_button.y)); if(mouse_inside) { - const size_t prev_selected_item = selected_item; - selected_item = i; - - if(selected_item != prev_selected_item && on_selection_changed) - on_selection_changed(item.text.get_string(), item.id); + if(selected_item != i && on_selection_changed) { + if(!on_selection_changed(item.text.get_string(), item.id)) + return false; + } + selected_item = i; return false; } @@ -158,18 +158,18 @@ namespace gsr { for(size_t i = 0; i < items.size(); ++i) { auto &item = items[i]; if(item.id == id) { - const size_t prev_selected_item = selected_item; - selected_item = i; - - if(trigger_event && (trigger_event_even_if_selection_not_changed || selected_item != prev_selected_item) && on_selection_changed) - on_selection_changed(item.text.get_string(), item.id); + if(trigger_event && (trigger_event_even_if_selection_not_changed || selected_item != i) && on_selection_changed) { + if(!on_selection_changed(item.text.get_string(), item.id)) + break; + } + selected_item = i; break; } } } - const std::string RadioButton::get_selected_id() const { + const std::string& RadioButton::get_selected_id() const { if(items.empty()) { static std::string dummy; return dummy; @@ -177,4 +177,13 @@ namespace gsr { return items[selected_item].id; } } + + const std::string& RadioButton::get_selected_text() const { + if(items.empty()) { + static std::string dummy; + return dummy; + } else { + return items[selected_item].text.get_string(); + } + } }
\ No newline at end of file diff --git a/src/gui/ScreenshotSettingsPage.cpp b/src/gui/ScreenshotSettingsPage.cpp new file mode 100644 index 0000000..27a94b0 --- /dev/null +++ b/src/gui/ScreenshotSettingsPage.cpp @@ -0,0 +1,332 @@ +#include "../../include/gui/ScreenshotSettingsPage.hpp" +#include "../../include/gui/GsrPage.hpp" +#include "../../include/gui/PageStack.hpp" +#include "../../include/Theme.hpp" +#include "../../include/GsrInfo.hpp" +#include "../../include/Utils.hpp" +#include "../../include/gui/List.hpp" +#include "../../include/gui/ScrollablePage.hpp" +#include "../../include/gui/Label.hpp" +#include "../../include/gui/Subsection.hpp" +#include "../../include/gui/FileChooser.hpp" + +namespace gsr { + ScreenshotSettingsPage::ScreenshotSettingsPage(const GsrInfo *gsr_info, Config &config, PageStack *page_stack) : + StaticPage(mgl::vec2f(get_theme().window_width, get_theme().window_height).floor()), + config(config), + gsr_info(gsr_info), + page_stack(page_stack) + { + capture_options = get_supported_capture_options(*gsr_info); + + auto content_page = std::make_unique<GsrPage>("Screenshot", "Settings"); + content_page->add_button("Back", "back", get_color_theme().page_bg_color); + content_page->on_click = [page_stack](const std::string &id) { + if(id == "back") + page_stack->pop(); + }; + content_page_ptr = content_page.get(); + add_widget(std::move(content_page)); + + add_widgets(); + load(); + } + + std::unique_ptr<ComboBox> ScreenshotSettingsPage::create_record_area_box() { + auto record_area_box = std::make_unique<ComboBox>(&get_theme().body_font); + // TODO: Show options not supported but disable them + if(capture_options.window) + record_area_box->add_item("Window", "window"); + if(capture_options.region) + record_area_box->add_item("Region", "region"); + if(!capture_options.monitors.empty()) + record_area_box->add_item(gsr_info->system_info.display_server == DisplayServer::WAYLAND ? "Focused monitor (Experimental on Wayland)" : "Focused monitor", "focused_monitor"); + for(const auto &monitor : capture_options.monitors) { + char name[256]; + snprintf(name, sizeof(name), "Monitor %s (%dx%d)", monitor.name.c_str(), monitor.size.x, monitor.size.y); + record_area_box->add_item(name, monitor.name); + } + if(capture_options.portal) + record_area_box->add_item("Desktop portal", "portal"); + record_area_box_ptr = record_area_box.get(); + return record_area_box; + } + + std::unique_ptr<Widget> ScreenshotSettingsPage::create_record_area() { + auto record_area_list = std::make_unique<List>(List::Orientation::VERTICAL); + record_area_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Capture target:", get_color_theme().text_color)); + record_area_list->add_widget(create_record_area_box()); + return record_area_list; + } + + std::unique_ptr<Entry> ScreenshotSettingsPage::create_image_width_entry() { + auto image_width_entry = std::make_unique<Entry>(&get_theme().body_font, "1920", get_theme().body_font.get_character_size() * 3); + image_width_entry->validate_handler = create_entry_validator_integer_in_range(1, 1 << 15); + image_width_entry_ptr = image_width_entry.get(); + return image_width_entry; + } + + std::unique_ptr<Entry> ScreenshotSettingsPage::create_image_height_entry() { + auto image_height_entry = std::make_unique<Entry>(&get_theme().body_font, "1080", get_theme().body_font.get_character_size() * 3); + image_height_entry->validate_handler = create_entry_validator_integer_in_range(1, 1 << 15); + image_height_entry_ptr = image_height_entry.get(); + return image_height_entry; + } + + std::unique_ptr<List> ScreenshotSettingsPage::create_image_resolution() { + auto area_size_params_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + area_size_params_list->add_widget(create_image_width_entry()); + area_size_params_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "x", get_color_theme().text_color)); + area_size_params_list->add_widget(create_image_height_entry()); + return area_size_params_list; + } + + std::unique_ptr<List> ScreenshotSettingsPage::create_image_resolution_section() { + auto image_resolution_list = std::make_unique<List>(List::Orientation::VERTICAL); + image_resolution_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Image resolution limit:", get_color_theme().text_color)); + image_resolution_list->add_widget(create_image_resolution()); + image_resolution_list_ptr = image_resolution_list.get(); + return image_resolution_list; + } + + std::unique_ptr<CheckBox> ScreenshotSettingsPage::create_restore_portal_session_checkbox() { + auto restore_portal_session_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Restore portal session"); + restore_portal_session_checkbox->set_checked(true); + restore_portal_session_checkbox_ptr = restore_portal_session_checkbox.get(); + return restore_portal_session_checkbox; + } + + std::unique_ptr<List> ScreenshotSettingsPage::create_restore_portal_session_section() { + auto restore_portal_session_list = std::make_unique<List>(List::Orientation::VERTICAL); + restore_portal_session_list->add_widget(std::make_unique<Label>(&get_theme().body_font, " ", get_color_theme().text_color)); + restore_portal_session_list->add_widget(create_restore_portal_session_checkbox()); + restore_portal_session_list_ptr = restore_portal_session_list.get(); + return restore_portal_session_list; + } + + std::unique_ptr<Widget> ScreenshotSettingsPage::create_change_image_resolution_section() { + auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Change image resolution"); + change_image_resolution_checkbox_ptr = checkbox.get(); + return checkbox; + } + + std::unique_ptr<Widget> ScreenshotSettingsPage::create_capture_target_section() { + auto ll = std::make_unique<List>(List::Orientation::VERTICAL); + + auto capture_target_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + capture_target_list->add_widget(create_record_area()); + capture_target_list->add_widget(create_image_resolution_section()); + capture_target_list->add_widget(create_restore_portal_session_section()); + + ll->add_widget(std::move(capture_target_list)); + ll->add_widget(create_change_image_resolution_section()); + return std::make_unique<Subsection>("Record area", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); + } + + std::unique_ptr<List> ScreenshotSettingsPage::create_image_quality_section() { + auto list = std::make_unique<List>(List::Orientation::VERTICAL); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Image quality:", get_color_theme().text_color)); + + auto image_quality_box = std::make_unique<ComboBox>(&get_theme().body_font); + image_quality_box->add_item("Medium", "medium"); + image_quality_box->add_item("High", "high"); + image_quality_box->add_item("Very high (Recommended)", "very_high"); + image_quality_box->add_item("Ultra", "ultra"); + image_quality_box->set_selected_item("very_high"); + + image_quality_box_ptr = image_quality_box.get(); + list->add_widget(std::move(image_quality_box)); + + return list; + } + + std::unique_ptr<Widget> ScreenshotSettingsPage::create_record_cursor_section() { + auto record_cursor_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Record cursor"); + record_cursor_checkbox->set_checked(true); + record_cursor_checkbox_ptr = record_cursor_checkbox.get(); + return record_cursor_checkbox; + } + + std::unique_ptr<Widget> ScreenshotSettingsPage::create_image_section() { + auto image_section_list = std::make_unique<List>(List::Orientation::VERTICAL); + image_section_list->add_widget(create_image_quality_section()); + image_section_list->add_widget(create_record_cursor_section()); + return std::make_unique<Subsection>("Image", std::move(image_section_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); + } + + std::unique_ptr<List> ScreenshotSettingsPage::create_save_directory(const char *label) { + auto save_directory_list = std::make_unique<List>(List::Orientation::VERTICAL); + save_directory_list->add_widget(std::make_unique<Label>(&get_theme().body_font, label, get_color_theme().text_color)); + auto save_directory_button = std::make_unique<Button>(&get_theme().body_font, get_pictures_dir().c_str(), mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + save_directory_button_ptr = save_directory_button.get(); + save_directory_button->on_click = [this]() { + auto select_directory_page = std::make_unique<GsrPage>("File", "Settings"); + select_directory_page->add_button("Save", "save", get_color_theme().tint_color); + select_directory_page->add_button("Cancel", "cancel", get_color_theme().page_bg_color); + + auto file_chooser = std::make_unique<FileChooser>(save_directory_button_ptr->get_text().c_str(), select_directory_page->get_inner_size()); + FileChooser *file_chooser_ptr = file_chooser.get(); + select_directory_page->add_widget(std::move(file_chooser)); + + select_directory_page->on_click = [this, file_chooser_ptr](const std::string &id) { + if(id == "save") { + save_directory_button_ptr->set_text(file_chooser_ptr->get_current_directory()); + page_stack->pop(); + } else if(id == "cancel") { + page_stack->pop(); + } + }; + + page_stack->push(std::move(select_directory_page)); + }; + save_directory_list->add_widget(std::move(save_directory_button)); + return save_directory_list; + } + + std::unique_ptr<ComboBox> ScreenshotSettingsPage::create_image_format_box() { + auto box = std::make_unique<ComboBox>(&get_theme().body_font); + if(gsr_info->supported_image_formats.jpeg) + box->add_item("jpg", "jpg"); + if(gsr_info->supported_image_formats.png) + box->add_item("png", "png"); + image_format_box_ptr = box.get(); + return box; + } + + std::unique_ptr<List> ScreenshotSettingsPage::create_image_format_section() { + auto list = std::make_unique<List>(List::Orientation::VERTICAL); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Image format:", get_color_theme().text_color)); + list->add_widget(create_image_format_box()); + return list; + } + + std::unique_ptr<Widget> ScreenshotSettingsPage::create_file_info_section() { + auto file_info_data_list = std::make_unique<List>(List::Orientation::HORIZONTAL); + file_info_data_list->add_widget(create_save_directory("Directory to save the screenshot:")); + file_info_data_list->add_widget(create_image_format_section()); + return std::make_unique<Subsection>("File info", std::move(file_info_data_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); + } + + std::unique_ptr<CheckBox> ScreenshotSettingsPage::create_save_screenshot_in_game_folder() { + char text[256]; + snprintf(text, sizeof(text), "Save screenshot in a folder with the name of the game%s", gsr_info->system_info.display_server == DisplayServer::X11 ? "" : " (X11 applications only)"); + auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, text); + save_screenshot_in_game_folder_checkbox_ptr = checkbox.get(); + return checkbox; + } + + std::unique_ptr<Widget> ScreenshotSettingsPage::create_general_section() { + return std::make_unique<Subsection>("General", create_save_screenshot_in_game_folder(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); + } + + std::unique_ptr<Widget> ScreenshotSettingsPage::create_notifications_section() { + auto show_screenshot_saved_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show screenshot saved notification"); + show_screenshot_saved_notification_checkbox->set_checked(true); + show_screenshot_saved_notification_checkbox_ptr = show_screenshot_saved_notification_checkbox.get(); + return std::make_unique<Subsection>("Notifications", std::move(show_screenshot_saved_notification_checkbox), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); + } + + std::unique_ptr<Widget> ScreenshotSettingsPage::create_settings() { + auto page_list = std::make_unique<List>(List::Orientation::VERTICAL); + page_list->set_spacing(0.018f); + auto scrollable_page = std::make_unique<ScrollablePage>(content_page_ptr->get_inner_size() - mgl::vec2f(0.0f, page_list->get_size().y + 0.018f * get_theme().window_height)); + settings_scrollable_page_ptr = scrollable_page.get(); + page_list->add_widget(std::move(scrollable_page)); + + auto settings_list = std::make_unique<List>(List::Orientation::VERTICAL); + settings_list->set_spacing(0.018f); + settings_list->add_widget(create_capture_target_section()); + settings_list->add_widget(create_image_section()); + settings_list->add_widget(create_file_info_section()); + settings_list->add_widget(create_general_section()); + settings_list->add_widget(create_notifications_section()); + settings_scrollable_page_ptr->add_widget(std::move(settings_list)); + return page_list; + } + + void ScreenshotSettingsPage::add_widgets() { + content_page_ptr->add_widget(create_settings()); + + record_area_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) { + const bool portal_selected = id == "portal"; + image_resolution_list_ptr->set_visible(change_image_resolution_checkbox_ptr->is_checked()); + restore_portal_session_list_ptr->set_visible(portal_selected); + return true; + }; + + change_image_resolution_checkbox_ptr->on_changed = [this](bool checked) { + image_resolution_list_ptr->set_visible(checked); + }; + + if(!capture_options.monitors.empty()) + record_area_box_ptr->set_selected_item("focused_monitor"); + else if(capture_options.portal) + record_area_box_ptr->set_selected_item("portal"); + else if(capture_options.window) + record_area_box_ptr->set_selected_item("window"); + else + record_area_box_ptr->on_selection_changed("", ""); + } + + void ScreenshotSettingsPage::on_navigate_away_from_page() { + save(); + } + + void ScreenshotSettingsPage::load() { + record_area_box_ptr->set_selected_item(config.screenshot_config.record_area_option); + change_image_resolution_checkbox_ptr->set_checked(config.screenshot_config.change_image_resolution); + image_quality_box_ptr->set_selected_item(config.screenshot_config.image_quality); + image_format_box_ptr->set_selected_item(config.screenshot_config.image_format); + record_cursor_checkbox_ptr->set_checked(config.screenshot_config.record_cursor); + restore_portal_session_checkbox_ptr->set_checked(config.screenshot_config.restore_portal_session); + save_directory_button_ptr->set_text(config.screenshot_config.save_directory); + save_screenshot_in_game_folder_checkbox_ptr->set_checked(config.screenshot_config.save_screenshot_in_game_folder); + show_screenshot_saved_notification_checkbox_ptr->set_checked(config.screenshot_config.show_screenshot_saved_notifications); + + if(config.screenshot_config.image_width == 0) + config.screenshot_config.image_width = 1920; + + if(config.screenshot_config.image_height == 0) + config.screenshot_config.image_height = 1080; + + if(config.screenshot_config.image_width < 32) + config.screenshot_config.image_width = 32; + image_width_entry_ptr->set_text(std::to_string(config.screenshot_config.image_width)); + + if(config.screenshot_config.image_height < 32) + config.screenshot_config.image_height = 32; + image_height_entry_ptr->set_text(std::to_string(config.screenshot_config.image_height)); + } + + void ScreenshotSettingsPage::save() { + config.screenshot_config.record_area_option = record_area_box_ptr->get_selected_id(); + config.screenshot_config.image_width = atoi(image_width_entry_ptr->get_text().c_str()); + config.screenshot_config.image_height = atoi(image_height_entry_ptr->get_text().c_str()); + config.screenshot_config.change_image_resolution = change_image_resolution_checkbox_ptr->is_checked(); + config.screenshot_config.image_quality = image_quality_box_ptr->get_selected_id(); + config.screenshot_config.image_format = image_format_box_ptr->get_selected_id(); + config.screenshot_config.record_cursor = record_cursor_checkbox_ptr->is_checked(); + config.screenshot_config.restore_portal_session = restore_portal_session_checkbox_ptr->is_checked(); + config.screenshot_config.save_directory = save_directory_button_ptr->get_text(); + config.screenshot_config.save_screenshot_in_game_folder = save_screenshot_in_game_folder_checkbox_ptr->is_checked(); + config.screenshot_config.show_screenshot_saved_notifications = show_screenshot_saved_notification_checkbox_ptr->is_checked(); + + if(config.screenshot_config.image_width == 0) + config.screenshot_config.image_width = 1920; + + if(config.screenshot_config.image_height == 0) + config.screenshot_config.image_height = 1080; + + if(config.screenshot_config.image_width < 32) { + config.screenshot_config.image_width = 32; + image_width_entry_ptr->set_text("32"); + } + + if(config.screenshot_config.image_height < 32) { + config.screenshot_config.image_height = 32; + image_height_entry_ptr->set_text("32"); + } + + save_config(config); + } +}
\ No newline at end of file diff --git a/src/gui/ScrollablePage.cpp b/src/gui/ScrollablePage.cpp index 74dd715..cec20d3 100644 --- a/src/gui/ScrollablePage.cpp +++ b/src/gui/ScrollablePage.cpp @@ -15,19 +15,49 @@ namespace gsr { ScrollablePage::ScrollablePage(mgl::vec2f size) : size(size) {} + ScrollablePage::~ScrollablePage() { + widgets.for_each([this](std::unique_ptr<Widget> &widget) { + if(widget->parent_widget == this) + widget->parent_widget = nullptr; + return true; + }, true); + } + bool ScrollablePage::on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) { if(!visible) return true; - offset = position + offset + mgl::vec2f(0.0f, scroll_y); + offset = position + offset; + + const mgl::vec2f content_size = get_inner_size(); + const mgl::vec2i scissor_pos(offset.x, offset.y); + const mgl::vec2i scissor_size(content_size.x, content_size.y); + + offset.y += scroll_y; Widget *selected_widget = selected_child_widget; + if(event.type == mgl::Event::MouseButtonPressed && scrollbar_rect.contains(mgl::vec2f(event.mouse_button.x, event.mouse_button.y))) { + set_widget_as_selected_in_parent(); + moving_scrollbar_with_cursor = true; + scrollbar_move_cursor_start_pos = mgl::vec2f(event.mouse_button.x, event.mouse_button.y); + scrollbar_move_cursor_scroll_y_start = scroll_y; + return false; + } + if(event.type == mgl::Event::MouseButtonReleased && moving_scrollbar_with_cursor) { moving_scrollbar_with_cursor = false; remove_widget_as_selected_in_parent(); return false; } + if(event.type == mgl::Event::MouseButtonPressed || event.type == mgl::Event::MouseButtonReleased) { + if(!mgl::IntRect(scissor_pos, scissor_size).contains({event.mouse_button.x, event.mouse_button.y})) + return true; + } else if(event.type == mgl::Event::MouseMoved) { + if(!mgl::IntRect(scissor_pos, scissor_size).contains({event.mouse_move.x, event.mouse_move.y})) + return true; + } + if(selected_widget) { if(!selected_widget->on_event(event, window, offset)) return false; @@ -35,8 +65,9 @@ namespace gsr { // Process widgets by visibility (backwards) const bool continue_events = widgets.for_each_reverse([selected_widget, &window, &event, offset](std::unique_ptr<Widget> &widget) { - if(widget.get() != selected_widget) { - if(!widget->on_event(event, window, offset)) + Widget *p = widget.get(); + if(p != selected_widget) { + if(!p->on_event(event, window, offset)) return false; } return true; @@ -51,14 +82,6 @@ namespace gsr { return false; } - if(event.type == mgl::Event::MouseButtonPressed && scrollbar_rect.contains(mgl::vec2f(event.mouse_button.x, event.mouse_button.y))) { - set_widget_as_selected_in_parent(); - moving_scrollbar_with_cursor = true; - scrollbar_move_cursor_start_pos = mgl::vec2f(event.mouse_button.x, event.mouse_button.y); - scrollbar_move_cursor_scroll_y_start = scroll_y; - return false; - } - return true; } @@ -75,11 +98,10 @@ namespace gsr { offset = position + offset; - mgl_scissor prev_scissor; - mgl_window_get_scissor(window.internal_window(), &prev_scissor); + const mgl::Scissor prev_scissor = window.get_scissor(); const mgl::vec2f content_size = get_inner_size(); - mgl_scissor new_scissor = { + const mgl_scissor new_scissor = { mgl_vec2i{(int)offset.x, (int)offset.y}, mgl_vec2i{(int)content_size.x, (int)content_size.y} }; @@ -136,7 +158,7 @@ namespace gsr { apply_animation(); limit_scroll(child_height); - mgl_window_set_scissor(window.internal_window(), &prev_scissor); + window.set_scissor(prev_scissor); double scrollbar_height = 1.0; if(child_height > 0.001) diff --git a/src/gui/SettingsPage.cpp b/src/gui/SettingsPage.cpp index dc6f2c7..26e7335 100644 --- a/src/gui/SettingsPage.cpp +++ b/src/gui/SettingsPage.cpp @@ -8,20 +8,26 @@ #include "../../include/GsrInfo.hpp" #include "../../include/Utils.hpp" -#include <mglpp/graphics/Rectangle.hpp> -#include <mglpp/graphics/Sprite.hpp> -#include <mglpp/graphics/Text.hpp> -#include <mglpp/window/Window.hpp> - #include <string.h> namespace gsr { + static const char *custom_app_audio_tag = "[custom]"; + enum class AudioTrackType { DEVICE, APPLICATION, APPLICATION_CUSTOM }; + static const char* settings_page_type_to_title_text(SettingsPage::Type type) { + switch(type) { + case SettingsPage::Type::REPLAY: return "Instant Replay"; + case SettingsPage::Type::RECORD: return "Record"; + case SettingsPage::Type::STREAM: return "Livestream"; + } + return ""; + } + SettingsPage::SettingsPage(Type type, const GsrInfo *gsr_info, Config &config, PageStack *page_stack) : StaticPage(mgl::vec2f(get_theme().window_width, get_theme().window_height).floor()), type(type), @@ -33,7 +39,7 @@ namespace gsr { application_audio = get_application_audio(); capture_options = get_supported_capture_options(*gsr_info); - auto content_page = std::make_unique<GsrPage>(); + auto content_page = std::make_unique<GsrPage>(settings_page_type_to_title_text(type), "Settings"); content_page->add_button("Back", "back", get_color_theme().page_bg_color); content_page->on_click = [page_stack](const std::string &id) { if(id == "back") @@ -59,13 +65,14 @@ namespace gsr { std::unique_ptr<ComboBox> SettingsPage::create_record_area_box() { auto record_area_box = std::make_unique<ComboBox>(&get_theme().body_font); // TODO: Show options not supported but disable them - // TODO: Enable this - //if(capture_options.window) - // record_area_box->add_item("Window", "window"); + if(capture_options.window) + record_area_box->add_item("Window", "window"); if(capture_options.focused) record_area_box->add_item("Follow focused window", "focused"); - if(capture_options.screen) - record_area_box->add_item("All monitors", "screen"); + if(capture_options.region) + record_area_box->add_item("Region", "region"); + if(!capture_options.monitors.empty()) + record_area_box->add_item(gsr_info->system_info.display_server == DisplayServer::WAYLAND ? "Focused monitor (Experimental on Wayland)" : "Focused monitor", "focused_monitor"); for(const auto &monitor : capture_options.monitors) { char name[256]; snprintf(name, sizeof(name), "Monitor %s (%dx%d)", monitor.name.c_str(), monitor.size.x, monitor.size.y); @@ -84,14 +91,6 @@ namespace gsr { return record_area_list; } - std::unique_ptr<List> SettingsPage::create_select_window() { - auto select_window_list = std::make_unique<List>(List::Orientation::VERTICAL); - select_window_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Select window:", get_color_theme().text_color)); - select_window_list->add_widget(std::make_unique<Button>(&get_theme().body_font, "Click here to select a window...", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120))); - select_window_list_ptr = select_window_list.get(); - return select_window_list; - } - std::unique_ptr<Entry> SettingsPage::create_area_width_entry() { auto area_width_entry = std::make_unique<Entry>(&get_theme().body_font, "1920", get_theme().body_font.get_character_size() * 3); area_width_entry->validate_handler = create_entry_validator_integer_in_range(1, 1 << 15); @@ -173,12 +172,11 @@ namespace gsr { return checkbox; } - std::unique_ptr<Widget> SettingsPage::create_capture_target() { + std::unique_ptr<Widget> SettingsPage::create_capture_target_section() { auto ll = std::make_unique<List>(List::Orientation::VERTICAL); auto capture_target_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); capture_target_list->add_widget(create_record_area()); - capture_target_list->add_widget(create_select_window()); capture_target_list->add_widget(create_area_size_section()); capture_target_list->add_widget(create_video_resolution_section()); capture_target_list->add_widget(create_restore_portal_session_section()); @@ -188,128 +186,229 @@ namespace gsr { return std::make_unique<Subsection>("Record area", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); } - std::unique_ptr<ComboBox> SettingsPage::create_audio_device_selection_combobox() { + static bool audio_device_is_output(const std::string &audio_device_id) { + return audio_device_id == "default_output" || ends_with(audio_device_id, ".monitor"); + } + + std::unique_ptr<ComboBox> SettingsPage::create_audio_device_selection_combobox(AudioDeviceType device_type) { auto audio_device_box = std::make_unique<ComboBox>(&get_theme().body_font); for(const auto &audio_device : audio_devices) { - audio_device_box->add_item(audio_device.description, audio_device.name); + const bool device_is_output = audio_device_is_output(audio_device.name); + if((device_type == AudioDeviceType::OUTPUT && device_is_output) || (device_type == AudioDeviceType::INPUT && !device_is_output)) { + std::string description = audio_device.description; + if(starts_with(description, "Monitor of ")) + description.erase(0, 11); + audio_device_box->add_item(description, audio_device.name); + } } return audio_device_box; } - std::unique_ptr<Button> SettingsPage::create_remove_audio_device_button(List *audio_device_list_ptr) { - auto remove_audio_track_button = std::make_unique<Button>(&get_theme().body_font, "Remove", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); - remove_audio_track_button->on_click = [this, audio_device_list_ptr]() { - audio_track_list_ptr->remove_widget(audio_device_list_ptr); + static void set_application_audio_options_visible(Subsection *audio_track_subsection, bool visible, const GsrInfo &gsr_info) { + if(!gsr_info.system_info.supports_app_audio) + visible = false; + + List *audio_track_items_list = dynamic_cast<List*>(audio_track_subsection->get_inner_widget()); + + List *buttons_list = dynamic_cast<List*>(audio_track_items_list->get_child_widget_by_index(1)); + Button *add_application_audio_button = dynamic_cast<Button*>(buttons_list->get_child_widget_by_index(2)); + add_application_audio_button->set_visible(visible); + + CheckBox *invert_app_audio_checkbox = dynamic_cast<CheckBox*>(audio_track_items_list->get_child_widget_by_index(3)); + invert_app_audio_checkbox->set_visible(visible); + } + + static void set_application_audio_options_visible(List *audio_track_section_list_ptr, bool visible, const GsrInfo &gsr_info) { + audio_track_section_list_ptr->for_each_child_widget([visible, &gsr_info](std::unique_ptr<Widget> &widget) { + Subsection *audio_track_subsection = dynamic_cast<Subsection*>(widget.get()); + set_application_audio_options_visible(audio_track_subsection, visible, gsr_info); + return true; + }); + } + + std::unique_ptr<Button> SettingsPage::create_remove_audio_device_button(List *audio_input_list_ptr, List *audio_device_list_ptr) { + auto remove_audio_track_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 0)); + remove_audio_track_button->set_icon(&get_theme().trash_texture); + remove_audio_track_button->set_icon_padding_scale(0.75f); + remove_audio_track_button->on_click = [audio_input_list_ptr, audio_device_list_ptr]() { + audio_input_list_ptr->remove_widget(audio_device_list_ptr); }; return remove_audio_track_button; } - std::unique_ptr<List> SettingsPage::create_audio_device() { + std::unique_ptr<List> SettingsPage::create_audio_device(AudioDeviceType device_type, List *audio_input_list_ptr) { auto audio_device_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); audio_device_list->userdata = (void*)(uintptr_t)AudioTrackType::DEVICE; - audio_device_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Device:", get_color_theme().text_color)); - audio_device_list->add_widget(create_audio_device_selection_combobox()); - audio_device_list->add_widget(create_remove_audio_device_button(audio_device_list.get())); + audio_device_list->add_widget(std::make_unique<Label>(&get_theme().body_font, device_type == AudioDeviceType::OUTPUT ? "Output device:" : "Input device: ", get_color_theme().text_color)); + audio_device_list->add_widget(create_audio_device_selection_combobox(device_type)); + audio_device_list->add_widget(create_remove_audio_device_button(audio_input_list_ptr, audio_device_list.get())); return audio_device_list; } - std::unique_ptr<Button> SettingsPage::create_add_audio_device_button() { - auto add_audio_track_button = std::make_unique<Button>(&get_theme().body_font, "Add audio device", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); - add_audio_track_button->on_click = [this]() { + std::unique_ptr<Button> SettingsPage::create_add_audio_track_button() { + auto button = std::make_unique<Button>(&get_theme().body_font, "Add audio track", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + button->on_click = [this]() { + audio_track_section_list_ptr->add_widget(create_audio_track_section(audio_section_ptr)); + }; + button->set_visible(type != Type::STREAM); + return button; + } + + std::unique_ptr<Button> SettingsPage::create_add_audio_output_device_button(List *audio_input_list_ptr) { + auto button = std::make_unique<Button>(&get_theme().body_font, "Add output device", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + button->on_click = [this, audio_input_list_ptr]() { audio_devices = get_audio_devices(); - audio_track_list_ptr->add_widget(create_audio_device()); + audio_input_list_ptr->add_widget(create_audio_device(AudioDeviceType::OUTPUT, audio_input_list_ptr)); }; - return add_audio_track_button; + return button; + } + + std::unique_ptr<Button> SettingsPage::create_add_audio_input_device_button(List *audio_input_list_ptr) { + auto button = std::make_unique<Button>(&get_theme().body_font, "Add input device", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + button->on_click = [this, audio_input_list_ptr]() { + audio_devices = get_audio_devices(); + audio_input_list_ptr->add_widget(create_audio_device(AudioDeviceType::INPUT, audio_input_list_ptr)); + }; + return button; } - std::unique_ptr<ComboBox> SettingsPage::create_application_audio_selection_combobox() { + std::unique_ptr<ComboBox> SettingsPage::create_application_audio_selection_combobox(List *application_audio_row) { auto audio_device_box = std::make_unique<ComboBox>(&get_theme().body_font); + ComboBox *audio_device_box_ptr = audio_device_box.get(); for(const auto &app_audio : application_audio) { audio_device_box->add_item(app_audio, app_audio); } + audio_device_box->add_item("Custom...", custom_app_audio_tag); + + audio_device_box->on_selection_changed = [application_audio_row, audio_device_box_ptr](const std::string&, const std::string &id) { + if(id == custom_app_audio_tag) { + application_audio_row->userdata = (void*)(uintptr_t)AudioTrackType::APPLICATION_CUSTOM; + auto custom_app_audio_entry = std::make_unique<Entry>(&get_theme().body_font, "", (int)(get_theme().body_font.get_character_size() * 10.0f)); + application_audio_row->replace_widget(audio_device_box_ptr, std::move(custom_app_audio_entry)); + } + }; + return audio_device_box; } - std::unique_ptr<List> SettingsPage::create_application_audio() { + std::unique_ptr<List> SettingsPage::create_application_audio(List *audio_input_list_ptr) { auto application_audio_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); application_audio_list->userdata = (void*)(uintptr_t)AudioTrackType::APPLICATION; - application_audio_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "App: ", get_color_theme().text_color)); - application_audio_list->add_widget(create_application_audio_selection_combobox()); - application_audio_list->add_widget(create_remove_audio_device_button(application_audio_list.get())); + application_audio_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Application: ", get_color_theme().text_color)); + application_audio_list->add_widget(create_application_audio_selection_combobox(application_audio_list.get())); + application_audio_list->add_widget(create_remove_audio_device_button(audio_input_list_ptr, application_audio_list.get())); return application_audio_list; } - std::unique_ptr<List> SettingsPage::create_custom_application_audio() { + std::unique_ptr<List> SettingsPage::create_custom_application_audio(List *audio_input_list_ptr) { auto application_audio_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); application_audio_list->userdata = (void*)(uintptr_t)AudioTrackType::APPLICATION_CUSTOM; - application_audio_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "App: ", get_color_theme().text_color)); + application_audio_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Application: ", get_color_theme().text_color)); application_audio_list->add_widget(std::make_unique<Entry>(&get_theme().body_font, "", (int)(get_theme().body_font.get_character_size() * 10.0f))); - application_audio_list->add_widget(create_remove_audio_device_button(application_audio_list.get())); + application_audio_list->add_widget(create_remove_audio_device_button(audio_input_list_ptr, application_audio_list.get())); return application_audio_list; } - std::unique_ptr<Button> SettingsPage::create_add_application_audio_button() { + std::unique_ptr<Button> SettingsPage::create_add_application_audio_button(List *audio_input_list_ptr) { auto add_audio_track_button = std::make_unique<Button>(&get_theme().body_font, "Add application audio", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); - add_application_audio_button_ptr = add_audio_track_button.get(); - add_audio_track_button->on_click = [this]() { + add_audio_track_button->on_click = [this, audio_input_list_ptr]() { application_audio = get_application_audio(); - audio_track_list_ptr->add_widget(create_application_audio()); - }; - return add_audio_track_button; - } - - std::unique_ptr<Button> SettingsPage::create_add_custom_application_audio_button() { - auto add_audio_track_button = std::make_unique<Button>(&get_theme().body_font, "Add custom application audio", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); - add_custom_application_audio_button_ptr = add_audio_track_button.get(); - add_audio_track_button->on_click = [this]() { - audio_track_list_ptr->add_widget(create_custom_application_audio()); + if(application_audio.empty()) + audio_input_list_ptr->add_widget(create_custom_application_audio(audio_input_list_ptr)); + else + audio_input_list_ptr->add_widget(create_application_audio(audio_input_list_ptr)); }; return add_audio_track_button; } - std::unique_ptr<List> SettingsPage::create_add_audio_buttons() { + std::unique_ptr<List> SettingsPage::create_add_audio_buttons(List *audio_input_list_ptr) { auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); - list->add_widget(create_add_audio_device_button()); - list->add_widget(create_add_application_audio_button()); - list->add_widget(create_add_custom_application_audio_button()); + list->add_widget(create_add_audio_output_device_button(audio_input_list_ptr)); + list->add_widget(create_add_audio_input_device_button(audio_input_list_ptr)); + list->add_widget(create_add_application_audio_button(audio_input_list_ptr)); return list; } - std::unique_ptr<List> SettingsPage::create_audio_track_track_section() { + std::unique_ptr<List> SettingsPage::create_audio_input_section() { auto list = std::make_unique<List>(List::Orientation::VERTICAL); - audio_track_list_ptr = list.get(); - audio_track_list_ptr->add_widget(create_audio_device()); // Add default_output by default + //list->add_widget(create_audio_device(list.get())); // Add default_output by default return list; } - std::unique_ptr<CheckBox> SettingsPage::create_merge_audio_tracks_checkbox() { - auto merge_audio_tracks_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Merge audio tracks"); - merge_audio_tracks_checkbox->set_checked(true); - merge_audio_tracks_checkbox_ptr = merge_audio_tracks_checkbox.get(); - return merge_audio_tracks_checkbox; - } - std::unique_ptr<CheckBox> SettingsPage::create_application_audio_invert_checkbox() { auto application_audio_invert_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Record audio from all applications except the selected ones"); application_audio_invert_checkbox->set_checked(false); - application_audio_invert_checkbox_ptr = application_audio_invert_checkbox.get(); return application_audio_invert_checkbox; } - std::unique_ptr<Widget> SettingsPage::create_audio_track_section() { + static void update_audio_track_titles(List *audio_track_section_list_ptr) { + int index = 0; + audio_track_section_list_ptr->for_each_child_widget([&index](std::unique_ptr<Widget> &widget) { + char audio_track_name[32]; + snprintf(audio_track_name, sizeof(audio_track_name), "Audio track #%d", 1 + index); + ++index; + + Subsection *subsection = dynamic_cast<Subsection*>(widget.get()); + List *subesection_items = dynamic_cast<List*>(subsection->get_inner_widget()); + Label *audio_track_title = dynamic_cast<Label*>(dynamic_cast<List*>(subesection_items->get_child_widget_by_index(0))->get_child_widget_by_index(0)); + audio_track_title->set_text(audio_track_name); + return true; + }); + } + + std::unique_ptr<List> SettingsPage::create_audio_track_title_and_remove(Subsection *audio_track_subsection, const char *title) { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + list->add_widget(std::make_unique<Label>(&get_theme().title_font, title, get_color_theme().text_color)); + + auto remove_track_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 0)); + remove_track_button->set_icon(&get_theme().trash_texture); + remove_track_button->set_icon_padding_scale(0.75f); + remove_track_button->on_click = [this, audio_track_subsection]() { + audio_track_section_list_ptr->remove_widget(audio_track_subsection); + update_audio_track_titles(audio_track_section_list_ptr); + }; + list->add_widget(std::move(remove_track_button)); + list->set_visible(type != Type::STREAM); + return list; + } + + std::unique_ptr<Subsection> SettingsPage::create_audio_track_section(Widget *parent_widget) { + char audio_track_name[32]; + snprintf(audio_track_name, sizeof(audio_track_name), "Audio track #%d", 1 + (int)audio_track_section_list_ptr->get_num_children()); + + auto audio_input_section = create_audio_input_section(); + List *audio_input_section_ptr = audio_input_section.get(); + + auto list = std::make_unique<List>(List::Orientation::VERTICAL); + List *list_ptr = list.get(); + auto subsection = std::make_unique<Subsection>("", std::move(std::move(list)), mgl::vec2f(parent_widget->get_inner_size().x, 0.0f)); + subsection->set_bg_color(mgl::Color(35, 40, 44)); + + list_ptr->add_widget(create_audio_track_title_and_remove(subsection.get(), audio_track_name)); + list_ptr->add_widget(create_add_audio_buttons(audio_input_section_ptr)); + list_ptr->add_widget(std::move(audio_input_section)); + list_ptr->add_widget(create_application_audio_invert_checkbox()); + + set_application_audio_options_visible(subsection.get(), view_radio_button_ptr->get_selected_id() == "advanced", *gsr_info); + return subsection; + } + + std::unique_ptr<List> SettingsPage::create_audio_track_section_list() { auto list = std::make_unique<List>(List::Orientation::VERTICAL); - list->add_widget(create_add_audio_buttons()); - list->add_widget(create_audio_track_track_section()); + audio_track_section_list_ptr = list.get(); return list; } std::unique_ptr<Widget> SettingsPage::create_audio_section() { auto audio_device_section_list = std::make_unique<List>(List::Orientation::VERTICAL); - audio_device_section_list->add_widget(create_audio_track_section()); - audio_device_section_list->add_widget(create_merge_audio_tracks_checkbox()); - audio_device_section_list->add_widget(create_application_audio_invert_checkbox()); - audio_device_section_list->add_widget(create_audio_codec()); - return std::make_unique<Subsection>("Audio", std::move(audio_device_section_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); + List *audio_device_section_list_ptr = audio_device_section_list.get(); + + auto subsection = std::make_unique<Subsection>("Audio", std::move(audio_device_section_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); + audio_section_ptr = subsection.get(); + audio_device_section_list_ptr->add_widget(create_add_audio_track_button()); + audio_device_section_list_ptr->add_widget(create_audio_track_section_list()); + audio_device_section_list_ptr->add_widget(create_audio_codec()); + return subsection; } std::unique_ptr<List> SettingsPage::create_video_quality_box() { @@ -340,16 +439,32 @@ namespace gsr { return list; } - std::unique_ptr<Entry> SettingsPage::create_video_bitrate_entry() { - auto video_bitrate_entry = std::make_unique<Entry>(&get_theme().body_font, "15000", (int)(get_theme().body_font.get_character_size() * 4.0f)); + std::unique_ptr<List> SettingsPage::create_video_bitrate_entry() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + auto video_bitrate_entry = std::make_unique<Entry>(&get_theme().body_font, "8000", (int)(get_theme().body_font.get_character_size() * 4.0f)); video_bitrate_entry->validate_handler = create_entry_validator_integer_in_range(1, 500000); video_bitrate_entry_ptr = video_bitrate_entry.get(); - return video_bitrate_entry; + list->add_widget(std::move(video_bitrate_entry)); + + if(type == Type::STREAM) { + auto size_mb_label = std::make_unique<Label>(&get_theme().body_font, "", get_color_theme().text_color); + Label *size_mb_label_ptr = size_mb_label.get(); + list->add_widget(std::move(size_mb_label)); + + video_bitrate_entry_ptr->on_changed = [size_mb_label_ptr](const std::string &text) { + const double video_bitrate_mbits_per_seconds = (double)atoi(text.c_str()) / 1024.0; + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%.2fMbps", video_bitrate_mbits_per_seconds); + size_mb_label_ptr->set_text(buffer); + }; + } + + return list; } std::unique_ptr<List> SettingsPage::create_video_bitrate() { auto video_bitrate_list = std::make_unique<List>(List::Orientation::VERTICAL); - video_bitrate_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Video bitrate (kbps):", get_color_theme().text_color)); + video_bitrate_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Video bitrate (Kbps):", get_color_theme().text_color)); video_bitrate_list->add_widget(create_video_bitrate_entry()); video_bitrate_list_ptr = video_bitrate_list.get(); return video_bitrate_list; @@ -389,20 +504,20 @@ namespace gsr { video_codec_box->add_item("H264", "h264"); if(gsr_info->supported_video_codecs.hevc) video_codec_box->add_item("HEVC", "hevc"); + if(gsr_info->supported_video_codecs.hevc_10bit) + video_codec_box->add_item("HEVC (10 bit, reduces banding)", "hevc_10bit"); + if(gsr_info->supported_video_codecs.hevc_hdr) + video_codec_box->add_item("HEVC (HDR)", "hevc_hdr"); if(gsr_info->supported_video_codecs.av1) video_codec_box->add_item("AV1", "av1"); + if(gsr_info->supported_video_codecs.av1_10bit) + video_codec_box->add_item("AV1 (10 bit, reduces banding)", "av1_10bit"); + if(gsr_info->supported_video_codecs.av1_hdr) + video_codec_box->add_item("AV1 (HDR)", "av1_hdr"); if(gsr_info->supported_video_codecs.vp8) video_codec_box->add_item("VP8", "vp8"); if(gsr_info->supported_video_codecs.vp9) video_codec_box->add_item("VP9", "vp9"); - if(gsr_info->supported_video_codecs.hevc_hdr) - video_codec_box->add_item("HEVC (HDR)", "hevc_hdr"); - if(gsr_info->supported_video_codecs.hevc_10bit) - video_codec_box->add_item("HEVC (10 bit, reduces banding)", "hevc_10bit"); - if(gsr_info->supported_video_codecs.av1_hdr) - video_codec_box->add_item("AV1 (HDR)", "av1_hdr"); - if(gsr_info->supported_video_codecs.av1_10bit) - video_codec_box->add_item("AV1 (10 bit, reduces banding)", "av1_10bit"); if(gsr_info->supported_video_codecs.h264_software) video_codec_box->add_item("H264 Software Encoder (Slow, not recommended)", "h264_software"); video_codec_box_ptr = video_codec_box.get(); @@ -497,7 +612,7 @@ namespace gsr { auto settings_list = std::make_unique<List>(List::Orientation::VERTICAL); settings_list->set_spacing(0.018f); - settings_list->add_widget(create_capture_target()); + settings_list->add_widget(create_capture_target_section()); settings_list->add_widget(create_audio_section()); settings_list->add_widget(create_video_section()); settings_list_ptr = settings_list.get(); @@ -508,16 +623,14 @@ namespace gsr { void SettingsPage::add_widgets() { content_page_ptr->add_widget(create_settings()); - record_area_box_ptr->on_selection_changed = [this](const std::string &text, const std::string &id) { - (void)text; - const bool window_selected = id == "window"; + record_area_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) { const bool focused_selected = id == "focused"; const bool portal_selected = id == "portal"; - select_window_list_ptr->set_visible(window_selected); area_size_list_ptr->set_visible(focused_selected); video_resolution_list_ptr->set_visible(!focused_selected && change_video_resolution_checkbox_ptr->is_checked()); change_video_resolution_checkbox_ptr->set_visible(!focused_selected); restore_portal_session_list_ptr->set_visible(portal_selected); + return true; }; change_video_resolution_checkbox_ptr->on_changed = [this](bool checked) { @@ -525,30 +638,25 @@ namespace gsr { video_resolution_list_ptr->set_visible(!focused_selected && checked); }; - video_quality_box_ptr->on_selection_changed = [this](const std::string &text, const std::string &id) { - (void)text; + video_quality_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) { const bool custom_selected = id == "custom"; video_bitrate_list_ptr->set_visible(custom_selected); if(estimated_file_size_ptr) estimated_file_size_ptr->set_visible(custom_selected); + + return true; }; video_quality_box_ptr->on_selection_changed("", video_quality_box_ptr->get_selected_id()); if(!capture_options.monitors.empty()) - record_area_box_ptr->set_selected_item(capture_options.monitors.front().name); + record_area_box_ptr->set_selected_item("focused_monitor"); else if(capture_options.portal) record_area_box_ptr->set_selected_item("portal"); else if(capture_options.window) record_area_box_ptr->set_selected_item("window"); else record_area_box_ptr->on_selection_changed("", ""); - - if(!gsr_info->system_info.supports_app_audio) { - add_application_audio_button_ptr->set_visible(false); - add_custom_application_audio_button_ptr->set_visible(false); - application_audio_invert_checkbox_ptr->set_visible(false); - } } void SettingsPage::add_page_specific_widgets() { @@ -571,7 +679,7 @@ namespace gsr { auto save_directory_button = std::make_unique<Button>(&get_theme().body_font, get_videos_dir().c_str(), mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); save_directory_button_ptr = save_directory_button.get(); save_directory_button->on_click = [this]() { - auto select_directory_page = std::make_unique<GsrPage>(); + auto select_directory_page = std::make_unique<GsrPage>("File", "Settings"); select_directory_page->add_button("Save", "save", get_color_theme().tint_color); select_directory_page->add_button("Cancel", "cancel", get_color_theme().page_bg_color); @@ -580,10 +688,12 @@ namespace gsr { select_directory_page->add_widget(std::move(file_chooser)); select_directory_page->on_click = [this, file_chooser_ptr](const std::string &id) { - if(id == "save") + if(id == "save") { save_directory_button_ptr->set_text(file_chooser_ptr->get_current_directory()); - else if(id == "cancel") page_stack->pop(); + } else if(id == "cancel") { + page_stack->pop(); + } }; page_stack->push(std::move(select_directory_page)); @@ -609,27 +719,53 @@ namespace gsr { return container_list; } - std::unique_ptr<Entry> SettingsPage::create_replay_time_entry() { + std::unique_ptr<List> SettingsPage::create_replay_time_entry() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + auto replay_time_entry = std::make_unique<Entry>(&get_theme().body_font, "60", get_theme().body_font.get_character_size() * 3); - replay_time_entry->validate_handler = create_entry_validator_integer_in_range(1, 1200); + replay_time_entry->validate_handler = create_entry_validator_integer_in_range(1, 86400); replay_time_entry_ptr = replay_time_entry.get(); - return replay_time_entry; + list->add_widget(std::move(replay_time_entry)); + + auto replay_time_label = std::make_unique<Label>(&get_theme().body_font, "00h:00m:00s", get_color_theme().text_color); + replay_time_label_ptr = replay_time_label.get(); + list->add_widget(std::move(replay_time_label)); + + return list; } std::unique_ptr<List> SettingsPage::create_replay_time() { auto replay_time_list = std::make_unique<List>(List::Orientation::VERTICAL); - replay_time_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Replay time in seconds:", get_color_theme().text_color)); + replay_time_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Replay duration in seconds:", get_color_theme().text_color)); replay_time_list->add_widget(create_replay_time_entry()); return replay_time_list; } + std::unique_ptr<List> SettingsPage::create_replay_storage() { + auto list = std::make_unique<List>(List::Orientation::VERTICAL); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Where should temporary replay data be stored?", get_color_theme().text_color)); + auto replay_storage_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL); + replay_storage_button_ptr = replay_storage_button.get(); + replay_storage_button->add_item("RAM", "ram"); + replay_storage_button->add_item("Disk (Not recommended on SSDs)", "disk"); + + replay_storage_button->on_selection_changed = [this](const std::string&, const std::string &id) { + update_estimated_replay_file_size(id); + return true; + }; + + list->add_widget(std::move(replay_storage_button)); + list->set_visible(gsr_info->system_info.gsr_version >= GsrVersion{5, 5, 0}); + return list; + } + std::unique_ptr<RadioButton> SettingsPage::create_start_replay_automatically() { char fullscreen_text[256]; - snprintf(fullscreen_text, sizeof(fullscreen_text), "Turn on replay when starting a fullscreen application%s", gsr_info->system_info.display_server == DisplayServer::X11 ? "" : " (X11 only)"); + snprintf(fullscreen_text, sizeof(fullscreen_text), "Turn on replay when starting a fullscreen application%s", gsr_info->system_info.display_server == DisplayServer::X11 ? "" : " (X11 applications only)"); auto radiobutton = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::VERTICAL); radiobutton->add_item("Don't turn on replay automatically", "dont_turn_on_automatically"); - radiobutton->add_item("Turn on replay at system startup", "turn_on_at_system_startup"); + radiobutton->add_item("Turn on replay when this program starts", "turn_on_at_system_startup"); radiobutton->add_item(fullscreen_text, "turn_on_at_fullscreen"); radiobutton->add_item("Turn on replay when power supply is connected", "turn_on_at_power_supply_connected"); turn_on_replay_automatically_mode_ptr = radiobutton.get(); @@ -638,28 +774,58 @@ namespace gsr { std::unique_ptr<CheckBox> SettingsPage::create_save_replay_in_game_folder() { char text[256]; - snprintf(text, sizeof(text), "Save video in a folder with the name of the game%s", gsr_info->system_info.display_server == DisplayServer::X11 ? "" : " (X11 only)"); + snprintf(text, sizeof(text), "Save video in a folder with the name of the game%s", gsr_info->system_info.display_server == DisplayServer::X11 ? "" : " (X11 applications only)"); auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, text); save_replay_in_game_folder_ptr = checkbox.get(); return checkbox; } - std::unique_ptr<Label> SettingsPage::create_estimated_file_size() { - auto label = std::make_unique<Label>(&get_theme().body_font, "Estimated video max file size in RAM: 5.23MB", get_color_theme().text_color); + std::unique_ptr<CheckBox> SettingsPage::create_restart_replay_on_save() { + auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Restart replay on save"); + restart_replay_on_save = checkbox.get(); + return checkbox; + } + + std::unique_ptr<Label> SettingsPage::create_estimated_replay_file_size() { + auto label = std::make_unique<Label>(&get_theme().body_font, "Estimated video max file size in RAM: 57.60MB", get_color_theme().text_color); estimated_file_size_ptr = label.get(); return label; } - void SettingsPage::update_estimated_file_size() { + void SettingsPage::update_estimated_replay_file_size(const std::string &replay_storage_type) { const int64_t replay_time_seconds = atoi(replay_time_entry_ptr->get_text().c_str()); const int64_t video_bitrate_bps = atoi(video_bitrate_entry_ptr->get_text().c_str()) * 1000LL / 8LL; - const double video_filesize_mb = ((double)replay_time_seconds * (double)video_bitrate_bps) / 1024.0 / 1024.0; + const double video_filesize_mb = ((double)replay_time_seconds * (double)video_bitrate_bps) / 1000.0 / 1000.0 * 1.024; - char buffer[512]; - snprintf(buffer, sizeof(buffer), "Estimated video max file size in RAM: %.2fMB", video_filesize_mb); + char buffer[256]; + snprintf(buffer, sizeof(buffer), "Estimated video max file size %s: %.2fMB.\nChange video bitrate or replay duration to change file size.", replay_storage_type == "ram" ? "in RAM" : "on disk", video_filesize_mb); estimated_file_size_ptr->set_text(buffer); } + void SettingsPage::update_replay_time_text() { + int seconds = atoi(replay_time_entry_ptr->get_text().c_str()); + + const int hours = seconds / 60 / 60; + seconds -= (hours * 60 * 60); + + const int minutes = seconds / 60; + seconds -= (minutes * 60); + + char buffer[256]; + snprintf(buffer, sizeof(buffer), "%02dh:%02dm:%02ds", hours, minutes, seconds); + replay_time_label_ptr->set_text(buffer); + } + + void SettingsPage::view_changed(bool advanced_view, Subsection *notifications_subsection_ptr) { + color_range_list_ptr->set_visible(advanced_view); + audio_codec_ptr->set_visible(advanced_view); + video_codec_ptr->set_visible(advanced_view); + framerate_mode_list_ptr->set_visible(advanced_view); + notifications_subsection_ptr->set_visible(advanced_view); + set_application_audio_options_visible(audio_track_section_list_ptr, advanced_view, *gsr_info); + settings_scrollable_page_ptr->reset_scroll(); + } + void SettingsPage::add_replay_widgets() { auto file_info_list = std::make_unique<List>(List::Orientation::VERTICAL); auto file_info_data_list = std::make_unique<List>(List::Orientation::HORIZONTAL); @@ -667,14 +833,18 @@ namespace gsr { file_info_data_list->add_widget(create_container_section()); file_info_data_list->add_widget(create_replay_time()); file_info_list->add_widget(std::move(file_info_data_list)); - file_info_list->add_widget(create_estimated_file_size()); + file_info_list->add_widget(create_estimated_replay_file_size()); settings_list_ptr->add_widget(std::make_unique<Subsection>("File info", std::move(file_info_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f))); auto general_list = std::make_unique<List>(List::Orientation::VERTICAL); - general_list->add_widget(create_start_replay_automatically()); + general_list->add_widget(create_replay_storage()); general_list->add_widget(create_save_replay_in_game_folder()); + if(gsr_info->system_info.gsr_version >= GsrVersion{5, 0, 3}) + general_list->add_widget(create_restart_replay_on_save()); settings_list_ptr->add_widget(std::make_unique<Subsection>("General", std::move(general_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f))); + settings_list_ptr->add_widget(std::make_unique<Subsection>("Autostart", create_start_replay_automatically(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f))); + auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL); auto show_replay_started_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show replay started notification"); @@ -696,44 +866,55 @@ namespace gsr { Subsection *notifications_subsection_ptr = notifications_subsection.get(); settings_list_ptr->add_widget(std::move(notifications_subsection)); - view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string &text, const std::string &id) { - (void)text; - const bool advanced_view = id == "advanced"; - color_range_list_ptr->set_visible(advanced_view); - audio_codec_ptr->set_visible(advanced_view); - video_codec_ptr->set_visible(advanced_view); - framerate_mode_list_ptr->set_visible(advanced_view); - notifications_subsection_ptr->set_visible(advanced_view); - settings_scrollable_page_ptr->reset_scroll(); + view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string&, const std::string &id) { + view_changed(id == "advanced", notifications_subsection_ptr); + return true; }; view_radio_button_ptr->on_selection_changed("Simple", "simple"); replay_time_entry_ptr->on_changed = [this](const std::string&) { - update_estimated_file_size(); + update_estimated_replay_file_size(replay_storage_button_ptr->get_selected_id()); + update_replay_time_text(); }; video_bitrate_entry_ptr->on_changed = [this](const std::string&) { - update_estimated_file_size(); + update_estimated_replay_file_size(replay_storage_button_ptr->get_selected_id()); }; } std::unique_ptr<CheckBox> SettingsPage::create_save_recording_in_game_folder() { char text[256]; - snprintf(text, sizeof(text), "Save video in a folder with the name of the game%s", gsr_info->system_info.display_server == DisplayServer::X11 ? "" : " (X11 only)"); + snprintf(text, sizeof(text), "Save video in a folder with the name of the game%s", gsr_info->system_info.display_server == DisplayServer::X11 ? "" : " (X11 applications only)"); auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, text); save_recording_in_game_folder_ptr = checkbox.get(); return checkbox; } + std::unique_ptr<Label> SettingsPage::create_estimated_record_file_size() { + auto label = std::make_unique<Label>(&get_theme().body_font, "Estimated video file size per minute (excluding audio): 345.60MB", get_color_theme().text_color); + estimated_file_size_ptr = label.get(); + return label; + } + + void SettingsPage::update_estimated_record_file_size() { + const int64_t video_bitrate_bps = atoi(video_bitrate_entry_ptr->get_text().c_str()) * 1000LL / 8LL; + const double video_filesize_mb_per_minute = (60.0 * (double)video_bitrate_bps) / 1000.0 / 1000.0 * 1.024; + + char buffer[512]; + snprintf(buffer, sizeof(buffer), "Estimated video file size per minute (excluding audio): %.2fMB", video_filesize_mb_per_minute); + estimated_file_size_ptr->set_text(buffer); + } + void SettingsPage::add_record_widgets() { - auto file_list = std::make_unique<List>(List::Orientation::HORIZONTAL); - file_list->add_widget(create_save_directory("Directory to save the video:")); - file_list->add_widget(create_container_section()); - settings_list_ptr->add_widget(std::make_unique<Subsection>("File info", std::move(file_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f))); + auto file_info_list = std::make_unique<List>(List::Orientation::VERTICAL); + auto file_info_data_list = std::make_unique<List>(List::Orientation::HORIZONTAL); + file_info_data_list->add_widget(create_save_directory("Directory to save the video:")); + file_info_data_list->add_widget(create_container_section()); + file_info_list->add_widget(std::move(file_info_data_list)); + file_info_list->add_widget(create_estimated_record_file_size()); + settings_list_ptr->add_widget(std::make_unique<Subsection>("File info", std::move(file_info_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f))); - auto general_list = std::make_unique<List>(List::Orientation::VERTICAL); - general_list->add_widget(create_save_recording_in_game_folder()); - settings_list_ptr->add_widget(std::make_unique<Subsection>("General", std::move(general_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f))); + settings_list_ptr->add_widget(std::make_unique<Subsection>("General", create_save_recording_in_game_folder(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f))); auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL); @@ -747,27 +928,31 @@ namespace gsr { show_video_saved_notification_checkbox_ptr = show_video_saved_notification_checkbox.get(); checkboxes_list->add_widget(std::move(show_video_saved_notification_checkbox)); + auto show_video_paused_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show video paused/unpaused notification"); + show_video_paused_notification_checkbox->set_checked(true); + show_video_paused_notification_checkbox_ptr = show_video_paused_notification_checkbox.get(); + checkboxes_list->add_widget(std::move(show_video_paused_notification_checkbox)); + auto notifications_subsection = std::make_unique<Subsection>("Notifications", std::move(checkboxes_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); Subsection *notifications_subsection_ptr = notifications_subsection.get(); settings_list_ptr->add_widget(std::move(notifications_subsection)); - view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string &text, const std::string &id) { - (void)text; - const bool advanced_view = id == "advanced"; - color_range_list_ptr->set_visible(advanced_view); - audio_codec_ptr->set_visible(advanced_view); - video_codec_ptr->set_visible(advanced_view); - framerate_mode_list_ptr->set_visible(advanced_view); - notifications_subsection_ptr->set_visible(advanced_view); - settings_scrollable_page_ptr->reset_scroll(); + view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string&, const std::string &id) { + view_changed(id == "advanced", notifications_subsection_ptr); + return true; }; view_radio_button_ptr->on_selection_changed("Simple", "simple"); + + video_bitrate_entry_ptr->on_changed = [this](const std::string&) { + update_estimated_record_file_size(); + }; } std::unique_ptr<ComboBox> SettingsPage::create_streaming_service_box() { auto streaming_service_box = std::make_unique<ComboBox>(&get_theme().body_font); streaming_service_box->add_item("Twitch", "twitch"); streaming_service_box->add_item("YouTube", "youtube"); + streaming_service_box->add_item("Rumble", "rumble"); streaming_service_box->add_item("Custom", "custom"); streaming_service_box_ptr = streaming_service_box.get(); return streaming_service_box; @@ -792,6 +977,10 @@ namespace gsr { youtube_stream_key_entry_ptr = youtube_stream_key_entry.get(); stream_key_list->add_widget(std::move(youtube_stream_key_entry)); + auto rumble_stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20); + rumble_stream_key_entry_ptr = rumble_stream_key_entry.get(); + stream_key_list->add_widget(std::move(rumble_stream_key_entry)); + stream_key_list_ptr = stream_key_list.get(); return stream_key_list; } @@ -850,28 +1039,24 @@ namespace gsr { Subsection *notifications_subsection_ptr = notifications_subsection.get(); settings_list_ptr->add_widget(std::move(notifications_subsection)); - streaming_service_box_ptr->on_selection_changed = [this](const std::string &text, const std::string &id) { - (void)text; + streaming_service_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) { const bool twitch_option = id == "twitch"; const bool youtube_option = id == "youtube"; + const bool rumble_option = id == "rumble"; const bool custom_option = id == "custom"; stream_key_list_ptr->set_visible(!custom_option); stream_url_list_ptr->set_visible(custom_option); container_list_ptr->set_visible(custom_option); twitch_stream_key_entry_ptr->set_visible(twitch_option); youtube_stream_key_entry_ptr->set_visible(youtube_option); + rumble_stream_key_entry_ptr->set_visible(rumble_option); + return true; }; streaming_service_box_ptr->on_selection_changed("Twitch", "twitch"); - view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string &text, const std::string &id) { - (void)text; - const bool advanced_view = id == "advanced"; - color_range_list_ptr->set_visible(advanced_view); - audio_codec_ptr->set_visible(advanced_view); - video_codec_ptr->set_visible(advanced_view); - framerate_mode_list_ptr->set_visible(advanced_view); - notifications_subsection_ptr->set_visible(advanced_view); - settings_scrollable_page_ptr->reset_scroll(); + view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string&, const std::string &id) { + view_changed(id == "advanced", notifications_subsection_ptr); + return true; }; view_radio_button_ptr->on_selection_changed("Simple", "simple"); } @@ -895,6 +1080,7 @@ namespace gsr { } void SettingsPage::save() { + Config prev_config = config; switch(type) { case Type::REPLAY: save_replay(); @@ -907,6 +1093,9 @@ namespace gsr { break; } save_config(config); + + if(on_config_changed && config != prev_config) + on_config_changed(); } static const std::string* get_application_audio_by_name_case_insensitive(const std::vector<std::string> &application_audio, const std::string &name) { @@ -917,49 +1106,64 @@ namespace gsr { return nullptr; } - static bool starts_with(std::string_view str, const char *substr) { - size_t len = strlen(substr); - return str.size() >= len && memcmp(str.data(), substr, len) == 0; - } - void SettingsPage::load_audio_tracks(const RecordOptions &record_options) { - audio_track_list_ptr->clear(); - for(const std::string &audio_track : record_options.audio_tracks) { - if(starts_with(audio_track, "app:")) { - if(!gsr_info->system_info.supports_app_audio) - continue; - - std::string audio_track_name = audio_track.substr(4); - const std::string *app_audio = get_application_audio_by_name_case_insensitive(application_audio, audio_track_name); - if(app_audio) { - std::unique_ptr<List> application_audio_widget = create_application_audio(); - ComboBox *application_audio_box = static_cast<ComboBox*>(application_audio_widget->get_child_widget_by_index(1)); - application_audio_box->set_selected_item(*app_audio); - audio_track_list_ptr->add_widget(std::move(application_audio_widget)); + audio_track_section_list_ptr->clear(); + for(const AudioTrack &audio_track : record_options.audio_tracks_list) { + auto audio_track_section = create_audio_track_section(audio_section_ptr); + List *audio_track_section_items_list_ptr = dynamic_cast<List*>(audio_track_section->get_inner_widget()); + List *audio_input_list_ptr = dynamic_cast<List*>(audio_track_section_items_list_ptr->get_child_widget_by_index(2)); + CheckBox *application_audio_invert_checkbox_ptr = dynamic_cast<CheckBox*>(audio_track_section_items_list_ptr->get_child_widget_by_index(3)); + application_audio_invert_checkbox_ptr->set_checked(audio_track.application_audio_invert); + + audio_input_list_ptr->clear(); + for(const std::string &audio_input : audio_track.audio_inputs) { + if(starts_with(audio_input, "app:")) { + if(!gsr_info->system_info.supports_app_audio) + continue; + + std::string audio_track_name = audio_input.substr(4); + const std::string *app_audio = get_application_audio_by_name_case_insensitive(application_audio, audio_track_name); + if(app_audio) { + std::unique_ptr<List> application_audio_widget = create_application_audio(audio_input_list_ptr); + ComboBox *application_audio_box = dynamic_cast<ComboBox*>(application_audio_widget->get_child_widget_by_index(1)); + application_audio_box->set_selected_item(*app_audio); + audio_input_list_ptr->add_widget(std::move(application_audio_widget)); + } else { + std::unique_ptr<List> application_audio_widget = create_custom_application_audio(audio_input_list_ptr); + Entry *application_audio_entry = dynamic_cast<Entry*>(application_audio_widget->get_child_widget_by_index(1)); + application_audio_entry->set_text(std::move(audio_track_name)); + audio_input_list_ptr->add_widget(std::move(application_audio_widget)); + } + } else if(starts_with(audio_input, "device:")) { + const std::string device_name = audio_input.substr(7); + const AudioDeviceType audio_device_type = audio_device_is_output(device_name) ? AudioDeviceType::OUTPUT : AudioDeviceType::INPUT; + std::unique_ptr<List> audio_track_widget = create_audio_device(audio_device_type, audio_input_list_ptr); + ComboBox *audio_device_box = dynamic_cast<ComboBox*>(audio_track_widget->get_child_widget_by_index(1)); + audio_device_box->set_selected_item(device_name); + audio_input_list_ptr->add_widget(std::move(audio_track_widget)); } else { - std::unique_ptr<List> application_audio_widget = create_custom_application_audio(); - Entry *application_audio_entry = static_cast<Entry*>(application_audio_widget->get_child_widget_by_index(1)); - application_audio_entry->set_text(std::move(audio_track_name)); - audio_track_list_ptr->add_widget(std::move(application_audio_widget)); + const AudioDeviceType audio_device_type = audio_device_is_output(audio_input) ? AudioDeviceType::OUTPUT : AudioDeviceType::INPUT; + std::unique_ptr<List> audio_track_widget = create_audio_device(audio_device_type, audio_input_list_ptr); + ComboBox *audio_device_box = dynamic_cast<ComboBox*>(audio_track_widget->get_child_widget_by_index(1)); + audio_device_box->set_selected_item(audio_input); + audio_input_list_ptr->add_widget(std::move(audio_track_widget)); } - } else if(starts_with(audio_track, "device:")) { - std::unique_ptr<List> audio_track_widget = create_audio_device(); - ComboBox *audio_device_box = static_cast<ComboBox*>(audio_track_widget->get_child_widget_by_index(1)); - audio_device_box->set_selected_item(audio_track.substr(7)); - audio_track_list_ptr->add_widget(std::move(audio_track_widget)); - } else { - std::unique_ptr<List> audio_track_widget = create_audio_device(); - ComboBox *audio_device_box = static_cast<ComboBox*>(audio_track_widget->get_child_widget_by_index(1)); - audio_device_box->set_selected_item(audio_track); - audio_track_list_ptr->add_widget(std::move(audio_track_widget)); } + + audio_track_section_list_ptr->add_widget(std::move(audio_track_section)); + + if(type == Type::STREAM) + break; + } + + if(type == Type::STREAM && audio_track_section_list_ptr->get_num_children() == 0) { + auto audio_track_section = create_audio_track_section(audio_section_ptr); + audio_track_section_list_ptr->add_widget(std::move(audio_track_section)); } } void SettingsPage::load_common(RecordOptions &record_options) { record_area_box_ptr->set_selected_item(record_options.record_area_option); - merge_audio_tracks_checkbox_ptr->set_checked(record_options.merge_audio_tracks); - application_audio_invert_checkbox_ptr->set_checked(record_options.application_audio_invert); change_video_resolution_checkbox_ptr->set_checked(record_options.change_video_resolution); load_audio_tracks(record_options); color_range_box_ptr->set_selected_item(record_options.color_range); @@ -1012,16 +1216,21 @@ namespace gsr { void SettingsPage::load_replay() { load_common(config.replay_config.record_options); + replay_storage_button_ptr->set_selected_item(config.replay_config.replay_storage); turn_on_replay_automatically_mode_ptr->set_selected_item(config.replay_config.turn_on_replay_automatically_mode); save_replay_in_game_folder_ptr->set_checked(config.replay_config.save_video_in_game_folder); + if(restart_replay_on_save) + restart_replay_on_save->set_checked(config.replay_config.restart_replay_on_save); show_replay_started_notification_checkbox_ptr->set_checked(config.replay_config.show_replay_started_notifications); show_replay_stopped_notification_checkbox_ptr->set_checked(config.replay_config.show_replay_stopped_notifications); show_replay_saved_notification_checkbox_ptr->set_checked(config.replay_config.show_replay_saved_notifications); save_directory_button_ptr->set_text(config.replay_config.save_directory); container_box_ptr->set_selected_item(config.replay_config.container); - if(config.replay_config.replay_time < 5) - config.replay_config.replay_time = 5; + if(config.replay_config.replay_time < 2) + config.replay_config.replay_time = 2; + if(config.replay_config.replay_time > 86400) + config.replay_config.replay_time = 86400; replay_time_entry_ptr->set_text(std::to_string(config.replay_config.replay_time)); } @@ -1030,6 +1239,7 @@ namespace gsr { save_recording_in_game_folder_ptr->set_checked(config.record_config.save_video_in_game_folder); show_recording_started_notification_checkbox_ptr->set_checked(config.record_config.show_recording_started_notifications); show_video_saved_notification_checkbox_ptr->set_checked(config.record_config.show_video_saved_notifications); + show_video_paused_notification_checkbox_ptr->set_checked(config.record_config.show_video_paused_notifications); save_directory_button_ptr->set_text(config.record_config.save_directory); container_box_ptr->set_selected_item(config.record_config.container); } @@ -1041,32 +1251,43 @@ namespace gsr { streaming_service_box_ptr->set_selected_item(config.streaming_config.streaming_service); youtube_stream_key_entry_ptr->set_text(config.streaming_config.youtube.stream_key); twitch_stream_key_entry_ptr->set_text(config.streaming_config.twitch.stream_key); + rumble_stream_key_entry_ptr->set_text(config.streaming_config.rumble.stream_key); stream_url_entry_ptr->set_text(config.streaming_config.custom.url); container_box_ptr->set_selected_item(config.streaming_config.custom.container); } - static void save_audio_tracks(std::vector<std::string> &audio_devices, List *audio_devices_list_ptr) { - audio_devices.clear(); - audio_devices_list_ptr->for_each_child_widget([&audio_devices](std::unique_ptr<Widget> &child_widget) { - List *audio_track_line = static_cast<List*>(child_widget.get()); - const AudioTrackType audio_track_type = (AudioTrackType)(uintptr_t)audio_track_line->userdata; - switch(audio_track_type) { - case AudioTrackType::DEVICE: { - ComboBox *audio_device_box = static_cast<ComboBox*>(audio_track_line->get_child_widget_by_index(1)); - audio_devices.push_back("device:" + audio_device_box->get_selected_id()); - break; + static void save_audio_tracks(std::vector<AudioTrack> &audio_tracks, List *audio_track_section_list_ptr) { + audio_tracks.clear(); + audio_track_section_list_ptr->for_each_child_widget([&audio_tracks](std::unique_ptr<Widget> &child_widget) { + Subsection *audio_subsection = dynamic_cast<Subsection*>(child_widget.get()); + List *audio_track_section_items_list_ptr = dynamic_cast<List*>(audio_subsection->get_inner_widget()); + List *audio_input_list_ptr = dynamic_cast<List*>(audio_track_section_items_list_ptr->get_child_widget_by_index(2)); + CheckBox *application_audio_invert_checkbox_ptr = dynamic_cast<CheckBox*>(audio_track_section_items_list_ptr->get_child_widget_by_index(3)); + + audio_tracks.push_back({std::vector<std::string>{}, application_audio_invert_checkbox_ptr->is_checked()}); + audio_input_list_ptr->for_each_child_widget([&audio_tracks](std::unique_ptr<Widget> &child_widget){ + List *audio_track_line = dynamic_cast<List*>(child_widget.get()); + const AudioTrackType audio_track_type = (AudioTrackType)(uintptr_t)audio_track_line->userdata; + switch(audio_track_type) { + case AudioTrackType::DEVICE: { + ComboBox *audio_device_box = dynamic_cast<ComboBox*>(audio_track_line->get_child_widget_by_index(1)); + audio_tracks.back().audio_inputs.push_back("device:" + audio_device_box->get_selected_id()); + break; + } + case AudioTrackType::APPLICATION: { + ComboBox *application_audio_box = dynamic_cast<ComboBox*>(audio_track_line->get_child_widget_by_index(1)); + audio_tracks.back().audio_inputs.push_back("app:" + application_audio_box->get_selected_id()); + break; + } + case AudioTrackType::APPLICATION_CUSTOM: { + Entry *application_audio_entry = dynamic_cast<Entry*>(audio_track_line->get_child_widget_by_index(1)); + audio_tracks.back().audio_inputs.push_back("app:" + application_audio_entry->get_text()); + break; + } } - case AudioTrackType::APPLICATION: { - ComboBox *application_audio_box = static_cast<ComboBox*>(audio_track_line->get_child_widget_by_index(1)); - audio_devices.push_back("app:" + application_audio_box->get_selected_id()); - break; - } - case AudioTrackType::APPLICATION_CUSTOM: { - Entry *application_audio_entry = static_cast<Entry*>(audio_track_line->get_child_widget_by_index(1)); - audio_devices.push_back("app:" + application_audio_entry->get_text()); - break; - } - } + return true; + }); + return true; }); } @@ -1079,10 +1300,8 @@ namespace gsr { record_options.video_height = atoi(video_height_entry_ptr->get_text().c_str()); record_options.fps = atoi(framerate_entry_ptr->get_text().c_str()); record_options.video_bitrate = atoi(video_bitrate_entry_ptr->get_text().c_str()); - record_options.merge_audio_tracks = merge_audio_tracks_checkbox_ptr->is_checked(); - record_options.application_audio_invert = application_audio_invert_checkbox_ptr->is_checked(); record_options.change_video_resolution = change_video_resolution_checkbox_ptr->is_checked(); - save_audio_tracks(record_options.audio_tracks, audio_track_list_ptr); + save_audio_tracks(record_options.audio_tracks_list, audio_track_section_list_ptr); record_options.color_range = color_range_box_ptr->get_selected_id(); record_options.video_quality = video_quality_box_ptr->get_selected_id(); record_options.video_codec = video_codec_box_ptr->get_selected_id(); @@ -1141,12 +1360,15 @@ namespace gsr { save_common(config.replay_config.record_options); config.replay_config.turn_on_replay_automatically_mode = turn_on_replay_automatically_mode_ptr->get_selected_id(); config.replay_config.save_video_in_game_folder = save_replay_in_game_folder_ptr->is_checked(); + if(restart_replay_on_save) + config.replay_config.restart_replay_on_save = restart_replay_on_save->is_checked(); config.replay_config.show_replay_started_notifications = show_replay_started_notification_checkbox_ptr->is_checked(); config.replay_config.show_replay_stopped_notifications = show_replay_stopped_notification_checkbox_ptr->is_checked(); config.replay_config.show_replay_saved_notifications = show_replay_saved_notification_checkbox_ptr->is_checked(); config.replay_config.save_directory = save_directory_button_ptr->get_text(); config.replay_config.container = container_box_ptr->get_selected_id(); config.replay_config.replay_time = atoi(replay_time_entry_ptr->get_text().c_str()); + config.replay_config.replay_storage = replay_storage_button_ptr->get_selected_id(); if(config.replay_config.replay_time < 5) { config.replay_config.replay_time = 5; @@ -1159,6 +1381,7 @@ namespace gsr { config.record_config.save_video_in_game_folder = save_recording_in_game_folder_ptr->is_checked(); config.record_config.show_recording_started_notifications = show_recording_started_notification_checkbox_ptr->is_checked(); config.record_config.show_video_saved_notifications = show_video_saved_notification_checkbox_ptr->is_checked(); + config.record_config.show_video_paused_notifications = show_video_paused_notification_checkbox_ptr->is_checked(); config.record_config.save_directory = save_directory_button_ptr->get_text(); config.record_config.container = container_box_ptr->get_selected_id(); } @@ -1170,6 +1393,7 @@ namespace gsr { config.streaming_config.streaming_service = streaming_service_box_ptr->get_selected_id(); config.streaming_config.youtube.stream_key = youtube_stream_key_entry_ptr->get_text(); config.streaming_config.twitch.stream_key = twitch_stream_key_entry_ptr->get_text(); + config.streaming_config.rumble.stream_key = rumble_stream_key_entry_ptr->get_text(); config.streaming_config.custom.url = stream_url_entry_ptr->get_text(); config.streaming_config.custom.container = container_box_ptr->get_selected_id(); } diff --git a/src/gui/StaticPage.cpp b/src/gui/StaticPage.cpp index a89fc42..5147819 100644 --- a/src/gui/StaticPage.cpp +++ b/src/gui/StaticPage.cpp @@ -20,8 +20,9 @@ namespace gsr { // Process widgets by visibility (backwards) return widgets.for_each_reverse([selected_widget, &window, &event, offset](std::unique_ptr<Widget> &widget) { - if(widget.get() != selected_widget) { - if(!widget->on_event(event, window, offset)) + Widget *p = widget.get(); + if(p != selected_widget) { + if(!p->on_event(event, window, offset)) return false; } return true; @@ -36,14 +37,8 @@ namespace gsr { offset = draw_pos; Widget *selected_widget = selected_child_widget; - mgl_scissor prev_scissor; - mgl_window_get_scissor(window.internal_window(), &prev_scissor); - - mgl_scissor new_scissor = { - mgl_vec2i{(int)draw_pos.x, (int)draw_pos.y}, - mgl_vec2i{(int)size.x, (int)size.y} - }; - mgl_window_set_scissor(window.internal_window(), &new_scissor); + const mgl::Scissor prev_scissor = window.get_scissor(); + window.set_scissor({draw_pos.to_vec2i(), size.to_vec2i()}); for(size_t i = 0; i < widgets.size(); ++i) { auto &widget = widgets[i]; @@ -54,7 +49,7 @@ namespace gsr { if(selected_widget) selected_widget->draw(window, offset); - mgl_window_set_scissor(window.internal_window(), &prev_scissor); + window.set_scissor(prev_scissor); } mgl::vec2f StaticPage::get_size() { diff --git a/src/gui/Subsection.cpp b/src/gui/Subsection.cpp index c97460e..bc75a9c 100644 --- a/src/gui/Subsection.cpp +++ b/src/gui/Subsection.cpp @@ -12,12 +12,17 @@ namespace gsr { static const float title_spacing_scale = 0.010f; Subsection::Subsection(const char *title, std::unique_ptr<Widget> inner_widget, mgl::vec2f size) : - label(&get_theme().title_font, title, get_color_theme().text_color), + label(&get_theme().title_font, title ? title : "", get_color_theme().text_color), inner_widget(std::move(inner_widget)), size(size) { this->inner_widget->parent_widget = this; } + + Subsection::~Subsection() { + if(inner_widget->parent_widget == this) + inner_widget->parent_widget = nullptr; + } bool Subsection::on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f) { if(!visible) @@ -32,7 +37,7 @@ namespace gsr { mgl::vec2f draw_pos = position + offset; mgl::Rectangle background(draw_pos.floor(), get_size().floor()); - background.set_color(mgl::Color(25, 30, 34)); + background.set_color(bg_color); window.draw(background); draw_pos += mgl::vec2f(margin_left_scale, margin_top_scale) * mgl::vec2f(get_theme().window_height, get_theme().window_height); @@ -69,4 +74,12 @@ namespace gsr { const mgl::vec2f margin_size = mgl::vec2f(margin_left_scale + margin_right_scale, margin_top_scale + margin_bottom_scale) * mgl::vec2f(get_theme().window_height, get_theme().window_height); return get_size() - margin_size; } + + Widget* Subsection::get_inner_widget() { + return inner_widget.get(); + } + + void Subsection::set_bg_color(mgl::Color color) { + bg_color = color; + } }
\ No newline at end of file diff --git a/src/gui/Utils.cpp b/src/gui/Utils.cpp index d1643f2..8f77f17 100644 --- a/src/gui/Utils.cpp +++ b/src/gui/Utils.cpp @@ -67,4 +67,11 @@ namespace gsr { return from; } + + mgl::vec2f clamp_keep_aspect_ratio(mgl::vec2f from, mgl::vec2f to) { + if(from.x > to.x || from.y > to.y) + return scale_keep_aspect_ratio(from, to); + else + return from; + } }
\ No newline at end of file diff --git a/src/gui/Widget.cpp b/src/gui/Widget.cpp index 8732bd7..66cf193 100644 --- a/src/gui/Widget.cpp +++ b/src/gui/Widget.cpp @@ -1,14 +1,15 @@ #include "../../include/gui/Widget.hpp" +#include <vector> namespace gsr { + static std::vector<std::unique_ptr<Widget>> widgets_to_remove; + Widget::Widget() { } Widget::~Widget() { remove_widget_as_selected_in_parent(); - // if(parent_widget) - // parent_widget->remove_child_widget(this); } void Widget::set_position(mgl::vec2f position) { @@ -62,4 +63,15 @@ namespace gsr { void Widget::set_visible(bool visible) { this->visible = visible; } + + void add_widget_to_remove(std::unique_ptr<Widget> widget) { + widgets_to_remove.push_back(std::move(widget)); + } + + void remove_widgets_to_be_removed() { + for(size_t i = 0; i < widgets_to_remove.size(); ++i) { + widgets_to_remove[i].reset(); + } + widgets_to_remove.clear(); + } }
\ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 98b1ce3..a68ff7d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,16 +1,15 @@ #include "../include/GsrInfo.hpp" -#include "../include/Theme.hpp" -#include "../include/window_texture.h" #include "../include/Overlay.hpp" -#include "../include/GlobalHotkeysX11.hpp" -#include "../include/GlobalHotkeysLinux.hpp" #include "../include/gui/Utils.hpp" +#include "../include/Process.hpp" +#include "../include/Rpc.hpp" #include <unistd.h> #include <signal.h> -#include <thread> +#include <string.h> +#include <limits.h> +#include <malloc.h> -#include <X11/keysym.h> #include <mglpp/mglpp.hpp> #include <mglpp/system/Clock.hpp> @@ -31,117 +30,225 @@ static void sigint_handler(int signal) { running = 0; } +static void signal_ignore(int) { + +} + static void disable_prime_run() { unsetenv("__NV_PRIME_RENDER_OFFLOAD"); unsetenv("__NV_PRIME_RENDER_OFFLOAD_PROVIDER"); unsetenv("__GLX_VENDOR_LIBRARY_NAME"); unsetenv("__VK_LAYER_NV_optimus"); + unsetenv("DRI_PRIME"); } -static std::unique_ptr<gsr::GlobalHotkeysX11> register_x11_hotkeys(gsr::Overlay *overlay) { - auto global_hotkeys = std::make_unique<gsr::GlobalHotkeysX11>(); - const bool show_hotkey_registered = global_hotkeys->bind_key_press({ XK_z, Mod1Mask }, "show_hide", [overlay](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); +static void rpc_add_commands(gsr::Rpc *rpc, gsr::Overlay *overlay) { + rpc->add_handler("show_ui", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); + overlay->show(); + }); + + rpc->add_handler("toggle-show", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); overlay->toggle_show(); }); - const bool record_hotkey_registered = global_hotkeys->bind_key_press({ XK_F9, Mod1Mask }, "record", [overlay](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); + rpc->add_handler("toggle-record", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); overlay->toggle_record(); }); - const bool pause_hotkey_registered = global_hotkeys->bind_key_press({ XK_F7, Mod1Mask }, "pause", [overlay](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); + rpc->add_handler("toggle-pause", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); overlay->toggle_pause(); }); - const bool stream_hotkey_registered = global_hotkeys->bind_key_press({ XK_F8, Mod1Mask }, "stream", [overlay](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); + rpc->add_handler("toggle-stream", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); overlay->toggle_stream(); }); - const bool replay_hotkey_registered = global_hotkeys->bind_key_press({ XK_F10, ShiftMask | Mod1Mask }, "replay_start", [overlay](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); + rpc->add_handler("toggle-replay", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); overlay->toggle_replay(); }); - const bool replay_save_hotkey_registered = global_hotkeys->bind_key_press({ XK_F10, Mod1Mask }, "replay_save", [overlay](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); + rpc->add_handler("replay-save", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); overlay->save_replay(); }); - if(!show_hotkey_registered) - fprintf(stderr, "error: failed to register hotkey alt+z for showing the overlay because the hotkey is registered by another program\n"); - - if(!record_hotkey_registered) - fprintf(stderr, "error: failed to register hotkey alt+f9 for recording because the hotkey is registered by another program\n"); - - if(!pause_hotkey_registered) - fprintf(stderr, "error: failed to register hotkey alt+f7 for pausing because the hotkey is registered by another program\n"); + rpc->add_handler("replay-save-1-min", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); + overlay->save_replay_1_min(); + }); - if(!stream_hotkey_registered) - fprintf(stderr, "error: failed to register hotkey alt+f8 for streaming because the hotkey is registered by another program\n"); + rpc->add_handler("replay-save-10-min", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); + overlay->save_replay_10_min(); + }); - if(!replay_hotkey_registered) - fprintf(stderr, "error: failed to register hotkey alt+shift+f10 for starting replay because the hotkey is registered by another program\n"); + rpc->add_handler("take-screenshot", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); + overlay->take_screenshot(); + }); - if(!replay_save_hotkey_registered) - fprintf(stderr, "error: failed to register hotkey alt+f10 for saving replay because the hotkey is registered by another program\n"); + rpc->add_handler("take-screenshot-region", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); + overlay->take_screenshot_region(); + }); +} - if(!show_hotkey_registered || !record_hotkey_registered || !pause_hotkey_registered || !stream_hotkey_registered || !replay_hotkey_registered || !replay_save_hotkey_registered) - return nullptr; +static bool is_gsr_ui_virtual_keyboard_running() { + FILE *f = fopen("/proc/bus/input/devices", "rb"); + if(!f) + return false; + + bool virtual_keyboard_running = false; + char line[1024]; + while(fgets(line, sizeof(line), f)) { + if(strstr(line, "gsr-ui virtual keyboard")) { + virtual_keyboard_running = true; + break; + } + } - return global_hotkeys; + fclose(f); + return virtual_keyboard_running; } -static std::unique_ptr<gsr::GlobalHotkeysLinux> register_linux_hotkeys(gsr::Overlay *overlay) { - auto global_hotkeys = std::make_unique<gsr::GlobalHotkeysLinux>(); - if(!global_hotkeys->start()) - fprintf(stderr, "error: failed to start global hotkeys\n"); +static void install_flatpak_systemd_service() { + const bool systemd_service_exists = system( + "data_home=$(flatpak-spawn --host -- /bin/sh -c 'echo \"${XDG_DATA_HOME:-$HOME/.local/share}\"') && " + "flatpak-spawn --host -- ls \"$data_home/systemd/user/gpu-screen-recorder-ui.service\"") == 0; + if(systemd_service_exists) + return; + + bool service_install_successful = (system( + "data_home=$(flatpak-spawn --host -- /bin/sh -c 'echo \"${XDG_DATA_HOME:-$HOME/.local/share}\"') && " + "flatpak-spawn --host -- install -Dm644 /var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/share/gpu-screen-recorder/gpu-screen-recorder-ui.service \"$data_home/systemd/user/gpu-screen-recorder-ui.service\"") == 0); + service_install_successful &= (system("flatpak-spawn --host -- systemctl --user daemon-reload") == 0); + if(service_install_successful) + fprintf(stderr, "Info: the systemd service file was missing. It has now been installed\n"); + else + fprintf(stderr, "Error: the systemd service file is missing and failed to install it again\n"); +} - global_hotkeys->bind_action("show_hide", [overlay](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); - overlay->toggle_show(); - }); +static void remove_flatpak_systemd_service() { + char systemd_service_path[PATH_MAX]; + const char *xdg_data_home = getenv("XDG_DATA_HOME"); + const char *home = getenv("HOME"); + if(xdg_data_home) { + snprintf(systemd_service_path, sizeof(systemd_service_path), "%s/systemd/user/gpu-screen-recorder-ui.service", xdg_data_home); + } else if(home) { + snprintf(systemd_service_path, sizeof(systemd_service_path), "%s/.local/share/systemd/user/gpu-screen-recorder-ui.service", home); + } else { + fprintf(stderr, "Error: failed to get user home directory\n"); + return; + } - global_hotkeys->bind_action("record", [overlay](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); - overlay->toggle_record(); - }); + if(access(systemd_service_path, F_OK) != 0) + return; - global_hotkeys->bind_action("pause", [overlay](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); - overlay->toggle_pause(); - }); + remove(systemd_service_path); + system("systemctl --user daemon-reload"); + fprintf(stderr, "Info: conflicting flatpak version of the systemd service for gsr-ui was found at \"%s\", it has now been removed\n", systemd_service_path); +} - global_hotkeys->bind_action("stream", [overlay](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); - overlay->toggle_stream(); - }); +static bool is_flatpak() { + return getenv("FLATPAK_ID") != nullptr; +} - global_hotkeys->bind_action("replay_start", [overlay](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); - overlay->toggle_replay(); - }); +static void set_display_server_environment_variables() { + // Some users dont have properly setup environments (no display manager that does systemctl --user import-environment DISPLAY WAYLAND_DISPLAY) + const char *display = getenv("DISPLAY"); + if(!display) { + display = ":0"; + setenv("DISPLAY", display, true); + } - global_hotkeys->bind_action("replay_save", [overlay](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); - overlay->save_replay(); - }); + const char *wayland_display = getenv("WAYLAND_DISPLAY"); + if(!wayland_display) { + wayland_display = "wayland-1"; + setenv("WAYLAND_DISPLAY", wayland_display, true); + } +} - return global_hotkeys; +static void usage() { + printf("usage: gsr-ui [action]\n"); + printf("OPTIONS:\n"); + printf(" action The launch action. Should be either \"launch-show\", \"launch-hide\" or \"launch-daemon\". Optional, defaults to \"launch-hide\".\n"); + printf(" If \"launch-show\" is used then the program starts and the UI is immediately opened and can be shown/hidden with Alt+Z.\n"); + printf(" If \"launch-hide\" is used then the program starts but the UI is not opened until Alt+Z is pressed. The UI will be opened if the program is already running in another process.\n"); + printf(" If \"launch-daemon\" is used then the program starts but the UI is not opened until Alt+Z is pressed. The UI will not be opened if the program is already running in another process.\n"); + exit(1); } -int main(void) { +enum class LaunchAction { + LAUNCH_SHOW, + LAUNCH_HIDE, + LAUNCH_DAEMON +}; + +int main(int argc, char **argv) { setlocale(LC_ALL, "C"); // Sigh... stupid C + mallopt(M_MMAP_THRESHOLD, 65536); if(geteuid() == 0) { fprintf(stderr, "Error: don't run gsr-ui as the root user\n"); return 1; } - // Cant get window texture when prime-run is used - disable_prime_run(); + LaunchAction launch_action = LaunchAction::LAUNCH_HIDE; + if(argc == 1) { + launch_action = LaunchAction::LAUNCH_HIDE; + } else if(argc == 2) { + const char *launch_action_opt = argv[1]; + if(strcmp(launch_action_opt, "launch-show") == 0) { + launch_action = LaunchAction::LAUNCH_SHOW; + } else if(strcmp(launch_action_opt, "launch-hide") == 0) { + launch_action = LaunchAction::LAUNCH_HIDE; + } else if(strcmp(launch_action_opt, "launch-daemon") == 0) { + launch_action = LaunchAction::LAUNCH_DAEMON; + } else { + printf("error: invalid action \"%s\", expected \"launch-show\", \"launch-hide\" or \"launch-daemon\".\n", launch_action_opt); + usage(); + } + } else { + usage(); + } + + set_display_server_environment_variables(); + + // TODO: This is a shitty method to detect if multiple instances of gsr-ui is running but this will work properly even in flatpak + // that uses pid sandboxing. Replace this with a better method once we no longer rely on linux global hotkeys on some platform. + // TODO: This method doesn't work when disabling hotkeys and the method below with pidof gsr-ui doesn't work in flatpak. + // What do? creating a pid file doesn't work in flatpak either. + // TODO: This doesn't work in flatpak when disabling hotkeys. + if(is_gsr_ui_virtual_keyboard_running() || gsr::pidof("gsr-ui", getpid()) != -1) { + if(launch_action == LaunchAction::LAUNCH_DAEMON) + return 1; + + gsr::Rpc rpc; + if(rpc.open("gsr-ui") && rpc.write("show_ui\n", 8)) { + fprintf(stderr, "Error: another instance of gsr-ui is already running, opening that one instead\n"); + } else { + fprintf(stderr, "Error: failed to send command to running gsr-ui instance, user will have to open the UI manually with Alt+Z\n"); + const char *args[] = { "gsr-notify", "--text", "Another instance of GPU Screen Recorder UI is already running.\nPress Alt+Z to open the UI.", "--timeout", "5.0", "--icon-color", "ff0000", "--bg-color", "ff0000", nullptr }; + gsr::exec_program_daemonized(args); + } + return 1; + } + + if(gsr::pidof("gpu-screen-recorder", getpid()) != -1) { + const char *args[] = { "gsr-notify", "--text", "GPU Screen Recorder is already running in another process.\nPlease close it before using GPU Screen Recorder UI.", "--timeout", "5.0", "--icon-color", "ff0000", "--bg-color", "ff0000", nullptr }; + gsr::exec_program_daemonized(args); + } + + if(is_flatpak()) + install_flatpak_systemd_service(); + else + remove_flatpak_systemd_service(); // Stop nvidia driver from buffering frames setenv("__GL_MaxFramesAllowed", "1", true); @@ -154,28 +261,42 @@ int main(void) { unsetenv("vblank_mode"); signal(SIGINT, sigint_handler); - - if(mgl_init() != 0) { - fprintf(stderr, "error: failed to initialize mgl. Failed to either connect to the X11 server or setup opengl\n"); - exit(1); - } + signal(SIGTERM, sigint_handler); + signal(SIGUSR1, signal_ignore); + signal(SIGUSR2, signal_ignore); + signal(SIGRTMIN, signal_ignore); + signal(SIGRTMIN+1, signal_ignore); + signal(SIGRTMIN+2, signal_ignore); + signal(SIGRTMIN+3, signal_ignore); + signal(SIGRTMIN+4, signal_ignore); + signal(SIGRTMIN+5, signal_ignore); + signal(SIGRTMIN+6, signal_ignore); gsr::GsrInfo gsr_info; // TODO: Show the error in ui gsr::GsrInfoExitStatus gsr_info_exit_status = gsr::get_gpu_screen_recorder_info(&gsr_info); if(gsr_info_exit_status != gsr::GsrInfoExitStatus::OK) { - fprintf(stderr, "error: failed to get gpu-screen-recorder info, error: %d\n", (int)gsr_info_exit_status); + fprintf(stderr, "Error: failed to get gpu-screen-recorder info, error: %d\n", (int)gsr_info_exit_status); exit(1); } const gsr::DisplayServer display_server = gsr_info.system_info.display_server; - if(display_server == gsr::DisplayServer::WAYLAND) - fprintf(stderr, "warning: Wayland support is experimental and requires XWayland. Things may not work as expected.\n"); + if(display_server == gsr::DisplayServer::WAYLAND) { + fprintf(stderr, "Warning: Wayland doesn't support this program properly and XWayland is required. Things may not work as expected. Use X11 if you experience issues.\n"); + } else { + // Cant get window texture when prime-run is used + disable_prime_run(); + } + + if(mgl_init(MGL_WINDOW_SYSTEM_X11) != 0) { + fprintf(stderr, "Error: failed to initialize mgl. Failed to either connect to the X11 server or setup opengl\n"); + exit(1); + } gsr::SupportedCaptureOptions capture_options = gsr::get_supported_capture_options(gsr_info); std::string resources_path; - if(access("sibs-build", F_OK) == 0) { + if(access("sibs-build/linux_x86_64/debug/gsr-ui", F_OK) == 0) { resources_path = "./"; } else { #ifdef GSR_UI_RESOURCES_PATH @@ -198,43 +319,47 @@ int main(void) { exit(1); } - fprintf(stderr, "info: gsr ui is now ready, waiting for inputs. Press alt+z to show/hide the overlay\n"); + fprintf(stderr, "Info: gsr ui is now ready, waiting for inputs. Press alt+z to show/hide the overlay\n"); auto overlay = std::make_unique<gsr::Overlay>(resources_path, std::move(gsr_info), std::move(capture_options), egl_funcs); - //overlay.show(); - - std::unique_ptr<gsr::GlobalHotkeys> global_hotkeys = nullptr; - if(display_server == gsr::DisplayServer::X11) { - global_hotkeys = register_x11_hotkeys(overlay.get()); - if(!global_hotkeys) { - fprintf(stderr, "info: failed to register some x11 hotkeys because they are registered by another program. Will use linux hotkeys instead that can clash with keys used by other applications\n"); - global_hotkeys = register_linux_hotkeys(overlay.get()); - } - } else { - fprintf(stderr, "info: Global linux hotkeys are used which can clash with keys used by other applications. Use X11 instead if this is an issue for you\n"); - global_hotkeys = register_linux_hotkeys(overlay.get()); - } + if(launch_action == LaunchAction::LAUNCH_SHOW) + overlay->show(); + + auto rpc = std::make_unique<gsr::Rpc>(); + if(!rpc->create("gsr-ui")) + fprintf(stderr, "Error: Failed to create rpc, commands won't be received\n"); + + rpc_add_commands(rpc.get(), overlay.get()); // TODO: Add hotkeys in Overlay when using x11 global hotkeys. The hotkeys in Overlay should duplicate each key that is used for x11 global hotkeys. + std::string exit_reason; mgl::Clock frame_delta_clock; - while(running && mgl_is_connected_to_display_server()) { + + while(running && mgl_is_connected_to_display_server() && !overlay->should_exit(exit_reason)) { const double frame_delta_seconds = frame_delta_clock.restart(); gsr::set_frame_delta_seconds(frame_delta_seconds); - global_hotkeys->poll_events(); - overlay->handle_events(global_hotkeys.get()); + rpc->poll(); + overlay->handle_events(); if(!overlay->draw()) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + usleep(100 * 1000); // 100ms mgl_ping_display_server(); } } - fprintf(stderr, "info: shutting down!\n"); + fprintf(stderr, "Info: shutting down!\n"); + rpc.reset(); overlay.reset(); - gsr::deinit_theme(); - gsr::deinit_color_theme(); mgl_deinit(); - return 0; + if(exit_reason == "back-to-old-ui") { + const char *args[] = { "gpu-screen-recorder-gtk", "use-old-ui", nullptr }; + execvp(args[0], (char* const*)args); + return 0; + } else if(exit_reason == "exit") { + return 0; + } + + return mgl_is_connected_to_display_server() ? 0 : 1; } diff --git a/tools/gsr-global-hotkeys/README.md b/tools/gsr-global-hotkeys/README.md new file mode 100644 index 0000000..38585c1 --- /dev/null +++ b/tools/gsr-global-hotkeys/README.md @@ -0,0 +1,27 @@ +# About +Global hotkeys for X11 and all Wayland compositors by using linux device api. Keyboards are grabbed and only the non-hotkey keys are passed through to the system. +The program accepts text commands as input. Run the program with the option `--virtual` to only grab virtual devices. This is useful when using keyboard input mapping software such as +kanata, otherwise kanata may fail to launch or this program may fail to launch. +# Commands +## Bind +To add a key send `bind <action> <keycode+keycode+...><newline>` to the programs stdin, for example: +``` +bind show_hide 56+44 + +``` +which will bind alt+z. When alt+z is pressed the program will output `show_hide` (and a newline) to stdout. +The program only accepts one key for each keybind command but accepts a multiple modifier keys. +The keybinding requires at least one modifier key (ctrl, alt, super or shift) and a key to be used. +The keycodes are values from `<linux/input-event-codes.h>` linux api header (which is the same as X11 keycode value minus 8). +## Unbind +To unbind all keys send `unbind_all<newline>` to the programs stdin, for example: +``` +unbind_all + +``` +## Exit +To close gsr-global-hotkeys send `exit<newline>` to the programs stdin, for example: +``` +exit + +```
\ No newline at end of file diff --git a/tools/gsr-global-hotkeys/hotplug.c b/tools/gsr-global-hotkeys/hotplug.c new file mode 100644 index 0000000..2e8ca9f --- /dev/null +++ b/tools/gsr-global-hotkeys/hotplug.c @@ -0,0 +1,79 @@ +#include "hotplug.h" + +/* C stdlib */ +#include <string.h> + +/* POSIX */ +#include <unistd.h> +#include <sys/socket.h> + +/* LINUX */ +#include <linux/types.h> +#include <linux/netlink.h> + +bool hotplug_event_init(hotplug_event *self) { + memset(self, 0, sizeof(*self)); + + struct sockaddr_nl nls = { + .nl_family = AF_NETLINK, + .nl_pid = getpid(), + .nl_groups = -1 + }; + + const int fd = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT); + if(fd == -1) + return false; + + if(bind(fd, (void*)&nls, sizeof(struct sockaddr_nl))) { + close(fd); + return false; + } + + self->fd = fd; + return true; +} + +void hotplug_event_deinit(hotplug_event *self) { + if(self->fd > 0) { + close(self->fd); + self->fd = -1; + } +} + +int hotplug_event_steal_fd(hotplug_event *self) { + const int fd = self->fd; + self->fd = -1; + return fd; +} + +/* TODO: This assumes SUBSYSTEM= is output before DEVNAME=, is that always true? */ +static void hotplug_event_parse_netlink_data(hotplug_event *self, const char *line, hotplug_device_added_callback callback, void *userdata) { + const char *at_symbol = strchr(line, '@'); + if(at_symbol) { + self->event_is_add = strncmp(line, "add@", 4) == 0; + self->subsystem_is_input = false; + } else if(self->event_is_add) { + if(strcmp(line, "SUBSYSTEM=input") == 0) + self->subsystem_is_input = true; + + if(self->subsystem_is_input && strncmp(line, "DEVNAME=", 8) == 0) { + callback(line+8, userdata); + self->event_is_add = false; + } + } +} + +/* Netlink uevent structure is documented here: https://web.archive.org/web/20160127215232/https://www.kernel.org/doc/pending/hotplug.txt */ +void hotplug_event_process_event_data(hotplug_event *self, int fd, hotplug_device_added_callback callback, void *userdata) { + const int bytes_read = read(fd, self->event_data, sizeof(self->event_data) - 1); + if(bytes_read <= 0) + return; + self->event_data[bytes_read] = '\0'; + + /* Hotplug data ends with a newline and a null terminator */ + int data_index = 0; + while(data_index < bytes_read) { + hotplug_event_parse_netlink_data(self, self->event_data + data_index, callback, userdata); + data_index += strlen(self->event_data + data_index) + 1; /* Skip null terminator as well */ + } +} diff --git a/tools/gsr-global-hotkeys/hotplug.h b/tools/gsr-global-hotkeys/hotplug.h new file mode 100644 index 0000000..665485a --- /dev/null +++ b/tools/gsr-global-hotkeys/hotplug.h @@ -0,0 +1,22 @@ +#ifndef HOTPLUG_H +#define HOTPLUG_H + +/* C stdlib */ +#include <stdbool.h> + +typedef struct { + int fd; + bool event_is_add; + bool subsystem_is_input; + char event_data[1024]; +} hotplug_event; + +typedef void (*hotplug_device_added_callback)(const char *devname, void *userdata); + +bool hotplug_event_init(hotplug_event *self); +void hotplug_event_deinit(hotplug_event *self); + +int hotplug_event_steal_fd(hotplug_event *self); +void hotplug_event_process_event_data(hotplug_event *self, int fd, hotplug_device_added_callback callback, void *userdata); + +#endif /* HOTPLUG_H */ diff --git a/tools/gsr-global-hotkeys/keyboard_event.c b/tools/gsr-global-hotkeys/keyboard_event.c new file mode 100644 index 0000000..4ff7f11 --- /dev/null +++ b/tools/gsr-global-hotkeys/keyboard_event.c @@ -0,0 +1,845 @@ +#include "keyboard_event.h" +#include "keys.h" + +/* C stdlib */ +#include <stdio.h> +#include <string.h> +#include <errno.h> +#include <stdbool.h> +#include <stdlib.h> + +/* POSIX */ +#include <fcntl.h> +#include <unistd.h> +#include <dirent.h> +#include <poll.h> + +/* LINUX */ +#include <linux/input.h> +#include <linux/uinput.h> + +#define GSR_UI_VIRTUAL_KEYBOARD_NAME "gsr-ui virtual keyboard" + +#define KEY_RELEASE 0 +#define KEY_PRESS 1 +#define KEY_REPEAT 2 + +#define KEY_STATES_SIZE (KEY_MAX/8 + 1) + +static inline int count_num_bits_set(unsigned char c) { + int n = 0; + n += (c & 1); + c >>= 1; + n += (c & 1); + c >>= 1; + n += (c & 1); + c >>= 1; + n += (c & 1); + c >>= 1; + n += (c & 1); + c >>= 1; + n += (c & 1); + c >>= 1; + n += (c & 1); + c >>= 1; + n += (c & 1); + return n; +} + +static inline bool keyboard_event_has_exclusive_grab(const keyboard_event *self) { + return self->uinput_fd > 0; +} + +static int keyboard_event_get_num_keys_pressed(const unsigned char *key_states) { + if(!key_states) + return 0; + + int num_keys_pressed = 0; + for(int i = 0; i < KEY_STATES_SIZE; ++i) { + num_keys_pressed += count_num_bits_set(key_states[i]); + } + return num_keys_pressed; +} + +static void keyboard_event_fetch_update_key_states(keyboard_event *self, event_extra_data *extra_data, int fd) { + fsync(fd); + if(!extra_data->key_states) + return; + + if(ioctl(fd, EVIOCGKEY(KEY_STATES_SIZE), extra_data->key_states) == -1) + fprintf(stderr, "Warning: failed to fetch key states for device: /dev/input/event%d\n", extra_data->dev_input_id); + + if(!keyboard_event_has_exclusive_grab(self) || extra_data->grabbed || extra_data->is_non_keyboard_device) + return; + + extra_data->num_keys_pressed = keyboard_event_get_num_keys_pressed(extra_data->key_states); + if(extra_data->num_keys_pressed == 0) { + extra_data->grabbed = ioctl(fd, EVIOCGRAB, 1) != -1; + if(extra_data->grabbed) + fprintf(stderr, "Info: grabbed device: /dev/input/event%d\n", extra_data->dev_input_id); + else + fprintf(stderr, "Warning: failed to exclusively grab device: /dev/input/event%d. The focused application may receive keys used for global hotkeys\n", extra_data->dev_input_id); + } +} + +static void keyboard_event_process_key_state_change(keyboard_event *self, const struct input_event *event, event_extra_data *extra_data, int fd) { + if(event->type != EV_KEY) + return; + + if(!extra_data->key_states || event->code >= KEY_STATES_SIZE * 8) + return; + + const unsigned int byte_index = event->code / 8; + const unsigned char bit_index = event->code % 8; + unsigned char key_byte_state = extra_data->key_states[byte_index]; + const bool prev_key_pressed = (key_byte_state & (1 << bit_index)) != KEY_RELEASE; + + if(event->value == KEY_RELEASE) { + key_byte_state &= ~(1 << bit_index); + if(prev_key_pressed) + --extra_data->num_keys_pressed; + } else { + key_byte_state |= (1 << bit_index); + if(!prev_key_pressed) + ++extra_data->num_keys_pressed; + } + + extra_data->key_states[byte_index] = key_byte_state; + + if(!keyboard_event_has_exclusive_grab(self) || extra_data->grabbed || extra_data->is_non_keyboard_device) + return; + + if(extra_data->num_keys_pressed == 0) { + extra_data->grabbed = ioctl(fd, EVIOCGRAB, 1) != -1; + if(extra_data->grabbed) + fprintf(stderr, "Info: grabbed device: /dev/input/event%d\n", extra_data->dev_input_id); + else + fprintf(stderr, "Warning: failed to exclusively grab device: /dev/input/event%d. The focused application may receive keys used for global hotkeys\n", extra_data->dev_input_id); + } +} + +/* Return true if a global hotkey is assigned to the key combination */ +static bool keyboard_event_on_key_pressed(keyboard_event *self, const struct input_event *event, uint32_t modifiers) { + bool global_hotkey_match = false; + for(int i = 0; i < self->num_global_hotkeys; ++i) { + if(event->code == self->global_hotkeys[i].key && modifiers == self->global_hotkeys[i].modifiers) { + if(event->value == KEYBOARD_BUTTON_PRESSED) { + puts(self->global_hotkeys[i].action); + fflush(stdout); + } + global_hotkey_match = true; + } + } + return global_hotkey_match; +} + +static inline uint32_t set_bit(uint32_t value, uint32_t bit_flag, bool set) { + if(set) + return value | bit_flag; + else + return value & ~bit_flag; +} + +static uint32_t keycode_to_modifier_bit(uint32_t keycode) { + switch(keycode) { + case KEY_LEFTSHIFT: return KEYBOARD_MODKEY_LSHIFT; + case KEY_RIGHTSHIFT: return KEYBOARD_MODKEY_RSHIFT; + case KEY_LEFTCTRL: return KEYBOARD_MODKEY_LCTRL; + case KEY_RIGHTCTRL: return KEYBOARD_MODKEY_RCTRL; + case KEY_LEFTALT: return KEYBOARD_MODKEY_LALT; + case KEY_RIGHTALT: return KEYBOARD_MODKEY_RALT; + case KEY_LEFTMETA: return KEYBOARD_MODKEY_LSUPER; + case KEY_RIGHTMETA: return KEYBOARD_MODKEY_RSUPER; + } + return 0; +} + +/* Returns true if the state changed */ +static bool keyboard_event_set_key_presses_grabbed(const struct input_event *event, event_extra_data *extra_data) { + if(event->type != EV_KEY) + return false; + + if(!extra_data->key_presses_grabbed || event->code >= KEY_STATES_SIZE * 8) + return false; + + const unsigned int byte_index = event->code / 8; + const unsigned char bit_index = event->code % 8; + unsigned char key_byte_state = extra_data->key_presses_grabbed[byte_index]; + const bool prev_key_pressed = (key_byte_state & (1 << bit_index)) != KEY_RELEASE; + extra_data->key_presses_grabbed[byte_index] = set_bit(key_byte_state, bit_index, event->value >= 1); + + if(event->value == KEY_PRESS) + return !prev_key_pressed; + else if(event->value == KEY_RELEASE || event->value == KEY_REPEAT) + return prev_key_pressed; + + return false; +} + +static void keyboard_event_process_input_event_data(keyboard_event *self, event_extra_data *extra_data, int fd) { + struct input_event event; + if(read(fd, &event, sizeof(event)) != sizeof(event)) { + fprintf(stderr, "Error: failed to read input event data\n"); + return; + } + + if(event.type == EV_SYN && event.code == SYN_DROPPED) { + /* TODO: Don't do this on every SYN_DROPPED to prevent spamming this, instead wait until the next event or wait for timeout */ + keyboard_event_fetch_update_key_states(self, extra_data, fd); + return; + } + + //if(event.type == EV_KEY && event.code == KEY_A && event.value == KEY_PRESS) { + //fprintf(stderr, "fd: %d, type: %d, pressed %d, value: %d\n", fd, event.type, event.code, event.value); + //} + + const bool keyboard_key = is_keyboard_key(event.code); + if(event.type == EV_KEY && keyboard_key) { + keyboard_event_process_key_state_change(self, &event, extra_data, fd); + const uint32_t modifier_bit = keycode_to_modifier_bit(event.code); + if(modifier_bit == 0) { + if(keyboard_event_on_key_pressed(self, &event, self->modifier_button_states)) { + if(keyboard_event_set_key_presses_grabbed(&event, extra_data)) + return; + } else if(event.value == KEY_RELEASE) { + if(keyboard_event_set_key_presses_grabbed(&event, extra_data)) + return; + } + } else { + self->modifier_button_states = set_bit(self->modifier_button_states, modifier_bit, event.value >= 1); + } + } + + if(extra_data->grabbed) { + /* TODO: What to do on error? */ + if(write(self->uinput_fd, &event, sizeof(event)) != sizeof(event)) + fprintf(stderr, "Error: failed to write event data to virtual keyboard for exclusively grabbed device\n"); + } + + if(!extra_data->is_possibly_non_keyboard_device) + return; + + /* TODO: What if some key is being pressed down while this is done? will it remain pressed down? */ + if(!extra_data->is_non_keyboard_device && (event.type == EV_REL || event.type == EV_ABS || (event.type == EV_KEY && !keyboard_key))) { + fprintf(stderr, "Info: device /dev/input/event%d is likely a non-keyboard device as it received a non-keyboard event. This device will be ignored\n", extra_data->dev_input_id); + extra_data->is_non_keyboard_device = true; + if(extra_data->grabbed) { + extra_data->grabbed = false; + ioctl(fd, EVIOCGRAB, 0); + fprintf(stderr, "Info: ungrabbed device: /dev/input/event%d\n", extra_data->dev_input_id); + } + } +} + +/* Retarded linux takes very long time to close /dev/input/eventN files, even though they are virtual and opened read-only */ +static void* keyboard_event_close_fds_callback(void *userdata) { + keyboard_event *self = userdata; + while(self->running) { + pthread_mutex_lock(&self->close_dev_input_mutex); + for(int i = 0; i < self->num_close_fds; ++i) { + close(self->close_fds[i]); + } + self->num_close_fds = 0; + pthread_mutex_unlock(&self->close_dev_input_mutex); + + usleep(100 * 1000); /* 100 milliseconds */ + } + return NULL; +} + +static bool keyboard_event_try_add_close_fd(keyboard_event *self, int fd) { + bool success = false; + pthread_mutex_lock(&self->close_dev_input_mutex); + if(self->num_close_fds < MAX_CLOSE_FDS) { + self->close_fds[self->num_close_fds] = fd; + ++self->num_close_fds; + success = true; + } else { + success = false; + } + pthread_mutex_unlock(&self->close_dev_input_mutex); + return success; +} + +/* Returns -1 if invalid format. Expected |dev_input_filepath| to be in format /dev/input/eventN */ +static int get_dev_input_id_from_filepath(const char *dev_input_filepath) { + if(strncmp(dev_input_filepath, "/dev/input/event", 16) != 0) + return -1; + + int dev_input_id = -1; + if(sscanf(dev_input_filepath + 16, "%d", &dev_input_id) == 1) + return dev_input_id; + return -1; +} + +static bool keyboard_event_has_event_with_dev_input_fd(keyboard_event *self, int dev_input_id) { + for(int i = 0; i < self->num_event_polls; ++i) { + if(self->event_extra_data[i].dev_input_id == dev_input_id) + return true; + } + return false; +} + +/* TODO: Is there a more efficient way to do this? */ +static bool dev_input_is_virtual(int dev_input_id) { + DIR *dir = opendir("/sys/devices/virtual/input"); + if(!dir) + return false; + + bool is_virtual = false; + char virtual_input_filepath[1024]; + for(;;) { + struct dirent *entry = readdir(dir); + if(!entry) + break; + + if(strncmp(entry->d_name, "input", 5) != 0) + continue; + + snprintf(virtual_input_filepath, sizeof(virtual_input_filepath), "/sys/devices/virtual/input/%s/event%d", entry->d_name, dev_input_id); + if(access(virtual_input_filepath, F_OK) == 0) { + is_virtual = true; + break; + } + } + + closedir(dir); + return is_virtual; +} + +static inline bool supports_key(unsigned char *key_bits, unsigned int key) { + return key_bits[key/8] & (1 << (key % 8)); +} + +static bool supports_keyboard_keys(unsigned char *key_bits) { + const int keys[2] = { KEY_A, KEY_ESC }; + for(int i = 0; i < 2; ++i) { + if(supports_key(key_bits, keys[i])) + return true; + } + return false; +} + +static bool supports_mouse_keys(unsigned char *key_bits) { + const int keys[2] = { BTN_MOUSE, BTN_LEFT }; + for(int i = 0; i < 2; ++i) { + if(supports_key(key_bits, keys[i])) + return true; + } + return false; +} + +static bool supports_joystick_keys(unsigned char *key_bits) { + const int keys[9] = { BTN_JOYSTICK, BTN_A, BTN_B, BTN_X, BTN_Y, BTN_SELECT, BTN_START, BTN_SELECT, BTN_TRIGGER_HAPPY1 }; + for(int i = 0; i < 9; ++i) { + if(supports_key(key_bits, keys[i])) + return true; + } + return false; +} + +static bool supports_wheel_keys(unsigned char *key_bits) { + const int keys[2] = { BTN_WHEEL, BTN_GEAR_DOWN }; + for(int i = 0; i < 2; ++i) { + if(supports_key(key_bits, keys[i])) + return true; + } + return false; +} + +static bool keyboard_event_try_add_device_if_keyboard(keyboard_event *self, const char *dev_input_filepath) { + const int dev_input_id = get_dev_input_id_from_filepath(dev_input_filepath); + if(dev_input_id == -1) + return false; + + const bool is_virtual_device = dev_input_is_virtual(dev_input_id); + if(self->grab_type == KEYBOARD_GRAB_TYPE_VIRTUAL && !is_virtual_device) + return false; + + if(keyboard_event_has_event_with_dev_input_fd(self, dev_input_id)) + return false; + + const int fd = open(dev_input_filepath, O_RDONLY); + if(fd == -1) + return false; + + char device_name[256]; + device_name[0] = '\0'; + ioctl(fd, EVIOCGNAME(sizeof(device_name)), device_name); + + unsigned long evbit = 0; + ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit); + const bool is_keyboard = (evbit & (1 << EV_SYN)) && (evbit & (1 << EV_KEY)); + + if(is_keyboard && strcmp(device_name, GSR_UI_VIRTUAL_KEYBOARD_NAME) != 0) { + unsigned char key_bits[KEY_MAX/8 + 1] = {0}; + ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(key_bits)), &key_bits); + + const bool supports_key_events = supports_keyboard_keys(key_bits); + const bool supports_mouse_events = supports_mouse_keys(key_bits); + const bool supports_joystick_events = supports_joystick_keys(key_bits); + const bool supports_wheel_events = supports_wheel_keys(key_bits); + + if(supports_key_events && (is_virtual_device || (!supports_joystick_events && !supports_wheel_events))) { + unsigned char *key_states = calloc(1, KEY_STATES_SIZE); + unsigned char *key_presses_grabbed = calloc(1, KEY_STATES_SIZE); + if(key_states && key_presses_grabbed && self->num_event_polls < MAX_EVENT_POLLS) { + //fprintf(stderr, "%s (%s) supports key inputs\n", dev_input_filepath, device_name); + self->event_polls[self->num_event_polls] = (struct pollfd) { + .fd = fd, + .events = POLLIN, + .revents = 0 + }; + + self->event_extra_data[self->num_event_polls] = (event_extra_data) { + .dev_input_id = dev_input_id, + .grabbed = false, + .key_states = key_states, + .key_presses_grabbed = key_presses_grabbed, + .num_keys_pressed = 0 + }; + + if(supports_mouse_events || supports_joystick_events || supports_wheel_events) { + self->event_extra_data[self->num_event_polls].is_possibly_non_keyboard_device = true; + fprintf(stderr, "Info: device not grabbed yet because it might be a mouse: /dev/input/event%d\n", dev_input_id); + fsync(fd); + if(ioctl(fd, EVIOCGKEY(KEY_STATES_SIZE), self->event_extra_data[self->num_event_polls].key_states) == -1) + fprintf(stderr, "Warning: failed to fetch key states for device: /dev/input/event%d\n", dev_input_id); + } else { + keyboard_event_fetch_update_key_states(self, &self->event_extra_data[self->num_event_polls], fd); + if(self->event_extra_data[self->num_event_polls].num_keys_pressed > 0) + fprintf(stderr, "Info: device not grabbed yet because some keys are still being pressed: /dev/input/event%d\n", dev_input_id); + } + + ++self->num_event_polls; + return true; + } else { + fprintf(stderr, "Warning: the maximum number of keyboard devices have been registered. The newly added keyboard will be ignored\n"); + free(key_states); + free(key_presses_grabbed); + } + } + } + + if(!keyboard_event_try_add_close_fd(self, fd)) { + fprintf(stderr, "Error: failed to add immediately, closing now\n"); + close(fd); + } + return false; +} + +static bool keyboard_event_add_dev_input_devices(keyboard_event *self) { + DIR *dir = opendir("/dev/input"); + if(!dir) { + fprintf(stderr, "error: failed to open /dev/input, error: %s\n", strerror(errno)); + return false; + } + + char dev_input_filepath[1024]; + for(;;) { + struct dirent *entry = readdir(dir); + if(!entry) + break; + + if(strncmp(entry->d_name, "event", 5) != 0) + continue; + + snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/input/%s", entry->d_name); + keyboard_event_try_add_device_if_keyboard(self, dev_input_filepath); + } + + closedir(dir); + return true; +} + +static void keyboard_event_remove_event(keyboard_event *self, int index) { + if(index < 0 || index >= self->num_event_polls) + return; + + if(self->event_polls[index].fd > 0) { + ioctl(self->event_polls[index].fd, EVIOCGRAB, 0); + close(self->event_polls[index].fd); + } + free(self->event_extra_data[index].key_states); + free(self->event_extra_data[index].key_presses_grabbed); + + for(int i = index + 1; i < self->num_event_polls; ++i) { + self->event_polls[i - 1] = self->event_polls[i]; + self->event_extra_data[i - 1] = self->event_extra_data[i]; + } + --self->num_event_polls; +} + +/* Returns the fd to the uinput */ +/* Documented here: https://www.kernel.org/doc/html/v4.12/input/uinput.html */ +static int setup_virtual_keyboard_input(const char *name) { + /* TODO: O_NONBLOCK? */ + int fd = open("/dev/uinput", O_WRONLY); + if(fd == -1) { + fd = open("/dev/input/uinput", O_WRONLY); + if(fd == -1) { + fprintf(stderr, "Warning: failed to setup virtual device for exclusive grab (failed to open /dev/uinput or /dev/input/uinput), error: %s\n", strerror(errno)); + return -1; + } + } + + bool success = true; + success &= (ioctl(fd, UI_SET_EVBIT, EV_SYN) != -1); + success &= (ioctl(fd, UI_SET_EVBIT, EV_MSC) != -1); + success &= (ioctl(fd, UI_SET_EVBIT, EV_KEY) != -1); + success &= (ioctl(fd, UI_SET_EVBIT, EV_REP) != -1); + success &= (ioctl(fd, UI_SET_EVBIT, EV_REL) != -1); + //success &= (ioctl(fd, UI_SET_EVBIT, EV_LED) != -1); + + success &= (ioctl(fd, UI_SET_MSCBIT, MSC_SCAN) != -1); + for(int i = 1; i < KEY_MAX; ++i) { + // TODO: Check for joystick button? if we accidentally grab joystick + if(is_keyboard_key(i) || is_mouse_button(i)) + success &= (ioctl(fd, UI_SET_KEYBIT, i) != -1); + } + for(int i = 0; i < REL_MAX; ++i) { + success &= (ioctl(fd, UI_SET_RELBIT, i) != -1); + } + // for(int i = 0; i < LED_MAX; ++i) { + // success &= (ioctl(fd, UI_SET_LEDBIT, i) != -1); + // } + + // success &= (ioctl(fd, UI_SET_EVBIT, EV_ABS) != -1); + // success &= (ioctl(fd, UI_SET_ABSBIT, ABS_X) != -1); + // success &= (ioctl(fd, UI_SET_ABSBIT, ABS_Y) != -1); + // success &= (ioctl(fd, UI_SET_ABSBIT, ABS_Z) != -1); + + int ui_version = 0; + success &= (ioctl(fd, UI_GET_VERSION, &ui_version) != -1); + + if(ui_version >= 5) { + struct uinput_setup usetup; + memset(&usetup, 0, sizeof(usetup)); + usetup.id.bustype = BUS_USB; + usetup.id.vendor = 0xdec0; + usetup.id.product = 0x5eba; + snprintf(usetup.name, sizeof(usetup.name), "%s", name); + success &= (ioctl(fd, UI_DEV_SETUP, &usetup) != -1); + } else { + struct uinput_user_dev uud; + memset(&uud, 0, sizeof(uud)); + snprintf(uud.name, UINPUT_MAX_NAME_SIZE, "%s", name); + if(write(fd, &uud, sizeof(uud)) != sizeof(uud)) + success = false; + } + + success &= (ioctl(fd, UI_DEV_CREATE) != -1); + if(!success) { + close(fd); + return -1; + } + + return fd; +} + +bool keyboard_event_init(keyboard_event *self, bool exclusive_grab, keyboard_grab_type grab_type) { + memset(self, 0, sizeof(*self)); + self->stdin_event_index = -1; + self->hotplug_event_index = -1; + self->grab_type = grab_type; + self->running = true; + + pthread_mutex_init(&self->close_dev_input_mutex, NULL); + if(pthread_create(&self->close_dev_input_fds_thread, NULL, keyboard_event_close_fds_callback, self) != 0) { + self->close_dev_input_fds_thread = 0; + fprintf(stderr, "Error: failed to create close fds thread\n"); + return false; + } + + if(exclusive_grab) { + self->uinput_fd = setup_virtual_keyboard_input(GSR_UI_VIRTUAL_KEYBOARD_NAME); + if(self->uinput_fd <= 0) + fprintf(stderr, "Warning: failed to setup virtual keyboard input for exclusive grab. The focused application will receive keys used for global hotkeys\n"); + } + + self->event_polls[self->num_event_polls] = (struct pollfd) { + .fd = STDIN_FILENO, + .events = POLLIN, + .revents = 0 + }; + + self->event_extra_data[self->num_event_polls] = (event_extra_data) { + .dev_input_id = -1, + .grabbed = false, + .key_states = NULL, + .key_presses_grabbed = NULL, + .num_keys_pressed = 0 + }; + + self->stdin_event_index = self->num_event_polls; + ++self->num_event_polls; + + if(hotplug_event_init(&self->hotplug_ev)) { + self->event_polls[self->num_event_polls] = (struct pollfd) { + .fd = hotplug_event_steal_fd(&self->hotplug_ev), + .events = POLLIN, + .revents = 0 + }; + + self->event_extra_data[self->num_event_polls] = (event_extra_data) { + .dev_input_id = -1, + .grabbed = false, + .key_states = NULL, + .key_presses_grabbed = NULL, + .num_keys_pressed = 0 + }; + + self->hotplug_event_index = self->num_event_polls; + ++self->num_event_polls; + } else { + fprintf(stderr, "Warning: failed to setup hotplugging\n"); + } + + keyboard_event_add_dev_input_devices(self); + + /* Neither hotplugging nor any keyboard devices were found. We will never listen to keyboard events so might as well fail */ + if(self->num_event_polls == 0) { + keyboard_event_deinit(self); + return false; + } + + return true; +} + +void keyboard_event_deinit(keyboard_event *self) { + self->running = false; + + for(int i = 0; i < self->num_global_hotkeys; ++i) { + free(self->global_hotkeys[i].action); + } + self->num_global_hotkeys = 0; + + if(self->uinput_fd > 0) { + ioctl(self->uinput_fd, UI_DEV_DESTROY); + close(self->uinput_fd); + self->uinput_fd = -1; + } + + for(int i = 0; i < self->num_event_polls; ++i) { + if(self->event_polls[i].fd > 0) { + ioctl(self->event_polls[i].fd, EVIOCGRAB, 0); + close(self->event_polls[i].fd); + } + free(self->event_extra_data[i].key_states); + free(self->event_extra_data[i].key_presses_grabbed); + } + self->num_event_polls = 0; + + hotplug_event_deinit(&self->hotplug_ev); + + if(self->close_dev_input_fds_thread > 0) { + pthread_join(self->close_dev_input_fds_thread, NULL); + self->close_dev_input_fds_thread = 0; + } + + pthread_mutex_destroy(&self->close_dev_input_mutex); +} + +static void on_device_added_callback(const char *devname, void *userdata) { + keyboard_event *keyboard_ev = userdata; + char dev_input_filepath[256]; + snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/%s", devname); + keyboard_event_try_add_device_if_keyboard(keyboard_ev, dev_input_filepath); +} + +/* Returns -1 on error */ +static int parse_u8(const char *str, int size) { + if(size <= 0) + return -1; + + int result = 0; + for(int i = 0; i < size; ++i) { + char c = str[i]; + if(c >= '0' && c <= '9') { + result = result * 10 + (c - '0'); + if(result > 255) + return -1; + } else { + return -1; + } + } + return result; +} + +static bool is_key_alpha_numerical(uint8_t key) { + return (key >= KEY_1 && key <= KEY_0) + || (key >= KEY_Q && key <= KEY_P) + || (key >= KEY_A && key <= KEY_L) + || (key >= KEY_Z && key <= KEY_M); +} + +static bool keyboard_event_parse_bind_keys(const char *str, int size, uint8_t *key, uint32_t *modifiers) { + *key = 0; + *modifiers = 0; + + const char *number_start = str; + const char *end = str + size; + for(;;) { + const char *next = strchr(number_start, '+'); + if(!next) + next = end; + + const int number_len = next - number_start; + const int number = parse_u8(number_start, number_len); + if(number == -1) { + fprintf(stderr, "Error: bind command keys \"%s\" is in invalid format\n", str); + return false; + } + + const uint32_t modifier_bit = keycode_to_modifier_bit(number); + if(modifier_bit == 0) { + if(*key != 0) { + fprintf(stderr, "Error: can't bind hotkey with multiple non-modifier keys\n"); + return false; + } + *key = number; + } else { + *modifiers = set_bit(*modifiers, modifier_bit, true); + } + + number_start = next + 1; + if(next == end) + break; + } + + if(*key == 0) { + fprintf(stderr, "Error: can't bind hotkey without a non-modifier key\n"); + return false; + } + + if(*modifiers == 0 && is_key_alpha_numerical(*key)) { + fprintf(stderr, "Error: can't bind hotkey without a modifier unless the key is a non alpha-numerical key\n"); + return false; + } + + return true; +} + +/* |command| is null-terminated */ +static void keyboard_event_parse_stdin_command(keyboard_event *self, const char *command, int command_size) { + if(strncmp(command, "bind ", 5) == 0) { + /* Example: |bind show_hide 20+40| */ + if(self->num_global_hotkeys >= MAX_GLOBAL_HOTKEYS) { + fprintf(stderr, "Error: can't add another hotkey. The maximum number of hotkeys (%d) has been reached\n", MAX_GLOBAL_HOTKEYS); + return; + } + + const char *action_name_end = strchr(command + 5, ' '); + if(!action_name_end) { + fprintf(stderr, "Error: command \"%s\" is in invalid format\n", command); + return; + } + + const char *action_name = command + 5; + const int action_name_size = action_name_end - action_name; + + uint8_t key = 0; + uint32_t modifiers = 0; + const char *number_start = action_name_end + 1; + const char *end = command + command_size; + if(!keyboard_event_parse_bind_keys(number_start, end - number_start, &key, &modifiers)) + return; + + char *action = strndup(action_name, action_name_size); + if(!action) { + fprintf(stderr, "Error: failed to duplicate %.*s\n", action_name_size, action_name); + return; + } + + self->global_hotkeys[self->num_global_hotkeys] = (global_hotkey) { + .action = action, + .key = key, + .modifiers = modifiers + }; + ++self->num_global_hotkeys; + fprintf(stderr, "Info: binded hotkey: %s\n", action); + } else if(strncmp(command, "unbind_all", 10) == 0) { + for(int i = 0; i < self->num_global_hotkeys; ++i) { + free(self->global_hotkeys[i].action); + } + self->num_global_hotkeys = 0; + fprintf(stderr, "Info: unbinded all hotkeys\n"); + } else if(strncmp(command, "exit", 4) == 0) { + self->stdin_failed = true; + fprintf(stderr, "Info: received exit command\n"); + } else { + fprintf(stderr, "Warning: got invalid command: \"%s\", expected command to start with either \"bind\", \"unbind_all\" or \"exit\"\n", command); + } +} + +static void keyboard_event_process_stdin_command_data(keyboard_event *self, int fd) { + const int num_bytes_to_read = sizeof(self->stdin_command_data) - self->stdin_command_data_size; + if(num_bytes_to_read == 0) { + fprintf(stderr, "Error: failed to read data from stdin, buffer is full. Clearing buffer\n"); + self->stdin_command_data_size = 0; + return; + } + + const ssize_t bytes_read = read(fd, self->stdin_command_data + self->stdin_command_data_size, num_bytes_to_read); + if(bytes_read <= 0) + return; + + const char *command_start = self->stdin_command_data; + const char *search = self->stdin_command_data + self->stdin_command_data_size; + const char *end = search + bytes_read; + self->stdin_command_data_size += bytes_read; + + for(;;) { + char *next = memchr(search, '\n', end - search); + if(!next) + break; + + *next = '\0'; + keyboard_event_parse_stdin_command(self, command_start, next - command_start); + search = next + 1; + command_start = search; + if(next == end) + break; + } + + const int bytes_parsed = command_start - self->stdin_command_data; + if(bytes_parsed > 0) { + self->stdin_command_data_size -= bytes_parsed; + memmove(self->stdin_command_data, command_start, self->stdin_command_data_size); + } +} + +void keyboard_event_poll_events(keyboard_event *self, int timeout_milliseconds) { + if(poll(self->event_polls, self->num_event_polls, timeout_milliseconds) <= 0) + return; + + if(self->stdin_failed) + return; + + for(int i = 0; i < self->num_event_polls; ++i) { + if(i == self->stdin_event_index && (self->event_polls[i].revents & (POLLHUP|POLLERR))) + self->stdin_failed = true; + + if(self->event_polls[i].revents & POLLHUP) { /* TODO: What if this is the hotplug fd? */ + keyboard_event_remove_event(self, i); + --i; /* Repeat same index since the current element has been removed */ + continue; + } + + if(!(self->event_polls[i].revents & POLLIN)) + continue; + + if(i == self->hotplug_event_index) { + /* Device is added to end of |event_polls| so it's ok to add while iterating it via index */ + hotplug_event_process_event_data(&self->hotplug_ev, self->event_polls[i].fd, on_device_added_callback, self); + } else if(i == self->stdin_event_index) { + keyboard_event_process_stdin_command_data(self, self->event_polls[i].fd); + } else { + keyboard_event_process_input_event_data(self, &self->event_extra_data[i], self->event_polls[i].fd); + } + } +} + +bool keyboard_event_stdin_has_failed(const keyboard_event *self) { + return self->stdin_failed; +} diff --git a/tools/gsr-global-hotkeys/keyboard_event.h b/tools/gsr-global-hotkeys/keyboard_event.h new file mode 100644 index 0000000..4a4c1fd --- /dev/null +++ b/tools/gsr-global-hotkeys/keyboard_event.h @@ -0,0 +1,95 @@ +#ifndef KEYBOARD_EVENT_H +#define KEYBOARD_EVENT_H + +/* Read keyboard input from linux /dev/input/eventN devices, with hotplug support */ + +#include "hotplug.h" + +/* C stdlib */ +#include <stdbool.h> +#include <stdint.h> + +/* POSIX */ +#include <poll.h> +#include <pthread.h> + +/* LINUX */ +#include <linux/input-event-codes.h> + +#define MAX_EVENT_POLLS 32 +#define MAX_CLOSE_FDS 256 +#define MAX_GLOBAL_HOTKEYS 32 + +typedef enum { + KEYBOARD_MODKEY_LALT = 1 << 0, + KEYBOARD_MODKEY_RALT = 1 << 1, + KEYBOARD_MODKEY_LSUPER = 1 << 2, + KEYBOARD_MODKEY_RSUPER = 1 << 3, + KEYBOARD_MODKEY_LCTRL = 1 << 4, + KEYBOARD_MODKEY_RCTRL = 1 << 5, + KEYBOARD_MODKEY_LSHIFT = 1 << 6, + KEYBOARD_MODKEY_RSHIFT = 1 << 7 +} keyboard_modkeys; + +typedef enum { + KEYBOARD_BUTTON_RELEASED, + KEYBOARD_BUTTON_PRESSED +} keyboard_button_state; + +typedef struct { + int dev_input_id; + bool grabbed; + bool is_non_keyboard_device; + bool is_possibly_non_keyboard_device; + unsigned char *key_states; + unsigned char *key_presses_grabbed; + int num_keys_pressed; +} event_extra_data; + +typedef enum { + KEYBOARD_GRAB_TYPE_ALL, + KEYBOARD_GRAB_TYPE_VIRTUAL +} keyboard_grab_type; + +typedef struct { + uint32_t key; + uint32_t modifiers; /* keyboard_modkeys bitmask */ + char *action; +} global_hotkey; + +typedef struct { + struct pollfd event_polls[MAX_EVENT_POLLS]; /* Current size is |num_event_polls| */ + event_extra_data event_extra_data[MAX_EVENT_POLLS]; /* Current size is |num_event_polls| */ + int num_event_polls; + + int stdin_event_index; + int hotplug_event_index; + int uinput_fd; + bool stdin_failed; + keyboard_grab_type grab_type; + + pthread_t close_dev_input_fds_thread; + pthread_mutex_t close_dev_input_mutex; + int close_fds[MAX_CLOSE_FDS]; + int num_close_fds; + bool running; + + char stdin_command_data[512]; + int stdin_command_data_size; + + global_hotkey global_hotkeys[MAX_GLOBAL_HOTKEYS]; + int num_global_hotkeys; + + hotplug_event hotplug_ev; + + uint32_t modifier_button_states; +} keyboard_event; + +bool keyboard_event_init(keyboard_event *self, bool exclusive_grab, keyboard_grab_type grab_type); +void keyboard_event_deinit(keyboard_event *self); + +/* If |timeout_milliseconds| is -1 then wait until an event is received */ +void keyboard_event_poll_events(keyboard_event *self, int timeout_milliseconds); +bool keyboard_event_stdin_has_failed(const keyboard_event *self); + +#endif /* KEYBOARD_EVENT_H */ diff --git a/tools/gsr-global-hotkeys/keys.c b/tools/gsr-global-hotkeys/keys.c new file mode 100644 index 0000000..3b8fc8a --- /dev/null +++ b/tools/gsr-global-hotkeys/keys.c @@ -0,0 +1,21 @@ +#include "keys.h" +#include <linux/input-event-codes.h> + +bool is_keyboard_key(uint32_t keycode) { + return (keycode >= KEY_ESC && keycode <= KEY_KPDOT) + || (keycode >= KEY_ZENKAKUHANKAKU && keycode <= KEY_F24) + || (keycode >= KEY_PLAYCD && keycode <= KEY_MICMUTE) + || (keycode >= KEY_OK && keycode <= KEY_IMAGES) + || (keycode >= KEY_DEL_EOL && keycode <= KEY_DEL_LINE) + || (keycode >= KEY_FN && keycode <= KEY_FN_B) + || (keycode >= KEY_BRL_DOT1 && keycode <= KEY_BRL_DOT10) + || (keycode >= KEY_NUMERIC_0 && keycode <= KEY_LIGHTS_TOGGLE) + || (keycode == KEY_ALS_TOGGLE) + || (keycode >= KEY_BUTTONCONFIG && keycode <= KEY_VOICECOMMAND) + || (keycode >= KEY_BRIGHTNESS_MIN && keycode <= KEY_BRIGHTNESS_MAX) + || (keycode >= KEY_KBDINPUTASSIST_PREV && keycode <= KEY_ONSCREEN_KEYBOARD); +} + +bool is_mouse_button(uint32_t keycode) { + return (keycode >= BTN_MOUSE && keycode <= BTN_TASK); +} diff --git a/tools/gsr-global-hotkeys/keys.h b/tools/gsr-global-hotkeys/keys.h new file mode 100644 index 0000000..4f31882 --- /dev/null +++ b/tools/gsr-global-hotkeys/keys.h @@ -0,0 +1,10 @@ +#ifndef KEYS_H +#define KEYS_H + +#include <stdbool.h> +#include <stdint.h> + +bool is_keyboard_key(uint32_t keycode); +bool is_mouse_button(uint32_t keycode); + +#endif /* KEYS_H */ diff --git a/tools/gsr-global-hotkeys/main.c b/tools/gsr-global-hotkeys/main.c index 2823487..c7e0403 100644 --- a/tools/gsr-global-hotkeys/main.c +++ b/tools/gsr-global-hotkeys/main.c @@ -1,272 +1,90 @@ +#include "keyboard_event.h" + +/* C stdlib */ #include <stdio.h> -#include <unistd.h> -#include <fcntl.h> #include <string.h> -#include <errno.h> -#include <stdbool.h> -#include <poll.h> - -#include <libudev.h> -#include <libinput.h> -#include <libevdev/libevdev.h> -#include <xkbcommon/xkbcommon.h> - -typedef struct { - struct xkb_context *xkb_context; - struct xkb_keymap *xkb_keymap; - struct xkb_state *xkb_state; -} key_mapper; - -typedef enum { - MODKEY_ALT = 1 << 0, - MODKEY_SUPER = 1 << 1, - MODKEY_CTRL = 1 << 2, - MODKEY_SHIFT = 1 << 3 -} modkeys; - -typedef struct { - uint32_t key; - uint32_t modifiers; /* modkeys */ - const char *action; -} global_hotkey; - -#define NUM_GLOBAL_HOTKEYS 6 -static global_hotkey global_hotkeys[NUM_GLOBAL_HOTKEYS] = { - { .key = XKB_KEY_z, .modifiers = MODKEY_ALT, .action = "show_hide" }, - { .key = XKB_KEY_F9, .modifiers = MODKEY_ALT, .action = "record" }, - { .key = XKB_KEY_F7, .modifiers = MODKEY_ALT, .action = "pause" }, - { .key = XKB_KEY_F8, .modifiers = MODKEY_ALT, .action = "stream" }, - { .key = XKB_KEY_F10, .modifiers = MODKEY_ALT | MODKEY_SHIFT, .action = "replay_start" }, - { .key = XKB_KEY_F10, .modifiers = MODKEY_ALT, .action = "replay_save" } -}; - -static int open_restricted(const char *path, int flags, void *user_data) { - (void)user_data; - int fd = open(path, flags); - if(fd < 0) - fprintf(stderr, "error: failed to open %s, error: %s\n", path, strerror(errno)); - return fd < 0 ? -errno : fd; -} - -static void close_restricted(int fd, void *user_data) { - (void)user_data; - close(fd); -} +#include <locale.h> -static const struct libinput_interface interface = { - .open_restricted = open_restricted, - .close_restricted = close_restricted, -}; +/* POSIX */ +#include <unistd.h> -static bool is_mod_key(xkb_keycode_t xkb_key_code) { - return xkb_key_code >= XKB_KEY_Shift_L && xkb_key_code <= XKB_KEY_Hyper_R; +static void usage(void) { + fprintf(stderr, "usage: gsr-global-hotkeys [--all|--virtual]\n"); + fprintf(stderr, "OPTIONS:\n"); + fprintf(stderr, " --all Grab all devices.\n"); + fprintf(stderr, " --virtual Grab all virtual devices only.\n"); } -typedef struct { - const char *modname; - modkeys key; -} modname_to_modkey_map; - -static uint32_t xkb_state_to_modifiers(struct xkb_state *xkb_state) { - const modname_to_modkey_map modifier_keys[] = { - { .modname = XKB_MOD_NAME_ALT, .key = MODKEY_ALT }, - { .modname = XKB_MOD_NAME_LOGO, .key = MODKEY_SUPER }, - { .modname = XKB_MOD_NAME_SHIFT, .key = MODKEY_SHIFT }, - { .modname = XKB_MOD_NAME_CTRL, .key = MODKEY_CTRL } - }; - - uint32_t modifiers = 0; - for(int i = 0; i < 4; ++i) { - if(xkb_state_mod_name_is_active(xkb_state, modifier_keys[i].modname, XKB_STATE_MODS_EFFECTIVE) > 0) - modifiers |= modifier_keys[i].key; - } - return modifiers; -} - -#define KEY_CODE_EV_TO_XKB(key) ((key) + 8) - -static int print_key_event(struct libinput_event *event, key_mapper *mapper) { - struct libinput_event_keyboard *keyboard = libinput_event_get_keyboard_event(event); - const uint32_t key_code = libinput_event_keyboard_get_key(keyboard); - enum libinput_key_state state_code = libinput_event_keyboard_get_key_state(keyboard); - - const xkb_keycode_t xkb_key_code = KEY_CODE_EV_TO_XKB(key_code); - xkb_state_update_key(mapper->xkb_state, xkb_key_code, state_code == LIBINPUT_KEY_STATE_PRESSED ? XKB_KEY_DOWN : XKB_KEY_UP); - xkb_keysym_t xkb_key_sym = xkb_state_key_get_one_sym(mapper->xkb_state, xkb_key_code); - // char main_key[128]; - // main_key[0] = '\0'; - - // if(xkb_state_mod_name_is_active(mapper->xkb_state, XKB_MOD_NAME_LOGO, XKB_STATE_MODS_EFFECTIVE) > 0) - // strcat(main_key, "Super+"); - // if(xkb_state_mod_name_is_active(mapper->xkb_state, XKB_MOD_NAME_CTRL, XKB_STATE_MODS_EFFECTIVE) > 0) - // strcat(main_key, "Ctrl+"); - // if(xkb_state_mod_name_is_active(mapper->xkb_state, XKB_MOD_NAME_ALT, XKB_STATE_MODS_EFFECTIVE) > 0 && strcmp(main_key, "Meta") != 0) - // strcat(main_key, "Alt+"); - // if(xkb_state_mod_name_is_active(mapper->xkb_state, XKB_MOD_NAME_SHIFT, XKB_STATE_MODS_EFFECTIVE) > 0) - // strcat(main_key, "Shift+"); - - // if(!is_mod_key(xkb_key_sym)) { - // char reg_key[64]; - // reg_key[0] = '\0'; - // xkb_keysym_get_name(xkb_key_sym, reg_key, sizeof(reg_key)); - // strcat(main_key, reg_key); - // } - - if(state_code != LIBINPUT_KEY_STATE_PRESSED) - return 0; +static bool is_gsr_global_hotkeys_already_running(void) { + FILE *f = fopen("/proc/bus/input/devices", "rb"); + if(!f) + return false; - const uint32_t current_modifiers = xkb_state_to_modifiers(mapper->xkb_state); - for(int i = 0; i < NUM_GLOBAL_HOTKEYS; ++i) { - if(xkb_key_sym == global_hotkeys[i].key && current_modifiers == global_hotkeys[i].modifiers) { - puts(global_hotkeys[i].action); - fflush(stdout); + bool virtual_keyboard_running = false; + char line[1024]; + while(fgets(line, sizeof(line), f)) { + if(strstr(line, "gsr-ui virtual keyboard")) { + virtual_keyboard_running = true; break; } } - return 0; -} - -static int handle_events(struct libinput *libinput, key_mapper *mapper) { - int result = -1; - struct libinput_event *event; - - if(libinput_dispatch(libinput) < 0) - return result; - - while((event = libinput_get_event(libinput)) != NULL) { - if(libinput_event_get_type(event) == LIBINPUT_EVENT_KEYBOARD_KEY) - print_key_event(event, mapper); - - libinput_event_destroy(event); - result = 0; - } - - return result; + fclose(f); + return virtual_keyboard_running; } -static int run_mainloop(struct libinput *libinput, key_mapper *mapper) { - struct pollfd fds[2] = { - { - .fd = libinput_get_fd(libinput), - .events = POLLIN, - .revents = 0 - }, - { - .fd = STDOUT_FILENO, - .events = 0, - .revents = 0 +int main(int argc, char **argv) { + setlocale(LC_ALL, "C"); /* Sigh... stupid C */ + + keyboard_grab_type grab_type = KEYBOARD_GRAB_TYPE_ALL; + if(argc == 2) { + const char *grab_type_arg = argv[1]; + if(strcmp(grab_type_arg, "--all") == 0) { + grab_type = KEYBOARD_GRAB_TYPE_ALL; + } else if(strcmp(grab_type_arg, "--virtual") == 0) { + grab_type = KEYBOARD_GRAB_TYPE_VIRTUAL; + } else { + fprintf(stderr, "gsr-global-hotkeys error: expected --all or --virtual, got %s\n", grab_type_arg); + usage(); + return 1; } - }; - - if(handle_events(libinput, mapper) != 0) { - fprintf(stderr, "error: didn't receive device added events. Is this program not running as root?\n"); - return -1; - } - - while(poll(fds, 2, -1) >= 0) { - if(fds[0].revents & POLLIN) - handle_events(libinput, mapper); - if(fds[1].revents & (POLLHUP|POLLERR)) - break; - } - - return 0; -} - -static bool mapper_refresh_keymap(key_mapper *mapper) { - if(mapper->xkb_keymap != NULL) { - xkb_keymap_unref(mapper->xkb_keymap); - mapper->xkb_keymap = NULL; - } - - // TODO: - struct xkb_rule_names names = { - NULL, NULL, - NULL,//keymap_is_default(mapper->layout) ? NULL : mapper->layout, - NULL,//keymap_is_default(mapper->variant) ? NULL : mapper->variant, - NULL - }; - mapper->xkb_keymap = xkb_keymap_new_from_names(mapper->xkb_context, &names, XKB_KEYMAP_COMPILE_NO_FLAGS); - if(mapper->xkb_keymap == NULL) { - fprintf(stderr, "error: failed to create XKB keymap.\n"); - return false; - } - - if(mapper->xkb_state != NULL) { - xkb_state_unref(mapper->xkb_state); - mapper->xkb_state = NULL; + } else if(argc != 1) { + fprintf(stderr, "gsr-global-hotkeys error: expected 0 or 1 arguments, got %d argument(s)\n", argc); + usage(); + return 1; } - mapper->xkb_state = xkb_state_new(mapper->xkb_keymap); - if(mapper->xkb_state == NULL) { - fprintf(stderr, "error: failed to create XKB state.\n"); - return false; + if(is_gsr_global_hotkeys_already_running()) { + fprintf(stderr, "gsr-global-hotkeys error: gsr-global-hotkeys is already running\n"); + return 1; } - return true; -} - -int main(void) { - int result = 0; - struct udev *udev = NULL; - struct libinput *libinput = NULL; - const uid_t user_id = getuid(); if(geteuid() != 0) { if(setuid(0) == -1) { - fprintf(stderr, "error: failed to change user to root\n"); + fprintf(stderr, "gsr-global-hotkeys error: failed to change user to root, global hotkeys will not work. Make sure to set the correct capability on gsr-global-hotkeys\n"); return 1; } } - udev = udev_new(); - if(!udev) { - fprintf(stderr, "error: udev_new failed\n"); - result = 1; - goto done; - } - - libinput = libinput_udev_create_context(&interface, NULL, udev); - if(!libinput) { - fprintf(stderr, "error: libinput_udev_create_context failed\n"); - result = 1; - goto done; - } - - if(libinput_udev_assign_seat(libinput, "seat0") != 0) { - fprintf(stderr, "error: libinput_udev_assign_seat with seat0 failed\n"); - result = 1; - goto done; + keyboard_event keyboard_ev; + if(!keyboard_event_init(&keyboard_ev, true, grab_type)) { + fprintf(stderr, "gsr-global-hotkeys error: failed to setup hotplugging and no keyboard input devices were found\n"); + setuid(user_id); + return 1; } - key_mapper mapper; - mapper.xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); - if(!mapper.xkb_context) { - fprintf(stderr, "error: xkb_context_new failed\n"); - result = 1; - goto done; - } + fprintf(stderr, "gsr-global-hotkeys info: global hotkeys setup, waiting for hotkeys to be pressed\n"); - if(!mapper_refresh_keymap(&mapper)) { - fprintf(stderr, "error: key mapper failed\n"); - result = 1; - goto done; - } - - if(run_mainloop(libinput, &mapper) < 0) { - fprintf(stderr, "error: failed to start main loop\n"); - result = 1; - goto done; + for(;;) { + keyboard_event_poll_events(&keyboard_ev, -1); + if(keyboard_event_stdin_has_failed(&keyboard_ev)) { + fprintf(stderr, "gsr-global-hotkeys info: stdin closed (parent process likely closed this process), exiting...\n"); + break; + } } - done: - if(libinput) - libinput_unref(libinput); - - if(udev) - udev_unref(udev); - + keyboard_event_deinit(&keyboard_ev); setuid(user_id); - return result; + return 0; } diff --git a/tools/gsr-ui-cli/main.c b/tools/gsr-ui-cli/main.c new file mode 100644 index 0000000..feb5247 --- /dev/null +++ b/tools/gsr-ui-cli/main.c @@ -0,0 +1,123 @@ +#include <limits.h> +#include <stdio.h> +#include <stddef.h> +#include <stdlib.h> +#include <stdbool.h> +#include <string.h> +#include <unistd.h> +#include <fcntl.h> + +static void get_runtime_filepath(char *buffer, size_t buffer_size, const char *filename) { + char dir[PATH_MAX]; + + const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); + if(runtime_dir) + snprintf(dir, sizeof(dir), "%s", runtime_dir); + else + snprintf(dir, sizeof(dir), "/run/user/%d", geteuid()); + + if(access(dir, F_OK) != 0) + snprintf(dir, sizeof(dir), "/tmp"); + + snprintf(buffer, buffer_size, "%s/%s", dir, filename); +} + +/* Assumes |str| size is less than 256 */ +static void fifo_write_all(int file_fd, const char *str) { + char command[256]; + const ssize_t command_size = snprintf(command, sizeof(command), "%s\n", str); + if(command_size >= (ssize_t)sizeof(command)) { + fprintf(stderr, "Error: command too long: %s\n", str); + return; + } + + ssize_t offset = 0; + while(offset < (ssize_t)command_size) { + const ssize_t bytes_written = write(file_fd, str + offset, command_size - offset); + if(bytes_written > 0) + offset += bytes_written; + } +} + +static void usage(void) { + printf("usage: gsr-ui-cli <command>\n"); + printf("Run commands on the running gsr-ui instance.\n"); + printf("\n"); + printf("COMMANDS:\n"); + printf(" toggle-show\n"); + printf(" Show/hide the UI.\n"); + printf(" toggle-record\n"); + printf(" Start/stop recording.\n"); + printf(" toggle-pause\n"); + printf(" Pause/unpause recording. Only applies to regular recording.\n"); + printf(" toggle-stream\n"); + printf(" Start/stop streaming.\n"); + printf(" toggle-replay\n"); + printf(" Start/stop replay.\n"); + printf(" replay-save\n"); + printf(" Save replay.\n"); + printf(" replay-save-1-min\n"); + printf(" Save 1 minute replay.\n"); + printf(" replay-save-10-min\n"); + printf(" Save 10 minute replay.\n"); + printf(" take-screenshot\n"); + printf(" Take a screenshot.\n"); + printf(" take-screenshot-region\n"); + printf(" Take a screenshot of a region.\n"); + printf("\n"); + printf("EXAMPLES:\n"); + printf(" gsr-ui-cli toggle-show\n"); + printf(" gsr-ui-cli toggle-record\n"); + exit(1); +} + +static bool is_valid_command(const char *command) { + const char *commands[] = { + "toggle-show", + "toggle-record", + "toggle-pause", + "toggle-stream", + "toggle-replay", + "replay-save", + "replay-save-1-min", + "replay-save-10-min", + "take-screenshot", + "take-screenshot-region", + NULL + }; + + for(int i = 0; commands[i]; ++i) { + if(strcmp(command, commands[i]) == 0) + return true; + } + + return false; +} + +int main(int argc, char **argv) { + if(argc != 2) { + printf("Error: expected 1 argument, %d provided\n", argc - 1); + usage(); + } + + const char *command = argv[1]; + if(strcmp(command, "-h") == 0 || strcmp(command, "--help") == 0) + usage(); + + if(!is_valid_command(command)) { + fprintf(stderr, "Error: invalid command: \"%s\"\n", command); + usage(); + } + + char fifo_filepath[PATH_MAX]; + get_runtime_filepath(fifo_filepath, sizeof(fifo_filepath), "gsr-ui"); + const int fifo_fd = open(fifo_filepath, O_RDWR | O_NONBLOCK); + if(fifo_fd <= 0) { + fprintf(stderr, "Error: failed to open fifo file %s. Maybe gsr-ui is not running?\n", fifo_filepath); + exit(2); + } + + fifo_write_all(fifo_fd, command); + close(fifo_fd); + return 0; +} diff --git a/tools/gsr-window-name/main.c b/tools/gsr-window-name/main.c deleted file mode 100644 index 8ebf1e0..0000000 --- a/tools/gsr-window-name/main.c +++ /dev/null @@ -1,187 +0,0 @@ -#include <X11/Xlib.h> -#include <X11/Xatom.h> -#include <X11/Xutil.h> - -#include <stdbool.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> - -typedef enum { - CAPTURE_TYPE_FOCUSED, - CAPTURE_TYPE_CURSOR -} capture_type; - -static bool window_has_atom(Display *dpy, Window window, Atom atom) { - Atom type; - unsigned long len, bytes_left; - int format; - unsigned char *properties = NULL; - if(XGetWindowProperty(dpy, window, atom, 0, 1024, False, AnyPropertyType, &type, &format, &len, &bytes_left, &properties) < Success) - return false; - - if(properties) - XFree(properties); - - return type != None; -} - -static bool window_is_user_program(Display *dpy, Window window) { - const Atom net_wm_state_atom = XInternAtom(dpy, "_NET_WM_STATE", False); - const Atom wm_state_atom = XInternAtom(dpy, "WM_STATE", False); - return window_has_atom(dpy, window, net_wm_state_atom) || window_has_atom(dpy, window, wm_state_atom); -} - -static Window get_window_at_cursor_position(Display *dpy) { - Window root_window = None; - Window window = None; - int dummy_i; - unsigned int dummy_u; - int cursor_pos_x = 0; - int cursor_pos_y = 0; - XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &dummy_i, &dummy_i, &cursor_pos_x, &cursor_pos_y, &dummy_u); - return window; -} - -static Window get_focused_window(Display *dpy, capture_type cap_type) { - const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False); - Window focused_window = None; - - if(cap_type == CAPTURE_TYPE_FOCUSED) { - // Atom type = None; - // int format = 0; - // unsigned long num_items = 0; - // unsigned long bytes_left = 0; - // unsigned char *data = NULL; - // XGetWindowProperty(dpy, DefaultRootWindow(dpy), net_active_window_atom, 0, 1, False, XA_WINDOW, &type, &format, &num_items, &bytes_left, &data); - - // fprintf(stderr, "focused window: %p\n", (void*)data); - - // if(type == XA_WINDOW && num_items == 1 && data) - // return *(Window*)data; - - int revert_to = 0; - XGetInputFocus(dpy, &focused_window, &revert_to); - if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window)) - return focused_window; - } - - focused_window = get_window_at_cursor_position(dpy); - if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window)) - return focused_window; - - return None; -} - -static char* get_window_title(Display *dpy, Window window) { - const Atom net_wm_name_atom = XInternAtom(dpy, "_NET_WM_NAME", False); - const Atom wm_name_atom = XInternAtom(dpy, "_NET_WM_NAME", False); - const Atom utf8_string_atom = XInternAtom(dpy, "UTF8_STRING", False); - - Atom type = None; - int format = 0; - unsigned long num_items = 0; - unsigned long bytes_left = 0; - unsigned char *data = NULL; - XGetWindowProperty(dpy, window, net_wm_name_atom, 0, 1024, False, utf8_string_atom, &type, &format, &num_items, &bytes_left, &data); - - if(type == utf8_string_atom && format == 8 && data) - return (char*)data; - - type = None; - format = 0; - num_items = 0; - bytes_left = 0; - data = NULL; - XGetWindowProperty(dpy, window, wm_name_atom, 0, 1024, False, 0, &type, &format, &num_items, &bytes_left, &data); - - if((type == XA_STRING || type == utf8_string_atom) && data) - return (char*)data; - - return NULL; -} - -static const char* strip(const char *str, int *len) { - int str_len = strlen(str); - for(int i = 0; i < str_len; ++i) { - if(str[i] != ' ') { - str += i; - str_len -= i; - break; - } - } - - for(int i = str_len - 1; i >= 0; --i) { - if(str[i] != ' ') { - str_len = i + 1; - break; - } - } - - *len = str_len; - return str; -} - -static void print_str_strip(const char *str) { - int len = 0; - str = strip(str, &len); - printf("%.*s", len, str); -} - -static int x11_ignore_error(Display *dpy, XErrorEvent *error_event) { - (void)dpy; - (void)error_event; - return 0; -} - -static void usage(void) { - fprintf(stderr, "usage: gsr-window-name <focused|cursor>\n"); - fprintf(stderr, "options:\n"); - fprintf(stderr, " focused The class/name of the focused window is returned. If no window is focused then the window beneath the cursor is returned instead\n"); - fprintf(stderr, " cursor The class/name of the window beneath the cursor is returned\n"); - exit(1); -} - -int main(int argc, char **argv) { - if(argc != 2) - usage(); - - const char *cap_type_str = argv[1]; - capture_type cap_type = CAPTURE_TYPE_FOCUSED; - if(strcmp(cap_type_str, "focused") == 0) { - cap_type = CAPTURE_TYPE_FOCUSED; - } else if(strcmp(cap_type_str, "cursor") == 0) { - cap_type = CAPTURE_TYPE_CURSOR; - } else { - fprintf(stderr, "error: invalid option '%s', expected either 'focused' or 'cursor'\n", cap_type_str); - usage(); - } - - Display *dpy = XOpenDisplay(NULL); - if(!dpy) { - fprintf(stderr, "Error: failed to connect to the X11 server\n"); - exit(1); - } - - XSetErrorHandler(x11_ignore_error); - - const Window focused_window = get_focused_window(dpy, cap_type); - if(focused_window == None) - exit(2); - - // Window title is not always ideal (for example for a browser), but for games its pretty much required - char *window_title = get_window_title(dpy, focused_window); - if(window_title) { - print_str_strip(window_title); - exit(0); - } - - XClassHint class_hint = {0}; - XGetClassHint(dpy, focused_window, &class_hint); - if(class_hint.res_class) { - print_str_strip(class_hint.res_class); - exit(0); - } - - return 2; -} |