Compare commits

...

91 Commits
1.0.2 ... 1.2.2

Author SHA1 Message Date
dec05eba
3c8dd9c4db 1.2.2 2025-03-07 20:22:13 +01:00
dec05eba
c7fcf251e3 Fix shortcut keys changed 2025-03-07 20:21:25 +01:00
dec05eba
6d58b2495d Update screenshot image 2025-03-07 20:14:40 +01:00
dec05eba
347eced060 Change notification timeout 2025-03-07 17:38:50 +01:00
dec05eba
6449133c57 Update mglpp 2025-03-05 22:10:30 +01:00
dec05eba
1168e68278 1.2.1 2025-02-27 15:57:25 +01:00
dec05eba
4836c661ce 1.2.0 2025-02-27 15:56:35 +01:00
dec05eba
f0bbbbe4a9 Replay on startup: wait until audio devices are available before turning replay on 2025-02-25 17:37:25 +01:00
dec05eba
d9a1e5c2eb Add option to press backspace to remove hotkey 2025-02-25 01:21:23 +01:00
dec05eba
b6c59e1049 Update README 2025-02-22 23:01:06 +01:00
dec05eba
af6984cd7e 1.2.0 2025-02-22 14:39:40 +01:00
dec05eba
51a47193d7 Show correct process on screenshot failure 2025-02-22 13:39:06 +01:00
dec05eba
189736c1a9 Add option to take a screenshot (default hotkey: alt+f1) 2025-02-22 13:31:51 +01:00
dec05eba
8003c209fe m 2025-02-11 22:40:11 +01:00
dec05eba
1734d48af6 window get title: cleanup data 2025-02-10 19:41:56 +01:00
dec05eba
fc2f6f4c50 Better detect focused x11 window on wayland, properly get focused game name on wayland 2025-02-10 19:31:27 +01:00
dec05eba
f4e44cbef5 Prepare for sound. Fix game name being gsr-ui on wayland in some cases when saving video when the ui is open 2025-02-10 18:22:21 +01:00
dec05eba
3d6354c642 m 2025-02-08 03:18:16 +01:00
dec05eba
efb5fc53c1 Show notification when saving a large replay that is taking some time 2025-02-07 19:41:39 +01:00
dec05eba
51367ac078 Change replay duration max limit to 3 hours 2025-02-06 02:17:56 +01:00
dec05eba
b0ab2099fd 1.1.7 2025-02-05 22:36:19 +01:00
dec05eba
4a0612ae8f Update flatpak version 2025-02-05 22:16:22 +01:00
dec05eba
c650974a11 Launch gsr-global-hotkeys in flatpak through kms-server-proxy 2025-02-05 22:12:10 +01:00
dec05eba
6fe9f1a8d5 Fix global hotkeys when using virtual mapper that pretends to be a joystick as well (kanata) 2025-02-05 21:03:42 +01:00
dec05eba
8c148aceda Limit combobox item width, use multiple rows 2025-02-05 20:41:11 +01:00
dec05eba
b4c85910ce 1.1.6 2025-02-03 20:43:08 +01:00
dec05eba
fd63ac3626 Fix for steamdeck 2025-02-03 20:27:35 +01:00
dec05eba
2a0782eb02 Attempt to fix global hotkeys not working on steam deck (grabs keys, cant press buttons) 2025-02-03 19:58:42 +01:00
dec05eba
f505323d56 1.1.5 2025-02-03 01:25:32 +01:00
dec05eba
309cc3425b Use bundled cursor if cursor fails to load 2025-01-27 17:35:35 +01:00
dec05eba
81cb8f539f banana 2025-01-27 16:47:55 +01:00
dec05eba
5214fb1d7f Try fixing missing cursor texture on some broken systems 2025-01-27 16:46:54 +01:00
dec05eba
9aebe81ec4 amend 2025-01-27 11:56:37 +01:00
dec05eba
d73bd68a70 Default to default cursor if cursor not found 2025-01-27 11:53:49 +01:00
dec05eba
3cb156aecb Delegate keyboard grab until a button has been pressed if the device says its a mouse 2025-01-26 17:43:38 +01:00
dec05eba
dea4393588 Revert global hotkeys change, ignore mice again 2025-01-26 14:12:03 +01:00
dec05eba
269d55d7eb 1.1.3 2025-01-26 10:23:45 +01:00
dec05eba
c04e6a87e6 Fix hotkeys not working on some keyboards 2025-01-26 10:23:17 +01:00
dec05eba
d8acac6ba9 Minor visual change 2025-01-25 20:00:51 +01:00
dec05eba
010d4dd5aa Update images 2025-01-25 01:49:16 +01:00
dec05eba
e1397c1c97 Nicer hotkey input design 2025-01-25 00:23:24 +01:00
dec05eba
aed169aa40 Fix incorrect action text in hotkey configuration 2025-01-24 11:18:27 +01:00
dec05eba
c396a1f922 -overlay-replay > -restart-replay-on-save 2025-01-24 10:09:32 +01:00
dec05eba
f036fcbc0f Add 'restart replay on save' option 2025-01-24 00:42:33 +01:00
dec05eba
5b84d7421f Fix controller double click not working sometimes 2025-01-23 21:49:07 +01:00
dec05eba
1d9d4d6398 Make hotkeys reconfigurable, faster hotkey startup time, fix some keyboard locale issues 2025-01-23 21:25:45 +01:00
dec05eba
47ada4d798 Add option to save replay with controller (double-click share button), allow prime-run on wayland 2025-01-20 23:11:00 +01:00
dec05eba
92401d8bc8 Wayland: only show ui after stupid fullscreen animation has stopped 2025-01-19 12:35:08 +01:00
dec05eba
0d6d7a7459 Mention minimum gpu-screen-recorder version 2025-01-19 11:46:19 +01:00
dec05eba
c05e10fe48 README flatpak issue 2025-01-18 22:18:52 +01:00
dec05eba
9f40aeddc5 Flatpak: fix xopendisplay not working for global hotkey 2025-01-18 17:50:16 +01:00
dec05eba
62d5daad90 1.0.8 2025-01-17 15:51:03 +01:00
dec05eba
5be5b4c8eb X11: only grab mouse devices with xi 2025-01-17 15:50:24 +01:00
dec05eba
e938241fe8 1.0.7 2025-01-17 14:00:50 +01:00
dec05eba
a9637f87e7 Fix background not being transparent on sway when a wayland application is focused and opening the ui 2025-01-17 13:18:40 +01:00
dec05eba
eb4ce76f01 Install flatpak version of the systemd service if missing or remove it if not using flatpak version 2025-01-16 19:34:47 +01:00
dec05eba
d4d61b8c93 Mention flatpak and non-flatpak conflict 2025-01-15 18:32:49 +01:00
dec05eba
35a2fcc615 Fix window/game name for games such as 'the finals' that have weird names with utf8-bom and zero width space characters 2025-01-14 22:34:25 +01:00
dec05eba
bb54b67956 sys/poll.h -> poll.h 2025-01-10 00:14:18 +01:00
dec05eba
35578e79ec M 2025-01-09 20:26:54 +01:00
dec05eba
ce6c924f58 Use glx on wayland to workaround buggy nvidia driver 2025-01-09 19:23:48 +01:00
dec05eba
77dd32a3ff Revert "Dont use dlopen in gsr-global-hotkeys"
This reverts commit b0def958c7.
2025-01-09 13:08:12 +01:00
dec05eba
b0def958c7 Dont use dlopen in gsr-global-hotkeys 2025-01-09 12:42:10 +01:00
dec05eba
698538ac84 Unset DRI_PRIME as well 2025-01-08 17:17:48 +01:00
dec05eba
affa44e387 1.0.6 2025-01-07 16:55:26 +01:00
dec05eba
df2eec24a3 Check event window 2025-01-07 16:34:29 +01:00
dec05eba
9a5c20836a Try and support different keyboard layouts through x11 xkb mapping 2025-01-07 01:15:56 +01:00
dec05eba
ee123ceb0a Fix ui being on incorrect monitor on gnome and sway 2025-01-06 22:59:47 +01:00
dec05eba
f2544020b3 Hide decorations on gnome wayland when opening ui when non-x11 application is focused 2025-01-06 15:04:27 +01:00
dec05eba
0c6e2aff07 1.0.5 - fix ui not visible on some kde plasma wayland systems 2025-01-05 18:09:07 +01:00
dec05eba
26ff639f25 Comment 2025-01-05 18:04:36 +01:00
dec05eba
a812d7dbbb Hide window from taskbar 2025-01-05 17:55:40 +01:00
dec05eba
57ae00063c Test dialog 2025-01-05 17:48:16 +01:00
dec05eba
fa5b7a0c75 Only grab left alt, to allow altgr+z to be used for keyboard that type ż with it 2025-01-05 03:22:38 +01:00
dec05eba
52ce22ae22 Add option to only grab virtual devices, to support input remapping software 2025-01-04 05:39:16 +01:00
dec05eba
f379b87b33 Flatpak: disable hotkey section 2025-01-04 03:35:46 +01:00
dec05eba
2ddb9980e1 1.0.4 2025-01-04 03:33:44 +01:00
dec05eba
6b023051eb Fallback to focused window for x11 uncomposited 2025-01-04 03:06:49 +01:00
dec05eba
2aaf6b8380 Fix some games receiving mouse input on wayland 2025-01-04 02:50:48 +01:00
dec05eba
f4dc077299 pidof ignore self 2025-01-04 02:30:14 +01:00
dec05eba
36c7bbfda3 Simplify gsr-ui-cli 2025-01-04 01:44:25 +01:00
dec05eba
9998db8afa Check if gsr-ui is running with pidof equivalent, show ui on restart after enabling/disabling hotkeys 2025-01-03 23:34:53 +01:00
dec05eba
6c03137610 Add option to disable hotkeys, add gsr-ui-cli tool to control gsr-ui remotely 2025-01-03 22:37:13 +01:00
dec05eba
5439fa8a71 README global hotkey info 2025-01-03 21:28:23 +01:00
dec05eba
60e8da0aa9 Remove merge audio tracks option for streaming. Streaming sites dont support multiple audio tracks (in general) 2025-01-03 17:20:16 +01:00
dec05eba
170b2493fc Show estimated file size for cbr for recording and mb/sec for streaming 2025-01-03 04:33:12 +01:00
dec05eba
f7fbb06a92 README email 2025-01-03 02:31:21 +01:00
dec05eba
88b8c80c2b A little bit of glitch fixing, warp cursor after hiding ui when XI grabbing 2025-01-03 02:27:20 +01:00
dec05eba
2a07c74112 Fix some application (using xi) receiving mouse input when UI is shown 2025-01-03 01:57:30 +01:00
dec05eba
f526c175f6 1.0.3 2025-01-01 00:32:23 +01:00
dec05eba
4e25fd486b Fix regression: incorrect window resulting in incorrect game name and background for uncomposited x11 2025-01-01 00:32:05 +01:00
58 changed files with 4295 additions and 956 deletions

View File

@@ -2,15 +2,13 @@
# 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`.
Run `gsr-ui` and press `Left 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`.
There is also an option in the settings to enable/disable starting the program on system startup. This option only works on systems that use systemd.
You have to manually add `gsr-ui` to system startup on systems that uses another init system.\
Note that at the moment different keyboard layouts are not supported. The physical layout of keys are used for global hotkeys. If your Z and Y keys are swapped for example then you need to press Alt+Y instead of Alt+Z to open/hide the UI.
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`).\
@@ -24,31 +22,38 @@ GPU Screen Recorder UI uses meson build system so you need to install `meson` to
These are the dependencies needed to build GPU Screen Recorder UI:
* x11 (libx11, libxrandr, libxrender, libxcomposite, libxfixes, libxi)
* libxcursor
* libglvnd (which provides libgl, libglx and libegl)
* linux-api-headers
* libpulse (libpulse-simple)
## 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 input remapping software. To workaround this you can go into settings and select "Only grab virtual devices"
# 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`.
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 `Creative Commons Attribution-Share Alike 3.0`.
# Reporting bugs, contributing patches, questions or donation
See [https://git.dec05eba.com/?p=about](https://git.dec05eba.com/?p=about).
# Demo
[![Click here to watch a demo video on youtube](https://img.youtube.com/vi/SOqXusCTXXA/0.jpg)](https://www.youtube.com/watch?v=SOqXusCTXXA)
# Screenshots
![](https://dec05eba.com/images/gsr-overlay-screenshot-front.webp)
![](https://dec05eba.com/images/gsr-overlay-screenshot-settings.webp)
# Donations
If you want to donate you can donate via bitcoin or monero.
* Bitcoin: bc1qqvuqnwrdyppf707ge27fqz2n9y9gu7lf5ypyuf
* Monero: 4An9kp2qW1C9Gah7ewv4JzcNFQ5TAX7ineGCqXWK6vQnhsGGcRpNgcn8r9EC3tMcgY7vqCKs3nSRXhejMHBaGvFdN2egYet
![](https://dec05eba.com/images/front_page.jpg)
![](https://dec05eba.com/images/settings_page.jpg)
# Known issues
* Some games receive mouse input while the UI is open
* 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.
* Different keyboard layouts are not supported at the moment. The physical layout of keys are used for global hotkeys. If your Z and Y keys are swapped for example then you need to press Alt+Y instead of Alt+Z to open/hide the UI.
* 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.
* Opening the UI when a game is fullscreened can mess up the game window a bit on Hyprland. I believe this is an issue with Hyprland.
# 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 non-flatpak version of the systemd service will conflict with that. Run `gsr-ui` to fix that.

80
TODO
View File

@@ -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)
@@ -24,12 +16,8 @@ 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,7 +35,7 @@ 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.
@@ -70,46 +58,33 @@ On nvidia check if suspend fix is applied. If not, show a popup asking the user
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.
Test global hotkeys with azerty instead of qwerty.
Fix cursor grab not working in owlboy, need to use xigrab.
Dont allow autostart of replay if capture option is window recording (when window recording is added).
Use global shortcuts desktop portal protocol on wayland when available.
When support for window capture is enabled on x11 then make sure to not save the window except temporary while the program is open.
Support CJK.
Move ui hover code from ::draw to ::on_event, to properly handle widget event stack.
Save audio devices by name instead of id. This is more robust since audio id can change(?).
Improve linux global hotkeys startup time by parsing /proc/bus/input/devices instead of ioctl.
Improve linux global hotkeys startup time by parsing /proc/bus/input/devices instead of ioctl. <- Do this!
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>/.
@@ -117,10 +92,49 @@ All steam game names by ID are available at https://api.steampowered.com/ISteamA
Dont put widget position to int position when scrolling. This makes the UI jitter when it's coming to a halt.
Show a popup asking if the user wants to add the program to system startup when launching the program, with a dismiss option and "Do not show again".
Show warning if another instance of gpu screen recorder is already running when starting recording?
Change gsr-global-hotkeys to outputs keys pressed instead of the command. This can be done pretty safely by only output keys when modifiers (such as alt) is pressed.
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.
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.
When adding window capture only add it to recording and streaming and do the window selection when recording starts, to make it more ergonomic with hotkeys.
If hotkey for recording/streaming start is pressed on the button for start is clicked then hide the ui if it's visible and show the window selection option (cursor).
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.
Is it possible to configure hotkey and the new hotkey to get triggered immediately?
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.
Instead of installing gsr-global-hotkeys in flatpak use kms-server-proxy to launch gsr-global-hotkeys inside the flatpak with root, just like gsr-kms-server. This removes the need to update gsr-global-hotkeys everytime there is an update.
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).
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.
Add support for window capture. This should not prompt for window selection directly but instead prompt for window selection when recording starts and hide the ui first.
For screenshots window capture should exist but "follow focused" option should not exist.
Improve audio design. It should have a button to add/remove audio tracks and button to add audio into each audio track separately and "record audio from all applications except the selected ones" for each audio track. Then also remove the "merge audio tracks" option.
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.
Allow using a hotkey such as printscreen or any other non-alphanumeric key without a modifier. Allow that in gsr-ui and gsr-global-hotkeys. Update the ui to match that.

BIN
images/default.cur Normal file

Binary file not shown.

BIN
images/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

22
include/AudioPlayer.hpp Normal file
View File

@@ -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;
};
}

View File

@@ -6,15 +6,28 @@
#include <vector>
#include <optional>
#define GSR_CONFIG_FILE_VERSION 1
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 RecordOptions {
@@ -25,7 +38,7 @@ namespace gsr {
int32_t video_height = 0;
int32_t fps = 60;
int32_t video_bitrate = 15000;
bool merge_audio_tracks = true;
bool merge_audio_tracks = true; // Currently unused for streaming because all known streaming sites only support 1 audio track
bool application_audio_invert = false;
bool change_video_resolution = false;
std::vector<std::string> audio_tracks;
@@ -41,9 +54,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 {
@@ -67,7 +83,7 @@ namespace gsr {
YoutubeStreamConfig youtube;
TwitchStreamConfig twitch;
CustomStreamConfig custom;
ConfigHotkey start_stop_recording_hotkey;
ConfigHotkey start_stop_hotkey;
};
struct RecordConfig {
@@ -77,22 +93,39 @@ namespace gsr {
bool show_video_saved_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;
ConfigHotkey start_stop_hotkey;
ConfigHotkey save_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;
};
struct Config {
@@ -100,10 +133,13 @@ namespace gsr {
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);

View File

@@ -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)>;

View File

@@ -0,0 +1,54 @@
#pragma once
#include "GlobalHotkeys.hpp"
#include "Hotplug.hpp"
#include <unordered_map>
#include <optional>
#include <thread>
#include <poll.h>
#include <mglpp/system/Clock.hpp>
#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();
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;
mgl::Clock double_click_clock;
std::optional<double> prev_time_clicked;
bool save_replay = false;
int hotplug_poll_index = -1;
Hotplug hotplug;
};
}

View File

@@ -7,18 +7,26 @@
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:
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;
};
}

View File

@@ -2,6 +2,7 @@
#include <string>
#include <vector>
#include <stdint.h>
#include <mglpp/system/vec.hpp>
@@ -19,11 +20,31 @@ 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 focused = false;
@@ -40,6 +61,7 @@ namespace gsr {
struct SystemInfo {
DisplayServer display_server = DisplayServer::UNKNOWN;
bool supports_app_audio = false;
GsrVersion gsr_version;
};
enum class GpuVendor {
@@ -58,6 +80,7 @@ namespace gsr {
SystemInfo system_info;
GpuInfo gpu_info;
SupportedVideoCodecs supported_video_codecs;
SupportedImageFormats supported_image_formats;
};
enum class GsrInfoExitStatus {

33
include/Hotplug.hpp Normal file
View File

@@ -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];
};
}

View File

@@ -5,6 +5,10 @@
#include "GsrInfo.hpp"
#include "Config.hpp"
#include "window_texture.h"
#include "WindowUtils.hpp"
#include "GlobalHotkeysLinux.hpp"
#include "GlobalHotkeysJoystick.hpp"
#include "AudioPlayer.hpp"
#include <mglpp/window/Window.hpp>
#include <mglpp/window/Event.hpp>
@@ -31,7 +35,8 @@ namespace gsr {
NONE,
RECORD,
REPLAY,
STREAM
STREAM,
SCREENSHOT
};
class Overlay {
@@ -41,8 +46,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 +58,41 @@ namespace gsr {
void toggle_stream();
void toggle_replay();
void save_replay();
void take_screenshot();
void show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type);
bool is_open() const;
bool should_exit(std::string &reason) const;
void exit();
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 xi_grab_all_mouse_devices();
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 on_replay_saved(const char *replay_saved_filepath);
void update_gsr_replay_save();
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);
@@ -93,10 +109,11 @@ namespace gsr {
void update_ui_replay_stopped();
void on_press_save_replay();
void on_press_start_replay(bool disable_notification);
bool 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_take_screenshot();
bool update_compositor_texture(const Monitor &monitor);
void force_window_on_top();
private:
@@ -124,7 +141,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;
@@ -139,6 +155,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;
@@ -155,6 +172,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;
@@ -166,5 +184,23 @@ namespace gsr {
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;
AudioPlayer audio_player;
};
}

View File

@@ -12,14 +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);
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);
pid_t pidof(const char *process_name);
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);
}

View File

@@ -41,6 +41,7 @@ namespace gsr {
mgl::Texture stop_texture;
mgl::Texture pause_texture;
mgl::Texture save_texture;
mgl::Texture screenshot_texture;
double double_click_timeout_seconds = 0.4;

View File

@@ -24,6 +24,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);

View File

@@ -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,19 @@ namespace gsr {
CURSOR
};
struct Monitor {
mgl::vec2i position;
mgl::vec2i size;
};
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);
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);
}

View File

@@ -30,6 +30,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;

View File

@@ -20,6 +20,7 @@ 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_description(std::string description_text);
void set_activated(bool activated);

View File

@@ -5,18 +5,33 @@
#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,
RECORD_START_STOP,
RECORD_PAUSE_UNPAUSE,
STREAM_START_STOP,
TAKE_SCREENSHOT,
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;
@@ -24,18 +39,41 @@ namespace gsr {
void save();
void on_navigate_away_from_page() override;
// Called with (enable, exit_status)
std::function<void(bool, int)> on_startup_changed;
// Called with (reason)
std::function<void(const char*)> on_click_exit_program_button;
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_record_hotkey_options();
std::unique_ptr<List> create_stream_hotkey_options();
std::unique_ptr<List> create_screenshot_hotkey_options();
std::unique_ptr<List> create_hotkey_control_buttons();
std::unique_ptr<Subsection> create_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;
@@ -43,5 +81,21 @@ namespace gsr {
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 *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 *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;
};
}

View File

@@ -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;
};
}

View File

@@ -0,0 +1,78 @@
#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<List> create_select_window();
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 *select_window_list_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;
};
}

View File

@@ -52,7 +52,7 @@ 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<Widget> create_capture_target_section();
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();
@@ -69,7 +69,7 @@ namespace gsr {
std::unique_ptr<Widget> create_audio_track_section();
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();
@@ -93,13 +93,17 @@ 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<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();
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();
@@ -168,6 +172,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;
@@ -182,6 +187,7 @@ namespace gsr {
Entry *youtube_stream_key_entry_ptr = nullptr;
Entry *stream_url_entry_ptr = nullptr;
Entry *replay_time_entry_ptr = nullptr;
Label *replay_time_label_ptr = nullptr;
RadioButton *turn_on_replay_automatically_mode_ptr = nullptr;
PageStack *page_stack = nullptr;

View File

@@ -1,4 +1,4 @@
project('gsr-ui', ['c', 'cpp'], version : '1.0.2', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
project('gsr-ui', ['c', 'cpp'], version : '1.2.2', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
if get_option('buildtype') == 'debug'
add_project_arguments('-g3', language : ['c', 'cpp'])
@@ -27,6 +27,7 @@ src = [
'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',
@@ -38,6 +39,9 @@ src = [
'src/Overlay.cpp',
'src/GlobalHotkeysX11.cpp',
'src/GlobalHotkeysLinux.cpp',
'src/GlobalHotkeysJoystick.cpp',
'src/AudioPlayer.cpp',
'src/Hotplug.cpp',
'src/Rpc.cpp',
'src/main.cpp',
]
@@ -49,15 +53,21 @@ 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.1.7"', language: ['c', 'cpp'])
executable(
meson.project_name(),
src,
install : true,
dependencies : [
mglpp_dep,
dependency('threads'),
dependency('xcomposite'),
dependency('xfixes'),
dependency('xi'),
dependency('xcursor'),
dependency('libpulse-simple'),
],
cpp_args : '-DGSR_UI_RESOURCES_PATH="' + gsr_ui_resources_path + '"',
)
@@ -67,12 +77,21 @@ executable(
[
'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-ui-cli',
[
'tools/gsr-ui-cli/main.c'
],
install : true
)
install_subdir('images', install_dir : gsr_ui_resources_path)
install_subdir('fonts', install_dir : gsr_ui_resources_path)

View File

@@ -1,7 +1,7 @@
[package]
name = "gsr-ui"
type = "executable"
version = "1.0.2"
version = "1.2.2"
platforms = ["posix"]
[lang.cpp]
@@ -13,4 +13,6 @@ ignore_dirs = ["build", "tools"]
[dependencies]
xcomposite = ">=0"
xfixes = ">=0"
xi = ">=0"
xi = ">=0"
xcursor = ">=1"
libpulse-simple = ">=0"

86
src/AudioPlayer.cpp Normal file
View File

@@ -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;
}
}

View File

@@ -1,50 +1,149 @@
#include "../include/Config.hpp"
#include "../include/Utils.hpp"
#include "../include/GsrInfo.hpp"
#include "../include/GlobalHotkeys.hpp"
#include <variant>
#include <limits.h>
#include <inttypes.h>
#include <libgen.h>
#include <iostream>
#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 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 keysym == other.keysym && modifiers == other.modifiers;
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;
}
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;
record_config.save_directory = default_save_directory;
record_config.save_directory = default_videos_save_directory;
record_config.record_options.audio_tracks.push_back("default_output");
record_config.record_options.video_bitrate = 45000;
replay_config.record_options.video_quality = "custom";
replay_config.save_directory = default_save_directory;
replay_config.save_directory = default_videos_save_directory;
replay_config.record_options.audio_tracks.push_back("default_output");
replay_config.record_options.video_bitrate = 45000;
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;
screenshot_config.record_area_option = capture_options.monitors.front().name;
}
}
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};
screenshot_config.take_screenshot_hotkey = {mgl::Keyboard::F1, HOTKEY_MOD_LALT};
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)
@@ -58,7 +157,10 @@ namespace gsr {
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},
@@ -87,7 +189,7 @@ namespace gsr {
{"streaming.twitch.key", &config.streaming_config.twitch.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},
@@ -114,8 +216,8 @@ namespace gsr {
{"record.show_video_saved_notifications", &config.record_config.show_video_saved_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},
@@ -139,14 +241,28 @@ 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.start_stop_hotkey", &config.replay_config.start_stop_hotkey},
{"replay.save_hotkey", &config.replay_config.save_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}
};
}
@@ -173,6 +289,8 @@ namespace gsr {
} 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 {
assert(false);
}
}
return true;
@@ -227,20 +345,22 @@ 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 {
assert(false);
}
return true;
});
if(config->main_config.config_file_version != CONFIG_FILE_VERSION) {
if(config->main_config.config_file_version != GSR_CONFIG_FILE_VERSION) {
fprintf(stderr, "Info: the config file is outdated, resetting it\n");
config = std::nullopt;
}
@@ -249,7 +369,7 @@ namespace gsr {
}
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";
@@ -278,12 +398,14 @@ 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());
}
} else {
assert(false);
}
}

View File

@@ -0,0 +1,243 @@
#include "../include/GlobalHotkeysJoystick.hpp"
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/eventfd.h>
namespace gsr {
static constexpr double double_click_timeout_seconds = 0.33;
// 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");
}
}
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) == 0)
return;
if(event.number == 8 && event.value == 1) {
const double now = double_click_clock.get_elapsed_time_seconds();
if(!prev_time_clicked.has_value()) {
prev_time_clicked = now;
return;
}
if(prev_time_clicked.has_value()) {
const bool double_clicked = (now - prev_time_clicked.value()) < double_click_timeout_seconds;
if(double_clicked) {
save_replay = true;
prev_time_clicked.reset();
} else {
prev_time_clicked = now;
}
}
}
}
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
};
++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;
}
}

View File

@@ -5,20 +5,73 @@
#include <limits.h>
#include <string.h>
extern "C" {
#include <mgl/mgl.h>
}
#include <X11/Xlib.h>
#include <linux/input-event-codes.h>
#define PIPE_READ 0
#define PIPE_WRITE 1
namespace gsr {
GlobalHotkeysLinux::GlobalHotkeysLinux() {
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;
}
GlobalHotkeysLinux::GlobalHotkeysLinux(GrabType grab_type) : grab_type(grab_type) {
for(int i = 0; i < 2; ++i) {
pipes[i] = -1;
read_pipes[i] = -1;
write_pipes[i] = -1;
}
}
GlobalHotkeysLinux::~GlobalHotkeysLinux() {
for(int i = 0; i < 2; ++i) {
if(pipes[i] > 0)
close(pipes[i]);
if(read_pipes[i] > 0)
close(read_pipes[i]);
if(write_pipes[i] > 0)
close(write_pipes[i]);
}
if(read_file)
@@ -32,74 +85,139 @@ namespace gsr {
}
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";
char gsr_global_hotkeys_flatpak[PATH_MAX];
snprintf(gsr_global_hotkeys_flatpak, sizeof(gsr_global_hotkeys_flatpak), "%s/.local/share/gpu-screen-recorder/gsr-global-hotkeys", user_homepath);
if(process_id > 0)
return false;
if(pipe(pipes) == -1)
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(pipes[i]);
pipes[i] = -1;
close(read_pipes[i]);
close(write_pipes[i]);
read_pipes[i] = -1;
write_pipes[i] = -1;
}
return false;
} else if(pid == 0) { /* child */
dup2(pipes[PIPE_WRITE], STDOUT_FILENO);
dup2(read_pipes[PIPE_WRITE], STDOUT_FILENO);
for(int i = 0; i < 2; ++i) {
close(pipes[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", "--", gsr_global_hotkeys_flatpak, NULL };
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", NULL };
const char *args[] = { "gsr-global-hotkeys", grab_type_arg, nullptr };
execvp(args[0], (char* const*)args);
}
perror("execvp");
perror("gsr-global-hotkeys");
_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);
close(read_pipes[PIPE_WRITE]);
read_pipes[PIPE_WRITE] = -1;
read_file = fdopen(pipes[PIPE_READ], "r");
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)
pipes[PIPE_READ] = -1;
read_pipes[PIPE_READ] = -1;
else
fprintf(stderr, "fdopen failed, error: %s\n", strerror(errno));
fprintf(stderr, "fdopen failed for read, 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, std::move(callback))).second;
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) {
//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];
const int 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");
//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");
//fprintf(stderr, "error: GlobalHotkeysLinux::poll_events failed, read file hasn't opened\n");
return;
}

View File

@@ -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;
}

View File

@@ -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);
}
}
@@ -68,11 +157,19 @@ 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
};
@@ -105,6 +202,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 +227,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 +258,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;
}

82
src/Hotplug.cpp Normal file
View File

@@ -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;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -40,12 +40,13 @@ namespace gsr {
return num_args;
}
bool exec_program_daemonized(const char **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);
const pid_t pid = vfork();
if(pid == -1) {
@@ -59,7 +60,7 @@ namespace gsr {
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:
@@ -72,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;
@@ -84,7 +85,8 @@ namespace gsr {
if(pipe(fds) == -1)
return -1;
debug_print_args(args);
if(debug)
debug_print_args(args);
const pid_t pid = vfork();
if(pid == -1) {
@@ -98,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]);
@@ -110,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;
const 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;
@@ -152,7 +154,7 @@ namespace gsr {
return exit_status;
}
int exec_program_on_host_get_stdout(const char **args, std::string &result) {
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;
@@ -170,9 +172,9 @@ namespace gsr {
}
modified_args[i] = arg;
}
return exec_program_get_stdout(modified_args, result);
return exec_program_get_stdout(modified_args, result, debug);
} else {
return exec_program_get_stdout(args, result);
return exec_program_get_stdout(args, result, debug);
}
}
@@ -206,7 +208,7 @@ namespace gsr {
return false;
}
pid_t pidof(const char *process_name) {
pid_t pidof(const char *process_name, pid_t ignore_pid) {
pid_t result = -1;
DIR *dir = opendir("/proc");
if(!dir)
@@ -222,8 +224,11 @@ namespace gsr {
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) {
result = atoi(entry->d_name);
break;
const pid_t pid = atoi(entry->d_name);
if(pid != ignore_pid) {
result = pid;
break;
}
}
}

View File

@@ -6,7 +6,7 @@
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/fcntl.h>
#include <fcntl.h>
namespace gsr {
static void get_runtime_filepath(char *buffer, size_t buffer_size, const char *filename) {

View File

@@ -108,6 +108,9 @@ namespace gsr {
if(!theme->save_texture.load_from_file((resources_path + "images/save.png").c_str()))
goto error;
if(!theme->screenshot_texture.load_from_file((resources_path + "images/screenshot.png").c_str()))
goto error;
return true;
error:

View File

@@ -114,6 +114,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;

View File

@@ -4,11 +4,41 @@
#include <X11/Xatom.h>
#include <X11/Xutil.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,37 +59,84 @@ 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) {
static 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) {
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);
// 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);
if(type == XA_WINDOW && num_items == 1 && data)
focused_window = *(Window*)data;
// if(type == XA_WINDOW && num_items == 1 && data)
// focused_window = *(Window*)data;
if(data)
XFree(data);
// if(data)
// XFree(data);
if(focused_window)
return focused_window;
// if(focused_window)
// return focused_window;
int revert_to = 0;
XGetInputFocus(dpy, &focused_window, &revert_to);
@@ -67,14 +144,34 @@ 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, "WM_NAME", False);
const Atom utf8_string_atom = XInternAtom(dpy, "UTF8_STRING", False);
@@ -86,8 +183,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;
@@ -96,17 +198,24 @@ 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;
if((type == XA_STRING || type == utf8_string_atom) && data) {
result = utf8_sanitize(data, num_items);
goto done;
}
return NULL;
done:
if(data)
XFree(data);
return result;
}
static const char* strip(const char *str, int *len) {
int str_len = strlen(str);
static 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] != ' ') {
str += i;
start_index += i;
str_len -= i;
break;
}
@@ -119,14 +228,7 @@ namespace gsr {
}
}
*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);
return str.substr(start_index, str_len);
}
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type) {
@@ -136,20 +238,297 @@ 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);
XFree(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);
return result;
}
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);
}
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);
XSizeHints *size_hints = XAllocSizeHints();
size_hints->width = size;
size_hints->height = size;
size_hints->min_width = size;
size_hints->min_height = size;
size_hints->max_width = size;
size_hints->max_height = size;
size_hints->flags = PSize | PMinSize | PMaxSize;
XSetWMNormalHints(display, window, size_hints);
XFree(size_hints);
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);
XSizeHints *size_hints = XAllocSizeHints();
size_hints->width = size;
size_hints->height = size;
size_hints->min_width = size;
size_hints->min_height = size;
size_hints->max_width = size;
size_hints->max_height = size;
size_hints->flags = PSize | PMinSize | PMaxSize;
XSetWMNormalHints(display, window, size_hints);
XFree(size_hints);
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;
}
static void get_monitors_callback(const mgl_monitor *monitor, void *userdata) {
std::vector<Monitor> *monitors = (std::vector<Monitor>*)userdata;
monitors->push_back({mgl::vec2i(monitor->pos.x, monitor->pos.y), mgl::vec2i(monitor->size.x, monitor->size.y)});
}
std::vector<Monitor> get_monitors(Display *dpy) {
std::vector<Monitor> monitors;
mgl_for_each_active_monitor_output(dpy, get_monitors_callback, &monitors);
return monitors;
}
}

View File

@@ -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.5f)).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,18 +80,25 @@ 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) {
@@ -110,13 +125,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 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());
const int padding_icon_top = padding_top_icon_scale * widget_height;
const int padding_icon_bottom = padding_bottom_icon_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;
return widget_height;
}
}

View File

@@ -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() * 22); // 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 : 0.0f) };
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 : 0.0f) };
}
float ComboBox::get_dropdown_arrow_height() const {

View File

@@ -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);
const 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() {

View File

@@ -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));
}
@@ -201,6 +201,15 @@ 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_description(std::string description_text) {
description.set_string(std::move(description_text));
}
@@ -242,4 +251,4 @@ namespace gsr {
update_if_dirty();
return size;
}
}
}

View File

@@ -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();

View File

@@ -1,5 +1,7 @@
#include "../../include/gui/GlobalSettingsPage.hpp"
#include "../../include/Overlay.hpp"
#include "../../include/GlobalHotkeys.hpp"
#include "../../include/Theme.hpp"
#include "../../include/Process.hpp"
#include "../../include/gui/GsrPage.hpp"
@@ -9,6 +11,25 @@
#include "../../include/gui/List.hpp"
#include "../../include/gui/Label.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) {
@@ -21,13 +42,39 @@ namespace gsr {
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";
}
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;
}
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")
@@ -38,11 +85,53 @@ 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("The hotkey has to contain one or more of these keys: Alt, Ctrl, Shift and Super. Press 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.1f).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());
window.draw(title_text);
hotkey_text.set_position(mgl::vec2f(bg_rect.get_position() + bg_rect.get_size()*0.5f - hotkey_text.get_bounds().size*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);
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(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");
@@ -88,6 +177,174 @@ namespace gsr {
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_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_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.screenshot_config.take_screenshot_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;
}
std::unique_ptr<Subsection> GlobalSettingsPage::create_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>("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<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_show_hide_hotkey_options());
list_ptr->add_widget(create_replay_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(std::make_unique<Label>(&get_theme().body_font, "Double-click the controller share button to save a replay", get_color_theme().text_color));
list_ptr->add_widget(create_hotkey_control_buttons());
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 = [&]() {
@@ -108,7 +365,6 @@ namespace gsr {
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)
@@ -116,6 +372,29 @@ namespace gsr {
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());
@@ -123,7 +402,9 @@ namespace gsr {
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_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));
@@ -131,6 +412,8 @@ namespace gsr {
void GlobalSettingsPage::on_navigate_away_from_page() {
save();
if(on_page_closed)
on_page_closed();
}
void GlobalSettingsPage::load() {
@@ -143,10 +426,221 @@ namespace gsr {
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());
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());
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);
}
}
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(configure_config_hotkey.modifiers != 0) {
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::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::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::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::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.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::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::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();
}
}

View File

@@ -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);
@@ -80,13 +81,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 +107,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();
const 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 +119,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() {

View File

@@ -0,0 +1,341 @@
#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
// TODO: Enable this
//if(capture_options.window)
// record_area_box->add_item("Window", "window");
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<List> ScreenshotSettingsPage::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> 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_select_window());
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 &text, const std::string &id) {
(void)text;
const bool window_selected = id == "window";
const bool portal_selected = id == "portal";
select_window_list_ptr->set_visible(window_selected);
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(capture_options.monitors.front().name);
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);
}
}

View File

@@ -89,8 +89,7 @@ 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();
const mgl_scissor new_scissor = {
@@ -150,7 +149,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)

View File

@@ -8,11 +8,6 @@
#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 {
@@ -22,6 +17,15 @@ namespace gsr {
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 +37,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")
@@ -171,7 +175,7 @@ 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);
@@ -304,7 +308,8 @@ namespace gsr {
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());
if(type != Type::STREAM)
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));
@@ -338,11 +343,27 @@ namespace gsr {
return list;
}
std::unique_ptr<Entry> SettingsPage::create_video_bitrate_entry() {
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, "15000", (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, "1.92MB", 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_mb_per_seconds = (double)atoi(text.c_str()) / 1000LL / 8LL * 1.024;
char buffer[32];
snprintf(buffer, sizeof(buffer), "%.2fMB", video_bitrate_mb_per_seconds);
size_mb_label_ptr->set_text(buffer);
};
}
return list;
}
std::unique_ptr<List> SettingsPage::create_video_bitrate() {
@@ -387,20 +408,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();
@@ -495,7 +516,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();
@@ -572,7 +593,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);
@@ -612,16 +633,24 @@ 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, 10800);
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;
}
@@ -647,22 +676,42 @@ namespace gsr {
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 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 in RAM: %.2fMB.\nChange video bitrate or replay duration to change file size.", 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::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);
@@ -670,12 +719,14 @@ 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_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)));
auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL);
@@ -713,11 +764,12 @@ namespace gsr {
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();
update_replay_time_text();
};
video_bitrate_entry_ptr->on_changed = [this](const std::string&) {
update_estimated_file_size();
update_estimated_replay_file_size();
};
}
@@ -729,15 +781,31 @@ namespace gsr {
return checkbox;
}
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)));
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;
}
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)));
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_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)));
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);
@@ -767,6 +835,10 @@ namespace gsr {
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() {
@@ -969,7 +1041,8 @@ namespace gsr {
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);
if(merge_audio_tracks_checkbox_ptr)
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);
@@ -1025,14 +1098,18 @@ namespace gsr {
load_common(config.replay_config.record_options);
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 > 10800)
config.replay_config.replay_time = 10800;
replay_time_entry_ptr->set_text(std::to_string(config.replay_config.replay_time));
}
@@ -1090,7 +1167,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();
if(merge_audio_tracks_checkbox_ptr)
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);
@@ -1152,6 +1230,8 @@ 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();

View File

@@ -36,14 +36,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);
const 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 +48,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() {

View File

@@ -1,18 +1,15 @@
#include "../include/GsrInfo.hpp"
#include "../include/Theme.hpp"
#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>
@@ -38,100 +35,49 @@ static void disable_prime_run() {
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");
if(!stream_hotkey_registered)
fprintf(stderr, "error: failed to register hotkey alt+f8 for streaming because the hotkey is registered by another program\n");
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");
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");
if(!show_hotkey_registered || !record_hotkey_registered || !pause_hotkey_registered || !stream_hotkey_registered || !replay_hotkey_registered || !replay_save_hotkey_registered)
return nullptr;
return global_hotkeys;
}
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");
global_hotkeys->bind_action("show_hide", [overlay](const std::string &id) {
fprintf(stderr, "pressed %s\n", id.c_str());
overlay->toggle_show();
rpc->add_handler("take-screenshot", [overlay](const std::string &name) {
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
overlay->take_screenshot();
});
global_hotkeys->bind_action("record", [overlay](const std::string &id) {
fprintf(stderr, "pressed %s\n", id.c_str());
overlay->toggle_record();
});
global_hotkeys->bind_action("pause", [overlay](const std::string &id) {
fprintf(stderr, "pressed %s\n", id.c_str());
overlay->toggle_pause();
});
global_hotkeys->bind_action("stream", [overlay](const std::string &id) {
fprintf(stderr, "pressed %s\n", id.c_str());
overlay->toggle_stream();
});
global_hotkeys->bind_action("replay_start", [overlay](const std::string &id) {
fprintf(stderr, "pressed %s\n", id.c_str());
overlay->toggle_replay();
});
global_hotkeys->bind_action("replay_save", [overlay](const std::string &id) {
fprintf(stderr, "pressed %s\n", id.c_str());
overlay->save_replay();
});
return global_hotkeys;
}
static bool is_gsr_ui_virtual_keyboard_running() {
@@ -152,6 +98,48 @@ static bool is_gsr_ui_virtual_keyboard_running() {
return virtual_keyboard_running;
}
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");
}
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;
}
if(access(systemd_service_path, F_OK) != 0)
return;
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);
}
static bool is_flatpak() {
return getenv("FLATPAK_ID") != nullptr;
}
static void usage() {
printf("usage: gsr-ui [action]\n");
printf("OPTIONS:\n");
@@ -168,6 +156,7 @@ enum class LaunchAction {
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");
@@ -191,9 +180,17 @@ int main(int argc, char **argv) {
usage();
}
if(is_flatpak())
install_flatpak_systemd_service();
else
remove_flatpak_systemd_service();
// 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.
if(is_gsr_ui_virtual_keyboard_running()) {
// 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) {
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");
@@ -204,15 +201,6 @@ int main(int argc, char **argv) {
}
return 1;
}
// const pid_t gsr_ui_pid = gsr::pidof("gsr-ui");
// if(gsr_ui_pid != -1) {
// const char *args[] = { "gsr-notify", "--text", "Another instance of GPU Screen Recorder UI is already running", "--timeout", "5.0", "--icon-color", "ff0000", "--bg-color", "ff0000", nullptr };
// gsr::exec_program_daemonized(args);
// return 1;
// }
// Cant get window texture when prime-run is used
disable_prime_run();
// Stop nvidia driver from buffering frames
setenv("__GL_MaxFramesAllowed", "1", true);
@@ -226,11 +214,6 @@ int main(int argc, char **argv) {
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);
}
gsr::GsrInfo gsr_info;
// TODO: Show the error in ui
gsr::GsrInfoExitStatus gsr_info_exit_status = gsr::get_gpu_screen_recorder_info(&gsr_info);
@@ -240,8 +223,17 @@ int main(int argc, char **argv) {
}
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() != 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);
@@ -271,20 +263,15 @@ int main(int argc, char **argv) {
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);
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");
auto overlay = std::make_unique<gsr::Overlay>(resources_path, std::move(gsr_info), std::move(capture_options), egl_funcs);
rpc->add_handler("show_ui", [&](const std::string&) {
overlay->show();
});
std::unique_ptr<gsr::GlobalHotkeys> global_hotkeys = register_linux_hotkeys(overlay.get());
if(launch_action == LaunchAction::LAUNCH_SHOW)
overlay->show();
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.
@@ -296,25 +283,24 @@ int main(int argc, char **argv) {
gsr::set_frame_delta_seconds(frame_delta_seconds);
rpc->poll();
global_hotkeys->poll_events();
overlay->handle_events(global_hotkeys.get());
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");
rpc.reset();
global_hotkeys.reset();
overlay.reset();
gsr::deinit_theme();
gsr::deinit_color_theme();
mgl_deinit();
if(exit_reason == "back-to-old-ui") {
const char *args[] = { "gpu-screen-recorder-gtk", "use-old-ui", nullptr };
execvp(args[0], (char* const*)args);
} else if(exit_reason == "restart") {
const char *args[] = { "gsr-ui", "launch-show", nullptr };
execvp(args[0], (char* const*)args);
}
return 0;

View File

@@ -0,0 +1,21 @@
# 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
```

View File

@@ -22,7 +22,7 @@ bool hotplug_event_init(hotplug_event *self) {
const int fd = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);
if(fd == -1)
return false; /* Not root user */
return false;
if(bind(fd, (void*)&nls, sizeof(struct sockaddr_nl))) {
close(fd);
@@ -56,19 +56,22 @@ static void hotplug_event_parse_netlink_data(hotplug_event *self, const char *li
if(strcmp(line, "SUBSYSTEM=input") == 0)
self->subsystem_is_input = true;
if(self->subsystem_is_input && strncmp(line, "DEVNAME=", 8) == 0)
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));
int data_index = 0;
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 */

View File

@@ -1,4 +1,5 @@
#include "keyboard_event.h"
#include "keys.h"
/* C stdlib */
#include <stdio.h>
@@ -11,7 +12,7 @@
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/poll.h>
#include <poll.h>
/* LINUX */
#include <linux/input.h>
@@ -81,19 +82,19 @@ static void keyboard_event_fetch_update_key_states(keyboard_event *self, event_e
}
}
static void keyboard_event_process_key_state_change(keyboard_event *self, struct input_event event, event_extra_data *extra_data, int fd) {
if(event.type != EV_KEY)
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)
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;
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) {
if(event->value == KEY_RELEASE) {
key_byte_state &= ~(1 << bit_index);
if(prev_key_pressed)
--extra_data->num_keys_pressed;
@@ -117,7 +118,44 @@ static void keyboard_event_process_key_state_change(keyboard_event *self, struct
}
}
static void keyboard_event_process_input_event_data(keyboard_event *self, event_extra_data *extra_data, int fd, key_callback callback, void *userdata) {
/* 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) {
if(event->value != KEYBOARD_BUTTON_PRESSED)
return false;
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) {
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;
}
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");
@@ -134,56 +172,14 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
//fprintf(stderr, "fd: %d, type: %d, pressed %d, value: %d\n", fd, event.type, event.code, event.value);
//}
if(event.type == EV_KEY) {
keyboard_event_process_key_state_change(self, event, extra_data, fd);
switch(event.code) {
case KEY_LEFTSHIFT:
self->lshift_button_state = event.value >= 1 ? KEYBOARD_BUTTON_PRESSED : KEYBOARD_BUTTON_RELEASED;
break;
case KEY_RIGHTSHIFT:
self->rshift_button_state = event.value >= 1 ? KEYBOARD_BUTTON_PRESSED : KEYBOARD_BUTTON_RELEASED;
break;
case KEY_LEFTCTRL:
self->lctrl_button_state = event.value >= 1 ? KEYBOARD_BUTTON_PRESSED : KEYBOARD_BUTTON_RELEASED;
break;
case KEY_RIGHTCTRL:
self->rctrl_button_state = event.value >= 1 ? KEYBOARD_BUTTON_PRESSED : KEYBOARD_BUTTON_RELEASED;
break;
case KEY_LEFTALT:
self->lalt_button_state = event.value >= 1 ? KEYBOARD_BUTTON_PRESSED : KEYBOARD_BUTTON_RELEASED;
break;
case KEY_RIGHTALT:
self->ralt_button_state = event.value >= 1 ? KEYBOARD_BUTTON_PRESSED : KEYBOARD_BUTTON_RELEASED;
break;
case KEY_LEFTMETA:
self->lmeta_button_state = event.value >= 1 ? KEYBOARD_BUTTON_PRESSED : KEYBOARD_BUTTON_RELEASED;
break;
case KEY_RIGHTMETA:
self->rmeta_button_state = event.value >= 1 ? KEYBOARD_BUTTON_PRESSED : KEYBOARD_BUTTON_RELEASED;
break;
default: {
const bool shift_pressed = self->lshift_button_state == KEYBOARD_BUTTON_PRESSED || self->rshift_button_state == KEYBOARD_BUTTON_PRESSED;
const bool ctrl_pressed = self->lctrl_button_state == KEYBOARD_BUTTON_PRESSED || self->rctrl_button_state == KEYBOARD_BUTTON_PRESSED;
const bool alt_pressed = self->lalt_button_state == KEYBOARD_BUTTON_PRESSED || self->ralt_button_state == KEYBOARD_BUTTON_PRESSED;
const bool meta_pressed = self->lmeta_button_state == KEYBOARD_BUTTON_PRESSED || self->rmeta_button_state == KEYBOARD_BUTTON_PRESSED;
//fprintf(stderr, "pressed key: %d, state: %d, shift: %s, ctrl: %s, alt: %s, meta: %s\n", event.code, event.value,
// shift_pressed ? "yes" : "no", ctrl_pressed ? "yes" : "no", alt_pressed ? "yes" : "no", meta_pressed ? "yes" : "no");
uint32_t modifiers = 0;
if(shift_pressed)
modifiers |= KEYBOARD_MODKEY_SHIFT;
if(ctrl_pressed)
modifiers |= KEYBOARD_MODKEY_CTRL;
if(alt_pressed)
modifiers |= KEYBOARD_MODKEY_ALT;
if(meta_pressed)
modifiers |= KEYBOARD_MODKEY_SUPER;
if(!callback(event.code, modifiers, event.value, userdata))
return;
break;
}
if(event.type == EV_KEY && is_keyboard_key(event.code)) {
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))
return;
} else {
self->modifier_button_states = set_bit(self->modifier_button_states, modifier_bit, event.value >= 1);
}
}
@@ -194,6 +190,36 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
}
}
/* 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)
@@ -213,11 +239,42 @@ static bool keyboard_event_has_event_with_dev_input_fd(keyboard_event *self, int
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 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;
@@ -231,7 +288,7 @@ static bool keyboard_event_try_add_device_if_keyboard(keyboard_event *self, cons
unsigned long evbit = 0;
ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);
const bool is_keyboard = evbit & (1 << EV_KEY);
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};
@@ -242,7 +299,7 @@ static bool keyboard_event_try_add_device_if_keyboard(keyboard_event *self, cons
//const bool supports_touch_events = key_bits[BTN_TOUCH/8] & (1 << (BTN_TOUCH % 8));
const bool supports_joystick_events = key_bits[BTN_JOYSTICK/8] & (1 << (BTN_JOYSTICK % 8));
const bool supports_wheel_events = key_bits[BTN_WHEEL/8] & (1 << (BTN_WHEEL % 8));
if(supports_key_events && !supports_mouse_events && !supports_joystick_events && !supports_wheel_events) {
if(supports_key_events && (is_virtual_device || (!supports_joystick_events && !supports_wheel_events))) {
unsigned char *key_states = calloc(1, KEY_STATES_SIZE);
if(key_states && self->num_event_polls < MAX_EVENT_POLLS) {
//fprintf(stderr, "%s (%s) supports key inputs\n", dev_input_filepath, device_name);
@@ -259,9 +316,16 @@ static bool keyboard_event_try_add_device_if_keyboard(keyboard_event *self, cons
.num_keys_pressed = 0
};
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);
if(supports_mouse_events || supports_joystick_events || supports_wheel_events) {
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;
@@ -271,7 +335,10 @@ static bool keyboard_event_try_add_device_if_keyboard(keyboard_event *self, cons
}
}
close(fd);
if(!keyboard_event_try_add_close_fd(self, fd)) {
fprintf(stderr, "Error: failed to add immediately, closing now\n");
close(fd);
}
return false;
}
@@ -331,14 +398,21 @@ static int setup_virtual_keyboard_input(const char *name) {
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);
for(int i = 1; i < KEY_MAX; ++i) {
success &= (ioctl(fd, UI_SET_KEYBIT, i) != -1);
}
success &= (ioctl(fd, UI_SET_EVBIT, EV_REP) != -1);
success &= (ioctl(fd, UI_SET_EVBIT, EV_REL) != -1);
success &= (ioctl(fd, UI_SET_RELBIT, REL_X) != -1);
success &= (ioctl(fd, UI_SET_RELBIT, REL_Y) != -1);
success &= (ioctl(fd, UI_SET_RELBIT, REL_Z) != -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) {
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);
@@ -373,10 +447,19 @@ static int setup_virtual_keyboard_input(const char *name) {
return fd;
}
bool keyboard_event_init(keyboard_event *self, bool poll_stdout_error, bool exclusive_grab) {
bool keyboard_event_init(keyboard_event *self, bool exclusive_grab, keyboard_grab_type grab_type) {
memset(self, 0, sizeof(*self));
self->stdout_event_index = -1;
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);
@@ -384,23 +467,21 @@ bool keyboard_event_init(keyboard_event *self, bool poll_stdout_error, bool excl
fprintf(stderr, "Warning: failed to setup virtual keyboard input for exclusive grab. The focused application will receive keys used for global hotkeys\n");
}
if(poll_stdout_error) {
self->event_polls[self->num_event_polls] = (struct pollfd) {
.fd = STDOUT_FILENO,
.events = 0,
.revents = 0
};
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,
.num_keys_pressed = 0
};
self->event_extra_data[self->num_event_polls] = (event_extra_data) {
.dev_input_id = -1,
.grabbed = false,
.key_states = NULL,
.num_keys_pressed = 0
};
self->stdout_event_index = self->num_event_polls;
++self->num_event_polls;
}
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) {
@@ -434,7 +515,15 @@ bool keyboard_event_init(keyboard_event *self, bool poll_stdout_error, bool excl
}
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;
}
@@ -447,22 +536,184 @@ void keyboard_event_deinit(keyboard_event *self) {
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[1024];
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);
}
void keyboard_event_poll_events(keyboard_event *self, int timeout_milliseconds, key_callback callback, void *userdata) {
/* 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 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) {
fprintf(stderr, "Error: can't bind hotkey without a modifier\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 {
fprintf(stderr, "Warning: got invalid command: \"%s\", expected command to start with either \"bind\" or \"unbind_all\"\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->stdout_event_index && (self->event_polls[i].revents & (POLLHUP|POLLERR)))
self->stdout_failed = true;
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);
@@ -476,14 +727,14 @@ void keyboard_event_poll_events(keyboard_event *self, int timeout_milliseconds,
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->stdout_event_index) {
/* Do nothing, this shouldn't happen anyways since we dont poll for input */
} 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, callback, userdata);
keyboard_event_process_input_event_data(self, &self->event_extra_data[i], self->event_polls[i].fd);
}
}
}
bool keyboard_event_stdout_has_failed(const keyboard_event *self) {
return self->stdout_failed;
bool keyboard_event_stdin_has_failed(const keyboard_event *self) {
return self->stdin_failed;
}

View File

@@ -10,18 +10,25 @@
#include <stdint.h>
/* POSIX */
#include <sys/poll.h>
#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_ALT = 1 << 0,
KEYBOARD_MODKEY_SUPER = 1 << 1,
KEYBOARD_MODKEY_CTRL = 1 << 2,
KEYBOARD_MODKEY_SHIFT = 1 << 3
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 {
@@ -36,37 +43,50 @@ typedef struct {
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 stdout_event_index;
int stdin_event_index;
int hotplug_event_index;
int uinput_fd;
bool stdout_failed;
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;
keyboard_button_state lshift_button_state;
keyboard_button_state rshift_button_state;
keyboard_button_state lctrl_button_state;
keyboard_button_state rctrl_button_state;
keyboard_button_state lalt_button_state;
keyboard_button_state ralt_button_state;
keyboard_button_state lmeta_button_state;
keyboard_button_state rmeta_button_state;
uint32_t modifier_button_states;
} keyboard_event;
/* |key| is a KEY_ from linux/input-event-codes.h. |modifiers| is a bitmask of keyboard_modkeys. |press_status| is 0 for released, 1 for pressed and 2 for repeat */
/* Return true to allow other applications to receive the key input (when using exclusive grab) */
typedef bool (*key_callback)(uint32_t key, uint32_t modifiers, int press_status, void *userdata);
bool keyboard_event_init(keyboard_event *self, bool poll_stdout_error, bool exclusive_grab);
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, key_callback callback, void *userdata);
bool keyboard_event_stdout_has_failed(const keyboard_event *self);
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 */

View File

@@ -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);
}

View File

@@ -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 */

View File

@@ -2,42 +2,63 @@
/* C stdlib */
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <locale.h>
/* POSIX */
#include <unistd.h>
typedef struct {
uint32_t key;
uint32_t modifiers; /* keyboard_modkeys bitmask */
const char *action;
} global_hotkey;
#define NUM_GLOBAL_HOTKEYS 6
static global_hotkey global_hotkeys[NUM_GLOBAL_HOTKEYS] = {
{ .key = KEY_Z, .modifiers = KEYBOARD_MODKEY_ALT, .action = "show_hide" },
{ .key = KEY_F9, .modifiers = KEYBOARD_MODKEY_ALT, .action = "record" },
{ .key = KEY_F7, .modifiers = KEYBOARD_MODKEY_ALT, .action = "pause" },
{ .key = KEY_F8, .modifiers = KEYBOARD_MODKEY_ALT, .action = "stream" },
{ .key = KEY_F10, .modifiers = KEYBOARD_MODKEY_ALT | KEYBOARD_MODKEY_SHIFT, .action = "replay_start" },
{ .key = KEY_F10, .modifiers = KEYBOARD_MODKEY_ALT, .action = "replay_save" }
};
static bool on_key_callback(uint32_t key, uint32_t modifiers, int press_status, void *userdata) {
(void)userdata;
for(int i = 0; i < NUM_GLOBAL_HOTKEYS; ++i) {
if(key == global_hotkeys[i].key && modifiers == global_hotkeys[i].modifiers) {
if(press_status == 1) { /* 1 == Pressed */
puts(global_hotkeys[i].action);
fflush(stdout);
}
return false;
}
}
return true;
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");
}
int main(void) {
static bool is_gsr_global_hotkeys_already_running(void) {
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;
}
}
fclose(f);
return virtual_keyboard_running;
}
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, "Error: expected --all or --virtual, got %s\n", grab_type_arg);
usage();
return 1;
}
} else if(argc != 1) {
fprintf(stderr, "Error: expected 0 or 1 arguments, got %d argument(s)\n", argc);
usage();
return 1;
}
if(is_gsr_global_hotkeys_already_running()) {
fprintf(stderr, "Error: gsr-global-hotkeys is already running\n");
return 1;
}
const uid_t user_id = getuid();
if(geteuid() != 0) {
if(setuid(0) == -1) {
@@ -47,7 +68,7 @@ int main(void) {
}
keyboard_event keyboard_ev;
if(!keyboard_event_init(&keyboard_ev, true, true)) {
if(!keyboard_event_init(&keyboard_ev, true, grab_type)) {
fprintf(stderr, "Error: failed to setup hotplugging and no keyboard input devices were found\n");
setuid(user_id);
return 1;
@@ -56,9 +77,9 @@ int main(void) {
fprintf(stderr, "Info: global hotkeys setup, waiting for hotkeys to be pressed\n");
for(;;) {
keyboard_event_poll_events(&keyboard_ev, -1, on_key_callback, NULL);
if(keyboard_event_stdout_has_failed(&keyboard_ev)) {
fprintf(stderr, "Info: stdout closed (parent process likely closed this process), exiting...\n");
keyboard_event_poll_events(&keyboard_ev, -1);
if(keyboard_event_stdin_has_failed(&keyboard_ev)) {
fprintf(stderr, "Info: stdin closed (parent process likely closed this process), exiting...\n");
break;
}
}

107
tools/gsr-ui-cli/main.c Normal file
View File

@@ -0,0 +1,107 @@
#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 Show/hide the UI.\n");
printf(" toggle-record Start/stop recording.\n");
printf(" toggle-pause Pause/unpause recording. Only applies to regular recording.\n");
printf(" toggle-stream Start/stop streaming.\n");
printf(" toggle-replay Start/stop replay.\n");
printf(" replay-save Save replay.\n");
printf(" take-screenshot Take a screenshot.\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",
"take-screenshot",
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;
}