mirror of
https://repo.dec05eba.com/gpu-screen-recorder-ui
synced 2026-04-05 11:16:28 +09:00
Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71d28f8ba3 | ||
|
|
bb1e9c6616 | ||
|
|
e14bb0cbcf | ||
|
|
5a13bd2491 | ||
|
|
b875f96885 | ||
|
|
0d3d4229bf | ||
|
|
ed23f56a29 | ||
|
|
a9a1f9d01c | ||
|
|
2506750243 | ||
|
|
f017f04bdc | ||
|
|
d1f8db3760 | ||
|
|
0995e86e89 | ||
|
|
4992185323 | ||
|
|
70df557c2b | ||
|
|
be07070789 | ||
|
|
2bc2252d30 | ||
|
|
9af3c85161 | ||
|
|
d7f6d2cc0c | ||
|
|
0f5b225107 | ||
|
|
85e8b04ee2 | ||
|
|
a6b1111230 | ||
|
|
d70b36000f | ||
|
|
12c090c7d3 | ||
|
|
d9496e0a0a | ||
|
|
c4ff7fd6b8 | ||
|
|
5bd600fad6 | ||
|
|
5144994575 | ||
|
|
1c24616388 | ||
|
|
ecd9a1f13f | ||
|
|
4181d80405 | ||
|
|
085f4d8bad | ||
|
|
bb320e97ed | ||
|
|
ccf96030da | ||
|
|
ca4061f171 | ||
|
|
0b4af1e6bb | ||
|
|
9e03cd0354 | ||
|
|
3d4badf5cd | ||
|
|
071ecf46de | ||
|
|
5ee2b95384 | ||
|
|
d610a980f8 | ||
|
|
70780ae14e | ||
|
|
5f7cb94f4e | ||
|
|
748c51e2b6 | ||
|
|
3ba9ce771b | ||
|
|
c18b062180 | ||
|
|
705da21363 | ||
|
|
609a3e54fd | ||
|
|
4e62d12e8c | ||
|
|
b4e003c8f7 | ||
|
|
9efe9d3c91 | ||
|
|
ef4a0fe7cb | ||
|
|
dacf6126bf | ||
|
|
9bbec944de | ||
|
|
6a55338b12 | ||
|
|
2d3abace0e | ||
|
|
47c02fc6c8 | ||
|
|
5f8c366b43 | ||
|
|
f4ed622510 | ||
|
|
f1ee19d014 | ||
|
|
67a8040e57 | ||
|
|
ff00be30df | ||
|
|
cf282bc225 | ||
|
|
c05a8290b7 | ||
|
|
a9e118ea8f | ||
|
|
8ed1fe4799 | ||
|
|
c1d76b5169 | ||
|
|
b1e650c7ec | ||
|
|
2e0dc48f3e | ||
|
|
d64e698eb1 | ||
|
|
315fab99a8 | ||
|
|
8ffd8de74a | ||
|
|
ad94cff59e | ||
|
|
b64cb6a3fd | ||
|
|
182c96d8e9 | ||
|
|
9192b3eba1 | ||
|
|
dd7aae3191 | ||
|
|
3fee07ad4c | ||
|
|
2daa8ba4aa | ||
|
|
a78cefc65b | ||
|
|
a0d1de55d7 | ||
|
|
c0cd6337fc | ||
|
|
0b8a3815b4 | ||
|
|
fc82d73728 | ||
|
|
0dfcb004e4 | ||
|
|
644d3f36d1 | ||
|
|
6607aba30b | ||
|
|
aa62c1bb9a | ||
|
|
9eab194c5f | ||
|
|
abeaf5cb61 | ||
|
|
575592a12d | ||
|
|
d72ce588fb | ||
|
|
7d2f2e9b47 | ||
|
|
636150ef08 | ||
|
|
612fe6a9c2 | ||
|
|
57448f6579 | ||
|
|
4d7526d21e | ||
|
|
fded9b8d57 | ||
|
|
b80e3f8beb | ||
|
|
b807712d79 | ||
|
|
2df417f23f | ||
|
|
a82d1a2dfc | ||
|
|
043b6df255 | ||
|
|
831f583f89 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,3 +4,5 @@ compile_commands.json
|
||||
|
||||
**/xdg-output-unstable-v1-client-protocol.h
|
||||
**/xdg-output-unstable-v1-protocol.c
|
||||
|
||||
depends/.wraplock
|
||||
|
||||
32
README.md
32
README.md
@@ -4,17 +4,17 @@
|
||||
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.
|
||||
|
||||
# Usage
|
||||
You can start the overlay UI and make it start automatically on system startup by running `systemctl enable --now --user gpu-screen-recorder-ui`.
|
||||
Alternatively you can run `gsr-ui` and go into settings and enable start on system startup setting.\
|
||||
Press `Left Alt+Z` to show/hide the UI. Go into settings to view all of the different hotkeys configured.\
|
||||
If you use a non-systemd distro and want to start the UI on system startup then you have to manually add `gsr-ui` to your system startup script.\
|
||||
A program called `gsr-ui-cli` is also installed when installing this software. This can be used to remotely control the UI. Run `gsr-ui-cli --help` to list the available commands.
|
||||
|
||||
# Installation
|
||||
If you are using an Arch Linux based distro then you can find gpu screen recorder ui on aur under the name gpu-screen-recorder-ui (`yay -S gpu-screen-recorder-ui`).\
|
||||
If you are running another distro then you can run `sudo ./install.sh`, but you need to manually install the dependencies, as described below.\
|
||||
You can also install gpu screen recorder from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder). This flatpak includes both this UI and gpu-screen-recorder so no need to install that first.
|
||||
You can also install gpu screen recorder from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder) which includes this UI.
|
||||
|
||||
# Usage
|
||||
Press `Left Alt+Z` to show/hide the UI. Go into settings (the icon on the right) to view all of the different hotkeys configured.\
|
||||
You can start the overlay UI and make it start automatically on system startup by running `systemctl enable --now --user gpu-screen-recorder-ui`.
|
||||
Alternatively you can run `gsr-ui` and go into settings and enable start on system startup setting.\
|
||||
If you use a non-systemd distro and want to start the UI on system startup then you have to manually add `gsr-ui launch-daemon` to your system startup script.\
|
||||
A program called `gsr-ui-cli` is also installed when installing this software. This can be used to remotely control the UI. Run `gsr-ui-cli --help` to list the available commands.
|
||||
|
||||
# Dependencies
|
||||
GPU Screen Recorder UI uses meson build system so you need to install `meson` to build GPU Screen Recorder UI.
|
||||
@@ -28,6 +28,7 @@ These are the dependencies needed to build GPU Screen Recorder UI:
|
||||
* libpulse (libpulse-simple)
|
||||
* libdrm
|
||||
* wayland (wayland-client, wayland-egl, wayland-scanner)
|
||||
* setcap (libcap)
|
||||
|
||||
## Runtime dependencies
|
||||
There are also additional dependencies needed at runtime:
|
||||
@@ -36,8 +37,8 @@ There are also additional dependencies needed at runtime:
|
||||
* [GPU Screen Recorder Notification](https://git.dec05eba.com/gpu-screen-recorder-notification/)
|
||||
|
||||
## Program behavior notes
|
||||
This program has to grab all keyboards and create a virtual keyboard (`gsr-ui virtual keyboard`) to make global hotkeys work on all Wayland compositors.\
|
||||
This might cause issues for you if you use keyboard remapping software. To workaround this you can go into settings and select "Only grab virtual devices".\
|
||||
By default this program has to grab all keyboards and creates a virtual keyboard (`gsr-ui virtual keyboard`) to make global hotkeys work on all Wayland compositors.\
|
||||
This might cause issues for you if you use keyboard remapping software. To workaround this you can go into settings and select "Yes, but only grab virtual devices" or "Yes, but don't grab devices".\
|
||||
If you use keyboard remapping software such as keyd then make sure to make it ignore "gsr-ui virtual keyboard" (dec0:5eba device id), otherwise your keyboard can get locked
|
||||
as gpu screen recorder tries to grab keys and keyd grabs gpu screen recorder, leading to a lock.\
|
||||
If you are stuck in such a lock where you cant press and keyboard keys you can press (left) ctrl+shift+alt+esc to close gpu screen recorder and remove it from system startup.
|
||||
@@ -60,11 +61,18 @@ I'm looking for somebody that can create sound effects for the notifications.
|
||||

|
||||
|
||||
# Known issues
|
||||
* 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 fullscreen can mess up the game window a bit on Hyprland. This is an issue with Hyprland.
|
||||
* When the UI is open the wallpaper is shown instead of the game on Hyprland. This is an issue with Hyprland. It cant be fixed until the UI is redesigned to not be a fullscreen overlay. Change your waybar dock mode to "dock" in its config to fix this.
|
||||
* Opening the UI when a game is fullscreen can mess up the game window a bit on Hyprland. This is an issue with Hyprland. Change your waybar dock mode to "dock" in its config to fix this.
|
||||
* The background of the UI is black when opening the UI while a Wayland application is focused on COSMIC. This is an issue with COSMIC.
|
||||
* Unable to close the region selection with escape key while a Wayland application is focused on COSMIC. This is an issue with COSMIC.
|
||||
|
||||
# FAQ
|
||||
## I get an error when trying to start the gpu-screen-recorder-ui.service systemd service
|
||||
If you have previously used the flatpak version of GPU Screen Recorder with the new UI then the non-flatpak version of the systemd service will conflict with that. Run `gsr-ui` to fix that.
|
||||
## I use a non-qwerty keyboard layout and I have an issue with incorrect keys registered in the software
|
||||
This is a KDE Plasma Wayland issue. Use `setxkbmap <language>` command, for example `setxkbmap se` to make sure X11 applications (such as this one) gets updated to use your languages keyboard layout.
|
||||
## "Save to clipboard" option doesn't work for screenshots
|
||||
Some Wayland compositors don't support copying images on the clipboard between X11 and Wayland applications. GPU Screen Recorder UI is an X11 application. It can't be done properly on Wayland
|
||||
since Wayland doesn't support a non-focused application from setting the clipboard, so it can't work with GPU Screen Recorder hotkey usage. Use X11 if you want a functioning desktop.
|
||||
## The controller hotkey and steam overlap (home button brings up steam overlay)
|
||||
You can either disable the steam overlay or in steam click Steam->Settings->Controller and then click "Begin Test" under "Test Device Inputs". Click on "Setup Device Inputs" and configure controller buttons there and when you get to the home button press X to unbind it from steam.
|
||||
|
||||
94
TODO
94
TODO
@@ -12,8 +12,6 @@ Handle events in draw function because the render position of elements is availa
|
||||
|
||||
Add nvidia overclock option.
|
||||
|
||||
Add support for window selection in capture.
|
||||
|
||||
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.
|
||||
|
||||
Restart replay on system start if monitor resolution changes.
|
||||
@@ -27,8 +25,6 @@ Have different modes. Overlay, window and side menu. Overlay can be used on x11,
|
||||
|
||||
Show navigation breadcrumbs for settings and deeper navigation (such as selecting a directory to save videos).
|
||||
|
||||
Add option to hide stream key like a password input.
|
||||
|
||||
Add global setting. In that setting there should be an option to enable/disable gsr-ui from system startup (the systemd service).
|
||||
|
||||
Add profiles and hotkey to switch between profiles (show notification when switching profile).
|
||||
@@ -37,8 +33,6 @@ Fix first frame being black when running without a compositor.
|
||||
|
||||
Add support for systray.
|
||||
|
||||
Add option to take screenshot.
|
||||
|
||||
Move event callbacks to a global list instead of std::function object in each widget. This reduces the size of widgets,
|
||||
since most widgets wont have the event callback set.
|
||||
This event callback would pass the widget as an argument.
|
||||
@@ -72,8 +66,6 @@ Run `systemctl status --user gpu-screen-recorder` when starting recording and gi
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Support CJK.
|
||||
@@ -92,15 +84,10 @@ Dont put widget position to int position when scrolling. This makes the UI jitte
|
||||
|
||||
Show warning if another instance of gpu screen recorder is already running when starting recording?
|
||||
|
||||
Keyboard leds get turned off when stopping gsr-global-hotkeys (for example numlock). The numlock key has to be pressed twice again to make it look correct to match its state.
|
||||
|
||||
Make gsr-ui flatpak systemd work nicely with non-flatpak gsr-ui. Maybe change ExecStart to do flatpak run ... || gsr-ui, but make it run as a shell command first with /bin/sh -c "".
|
||||
|
||||
When enabling X11 global hotkey again only grab lalt, not ralt.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -118,9 +105,6 @@ When clicking on current directory in file manager show a dropdown menu where yo
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
@@ -141,8 +125,6 @@ Make inactive buttons gray (in dropdown boxes and in the front page with save, e
|
||||
|
||||
Add option to do screen-direct recording. But make it clear that it should not be used, except for gsync on x11 nvidia.
|
||||
|
||||
Add window capture option (for x11).
|
||||
|
||||
Add systray for recording status.
|
||||
|
||||
Add a desktop icon when gsr-ui has a window mode option (which should be the default launch option).
|
||||
@@ -186,10 +168,82 @@ Add a bug report page that automatically includes system info (make this clear t
|
||||
|
||||
Make it possible to change controller hotkeys. Also read from /dev/input/eventN instead of /dev/input/jsN. This is readable for controllers.
|
||||
|
||||
Add option to copy screenshot to clipboard. Does it work properly on Wayland compositors? Maybe need to wait until the application becomes Wayland native instead of XWayland.
|
||||
|
||||
Show message that replay/streaming has to be restarted if recording settings are changed while replay/streaming is ongoing.
|
||||
|
||||
Support vector graphics. Maybe support svg, rendering it to a texture for better performance.
|
||||
|
||||
Support freetype for text rendering. Maybe load freetype as runtime (with dlopen) and use that when available and fallback to stb_freetype if not available.
|
||||
|
||||
Show .webm container option. It's currently chosen automatically if vp8/vp9 is chosen. The available containers should automatically switch depending on the video codec.
|
||||
|
||||
In settings show audio levels for each audio. Maybe show audio level image beside the audio name in the dropdown box and switch to a different image (have 3-4 different images for each level) depending on the volume.
|
||||
|
||||
Only use fake cursor on wayland if the focused x11 window is fullscreen.
|
||||
|
||||
Create window as a real overlay window, using layer shell protocol, when possible. This will however minimize windows on floating wms. Check if this can be fixed somehow, or only use layer shell in tiling wms.
|
||||
|
||||
Add timeout option for screenshots.
|
||||
|
||||
Add a window that shows a warning for wayland users, that wayland doesn't support this software and if they experience issues then they should use x11 instead.
|
||||
|
||||
Add a window that shows a warning if gpu video encoding isn't supported.
|
||||
|
||||
Disable system notifications when recording. Does the notification dbus interface support pausing notifications?
|
||||
|
||||
Disable hotkeys if virtual keyboard is found (either at startup or after running), if grab type if not virtual. Show a notification if that happens that hotkeys have been disabled.
|
||||
Detect if keyboard is locked by listening to gsr-ui virtual keyboard events and if no event is received after pressing a key (when writing to it after receiving input from another keyboard)
|
||||
then remove the keyboard grab and show a message or something.
|
||||
This can happen if the gsr-ui virtual keyboard is grabbed by some other software.
|
||||
Maybe this can be fixed automatically by grabbing gsr-ui virtual keyboard and releasing it just before we write to it and then release it again.
|
||||
But wont keyboard remapping software grab the keyboard first if they detect it quickly?
|
||||
If we fail to grab it because some other software did then dont grab any keyboards nor gsr-ui virtual keyboards, just listen to them.
|
||||
|
||||
Support localization.
|
||||
|
||||
Add option to not capture cursor in screenshot when doing region/window capture.
|
||||
|
||||
Window selection doesn't work when a window is fullscreen on x11.
|
||||
|
||||
Make it possible to change replay duration of the "save 1 min" and "save 10 min" by adding them to the replay settings as options.
|
||||
|
||||
If replay duration is set below the "save 1 min" or "save 10 min" then gray them out and when hovering over those buttons
|
||||
show a tooltip that says that those buttons cant be used because the replay duration in replay settings is set to a lower value than that (and display the replay duration there).
|
||||
|
||||
The UI is unusable on a vertical monitor.
|
||||
|
||||
Steam overlay interfers with controller input in gsr ui. Maybe move controller handling the gsr-global-hotkeys to do a grab on the controller, to only give the key input to gsr ui.
|
||||
|
||||
For joysticks (gamepads) create a virtual device for each one (/dev/uinput) that has the same vendor, product and name. This is to make sure that it behaves the same way in applications since applications
|
||||
access joysticks directly through /dev/input/eventN or /dev/input/jsN. It needs the same number of buttons and pretend to be a controller of the same time, for example a ps4 controller
|
||||
so that games automatically display ps4 buttons if supported.
|
||||
This also allows us to copy event bits and other data from the device instead of saying we support everything.
|
||||
This should fix the issue of not being able to write to /dev/uinput with ABS_X event bit set.
|
||||
Maybe do this for regular keyboard inputs as well?
|
||||
|
||||
Use generic icons for controller input in settings and allow configuring them.
|
||||
|
||||
Add option to include game name in file name (video and screenshot). Replace / with space.
|
||||
|
||||
Check if the focused window is on top on x11 when choosing to take screenshot or show the window as the background of the overlay.
|
||||
|
||||
Convert clipboard image to requested type (from jpg to png for example).
|
||||
|
||||
Save clipboard image with wayland on wayland. Some wayland compositors (such as hyprland, budgie and maybe more (wlroots based ones?)) don't support copying clipboard image data from x11 applications to wayland applications.
|
||||
This can be done because retarded wayland only supports setting clipboard when the application has focus. This doesn't work with hotkey screenshot use.
|
||||
This is specifically an issue when using wl_data_device_manager, which is a standard protocol. It can be done when using wlr specific protocol.
|
||||
|
||||
When gsr supports pausing recording done in replay/streaming session then add support for that in gsr-ui as well.
|
||||
|
||||
Add recording timer when recording to show for how long we have been recording for. Maybe the same for live streaming and replay.
|
||||
|
||||
Add option to trim video in the ui. Show a list of all videos recorded so you dont have to navigate to them (maybe add option to manually navigate to a video as well). Maybe use mpv to view it (embedded) in the ui and trim regions (multiple) and ffmpeg command to trim it.
|
||||
|
||||
Show the currently recorded capture in the ui, to preview if everything looks ok. This is also good for webcam overlay. Do this when gsr supports rpc, which would also include an option to get a fd to the capture texture.
|
||||
|
||||
Show a question mark beside options. When hovering the question mark show a tooltip that explains the options.
|
||||
|
||||
Remove all mgl::Clock usage in Overlay. We only need to get the time once per update in Overlay::handle_events. Also get time in other places outside handle_events.
|
||||
|
||||
Handle stopping replay/stream when recording is running (show notification that the video is saved and move the video to folder with game name).
|
||||
|
||||
Support translations.
|
||||
Submodule depends/mglpp updated: a784fdb62b...b569045831
@@ -2,7 +2,7 @@
|
||||
Description=GPU Screen Recorder UI Service
|
||||
|
||||
[Service]
|
||||
ExecStart=flatpak run com.dec05eba.gpu_screen_recorder gsr-ui launch-daemon
|
||||
ExecStart=flatpak run com.dec05eba.gpu_screen_recorder gsr-ui
|
||||
KillSignal=SIGINT
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
BIN
images/masked.png
Normal file
BIN
images/masked.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 930 B |
BIN
images/unmasked.png
Normal file
BIN
images/unmasked.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
60
include/ClipboardFile.hpp
Normal file
60
include/ClipboardFile.hpp
Normal file
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
namespace gsr {
|
||||
struct ClipboardCopy {
|
||||
Window requestor = None;
|
||||
uint64_t file_offset = 0;
|
||||
Atom property = None;
|
||||
Atom requestor_target = None;
|
||||
};
|
||||
|
||||
class ClipboardFile {
|
||||
public:
|
||||
enum class FileType {
|
||||
JPG,
|
||||
PNG
|
||||
};
|
||||
|
||||
ClipboardFile();
|
||||
~ClipboardFile();
|
||||
ClipboardFile(const ClipboardFile&) = delete;
|
||||
ClipboardFile& operator=(const ClipboardFile&) = delete;
|
||||
|
||||
// Set this to an empty string to unset clipboard
|
||||
void set_current_file(const std::string &filepath, FileType file_type);
|
||||
private:
|
||||
bool file_type_matches_request_atom(FileType file_type, Atom request_atom);
|
||||
const char* file_type_clipboard_get_name(Atom request_atom);
|
||||
const char* file_type_get_name(FileType file_type);
|
||||
void send_clipboard_start(XSelectionRequestEvent *xselectionrequest);
|
||||
void transfer_clipboard_data(XSelectionRequestEvent *xselectionrequest, ClipboardCopy *clipboard_copy);
|
||||
ClipboardCopy* get_clipboard_copy_by_requestor(Window requestor);
|
||||
void remove_clipboard_copy(Window requestor);
|
||||
private:
|
||||
Display *dpy = nullptr;
|
||||
Window clipboard_window = None;
|
||||
int file_fd = -1;
|
||||
uint64_t file_size = 0;
|
||||
FileType file_type = FileType::JPG;
|
||||
|
||||
Atom incr_atom = None;
|
||||
Atom targets_atom = None;
|
||||
Atom clipboard_atom = None;
|
||||
Atom image_jpg_atom = None;
|
||||
Atom image_jpeg_atom = None;
|
||||
Atom image_png_atom = None;
|
||||
|
||||
std::thread event_thread;
|
||||
std::mutex mutex;
|
||||
bool running = true;
|
||||
|
||||
std::vector<ClipboardCopy> clipboard_copies;
|
||||
bool should_clear_selection = false;
|
||||
};
|
||||
}
|
||||
@@ -45,7 +45,7 @@ namespace gsr {
|
||||
int32_t video_width = 0;
|
||||
int32_t video_height = 0;
|
||||
int32_t fps = 60;
|
||||
int32_t video_bitrate = 15000;
|
||||
int32_t video_bitrate = 8000;
|
||||
bool merge_audio_tracks = true; // TODO: Remove in the future
|
||||
bool application_audio_invert = false; // TODO: Remove in the future
|
||||
bool change_video_resolution = false;
|
||||
@@ -55,19 +55,24 @@ namespace gsr {
|
||||
std::string video_quality = "very_high";
|
||||
std::string video_codec = "auto";
|
||||
std::string audio_codec = "opus";
|
||||
std::string framerate_mode = "vfr";
|
||||
std::string framerate_mode = "auto";
|
||||
bool advanced_view = false;
|
||||
bool overclock = false;
|
||||
bool record_cursor = true;
|
||||
bool restore_portal_session = true;
|
||||
|
||||
bool show_notifications = true;
|
||||
bool use_led_indicator = false;
|
||||
};
|
||||
|
||||
struct MainConfig {
|
||||
int32_t config_file_version = GSR_CONFIG_FILE_VERSION;
|
||||
bool software_encoding_warning_shown = false;
|
||||
bool wayland_warning_shown = false;
|
||||
std::string hotkeys_enable_option = "enable_hotkeys";
|
||||
std::string joystick_hotkeys_enable_option = "disable_hotkeys";
|
||||
std::string tint_color;
|
||||
std::string notification_speed = "normal";
|
||||
ConfigHotkey show_hide_hotkey;
|
||||
};
|
||||
|
||||
@@ -79,18 +84,22 @@ namespace gsr {
|
||||
std::string stream_key;
|
||||
};
|
||||
|
||||
struct RumbleStreamConfig {
|
||||
std::string stream_key;
|
||||
};
|
||||
|
||||
struct CustomStreamConfig {
|
||||
std::string url;
|
||||
std::string key;
|
||||
std::string container = "flv";
|
||||
};
|
||||
|
||||
struct StreamingConfig {
|
||||
RecordOptions record_options;
|
||||
bool show_streaming_started_notifications = true;
|
||||
bool show_streaming_stopped_notifications = true;
|
||||
std::string streaming_service = "twitch";
|
||||
YoutubeStreamConfig youtube;
|
||||
TwitchStreamConfig twitch;
|
||||
RumbleStreamConfig rumble;
|
||||
CustomStreamConfig custom;
|
||||
ConfigHotkey start_stop_hotkey;
|
||||
};
|
||||
@@ -98,9 +107,6 @@ namespace gsr {
|
||||
struct RecordConfig {
|
||||
RecordOptions record_options;
|
||||
bool save_video_in_game_folder = false;
|
||||
bool show_recording_started_notifications = true;
|
||||
bool show_video_saved_notifications = true;
|
||||
bool show_video_paused_notifications = true;
|
||||
std::string save_directory;
|
||||
std::string container = "mp4";
|
||||
ConfigHotkey start_stop_hotkey;
|
||||
@@ -112,9 +118,6 @@ namespace gsr {
|
||||
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;
|
||||
@@ -136,10 +139,13 @@ namespace gsr {
|
||||
bool restore_portal_session = true;
|
||||
|
||||
bool save_screenshot_in_game_folder = false;
|
||||
bool show_screenshot_saved_notifications = true;
|
||||
bool save_screenshot_to_clipboard = false;
|
||||
bool show_notifications = true;
|
||||
bool use_led_indicator = false;
|
||||
std::string save_directory;
|
||||
ConfigHotkey take_screenshot_hotkey;
|
||||
ConfigHotkey take_screenshot_region_hotkey;
|
||||
ConfigHotkey take_screenshot_window_hotkey; // Or desktop portal, on wayland
|
||||
};
|
||||
|
||||
struct Config {
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace gsr {
|
||||
std::vector<WaylandOutput> monitors;
|
||||
struct zxdg_output_manager_v1 *xdg_output_manager = nullptr;
|
||||
private:
|
||||
void clear_monitors();
|
||||
void set_monitor_outputs_from_xdg_output(struct wl_display *dpy);
|
||||
private:
|
||||
int drm_fd = -1;
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
#include "../Hotplug.hpp"
|
||||
#include <unordered_map>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <poll.h>
|
||||
#include <linux/joystick.h>
|
||||
#include <linux/input.h>
|
||||
|
||||
namespace gsr {
|
||||
static constexpr int max_js_poll_fd = 16;
|
||||
@@ -30,8 +32,10 @@ namespace gsr {
|
||||
bool bind_action(const std::string &id, GlobalHotkeyCallback callback) override;
|
||||
void poll_events() override;
|
||||
private:
|
||||
void close_fds();
|
||||
void read_events();
|
||||
void process_js_event(int fd, js_event &event);
|
||||
void process_input_event(int fd, input_event &event);
|
||||
void add_all_joystick_devices();
|
||||
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);
|
||||
@@ -45,6 +49,11 @@ namespace gsr {
|
||||
std::unordered_map<std::string, GlobalHotkeyCallback> bound_actions_by_id;
|
||||
std::thread read_thread;
|
||||
|
||||
std::thread close_fd_thread;
|
||||
std::vector<int> fds_to_close;
|
||||
std::mutex close_fd_mutex;
|
||||
std::condition_variable close_fd_cv;
|
||||
|
||||
pollfd poll_fd[max_js_poll_fd];
|
||||
ExtraData extra_data[max_js_poll_fd];
|
||||
int num_poll_fd = 0;
|
||||
|
||||
@@ -9,7 +9,8 @@ namespace gsr {
|
||||
public:
|
||||
enum class GrabType {
|
||||
ALL,
|
||||
VIRTUAL
|
||||
VIRTUAL,
|
||||
NO_GRAB
|
||||
};
|
||||
|
||||
GlobalHotkeysLinux(GrabType grab_type);
|
||||
@@ -21,6 +22,8 @@ namespace gsr {
|
||||
bool bind_key_press(Hotkey hotkey, const std::string &id, GlobalHotkeyCallback callback) override;
|
||||
void unbind_all_keys() override;
|
||||
void poll_events() override;
|
||||
|
||||
std::function<void()> on_gsr_ui_virtual_keyboard_grabbed;
|
||||
private:
|
||||
void close_fds();
|
||||
private:
|
||||
|
||||
33
include/LedIndicator.hpp
Normal file
33
include/LedIndicator.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <vector>
|
||||
#include <mglpp/system/Clock.hpp>
|
||||
|
||||
namespace gsr {
|
||||
class LedIndicator {
|
||||
public:
|
||||
LedIndicator();
|
||||
LedIndicator(const LedIndicator&) = delete;
|
||||
LedIndicator& operator=(const LedIndicator&) = delete;
|
||||
~LedIndicator();
|
||||
|
||||
void set_led(bool enabled);
|
||||
void blink();
|
||||
void update();
|
||||
private:
|
||||
bool run_gsr_global_hotkeys_set_leds(bool enabled);
|
||||
void update_led(bool new_state);
|
||||
void update_led_with_active_status();
|
||||
void check_led_status_outdated();
|
||||
private:
|
||||
pid_t gsr_global_hotkeys_pid = -1;
|
||||
bool led_indicator_on = false;
|
||||
bool led_enabled = false;
|
||||
bool perform_blink = false;
|
||||
mgl::Clock blink_timer;
|
||||
|
||||
std::vector<int> led_brightness_files;
|
||||
mgl::Clock read_led_brightness_timer;
|
||||
};
|
||||
}
|
||||
@@ -9,6 +9,9 @@
|
||||
#include "GlobalHotkeys/GlobalHotkeysJoystick.hpp"
|
||||
#include "AudioPlayer.hpp"
|
||||
#include "RegionSelector.hpp"
|
||||
#include "WindowSelector.hpp"
|
||||
#include "ClipboardFile.hpp"
|
||||
#include "LedIndicator.hpp"
|
||||
#include "CursorTracker/CursorTracker.hpp"
|
||||
|
||||
#include <mglpp/window/Window.hpp>
|
||||
@@ -40,6 +43,22 @@ namespace gsr {
|
||||
SCREENSHOT
|
||||
};
|
||||
|
||||
enum class NotificationLevel {
|
||||
INFO,
|
||||
ERROR,
|
||||
};
|
||||
|
||||
enum class ScreenshotForceType {
|
||||
NONE,
|
||||
REGION,
|
||||
WINDOW
|
||||
};
|
||||
|
||||
enum class NotificationSpeed {
|
||||
NORMAL,
|
||||
FAST
|
||||
};
|
||||
|
||||
class Overlay {
|
||||
public:
|
||||
Overlay(std::string resources_path, GsrInfo gsr_info, SupportedCaptureOptions capture_options, egl_functions egl_funcs);
|
||||
@@ -52,7 +71,7 @@ namespace gsr {
|
||||
bool draw();
|
||||
|
||||
void show();
|
||||
void hide();
|
||||
void hide_next_frame();
|
||||
void toggle_show();
|
||||
void toggle_record();
|
||||
void toggle_pause();
|
||||
@@ -63,7 +82,8 @@ namespace gsr {
|
||||
void save_replay_10_min();
|
||||
void take_screenshot();
|
||||
void take_screenshot_region();
|
||||
void show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type, const char *capture_target = nullptr);
|
||||
void take_screenshot_window();
|
||||
void show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type, const char *capture_target = nullptr, NotificationLevel notification_level = NotificationLevel::INFO);
|
||||
bool is_open() const;
|
||||
bool should_exit(std::string &reason) const;
|
||||
void exit();
|
||||
@@ -73,10 +93,19 @@ namespace gsr {
|
||||
|
||||
void unbind_all_keyboard_hotkeys();
|
||||
void rebind_all_keyboard_hotkeys();
|
||||
|
||||
void set_notification_speed(NotificationSpeed notification_speed);
|
||||
|
||||
bool global_hotkeys_ungrab_keyboard = false;
|
||||
private:
|
||||
void update_upause_status();
|
||||
|
||||
void hide();
|
||||
|
||||
void handle_keyboard_mapping_event();
|
||||
void on_event(mgl::Event &event);
|
||||
|
||||
void recreate_global_hotkeys(const char *hotkey_option);
|
||||
void create_frontpage_ui_components();
|
||||
void xi_setup();
|
||||
void handle_xi_events();
|
||||
@@ -86,10 +115,12 @@ namespace gsr {
|
||||
|
||||
void close_gpu_screen_recorder_output();
|
||||
|
||||
double get_time_passed_in_replay_buffer_seconds();
|
||||
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 process_gsr_output();
|
||||
void on_gsr_process_error(int exit_code, NotificationType notification_type);
|
||||
void update_gsr_process_status();
|
||||
void update_gsr_screenshot_process_status();
|
||||
|
||||
@@ -116,10 +147,10 @@ namespace gsr {
|
||||
void on_press_save_replay();
|
||||
void on_press_save_replay_1_min_replay();
|
||||
void on_press_save_replay_10_min_replay();
|
||||
bool on_press_start_replay(bool disable_notification, bool finished_region_selection);
|
||||
void on_press_start_record(bool finished_region_selection);
|
||||
void on_press_start_stream(bool finished_region_selection);
|
||||
void on_press_take_screenshot(bool finished_region_selection, bool force_region_capture);
|
||||
bool on_press_start_replay(bool disable_notification, bool finished_selection);
|
||||
void on_press_start_record(bool finished_selection);
|
||||
void on_press_start_stream(bool finished_selection);
|
||||
void on_press_take_screenshot(bool finished_selection, ScreenshotForceType force_type);
|
||||
bool update_compositor_texture(const Monitor &monitor);
|
||||
|
||||
std::string get_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options);
|
||||
@@ -138,6 +169,7 @@ namespace gsr {
|
||||
GsrInfo gsr_info;
|
||||
egl_functions egl_funcs;
|
||||
Config config;
|
||||
Config current_recording_config;
|
||||
|
||||
bool visible = false;
|
||||
|
||||
@@ -174,6 +206,8 @@ namespace gsr {
|
||||
|
||||
RecordingStatus recording_status = RecordingStatus::NONE;
|
||||
bool paused = false;
|
||||
mgl::Clock paused_clock;
|
||||
double paused_total_time_seconds = 0.0;
|
||||
|
||||
mgl::Clock replay_status_update_clock;
|
||||
std::string power_supply_online_filepath;
|
||||
@@ -202,7 +236,7 @@ namespace gsr {
|
||||
|
||||
std::unique_ptr<GlobalHotkeys> global_hotkeys = nullptr;
|
||||
std::unique_ptr<GlobalHotkeysJoystick> global_hotkeys_js = nullptr;
|
||||
Display *x11_mapping_display = nullptr;
|
||||
Display *x11_dpy = nullptr;
|
||||
XEvent x11_mapping_xev;
|
||||
|
||||
mgl::Clock replay_save_clock;
|
||||
@@ -211,16 +245,32 @@ namespace gsr {
|
||||
bool try_replay_startup = true;
|
||||
bool replay_recording = false;
|
||||
int replay_save_duration_min = 0;
|
||||
mgl::Clock replay_duration_clock;
|
||||
double replay_saved_duration_sec = 0.0;
|
||||
bool replay_restart_on_save = false;
|
||||
|
||||
mgl::Clock recording_duration_clock;
|
||||
|
||||
AudioPlayer audio_player;
|
||||
|
||||
RegionSelector region_selector;
|
||||
bool start_region_capture = false;
|
||||
std::function<void()> on_region_selected;
|
||||
|
||||
WindowSelector window_selector;
|
||||
bool start_window_capture = false;
|
||||
std::function<void()> on_window_selected;
|
||||
|
||||
std::string recording_capture_target;
|
||||
std::string screenshot_capture_target;
|
||||
|
||||
std::unique_ptr<CursorTracker> cursor_tracker;
|
||||
mgl::Clock cursor_tracker_update_clock;
|
||||
|
||||
bool hide_ui = false;
|
||||
double notification_duration_multiplier = 1.0;
|
||||
ClipboardFile clipboard_file;
|
||||
|
||||
std::unique_ptr<LedIndicator> led_indicator = nullptr;
|
||||
};
|
||||
}
|
||||
@@ -26,7 +26,6 @@ namespace gsr {
|
||||
|
||||
bool failed() const;
|
||||
bool poll_events();
|
||||
bool is_selected() const;
|
||||
bool take_selection();
|
||||
bool take_canceled();
|
||||
Region get_selection() const;
|
||||
|
||||
@@ -4,31 +4,47 @@
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <poll.h>
|
||||
|
||||
typedef struct _IO_FILE FILE;
|
||||
#define GSR_RPC_MAX_CONNECTIONS 8
|
||||
#define GSR_RPC_MAX_POLLS (1 + GSR_RPC_MAX_CONNECTIONS) /* +1 to include the socket_fd itself for accept */
|
||||
#define GSR_RPC_MAX_MESSAGE_SIZE 128
|
||||
|
||||
namespace gsr {
|
||||
using RpcCallback = std::function<void(const std::string &name)>;
|
||||
|
||||
enum class RpcOpenResult {
|
||||
OK,
|
||||
CONNECTION_REFUSED,
|
||||
ERROR
|
||||
};
|
||||
|
||||
class Rpc {
|
||||
public:
|
||||
Rpc() = default;
|
||||
struct PollData {
|
||||
char buffer[GSR_RPC_MAX_MESSAGE_SIZE];
|
||||
int buffer_size = 0;
|
||||
};
|
||||
|
||||
Rpc();
|
||||
Rpc(const Rpc&) = delete;
|
||||
Rpc& operator=(const Rpc&) = delete;
|
||||
~Rpc();
|
||||
|
||||
bool create(const char *name);
|
||||
bool open(const char *name);
|
||||
RpcOpenResult open(const char *name);
|
||||
bool write(const char *str, size_t size);
|
||||
void poll();
|
||||
|
||||
bool add_handler(const std::string &name, RpcCallback callback);
|
||||
private:
|
||||
bool open_filepath(const char *filepath);
|
||||
void handle_client_data(int client_fd, PollData &poll_data);
|
||||
private:
|
||||
int fd = 0;
|
||||
FILE *file = nullptr;
|
||||
std::string fifo_filepath;
|
||||
int socket_fd = 0;
|
||||
std::string socket_filepath;
|
||||
struct pollfd polls[GSR_RPC_MAX_POLLS];
|
||||
PollData polls_data[GSR_RPC_MAX_POLLS];
|
||||
int num_polls = 0;
|
||||
std::unordered_map<std::string, RpcCallback> handlers_by_name;
|
||||
};
|
||||
}
|
||||
@@ -44,6 +44,8 @@ namespace gsr {
|
||||
mgl::Texture save_texture;
|
||||
mgl::Texture screenshot_texture;
|
||||
mgl::Texture trash_texture;
|
||||
mgl::Texture masked_texture;
|
||||
mgl::Texture unmasked_texture;
|
||||
|
||||
mgl::Texture ps4_home_texture;
|
||||
mgl::Texture ps4_options_texture;
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace gsr {
|
||||
void string_split_char(std::string_view str, char delimiter, StringSplitCallback callback_func);
|
||||
bool starts_with(std::string_view str, const char *substr);
|
||||
bool ends_with(std::string_view str, const char *substr);
|
||||
std::string strip(const std::string &str);
|
||||
|
||||
std::string get_home_dir();
|
||||
std::string get_config_dir();
|
||||
|
||||
33
include/WindowSelector.hpp
Normal file
33
include/WindowSelector.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
#include <mglpp/graphics/Color.hpp>
|
||||
|
||||
namespace gsr {
|
||||
class WindowSelector {
|
||||
public:
|
||||
WindowSelector();
|
||||
WindowSelector(const WindowSelector&) = delete;
|
||||
WindowSelector& operator=(const WindowSelector&) = delete;
|
||||
~WindowSelector();
|
||||
|
||||
bool start(mgl::Color border_color);
|
||||
void stop();
|
||||
bool is_started() const;
|
||||
|
||||
bool failed() const;
|
||||
bool poll_events();
|
||||
bool take_selection();
|
||||
bool take_canceled();
|
||||
Window get_selection() const;
|
||||
private:
|
||||
Display *dpy = nullptr;
|
||||
Cursor crosshair_cursor = None;
|
||||
Colormap border_window_colormap = None;
|
||||
Window border_window = None;
|
||||
Window selected_window = None;
|
||||
bool selected = false;
|
||||
bool canceled = false;
|
||||
};
|
||||
}
|
||||
@@ -24,6 +24,7 @@ namespace gsr {
|
||||
std::string get_window_name_at_position(Display *dpy, mgl::vec2i position, Window ignore_window);
|
||||
std::string get_window_name_at_cursor_position(Display *dpy, Window ignore_window);
|
||||
void set_window_size_not_resizable(Display *dpy, Window window, int width, int height);
|
||||
Window window_get_target_window_child(Display *display, Window window);
|
||||
mgl::vec2i get_cursor_position(Display *dpy, Window *window);
|
||||
mgl::vec2i create_window_get_center_position(Display *display);
|
||||
std::string get_window_manager_name(Display *display);
|
||||
|
||||
@@ -19,7 +19,9 @@ namespace gsr {
|
||||
void draw(mgl::Window &window, mgl::vec2f offset) override;
|
||||
|
||||
void add_item(const std::string &text, const std::string &id);
|
||||
// The item can only be selected if it's enabled
|
||||
void set_selected_item(const std::string &id, bool trigger_event = true, bool trigger_event_even_if_selection_not_changed = true);
|
||||
void set_item_enabled(const std::string &id, bool enabled);
|
||||
const std::string& get_selected_id() const;
|
||||
|
||||
mgl::vec2f get_size() override;
|
||||
@@ -36,6 +38,7 @@ namespace gsr {
|
||||
mgl::Text text;
|
||||
std::string id;
|
||||
mgl::vec2f position;
|
||||
bool enabled = true;
|
||||
};
|
||||
|
||||
mgl::vec2f max_size;
|
||||
|
||||
@@ -4,13 +4,31 @@
|
||||
#include <functional>
|
||||
|
||||
#include <mglpp/graphics/Color.hpp>
|
||||
#include <mglpp/graphics/Text.hpp>
|
||||
#include <mglpp/graphics/Text32.hpp>
|
||||
#include <mglpp/graphics/Rectangle.hpp>
|
||||
|
||||
namespace gsr {
|
||||
using EntryValidateHandler = std::function<bool(std::string &str)>;
|
||||
class Entry;
|
||||
|
||||
enum class EntryValidateHandlerResult {
|
||||
DENY,
|
||||
ALLOW,
|
||||
REPLACED
|
||||
};
|
||||
using EntryValidateHandler = std::function<EntryValidateHandlerResult(Entry &entry, const std::u32string &str)>;
|
||||
|
||||
struct CaretIndexPos {
|
||||
int index;
|
||||
mgl::vec2f pos;
|
||||
};
|
||||
|
||||
class Entry : public Widget {
|
||||
public:
|
||||
enum class Direction {
|
||||
LEFT,
|
||||
RIGHT
|
||||
};
|
||||
|
||||
Entry(mgl::Font *font, const char *text, float max_width);
|
||||
Entry(const Entry&) = delete;
|
||||
Entry& operator=(const Entry&) = delete;
|
||||
@@ -20,8 +38,11 @@ namespace gsr {
|
||||
|
||||
mgl::vec2f get_size() override;
|
||||
|
||||
void set_text(std::string str);
|
||||
const std::string& get_text() const;
|
||||
EntryValidateHandlerResult set_text(const std::string &str);
|
||||
std::string get_text() const;
|
||||
|
||||
void set_masked(bool masked);
|
||||
bool is_masked() const;
|
||||
|
||||
// Return false to specify that the string should not be accepted. This reverts the string back to its previous value.
|
||||
// The input can be changed by changing the input parameter and returning true.
|
||||
@@ -29,10 +50,31 @@ namespace gsr {
|
||||
|
||||
std::function<void(const std::string &text)> on_changed;
|
||||
private:
|
||||
mgl::Text text;
|
||||
// Also updates the cursor position
|
||||
void replace_text(size_t index, size_t size, const std::u32string &replacement);
|
||||
void move_caret_word(Direction direction, size_t max_codepoints);
|
||||
EntryValidateHandlerResult set_text_internal(std::u32string str);
|
||||
void draw_caret(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size);
|
||||
void draw_caret_selection(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size);
|
||||
CaretIndexPos find_closest_caret_index_by_position(mgl::vec2f position);
|
||||
private:
|
||||
struct Caret {
|
||||
float offset_x = 0.0f;
|
||||
int index = 0;
|
||||
};
|
||||
|
||||
mgl::Rectangle background;
|
||||
mgl::Text32 text;
|
||||
mgl::Text32 masked_text;
|
||||
float max_width;
|
||||
bool selected = false;
|
||||
float caret_offset_x = 0.0f;
|
||||
bool selecting_text = false;
|
||||
bool selecting_with_keyboard = false;
|
||||
bool show_selection = false;
|
||||
bool masked = false;
|
||||
Caret caret;
|
||||
Caret selection_start_caret;
|
||||
float text_overflow = 0.0f;
|
||||
};
|
||||
|
||||
EntryValidateHandler create_entry_validator_integer_in_range(int min, int max);
|
||||
|
||||
@@ -29,6 +29,7 @@ namespace gsr {
|
||||
STREAM_START_STOP,
|
||||
TAKE_SCREENSHOT,
|
||||
TAKE_SCREENSHOT_REGION,
|
||||
TAKE_SCREENSHOT_WINDOW,
|
||||
SHOW_HIDE
|
||||
};
|
||||
|
||||
@@ -63,13 +64,16 @@ namespace gsr {
|
||||
std::unique_ptr<List> create_stream_hotkey_options();
|
||||
std::unique_ptr<List> create_screenshot_hotkey_options();
|
||||
std::unique_ptr<List> create_screenshot_region_hotkey_options();
|
||||
std::unique_ptr<List> create_screenshot_window_hotkey_options();
|
||||
std::unique_ptr<List> create_hotkey_control_buttons();
|
||||
std::unique_ptr<Subsection> create_keyboard_hotkey_subsection(ScrollablePage *parent_page);
|
||||
std::unique_ptr<Subsection> create_controller_hotkey_subsection(ScrollablePage *parent_page);
|
||||
std::unique_ptr<Button> create_exit_program_button();
|
||||
std::unique_ptr<Button> create_go_back_to_old_ui_button();
|
||||
std::unique_ptr<List> create_notification_speed();
|
||||
std::unique_ptr<Subsection> create_application_options_subsection(ScrollablePage *parent_page);
|
||||
std::unique_ptr<Subsection> create_application_info_subsection(ScrollablePage *parent_page);
|
||||
std::unique_ptr<Subsection> create_donate_subsection(ScrollablePage *parent_page);
|
||||
void add_widgets();
|
||||
|
||||
Button* configure_hotkey_get_button_by_active_type();
|
||||
@@ -99,7 +103,9 @@ namespace gsr {
|
||||
Button *start_stop_streaming_button_ptr = nullptr;
|
||||
Button *take_screenshot_button_ptr = nullptr;
|
||||
Button *take_screenshot_region_button_ptr = nullptr;
|
||||
Button *take_screenshot_window_button_ptr = nullptr;
|
||||
Button *show_hide_button_ptr = nullptr;
|
||||
RadioButton *notification_speed_button_ptr = nullptr;
|
||||
|
||||
ConfigHotkey configure_config_hotkey;
|
||||
ConfigureHotkeyType configure_hotkey_type = ConfigureHotkeyType::NONE;
|
||||
|
||||
@@ -26,7 +26,6 @@ namespace gsr {
|
||||
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();
|
||||
@@ -43,8 +42,11 @@ namespace gsr {
|
||||
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<CheckBox> create_save_screenshot_to_clipboard();
|
||||
std::unique_ptr<Widget> create_notifications();
|
||||
std::unique_ptr<Widget> create_led_indicator();
|
||||
std::unique_ptr<Widget> create_general_section();
|
||||
std::unique_ptr<Widget> create_notifications_section();
|
||||
std::unique_ptr<Widget> create_screenshot_indicator_section();
|
||||
std::unique_ptr<Widget> create_settings();
|
||||
void add_widgets();
|
||||
|
||||
@@ -56,7 +58,6 @@ namespace gsr {
|
||||
|
||||
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;
|
||||
@@ -71,7 +72,9 @@ namespace gsr {
|
||||
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;
|
||||
CheckBox *save_screenshot_to_clipboard_checkbox_ptr = nullptr;
|
||||
CheckBox *show_notification_checkbox_ptr = nullptr;
|
||||
CheckBox *led_indicator_checkbox_ptr = nullptr;
|
||||
|
||||
PageStack *page_stack = nullptr;
|
||||
};
|
||||
|
||||
@@ -46,7 +46,6 @@ namespace gsr {
|
||||
std::unique_ptr<RadioButton> create_view_radio_button();
|
||||
std::unique_ptr<ComboBox> create_record_area_box();
|
||||
std::unique_ptr<Widget> create_record_area();
|
||||
std::unique_ptr<List> create_select_window();
|
||||
std::unique_ptr<Entry> create_area_width_entry();
|
||||
std::unique_ptr<Entry> create_area_height_entry();
|
||||
std::unique_ptr<List> create_area_size();
|
||||
@@ -113,15 +112,20 @@ namespace gsr {
|
||||
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();
|
||||
std::unique_ptr<CheckBox> create_led_indicator(const char *type);
|
||||
std::unique_ptr<CheckBox> create_notifications(const char *type);
|
||||
std::unique_ptr<List> create_indicator(const char *type);
|
||||
void add_replay_widgets();
|
||||
void add_record_widgets();
|
||||
|
||||
std::unique_ptr<ComboBox> create_streaming_service_box();
|
||||
std::unique_ptr<List> create_streaming_service_section();
|
||||
std::unique_ptr<List> create_stream_key_section();
|
||||
std::unique_ptr<List> create_stream_url_section();
|
||||
std::unique_ptr<List> create_stream_custom_url();
|
||||
std::unique_ptr<List> create_stream_custom_key();
|
||||
std::unique_ptr<List> create_stream_custom_section();
|
||||
std::unique_ptr<ComboBox> create_stream_container_box();
|
||||
std::unique_ptr<List> create_stream_container_section();
|
||||
std::unique_ptr<List> create_stream_container();
|
||||
void add_stream_widgets();
|
||||
|
||||
void load_audio_tracks(const RecordOptions &record_options);
|
||||
@@ -135,7 +139,7 @@ namespace gsr {
|
||||
void save_record();
|
||||
void save_stream();
|
||||
|
||||
void view_changed(bool advanced_view, Subsection *notifications_subsection_ptr);
|
||||
void view_changed(bool advanced_view);
|
||||
private:
|
||||
Type type;
|
||||
Config &config;
|
||||
@@ -147,7 +151,6 @@ namespace gsr {
|
||||
GsrPage *content_page_ptr = nullptr;
|
||||
ScrollablePage *settings_scrollable_page_ptr = nullptr;
|
||||
List *settings_list_ptr = nullptr;
|
||||
List *select_window_list_ptr = nullptr;
|
||||
List *area_size_list_ptr = nullptr;
|
||||
List *video_resolution_list_ptr = nullptr;
|
||||
List *restore_portal_session_list_ptr = nullptr;
|
||||
@@ -175,30 +178,25 @@ namespace gsr {
|
||||
ComboBox *container_box_ptr = nullptr;
|
||||
ComboBox *streaming_service_box_ptr = nullptr;
|
||||
List *stream_key_list_ptr = nullptr;
|
||||
List *stream_url_list_ptr = nullptr;
|
||||
List *container_list_ptr = nullptr;
|
||||
List *custom_stream_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;
|
||||
CheckBox *show_replay_saved_notification_checkbox_ptr = nullptr;
|
||||
CheckBox *save_recording_in_game_folder_ptr = nullptr;
|
||||
CheckBox *show_recording_started_notification_checkbox_ptr = nullptr;
|
||||
CheckBox *show_video_saved_notification_checkbox_ptr = nullptr;
|
||||
CheckBox *show_video_paused_notification_checkbox_ptr = nullptr;
|
||||
CheckBox *show_streaming_started_notification_checkbox_ptr = nullptr;
|
||||
CheckBox *show_streaming_stopped_notification_checkbox_ptr = nullptr;
|
||||
Button *save_directory_button_ptr = nullptr;
|
||||
Entry *twitch_stream_key_entry_ptr = nullptr;
|
||||
Entry *youtube_stream_key_entry_ptr = nullptr;
|
||||
Entry *rumble_stream_key_entry_ptr = nullptr;
|
||||
Entry *stream_url_entry_ptr = nullptr;
|
||||
Entry *stream_key_entry_ptr = nullptr;
|
||||
Entry *replay_time_entry_ptr = nullptr;
|
||||
RadioButton *replay_storage_button_ptr = nullptr;
|
||||
Label *replay_time_label_ptr = nullptr;
|
||||
RadioButton *turn_on_replay_automatically_mode_ptr = nullptr;
|
||||
Subsection *audio_section_ptr = nullptr;
|
||||
List *audio_track_section_list_ptr = nullptr;
|
||||
CheckBox *led_indicator_checkbox_ptr = nullptr;
|
||||
CheckBox *show_notification_checkbox_ptr = nullptr;
|
||||
|
||||
PageStack *page_stack = nullptr;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <mglpp/system/vec.hpp>
|
||||
#include <mglpp/graphics/Color.hpp>
|
||||
#include <mglpp/window/Window.hpp>
|
||||
|
||||
namespace mgl {
|
||||
class Window;
|
||||
@@ -14,4 +15,5 @@ namespace gsr {
|
||||
void set_frame_delta_seconds(double frame_delta);
|
||||
mgl::vec2f scale_keep_aspect_ratio(mgl::vec2f from, mgl::vec2f to);
|
||||
mgl::vec2f clamp_keep_aspect_ratio(mgl::vec2f from, mgl::vec2f to);
|
||||
mgl::Scissor scissor_get_sub_area(mgl::Scissor parent, mgl::Scissor child);
|
||||
}
|
||||
@@ -45,6 +45,8 @@ namespace gsr {
|
||||
|
||||
void set_visible(bool visible);
|
||||
|
||||
Widget* get_parent_widget();
|
||||
|
||||
void *userdata = nullptr;
|
||||
protected:
|
||||
void set_widget_as_selected_in_parent();
|
||||
|
||||
10
meson.build
10
meson.build
@@ -1,4 +1,6 @@
|
||||
project('gsr-ui', ['c', 'cpp'], version : '1.6.4', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
|
||||
project('gsr-ui', ['c', 'cpp'], version : '1.8.2', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
|
||||
|
||||
add_project_arguments('-D_FILE_OFFSET_BITS=64', language : ['c', 'cpp'])
|
||||
|
||||
if get_option('buildtype') == 'debug'
|
||||
add_project_arguments('-g3', language : ['c', 'cpp'])
|
||||
@@ -40,12 +42,15 @@ src = [
|
||||
'src/Utils.cpp',
|
||||
'src/WindowUtils.cpp',
|
||||
'src/RegionSelector.cpp',
|
||||
'src/WindowSelector.cpp',
|
||||
'src/Config.cpp',
|
||||
'src/GsrInfo.cpp',
|
||||
'src/Process.cpp',
|
||||
'src/Overlay.cpp',
|
||||
'src/AudioPlayer.cpp',
|
||||
'src/Hotplug.cpp',
|
||||
'src/ClipboardFile.cpp',
|
||||
'src/LedIndicator.cpp',
|
||||
'src/Rpc.cpp',
|
||||
'src/main.cpp',
|
||||
]
|
||||
@@ -61,7 +66,7 @@ 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.6.0"', language: ['c', 'cpp'])
|
||||
add_project_arguments('-DGSR_FLATPAK_VERSION="5.9.0"', language: ['c', 'cpp'])
|
||||
|
||||
executable(
|
||||
meson.project_name(),
|
||||
@@ -89,6 +94,7 @@ executable(
|
||||
'tools/gsr-global-hotkeys/hotplug.c',
|
||||
'tools/gsr-global-hotkeys/keyboard_event.c',
|
||||
'tools/gsr-global-hotkeys/keys.c',
|
||||
'tools/gsr-global-hotkeys/leds.c',
|
||||
'tools/gsr-global-hotkeys/main.c'
|
||||
],
|
||||
c_args : '-fstack-protector-all',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "gsr-ui"
|
||||
type = "executable"
|
||||
version = "1.6.4"
|
||||
version = "1.8.2"
|
||||
platforms = ["posix"]
|
||||
|
||||
[lang.cpp]
|
||||
@@ -10,6 +10,9 @@ version = "c++17"
|
||||
[config]
|
||||
ignore_dirs = ["build", "tools"]
|
||||
|
||||
[define]
|
||||
_FILE_OFFSET_BITS = "64"
|
||||
|
||||
[dependencies]
|
||||
xcomposite = ">=0"
|
||||
xfixes = ">=0"
|
||||
|
||||
299
src/ClipboardFile.cpp
Normal file
299
src/ClipboardFile.cpp
Normal file
@@ -0,0 +1,299 @@
|
||||
#include "../include/ClipboardFile.hpp"
|
||||
#include <stdio.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
#include <poll.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <X11/Xatom.h>
|
||||
|
||||
#define FORMAT_I64 "%" PRIi64
|
||||
#define FORMAT_U64 "%" PRIu64
|
||||
|
||||
namespace gsr {
|
||||
ClipboardFile::ClipboardFile() {
|
||||
dpy = XOpenDisplay(nullptr);
|
||||
if(!dpy) {
|
||||
fprintf(stderr, "gsr ui: error: ClipboardFile: failed to connect to the X11 server\n");
|
||||
return;
|
||||
}
|
||||
|
||||
clipboard_window = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0, 0, 8, 8, 0, 0, 0);
|
||||
if(!clipboard_window) {
|
||||
fprintf(stderr, "gsr ui: error: ClipboardFile: failed to create clipboard window\n");
|
||||
XCloseDisplay(dpy);
|
||||
dpy = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
incr_atom = XInternAtom(dpy, "INCR", False);
|
||||
targets_atom = XInternAtom(dpy, "TARGETS", False);
|
||||
clipboard_atom = XInternAtom(dpy, "CLIPBOARD", False);
|
||||
image_jpg_atom = XInternAtom(dpy, "image/jpg", False);
|
||||
image_jpeg_atom = XInternAtom(dpy, "image/jpeg", False);
|
||||
image_png_atom = XInternAtom(dpy, "image/png", False);
|
||||
|
||||
event_thread = std::thread([&]() {
|
||||
pollfd poll_fds[1];
|
||||
poll_fds[0].fd = ConnectionNumber(dpy);
|
||||
poll_fds[0].events = POLLIN;
|
||||
poll_fds[0].revents = 0;
|
||||
|
||||
XEvent xev;
|
||||
while(running) {
|
||||
poll(poll_fds, 1, 100);
|
||||
while(XPending(dpy)) {
|
||||
XNextEvent(dpy, &xev);
|
||||
switch(xev.type) {
|
||||
case SelectionClear: {
|
||||
should_clear_selection = true;
|
||||
if(clipboard_copies.empty()) {
|
||||
should_clear_selection = false;
|
||||
set_current_file("", file_type);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SelectionRequest:
|
||||
send_clipboard_start(&xev.xselectionrequest);
|
||||
break;
|
||||
case PropertyNotify: {
|
||||
if(xev.xproperty.state == PropertyDelete) {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
ClipboardCopy *clipboard_copy = get_clipboard_copy_by_requestor(xev.xproperty.window);
|
||||
if(!clipboard_copy || xev.xproperty.atom != clipboard_copy->property)
|
||||
return;
|
||||
|
||||
XSelectionRequestEvent xselectionrequest;
|
||||
xselectionrequest.display = xev.xproperty.display;;
|
||||
xselectionrequest.requestor = xev.xproperty.window;
|
||||
xselectionrequest.selection = clipboard_atom;
|
||||
xselectionrequest.target = clipboard_copy->requestor_target;
|
||||
xselectionrequest.property = clipboard_copy->property;
|
||||
xselectionrequest.time = xev.xproperty.time;
|
||||
transfer_clipboard_data(&xselectionrequest, clipboard_copy);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ClipboardFile::~ClipboardFile() {
|
||||
running = false;
|
||||
if(event_thread.joinable())
|
||||
event_thread.join();
|
||||
|
||||
if(file_fd > 0)
|
||||
close(file_fd);
|
||||
|
||||
if(dpy) {
|
||||
XDestroyWindow(dpy, clipboard_window);
|
||||
XCloseDisplay(dpy);
|
||||
}
|
||||
}
|
||||
|
||||
bool ClipboardFile::file_type_matches_request_atom(FileType file_type, Atom request_atom) {
|
||||
switch(file_type) {
|
||||
case FileType::JPG:
|
||||
return request_atom == image_jpg_atom || request_atom == image_jpeg_atom;
|
||||
case FileType::PNG:
|
||||
return request_atom == image_png_atom;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* ClipboardFile::file_type_clipboard_get_name(Atom request_atom) {
|
||||
if(request_atom == image_jpg_atom)
|
||||
return "image/jpg";
|
||||
else if(request_atom == image_jpeg_atom)
|
||||
return "image/jpeg";
|
||||
else if(request_atom == image_png_atom)
|
||||
return "image/png";
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
const char* ClipboardFile::file_type_get_name(FileType file_type) {
|
||||
switch(file_type) {
|
||||
case FileType::JPG:
|
||||
return "image/jpeg";
|
||||
case FileType::PNG:
|
||||
return "image/png";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
void ClipboardFile::send_clipboard_start(XSelectionRequestEvent *xselectionrequest) {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
if(file_fd <= 0) {
|
||||
fprintf(stderr, "gsr ui: warning: ClipboardFile::send_clipboard: requestor window " FORMAT_I64 " tried to get clipboard from us but we don't have any clipboard file open\n", (int64_t)xselectionrequest->requestor);
|
||||
return;
|
||||
}
|
||||
|
||||
if(xselectionrequest->selection != clipboard_atom) {
|
||||
fprintf(stderr, "gsr ui: warning: ClipboardFile::send_clipboard: requestor window " FORMAT_I64 " tried to non-clipboard selection from us\n", (int64_t)xselectionrequest->requestor);
|
||||
return;
|
||||
}
|
||||
|
||||
XSelectionEvent selection_event;
|
||||
selection_event.type = SelectionNotify;
|
||||
selection_event.display = xselectionrequest->display;
|
||||
selection_event.requestor = xselectionrequest->requestor;
|
||||
selection_event.selection = xselectionrequest->selection;
|
||||
selection_event.property = xselectionrequest->property;
|
||||
selection_event.time = xselectionrequest->time;
|
||||
selection_event.target = xselectionrequest->target;
|
||||
|
||||
if(xselectionrequest->target == targets_atom) {
|
||||
int num_targets = 1;
|
||||
Atom targets[4];
|
||||
targets[0] = targets_atom;
|
||||
|
||||
switch(file_type) {
|
||||
case FileType::JPG:
|
||||
num_targets = 4;
|
||||
targets[1] = image_jpg_atom;
|
||||
targets[2] = image_jpeg_atom;
|
||||
targets[3] = image_png_atom;
|
||||
break;
|
||||
case FileType::PNG:
|
||||
num_targets = 2;
|
||||
targets[1] = image_png_atom;
|
||||
targets[2] = image_jpg_atom;
|
||||
targets[3] = image_jpeg_atom;
|
||||
break;
|
||||
}
|
||||
|
||||
XChangeProperty(dpy, selection_event.requestor, selection_event.property, XA_ATOM, 32, PropModeReplace, (unsigned char*)targets, num_targets);
|
||||
} else if(xselectionrequest->target == image_jpg_atom || xselectionrequest->target == image_jpeg_atom || xselectionrequest->target == image_png_atom) {
|
||||
// TODO: Convert image to requested image type. Right now sending a jpg file when a png file is requested works ok in browsers (discord and element)
|
||||
if(!file_type_matches_request_atom(file_type, xselectionrequest->target)) {
|
||||
const char *expected_file_type = file_type_get_name(file_type);
|
||||
fprintf(stderr, "gsr ui: warning: ClipboardFile::send_clipboard: requestor window " FORMAT_I64 " tried to request clipboard of type %s, but %s was expected. Ignoring requestor and sending as %s\n", (int64_t)xselectionrequest->requestor, file_type_clipboard_get_name(xselectionrequest->target), expected_file_type, expected_file_type);
|
||||
//return;
|
||||
}
|
||||
|
||||
ClipboardCopy *clipboard_copy = get_clipboard_copy_by_requestor(xselectionrequest->requestor);
|
||||
if(!clipboard_copy) {
|
||||
clipboard_copies.push_back({ xselectionrequest->requestor, 0 });
|
||||
clipboard_copy = &clipboard_copies.back();
|
||||
}
|
||||
|
||||
*clipboard_copy = { xselectionrequest->requestor, 0 };
|
||||
clipboard_copy->property = selection_event.property;
|
||||
clipboard_copy->requestor_target = selection_event.target;
|
||||
XSelectInput(dpy, selection_event.requestor, PropertyChangeMask);
|
||||
|
||||
const long lower_bound = std::min((uint64_t)1<<16, file_size);
|
||||
XChangeProperty(dpy, selection_event.requestor, selection_event.property, incr_atom, 32, PropModeReplace, (const unsigned char*)&lower_bound, 1);
|
||||
} else {
|
||||
char *target_clipboard_name = XGetAtomName(dpy, xselectionrequest->target);
|
||||
fprintf(stderr, "gsr ui: warning: ClipboardFile::send_clipboard: requestor window " FORMAT_I64 " tried to request clipboard of type %s, expected TARGETS, image/jpg, image/jpeg or image/png\n", (int64_t)xselectionrequest->requestor, target_clipboard_name ? target_clipboard_name : "Unknown");
|
||||
if(target_clipboard_name)
|
||||
XFree(target_clipboard_name);
|
||||
selection_event.property = None;
|
||||
}
|
||||
|
||||
XSendEvent(dpy, selection_event.requestor, False, NoEventMask, (XEvent*)&selection_event);
|
||||
XFlush(dpy);
|
||||
}
|
||||
|
||||
void ClipboardFile::transfer_clipboard_data(XSelectionRequestEvent *xselectionrequest, ClipboardCopy *clipboard_copy) {
|
||||
uint8_t file_buffer[1<<16];
|
||||
ssize_t file_bytes_read = 0;
|
||||
|
||||
if(lseek(file_fd, clipboard_copy->file_offset, SEEK_SET) == -1) {
|
||||
fprintf(stderr, "gsr ui: error: ClipboardFile::send_clipboard: failed to seek in clipboard file to offset " FORMAT_U64 " for requestor window " FORMAT_I64 ", error: %s\n", (uint64_t)clipboard_copy->file_offset, (int64_t)xselectionrequest->requestor, strerror(errno));
|
||||
clipboard_copy->file_offset = 0;
|
||||
// TODO: Cancel transfer
|
||||
return;
|
||||
}
|
||||
|
||||
file_bytes_read = read(file_fd, file_buffer, sizeof(file_buffer));
|
||||
if(file_bytes_read < 0) {
|
||||
fprintf(stderr, "gsr ui: error: ClipbaordFile::send_clipboard: failed to read data from offset " FORMAT_U64 " for requestor window " FORMAT_I64 ", error: %s\n", (uint64_t)clipboard_copy->file_offset, (int64_t)xselectionrequest->requestor, strerror(errno));
|
||||
clipboard_copy->file_offset = 0;
|
||||
// TODO: Cancel transfer
|
||||
return;
|
||||
}
|
||||
|
||||
XChangeProperty(dpy, xselectionrequest->requestor, xselectionrequest->property, xselectionrequest->target, 8, PropModeReplace, (const unsigned char*)file_buffer, file_bytes_read);
|
||||
XSendEvent(dpy, xselectionrequest->requestor, False, NoEventMask, (XEvent*)xselectionrequest);
|
||||
XFlush(dpy);
|
||||
|
||||
clipboard_copy->file_offset += file_bytes_read;
|
||||
if(file_bytes_read == 0)
|
||||
remove_clipboard_copy(clipboard_copy->requestor);
|
||||
}
|
||||
|
||||
ClipboardCopy* ClipboardFile::get_clipboard_copy_by_requestor(Window requestor) {
|
||||
for(ClipboardCopy &clipboard_copy : clipboard_copies) {
|
||||
if(clipboard_copy.requestor == requestor)
|
||||
return &clipboard_copy;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ClipboardFile::remove_clipboard_copy(Window requestor) {
|
||||
for(auto it = clipboard_copies.begin(), end = clipboard_copies.end(); it != end; ++it) {
|
||||
if(it->requestor == requestor) {
|
||||
clipboard_copies.erase(it);
|
||||
XSelectInput(dpy, requestor, 0);
|
||||
|
||||
if(clipboard_copies.empty() && should_clear_selection) {
|
||||
should_clear_selection = false;
|
||||
set_current_file("", file_type);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ClipboardFile::set_current_file(const std::string &filepath, FileType file_type) {
|
||||
if(!dpy)
|
||||
return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
for(ClipboardCopy &clipboard_copy : clipboard_copies) {
|
||||
XSelectInput(dpy, clipboard_copy.requestor, 0);
|
||||
}
|
||||
clipboard_copies.clear();
|
||||
|
||||
if(filepath.empty()) {
|
||||
if(file_fd > 0) {
|
||||
close(file_fd);
|
||||
file_fd = -1;
|
||||
}
|
||||
file_size = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if(file_fd > 0) {
|
||||
close(file_fd);
|
||||
file_fd = -1;
|
||||
file_size = 0;
|
||||
}
|
||||
|
||||
file_fd = open(filepath.c_str(), O_RDONLY);
|
||||
if(file_fd <= 0) {
|
||||
fprintf(stderr, "gsr ui: error: ClipboardFile::set_current_file: failed to open file %s, error: %s\n", filepath.c_str(), strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
struct stat64 stat;
|
||||
if(fstat64(file_fd, &stat) == -1) {
|
||||
fprintf(stderr, "gsr ui: error: ClipboardFile::set_current_file: failed to get file size for file %s, error: %s\n", filepath.c_str(), strerror(errno));
|
||||
close(file_fd);
|
||||
file_fd = -1;
|
||||
return;
|
||||
}
|
||||
file_size = stat.st_size;
|
||||
this->file_type = file_type;
|
||||
|
||||
XSetSelectionOwner(dpy, clipboard_atom, clipboard_window, CurrentTime);
|
||||
XFlush(dpy);
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,7 @@ namespace gsr {
|
||||
|
||||
streaming_config.record_options.video_quality = "custom";
|
||||
streaming_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false});
|
||||
streaming_config.record_options.video_bitrate = 15000;
|
||||
streaming_config.record_options.video_bitrate = 8000;
|
||||
|
||||
record_config.save_directory = default_videos_save_directory;
|
||||
record_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false});
|
||||
@@ -153,6 +153,7 @@ namespace gsr {
|
||||
|
||||
screenshot_config.take_screenshot_hotkey = {mgl::Keyboard::Printscreen, 0};
|
||||
screenshot_config.take_screenshot_region_hotkey = {mgl::Keyboard::Printscreen, HOTKEY_MOD_LCTRL};
|
||||
screenshot_config.take_screenshot_window_hotkey = {mgl::Keyboard::Printscreen, HOTKEY_MOD_LSHIFT};
|
||||
|
||||
main_config.show_hide_hotkey = {mgl::Keyboard::Z, HOTKEY_MOD_LALT};
|
||||
}
|
||||
@@ -170,9 +171,11 @@ 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.wayland_warning_shown", &config.main_config.wayland_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.notification_speed", &config.main_config.notification_speed},
|
||||
{"main.show_hide_hotkey", &config.main_config.show_hide_hotkey},
|
||||
|
||||
{"streaming.record_options.record_area_option", &config.streaming_config.record_options.record_area_option},
|
||||
@@ -196,12 +199,14 @@ namespace gsr {
|
||||
{"streaming.record_options.overclock", &config.streaming_config.record_options.overclock},
|
||||
{"streaming.record_options.record_cursor", &config.streaming_config.record_options.record_cursor},
|
||||
{"streaming.record_options.restore_portal_session", &config.streaming_config.record_options.restore_portal_session},
|
||||
{"streaming.show_streaming_started_notifications", &config.streaming_config.show_streaming_started_notifications},
|
||||
{"streaming.show_streaming_stopped_notifications", &config.streaming_config.show_streaming_stopped_notifications},
|
||||
{"streaming.record_options.show_notifications", &config.streaming_config.record_options.show_notifications},
|
||||
{"streaming.record_options.use_led_indicator", &config.streaming_config.record_options.use_led_indicator},
|
||||
{"streaming.service", &config.streaming_config.streaming_service},
|
||||
{"streaming.youtube.key", &config.streaming_config.youtube.stream_key},
|
||||
{"streaming.twitch.key", &config.streaming_config.twitch.stream_key},
|
||||
{"streaming.rumble.key", &config.streaming_config.rumble.stream_key},
|
||||
{"streaming.custom.url", &config.streaming_config.custom.url},
|
||||
{"streaming.custom.key", &config.streaming_config.custom.key},
|
||||
{"streaming.custom.container", &config.streaming_config.custom.container},
|
||||
{"streaming.start_stop_hotkey", &config.streaming_config.start_stop_hotkey},
|
||||
|
||||
@@ -226,10 +231,9 @@ namespace gsr {
|
||||
{"record.record_options.overclock", &config.record_config.record_options.overclock},
|
||||
{"record.record_options.record_cursor", &config.record_config.record_options.record_cursor},
|
||||
{"record.record_options.restore_portal_session", &config.record_config.record_options.restore_portal_session},
|
||||
{"record.record_options.show_notifications", &config.record_config.record_options.show_notifications},
|
||||
{"record.record_options.use_led_indicator", &config.record_config.record_options.use_led_indicator},
|
||||
{"record.save_video_in_game_folder", &config.record_config.save_video_in_game_folder},
|
||||
{"record.show_recording_started_notifications", &config.record_config.show_recording_started_notifications},
|
||||
{"record.show_video_saved_notifications", &config.record_config.show_video_saved_notifications},
|
||||
{"record.show_video_paused_notifications", &config.record_config.show_video_paused_notifications},
|
||||
{"record.save_directory", &config.record_config.save_directory},
|
||||
{"record.container", &config.record_config.container},
|
||||
{"record.start_stop_hotkey", &config.record_config.start_stop_hotkey},
|
||||
@@ -256,12 +260,11 @@ namespace gsr {
|
||||
{"replay.record_options.overclock", &config.replay_config.record_options.overclock},
|
||||
{"replay.record_options.record_cursor", &config.replay_config.record_options.record_cursor},
|
||||
{"replay.record_options.restore_portal_session", &config.replay_config.record_options.restore_portal_session},
|
||||
{"replay.record_options.show_notifications", &config.replay_config.record_options.show_notifications},
|
||||
{"replay.record_options.use_led_indicator", &config.replay_config.record_options.use_led_indicator},
|
||||
{"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},
|
||||
@@ -280,10 +283,13 @@ namespace gsr {
|
||||
{"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_screenshot_to_clipboard", &config.screenshot_config.save_screenshot_to_clipboard},
|
||||
{"screenshot.show_notifications", &config.screenshot_config.show_notifications},
|
||||
{"screenshot.use_led_indicator", &config.screenshot_config.use_led_indicator},
|
||||
{"screenshot.save_directory", &config.screenshot_config.save_directory},
|
||||
{"screenshot.take_screenshot_hotkey", &config.screenshot_config.take_screenshot_hotkey},
|
||||
{"screenshot.take_screenshot_region_hotkey", &config.screenshot_config.take_screenshot_region_hotkey}
|
||||
{"screenshot.take_screenshot_region_hotkey", &config.screenshot_config.take_screenshot_region_hotkey},
|
||||
{"screenshot.take_screenshot_window_hotkey", &config.screenshot_config.take_screenshot_window_hotkey}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,14 +9,8 @@
|
||||
|
||||
namespace gsr {
|
||||
static const int MAX_CONNECTORS = 32;
|
||||
static const int CONNECTOR_TYPE_COUNTS = 32;
|
||||
static const uint32_t plane_property_all = 0xF;
|
||||
|
||||
typedef struct {
|
||||
int type;
|
||||
int count;
|
||||
} drm_connector_type_count;
|
||||
|
||||
typedef enum {
|
||||
PLANE_PROPERTY_CRTC_X = 1 << 0,
|
||||
PLANE_PROPERTY_CRTC_Y = 1 << 1,
|
||||
@@ -105,22 +99,6 @@ namespace gsr {
|
||||
return get_drm_property_by_name(drm_fd, &properties, name, result);
|
||||
}
|
||||
|
||||
static drm_connector_type_count* drm_connector_types_get_index(drm_connector_type_count *type_counts, int *num_type_counts, int connector_type) {
|
||||
for(int i = 0; i < *num_type_counts; ++i) {
|
||||
if(type_counts[i].type == connector_type)
|
||||
return &type_counts[i];
|
||||
}
|
||||
|
||||
if(*num_type_counts == CONNECTOR_TYPE_COUNTS)
|
||||
return NULL;
|
||||
|
||||
const int index = *num_type_counts;
|
||||
type_counts[index].type = connector_type;
|
||||
type_counts[index].count = 0;
|
||||
++*num_type_counts;
|
||||
return &type_counts[index];
|
||||
}
|
||||
|
||||
// Note: this monitor name logic is kept in sync with gpu screen recorder
|
||||
static std::string get_monitor_name_from_crtc_id(int drm_fd, uint32_t crtc_id) {
|
||||
std::string result;
|
||||
@@ -128,27 +106,23 @@ namespace gsr {
|
||||
if(!resources)
|
||||
return result;
|
||||
|
||||
drm_connector_type_count type_counts[CONNECTOR_TYPE_COUNTS];
|
||||
int num_type_counts = 0;
|
||||
|
||||
for(int i = 0; i < resources->count_connectors; ++i) {
|
||||
uint64_t connector_crtc_id = 0;
|
||||
drmModeConnectorPtr connector = drmModeGetConnectorCurrent(drm_fd, resources->connectors[i]);
|
||||
if(!connector)
|
||||
continue;
|
||||
|
||||
drm_connector_type_count *connector_type = drm_connector_types_get_index(type_counts, &num_type_counts, connector->connector_type);
|
||||
const char *connection_name = drmModeGetConnectorTypeName(connector->connector_type);
|
||||
if(connector_type)
|
||||
++connector_type->count;
|
||||
if(!connection_name)
|
||||
goto next;
|
||||
|
||||
if(connector->connection != DRM_MODE_CONNECTED)
|
||||
goto next;
|
||||
|
||||
if(connector_type && connector_get_property_by_name(drm_fd, connector, "CRTC_ID", &connector_crtc_id) && connector_crtc_id == crtc_id) {
|
||||
if(connector_get_property_by_name(drm_fd, connector, "CRTC_ID", &connector_crtc_id) && connector_crtc_id == crtc_id) {
|
||||
result = connection_name;
|
||||
result += "-";
|
||||
result += std::to_string(connector_type->count);
|
||||
result += std::to_string(connector->connector_type_id);
|
||||
drmModeFreeConnector(connector);
|
||||
break;
|
||||
}
|
||||
@@ -429,6 +403,7 @@ namespace gsr {
|
||||
}
|
||||
|
||||
CursorTrackerWayland::~CursorTrackerWayland() {
|
||||
clear_monitors();
|
||||
if(drm_fd > 0)
|
||||
close(drm_fd);
|
||||
}
|
||||
@@ -506,6 +481,21 @@ namespace gsr {
|
||||
wl_display_roundtrip(dpy);
|
||||
}
|
||||
|
||||
void CursorTrackerWayland::clear_monitors() {
|
||||
for(WaylandOutput &monitor : monitors) {
|
||||
if(monitor.output) {
|
||||
wl_output_destroy(monitor.output);
|
||||
monitor.output = nullptr;
|
||||
}
|
||||
|
||||
if(monitor.xdg_output) {
|
||||
zxdg_output_v1_destroy(monitor.xdg_output);
|
||||
monitor.xdg_output = nullptr;
|
||||
}
|
||||
}
|
||||
monitors.clear();
|
||||
}
|
||||
|
||||
std::optional<CursorInfo> CursorTrackerWayland::get_latest_cursor_info() {
|
||||
if(drm_fd <= 0 || latest_crtc_id == -1)
|
||||
return std::nullopt;
|
||||
@@ -520,7 +510,7 @@ namespace gsr {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
monitors.clear();
|
||||
clear_monitors();
|
||||
struct wl_registry *registry = wl_display_get_registry(dpy);
|
||||
wl_registry_add_listener(registry, ®istry_listener, this);
|
||||
|
||||
@@ -534,22 +524,13 @@ namespace gsr {
|
||||
|
||||
mgl::vec2i cursor_position = latest_cursor_position;
|
||||
const WaylandOutput *wayland_monitor = get_wayland_monitor_by_name(monitors, monitor_name);
|
||||
if(!wayland_monitor)
|
||||
if(!wayland_monitor) {
|
||||
clear_monitors();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
cursor_position = wayland_monitor->pos + latest_cursor_position;
|
||||
for(WaylandOutput &monitor : monitors) {
|
||||
if(monitor.output) {
|
||||
wl_output_destroy(monitor.output);
|
||||
monitor.output = nullptr;
|
||||
}
|
||||
|
||||
if(monitor.xdg_output) {
|
||||
zxdg_output_v1_destroy(monitor.xdg_output);
|
||||
monitor.xdg_output = nullptr;
|
||||
}
|
||||
}
|
||||
monitors.clear();
|
||||
clear_monitors();
|
||||
|
||||
if(xdg_output_manager) {
|
||||
zxdg_output_manager_v1_destroy(xdg_output_manager);
|
||||
|
||||
@@ -3,28 +3,48 @@
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/eventfd.h>
|
||||
|
||||
namespace gsr {
|
||||
static constexpr int button_pressed = 1;
|
||||
static constexpr int cross_button = 0;
|
||||
static constexpr int triangle_button = 2;
|
||||
static constexpr int options_button = 9;
|
||||
static constexpr int playstation_button = 10;
|
||||
static constexpr int axis_up_down = 7;
|
||||
static constexpr int axis_left_right = 6;
|
||||
|
||||
// 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)
|
||||
static int get_dev_input_event_id_from_filepath(const char *dev_input_filepath) {
|
||||
if(strncmp(dev_input_filepath, "/dev/input/event", 16) != 0)
|
||||
return -1;
|
||||
|
||||
int dev_input_id = -1;
|
||||
if(sscanf(dev_input_filepath + 13, "%d", &dev_input_id) == 1)
|
||||
if(sscanf(dev_input_filepath + 16, "%d", &dev_input_id) == 1)
|
||||
return dev_input_id;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static inline bool supports_key(unsigned char *key_bits, unsigned int key) {
|
||||
return key_bits[key/8] & (1 << (key % 8));
|
||||
}
|
||||
|
||||
static bool supports_joystick_keys(unsigned char *key_bits) {
|
||||
const int keys[7] = { BTN_A, BTN_B, BTN_X, BTN_Y, BTN_SELECT, BTN_START, BTN_SELECT };
|
||||
for(int i = 0; i < 7; ++i) {
|
||||
if(supports_key(key_bits, keys[i]))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_input_device_joystick(int input_fd) {
|
||||
unsigned long evbit = 0;
|
||||
ioctl(input_fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);
|
||||
if((evbit & (1 << EV_SYN)) && (evbit & (1 << EV_KEY))) {
|
||||
unsigned char key_bits[KEY_MAX/8 + 1];
|
||||
memset(key_bits, 0, sizeof(key_bits));
|
||||
ioctl(input_fd, EVIOCGBIT(EV_KEY, sizeof(key_bits)), &key_bits);
|
||||
return supports_joystick_keys(key_bits);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
GlobalHotkeysJoystick::~GlobalHotkeysJoystick() {
|
||||
if(event_fd > 0) {
|
||||
const uint64_t exit = 1;
|
||||
@@ -34,11 +54,22 @@ namespace gsr {
|
||||
if(read_thread.joinable())
|
||||
read_thread.join();
|
||||
|
||||
if(event_fd > 0)
|
||||
if(event_fd > 0) {
|
||||
close(event_fd);
|
||||
event_fd = 0;
|
||||
}
|
||||
|
||||
close_fd_cv.notify_one();
|
||||
if(close_fd_thread.joinable())
|
||||
close_fd_thread.join();
|
||||
|
||||
for(int fd : fds_to_close) {
|
||||
close(fd);
|
||||
}
|
||||
|
||||
for(int i = 0; i < num_poll_fd; ++i) {
|
||||
close(poll_fd[i].fd);
|
||||
if(poll_fd[i].fd > 0)
|
||||
close(poll_fd[i].fd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,16 +107,10 @@ namespace gsr {
|
||||
++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");
|
||||
add_all_joystick_devices();
|
||||
|
||||
read_thread = std::thread(&GlobalHotkeysJoystick::read_events, this);
|
||||
close_fd_thread = std::thread(&GlobalHotkeysJoystick::close_fds, this);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -149,14 +174,39 @@ namespace gsr {
|
||||
}
|
||||
}
|
||||
|
||||
// Retarded linux takes very long time to close /dev/input/eventN files, even though they are virtual and opened read-only
|
||||
void GlobalHotkeysJoystick::close_fds() {
|
||||
std::vector<int> current_fds_to_close;
|
||||
while(event_fd > 0) {
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(close_fd_mutex);
|
||||
close_fd_cv.wait(lock, [this]{ return !fds_to_close.empty() || event_fd <= 0; });
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(close_fd_mutex);
|
||||
current_fds_to_close = std::move(fds_to_close);
|
||||
fds_to_close.clear();
|
||||
}
|
||||
|
||||
for(int fd : current_fds_to_close) {
|
||||
close(fd);
|
||||
}
|
||||
current_fds_to_close.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void GlobalHotkeysJoystick::read_events() {
|
||||
js_event event;
|
||||
input_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;
|
||||
|
||||
char dev_input_filepath[256];
|
||||
snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/input/event%d", extra_data[i].dev_input_id);
|
||||
fprintf(stderr, "Info: removed joystick: %s\n", dev_input_filepath);
|
||||
if(remove_poll_fd(i))
|
||||
--i; // This item was removed so we want to repeat the same index to continue to the next item
|
||||
|
||||
@@ -170,25 +220,20 @@ namespace gsr {
|
||||
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);
|
||||
add_device(devname, false);
|
||||
break;
|
||||
}
|
||||
case HotplugAction::REMOVE: {
|
||||
if(remove_device(dev_input_filepath))
|
||||
if(remove_device(devname))
|
||||
--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);
|
||||
process_input_event(poll_fd[i].fd, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,45 +242,44 @@ namespace gsr {
|
||||
;
|
||||
}
|
||||
|
||||
void GlobalHotkeysJoystick::process_js_event(int fd, js_event &event) {
|
||||
void GlobalHotkeysJoystick::process_input_event(int fd, input_event &event) {
|
||||
if(read(fd, &event, sizeof(event)) != sizeof(event))
|
||||
return;
|
||||
|
||||
if((event.type & JS_EVENT_BUTTON) == JS_EVENT_BUTTON) {
|
||||
switch(event.number) {
|
||||
case playstation_button: {
|
||||
playstation_button_pressed = event.value == button_pressed;
|
||||
if(event.type == EV_KEY) {
|
||||
switch(event.code) {
|
||||
case BTN_MODE: {
|
||||
playstation_button_pressed = (event.value == button_pressed);
|
||||
break;
|
||||
}
|
||||
case options_button: {
|
||||
case BTN_START: {
|
||||
if(playstation_button_pressed && event.value == button_pressed)
|
||||
toggle_show = true;
|
||||
break;
|
||||
}
|
||||
case cross_button: {
|
||||
case BTN_SOUTH: {
|
||||
if(playstation_button_pressed && event.value == button_pressed)
|
||||
save_1_min_replay = true;
|
||||
break;
|
||||
}
|
||||
case triangle_button: {
|
||||
case BTN_NORTH: {
|
||||
if(playstation_button_pressed && event.value == button_pressed)
|
||||
save_10_min_replay = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if((event.type & JS_EVENT_AXIS) == JS_EVENT_AXIS && playstation_button_pressed) {
|
||||
const int trigger_threshold = 16383;
|
||||
} else if(event.type == EV_ABS && playstation_button_pressed) {
|
||||
const bool prev_up_pressed = up_pressed;
|
||||
const bool prev_down_pressed = down_pressed;
|
||||
const bool prev_left_pressed = left_pressed;
|
||||
const bool prev_right_pressed = right_pressed;
|
||||
|
||||
if(event.number == axis_up_down) {
|
||||
up_pressed = event.value <= -trigger_threshold;
|
||||
down_pressed = event.value >= trigger_threshold;
|
||||
} else if(event.number == axis_left_right) {
|
||||
left_pressed = event.value <= -trigger_threshold;
|
||||
right_pressed = event.value >= trigger_threshold;
|
||||
if(event.code == ABS_HAT0Y) {
|
||||
up_pressed = event.value == -1;
|
||||
down_pressed = event.value == 1;
|
||||
} else if(event.code == ABS_HAT0X) {
|
||||
left_pressed = event.value == -1;
|
||||
right_pressed = event.value == 1;
|
||||
}
|
||||
|
||||
if(up_pressed && !prev_up_pressed)
|
||||
@@ -249,13 +293,36 @@ namespace gsr {
|
||||
}
|
||||
}
|
||||
|
||||
void GlobalHotkeysJoystick::add_all_joystick_devices() {
|
||||
DIR *dir = opendir("/dev/input");
|
||||
if(!dir) {
|
||||
fprintf(stderr, "Error: failed to open /dev/input, error: %s\n", strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
char dev_input_filepath[1024];
|
||||
for(;;) {
|
||||
struct dirent *entry = readdir(dir);
|
||||
if(!entry)
|
||||
break;
|
||||
|
||||
if(strncmp(entry->d_name, "event", 5) != 0)
|
||||
continue;
|
||||
|
||||
snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/input/%s", entry->d_name);
|
||||
add_device(dev_input_filepath, false);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
}
|
||||
|
||||
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);
|
||||
const int dev_input_id = get_dev_input_event_id_from_filepath(dev_input_filepath);
|
||||
if(dev_input_id == -1)
|
||||
return false;
|
||||
|
||||
@@ -266,6 +333,15 @@ namespace gsr {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!is_input_device_joystick(fd)) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(close_fd_mutex);
|
||||
fds_to_close.push_back(fd);
|
||||
}
|
||||
close_fd_cv.notify_one();
|
||||
return false;
|
||||
}
|
||||
|
||||
poll_fd[num_poll_fd] = {
|
||||
fd,
|
||||
POLLIN,
|
||||
@@ -276,13 +352,15 @@ namespace gsr {
|
||||
dev_input_id
|
||||
};
|
||||
|
||||
//const DeviceId device_id = joystick_get_device_id(dev_input_filepath);
|
||||
|
||||
++num_poll_fd;
|
||||
fprintf(stderr, "Info: added joystick: %s\n", dev_input_filepath);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GlobalHotkeysJoystick::remove_device(const char *dev_input_filepath) {
|
||||
const int dev_input_id = get_js_dev_input_id_from_filepath(dev_input_filepath);
|
||||
const int dev_input_id = get_dev_input_event_id_from_filepath(dev_input_filepath);
|
||||
if(dev_input_id == -1)
|
||||
return false;
|
||||
|
||||
@@ -298,7 +376,14 @@ namespace gsr {
|
||||
if(index < 0 || index >= num_poll_fd)
|
||||
return false;
|
||||
|
||||
close(poll_fd[index].fd);
|
||||
if(poll_fd[index].fd > 0) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(close_fd_mutex);
|
||||
fds_to_close.push_back(poll_fd[index].fd);
|
||||
}
|
||||
close_fd_cv.notify_one();
|
||||
}
|
||||
|
||||
for(int i = index + 1; i < num_poll_fd; ++i) {
|
||||
poll_fd[i - 1] = poll_fd[i];
|
||||
extra_data[i - 1] = extra_data[i];
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace gsr {
|
||||
switch(grab_type) {
|
||||
case GlobalHotkeysLinux::GrabType::ALL: return "--all";
|
||||
case GlobalHotkeysLinux::GrabType::VIRTUAL: return "--virtual";
|
||||
case GlobalHotkeysLinux::GrabType::NO_GRAB: return "--no-grab";
|
||||
}
|
||||
return "--all";
|
||||
}
|
||||
@@ -192,7 +193,7 @@ namespace gsr {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(hotkey.key == 0) {
|
||||
if(hotkey.key == 0 || hotkey.key == XK_VoidSymbol) {
|
||||
//fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: hotkey requires a key\n");
|
||||
return false;
|
||||
}
|
||||
@@ -270,6 +271,8 @@ namespace gsr {
|
||||
auto it = bound_actions_by_id.find(action);
|
||||
if(it != bound_actions_by_id.end())
|
||||
it->second(action);
|
||||
else if(on_gsr_ui_virtual_keyboard_grabbed && action == "gsr-ui-virtual-keyboard-grabbed")
|
||||
on_gsr_ui_virtual_keyboard_grabbed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,10 +59,9 @@ namespace gsr {
|
||||
|
||||
/* 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;
|
||||
if(strncmp(line, "ACTION=", 7) == 0) {
|
||||
event_is_add = strncmp(line+7, "add", 3) == 0;
|
||||
event_is_remove = strncmp(line+7, "remove", 6) == 0;
|
||||
subsystem_is_input = false;
|
||||
} else if(event_is_add || event_is_remove) {
|
||||
if(strcmp(line, "SUBSYSTEM=input") == 0)
|
||||
|
||||
170
src/LedIndicator.cpp
Normal file
170
src/LedIndicator.cpp
Normal file
@@ -0,0 +1,170 @@
|
||||
#include "../include/LedIndicator.hpp"
|
||||
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/wait.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// TODO: Support hotplug for led indicator check (led_brightness_files)
|
||||
|
||||
namespace gsr {
|
||||
static bool string_starts_with(const char *str, const char *sub) {
|
||||
const int str_len = strlen(str);
|
||||
const int sub_len = strlen(sub);
|
||||
return str_len >= sub_len && memcmp(str, sub, sub_len) == 0;
|
||||
}
|
||||
|
||||
static bool string_ends_with(const char *str, const char *sub) {
|
||||
const int str_len = strlen(str);
|
||||
const int sub_len = strlen(sub);
|
||||
return str_len >= sub_len && memcmp(str + str_len - sub_len, sub, sub_len) == 0;
|
||||
}
|
||||
|
||||
static std::vector<int> open_device_leds_brightness_files(const char *led_name_path) {
|
||||
std::vector<int> files;
|
||||
|
||||
DIR *dir = opendir("/sys/class/leds");
|
||||
if(!dir)
|
||||
return files;
|
||||
|
||||
char brightness_filepath[1024];
|
||||
struct dirent *entry;
|
||||
while((entry = readdir(dir)) != NULL) {
|
||||
if(entry->d_name[0] == '.')
|
||||
continue;
|
||||
|
||||
if(!string_starts_with(entry->d_name, "input") || !string_ends_with(entry->d_name, led_name_path))
|
||||
continue;
|
||||
|
||||
snprintf(brightness_filepath, sizeof(brightness_filepath), "/sys/class/leds/%s/brightness", entry->d_name);
|
||||
const int led_brightness_file_fd = open(brightness_filepath, O_RDONLY | O_NONBLOCK);
|
||||
if(led_brightness_file_fd > 0)
|
||||
files.push_back(led_brightness_file_fd);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
LedIndicator::LedIndicator() {
|
||||
led_brightness_files = open_device_leds_brightness_files("scrolllock");
|
||||
run_gsr_global_hotkeys_set_leds(false);
|
||||
}
|
||||
|
||||
LedIndicator::~LedIndicator() {
|
||||
for(int led_brightness_file_fd : led_brightness_files) {
|
||||
close(led_brightness_file_fd);
|
||||
}
|
||||
|
||||
run_gsr_global_hotkeys_set_leds(false);
|
||||
if(gsr_global_hotkeys_pid > 0) {
|
||||
int status;
|
||||
waitpid(gsr_global_hotkeys_pid, &status, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void LedIndicator::set_led(bool enabled) {
|
||||
led_enabled = enabled;
|
||||
perform_blink = false;
|
||||
}
|
||||
|
||||
void LedIndicator::blink() {
|
||||
perform_blink = true;
|
||||
blink_timer.restart();
|
||||
}
|
||||
|
||||
bool LedIndicator::run_gsr_global_hotkeys_set_leds(bool enabled) {
|
||||
if(gsr_global_hotkeys_pid > 0) {
|
||||
int status;
|
||||
if(waitpid(gsr_global_hotkeys_pid, &status, WNOHANG) == 0) {
|
||||
// Still running
|
||||
return false;
|
||||
}
|
||||
gsr_global_hotkeys_pid = -1;
|
||||
}
|
||||
|
||||
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
|
||||
const char *user_homepath = getenv("HOME");
|
||||
if(!user_homepath)
|
||||
user_homepath = "/tmp";
|
||||
|
||||
gsr_global_hotkeys_pid = vfork();
|
||||
if(gsr_global_hotkeys_pid == -1) {
|
||||
fprintf(stderr, "Error: LedIndicator::run_gsr_global_hotkeys_set_leds: failed to fork\n");
|
||||
return false;
|
||||
} else if(gsr_global_hotkeys_pid == 0) { // Child
|
||||
if(inside_flatpak) {
|
||||
const char *args[] = { "flatpak-spawn", "--host", "/var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/bin/kms-server-proxy", "launch-gsr-global-hotkeys", user_homepath, "--set-led", "Scroll Lock", enabled ? "on" : "off", nullptr };
|
||||
execvp(args[0], (char* const*)args);
|
||||
} else {
|
||||
const char *args[] = { "gsr-global-hotkeys", "--set-led", "Scroll Lock", enabled ? "on" : "off", nullptr };
|
||||
execvp(args[0], (char* const*)args);
|
||||
}
|
||||
|
||||
perror("gsr-global-hotkeys");
|
||||
_exit(127);
|
||||
return false;
|
||||
} else { // Parent
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void LedIndicator::update_led(bool new_state) {
|
||||
if(new_state == led_indicator_on)
|
||||
return;
|
||||
|
||||
if(run_gsr_global_hotkeys_set_leds(new_state))
|
||||
led_indicator_on = new_state;
|
||||
}
|
||||
|
||||
void LedIndicator::update() {
|
||||
update_led_with_active_status();
|
||||
check_led_status_outdated();
|
||||
}
|
||||
|
||||
void LedIndicator::update_led_with_active_status() {
|
||||
if(perform_blink) {
|
||||
const double blink_elapsed_sec = blink_timer.get_elapsed_time_seconds();
|
||||
if(blink_elapsed_sec < 0.2) {
|
||||
update_led(false);
|
||||
} else if(blink_elapsed_sec < 0.4) {
|
||||
update_led(true);
|
||||
} else if(blink_elapsed_sec < 0.6) {
|
||||
update_led(false);
|
||||
} else if(blink_elapsed_sec < 0.8) {
|
||||
update_led(true);
|
||||
} else {
|
||||
perform_blink = false;
|
||||
}
|
||||
} else {
|
||||
update_led(led_enabled);
|
||||
}
|
||||
}
|
||||
|
||||
void LedIndicator::check_led_status_outdated() {
|
||||
// The display server will unset our scroll lock led when pressing capslock/numlock as it updates
|
||||
// all leds at the same time (not just the button pressed) (or at least xorg server does that).
|
||||
// When that is done we want to set the scroll lock led on again if it should be on.
|
||||
// TODO: Improve this. Dont do this with a timer.. but inotify doesn't work sysfs. netlink should work (man 7 netlink).
|
||||
if(read_led_brightness_timer.get_elapsed_time_seconds() > 0.2) {
|
||||
read_led_brightness_timer.restart();
|
||||
|
||||
bool led_status_outdated = false;
|
||||
char buffer[32];
|
||||
for(int led_brightness_file_fd : led_brightness_files) {
|
||||
const ssize_t bytes_read = read(led_brightness_file_fd, buffer, sizeof(buffer));
|
||||
if(bytes_read > 0) {
|
||||
if(buffer[0] == '0')
|
||||
led_status_outdated = true;
|
||||
lseek(led_brightness_file_fd, 0, SEEK_SET);
|
||||
}
|
||||
}
|
||||
|
||||
if(led_status_outdated && led_enabled)
|
||||
run_gsr_global_hotkeys_set_leds(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
945
src/Overlay.cpp
945
src/Overlay.cpp
File diff suppressed because it is too large
Load Diff
@@ -176,11 +176,21 @@ namespace gsr {
|
||||
}
|
||||
}
|
||||
|
||||
static const char *get_basename(const char *path, int size) {
|
||||
for(int i = size - 1; i >= 0; --i) {
|
||||
if(path[i] == '/')
|
||||
return path + i + 1;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
// |output_buffer| should be at least PATH_MAX in size
|
||||
bool read_cmdline_arg0(const char *filepath, char *output_buffer, int output_buffer_size) {
|
||||
output_buffer[0] = '\0';
|
||||
|
||||
const char *arg0_start = NULL;
|
||||
const char *arg0_end = NULL;
|
||||
int arg0_size = 0;
|
||||
int fd = open(filepath, O_RDONLY);
|
||||
if(fd == -1)
|
||||
return false;
|
||||
@@ -190,13 +200,16 @@ namespace gsr {
|
||||
if(bytes_read == -1)
|
||||
goto err;
|
||||
|
||||
arg0_end = (const char*)memchr(buffer, '\0', bytes_read);
|
||||
arg0_start = buffer;
|
||||
arg0_end = (const char*)memchr(arg0_start, '\0', bytes_read);
|
||||
if(!arg0_end)
|
||||
goto err;
|
||||
|
||||
if((arg0_end - buffer) + 1 <= output_buffer_size) {
|
||||
memcpy(output_buffer, buffer, arg0_end - buffer);
|
||||
output_buffer[arg0_end - buffer] = '\0';
|
||||
arg0_start = get_basename(arg0_start, arg0_end - arg0_start);
|
||||
arg0_size = arg0_end - arg0_start;
|
||||
if(arg0_size + 1 <= output_buffer_size) {
|
||||
memcpy(output_buffer, arg0_start, arg0_size);
|
||||
output_buffer[arg0_size] = '\0';
|
||||
close(fd);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/extensions/XInput2.h>
|
||||
#include <X11/extensions/Xrandr.h>
|
||||
#include <X11/extensions/shape.h>
|
||||
@@ -208,7 +209,7 @@ namespace gsr {
|
||||
window_attr.background_pixel = is_wayland ? 0 : border_color_x11;
|
||||
window_attr.border_pixel = 0;
|
||||
window_attr.override_redirect = true;
|
||||
window_attr.event_mask = StructureNotifyMask | PointerMotionMask;
|
||||
window_attr.event_mask = StructureNotifyMask | PointerMotionMask | ButtonPressMask | ButtonReleaseMask;
|
||||
window_attr.colormap = region_window_colormap;
|
||||
|
||||
Screen *screen = XDefaultScreenOfDisplay(dpy);
|
||||
@@ -221,6 +222,9 @@ namespace gsr {
|
||||
}
|
||||
set_window_size_not_resizable(dpy, region_window, XWidthOfScreen(screen), XHeightOfScreen(screen));
|
||||
|
||||
unsigned char data = 2; // Prefer being composed to allow transparency. Do this to prevent the compositor from getting turned on/off when taking a screenshot
|
||||
XChangeProperty(dpy, region_window, XInternAtom(dpy, "_NET_WM_BYPASS_COMPOSITOR", False), XA_CARDINAL, 32, PropModeReplace, &data, 1);
|
||||
|
||||
if(!is_wayland) {
|
||||
cursor_window = create_cursor_window(dpy, cursor_window_size, cursor_window_size, &vinfo, border_color_x11);
|
||||
if(!cursor_window)
|
||||
@@ -309,6 +313,9 @@ namespace gsr {
|
||||
region_window = 0;
|
||||
}
|
||||
|
||||
XFlush(dpy);
|
||||
XSync(dpy, False);
|
||||
|
||||
XCloseDisplay(dpy);
|
||||
dpy = nullptr;
|
||||
selecting_region = false;
|
||||
@@ -366,10 +373,6 @@ namespace gsr {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RegionSelector::is_selected() const {
|
||||
return selected;
|
||||
}
|
||||
|
||||
bool RegionSelector::take_selection() {
|
||||
const bool result = selected;
|
||||
selected = false;
|
||||
|
||||
203
src/Rpc.cpp
203
src/Rpc.cpp
@@ -5,11 +5,12 @@
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
namespace gsr {
|
||||
static void get_runtime_filepath(char *buffer, size_t buffer_size, const char *filename) {
|
||||
static void get_socket_filepath(char *buffer, size_t buffer_size, const char *filename) {
|
||||
char dir[PATH_MAX];
|
||||
|
||||
const char *runtime_dir = getenv("XDG_RUNTIME_DIR");
|
||||
@@ -24,78 +25,117 @@ namespace gsr {
|
||||
snprintf(buffer, buffer_size, "%s/%s", dir, filename);
|
||||
}
|
||||
|
||||
static int create_socket(const char *name, struct sockaddr_un *addr, std::string &socket_filepath) {
|
||||
char socket_filepath_tmp[PATH_MAX];
|
||||
get_socket_filepath(socket_filepath_tmp, sizeof(socket_filepath_tmp), name);
|
||||
socket_filepath = socket_filepath_tmp;
|
||||
|
||||
memset(addr, 0, sizeof(*addr));
|
||||
if(strlen(name) > sizeof(addr->sun_path))
|
||||
return false;
|
||||
|
||||
addr->sun_family = AF_UNIX;
|
||||
snprintf(addr->sun_path, sizeof(addr->sun_path), "%s", socket_filepath.c_str());
|
||||
|
||||
return socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0);
|
||||
}
|
||||
|
||||
Rpc::Rpc() {
|
||||
num_polls = 0;
|
||||
}
|
||||
|
||||
Rpc::~Rpc() {
|
||||
if(fd > 0)
|
||||
close(fd);
|
||||
if(socket_fd > 0)
|
||||
close(socket_fd);
|
||||
|
||||
if(file)
|
||||
fclose(file);
|
||||
|
||||
if(!fifo_filepath.empty())
|
||||
remove(fifo_filepath.c_str());
|
||||
if(!socket_filepath.empty())
|
||||
unlink(socket_filepath.c_str());
|
||||
}
|
||||
|
||||
bool Rpc::create(const char *name) {
|
||||
if(file) {
|
||||
if(socket_fd > 0) {
|
||||
fprintf(stderr, "Error: Rpc::create: already created/opened\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
char fifo_filepath_tmp[PATH_MAX];
|
||||
get_runtime_filepath(fifo_filepath_tmp, sizeof(fifo_filepath_tmp), name);
|
||||
fifo_filepath = fifo_filepath_tmp;
|
||||
remove(fifo_filepath.c_str());
|
||||
|
||||
if(mkfifo(fifo_filepath.c_str(), 0600) != 0) {
|
||||
fprintf(stderr, "Error: mkfifo failed, error: %s, %s\n", strerror(errno), fifo_filepath.c_str());
|
||||
struct sockaddr_un addr;
|
||||
socket_fd = create_socket(name, &addr, socket_filepath);
|
||||
if(socket_fd <= 0) {
|
||||
fprintf(stderr, "Error: Rpc::create: failed to create socket, error: %s\n", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!open_filepath(fifo_filepath.c_str())) {
|
||||
remove(fifo_filepath.c_str());
|
||||
fifo_filepath.clear();
|
||||
unlink(socket_filepath.c_str());
|
||||
|
||||
if(bind(socket_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
|
||||
const int err = errno;
|
||||
close(socket_fd);
|
||||
socket_fd = 0;
|
||||
|
||||
fprintf(stderr, "Error: Rpc::create: failed to bind, error: %s\n", strerror(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
if(listen(socket_fd, GSR_RPC_MAX_CONNECTIONS) == -1) {
|
||||
const int err = errno;
|
||||
close(socket_fd);
|
||||
socket_fd = 0;
|
||||
|
||||
fprintf(stderr, "Error: Rpc::create: failed to listen, error: %s\n", strerror(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
polls[0].fd = socket_fd;
|
||||
polls[0].events = POLLIN;
|
||||
polls[0].revents = 0;
|
||||
++num_polls;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Rpc::open(const char *name) {
|
||||
if(file) {
|
||||
RpcOpenResult Rpc::open(const char *name) {
|
||||
if(socket_fd > 0) {
|
||||
fprintf(stderr, "Error: Rpc::open: already created/opened\n");
|
||||
return false;
|
||||
return RpcOpenResult::ERROR;
|
||||
}
|
||||
|
||||
char fifo_filepath_tmp[PATH_MAX];
|
||||
get_runtime_filepath(fifo_filepath_tmp, sizeof(fifo_filepath_tmp), name);
|
||||
return open_filepath(fifo_filepath_tmp);
|
||||
}
|
||||
|
||||
bool Rpc::open_filepath(const char *filepath) {
|
||||
fd = ::open(filepath, O_RDWR | O_NONBLOCK);
|
||||
if(fd <= 0)
|
||||
return false;
|
||||
|
||||
file = fdopen(fd, "r+");
|
||||
if(!file) {
|
||||
close(fd);
|
||||
fd = 0;
|
||||
return false;
|
||||
struct sockaddr_un addr;
|
||||
socket_fd = create_socket(name, &addr, socket_filepath);
|
||||
socket_filepath.clear(); /* We dont want to delete the socket on exit as the client */
|
||||
if(socket_fd <= 0) {
|
||||
fprintf(stderr, "Error: Rpc::open: failed to create socket, error: %s\n", strerror(errno));
|
||||
return RpcOpenResult::ERROR;
|
||||
}
|
||||
fd = 0;
|
||||
return true;
|
||||
|
||||
while(true) {
|
||||
if(connect(socket_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
|
||||
const int err = errno;
|
||||
if(err == EWOULDBLOCK) {
|
||||
usleep(10 * 1000);
|
||||
} else {
|
||||
close(socket_fd);
|
||||
socket_fd = 0;
|
||||
if(err != ENOENT && err != ECONNREFUSED)
|
||||
fprintf(stderr, "Error: Rpc::create: failed to connect, error: %s\n", strerror(err));
|
||||
return RpcOpenResult::ERROR;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return RpcOpenResult::OK;
|
||||
}
|
||||
|
||||
bool Rpc::write(const char *str, size_t size) {
|
||||
if(!file) {
|
||||
fprintf(stderr, "Error: Rpc::write: fifo not created/opened yet\n");
|
||||
if(socket_fd <= 0) {
|
||||
fprintf(stderr, "Error: Rpc::write: unix domain socket not created/opened yet\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
ssize_t offset = 0;
|
||||
while(offset < (ssize_t)size) {
|
||||
const ssize_t bytes_written = fwrite(str + offset, 1, size - offset, file);
|
||||
fflush(file);
|
||||
const ssize_t bytes_written = ::write(socket_fd, str + offset, size - offset);
|
||||
if(bytes_written > 0)
|
||||
offset += bytes_written;
|
||||
}
|
||||
@@ -103,30 +143,73 @@ namespace gsr {
|
||||
}
|
||||
|
||||
void Rpc::poll() {
|
||||
if(!file) {
|
||||
//fprintf(stderr, "Error: Rpc::poll: fifo not created/opened yet\n");
|
||||
if(socket_fd <= 0) {
|
||||
//fprintf(stderr, "Error: Rpc::poll: unix domain socket not created/opened yet\n");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string name;
|
||||
char line[1024];
|
||||
while(fgets(line, sizeof(line), file)) {
|
||||
int line_len = strlen(line);
|
||||
if(line_len == 0)
|
||||
continue;
|
||||
while(::poll(polls, num_polls, 0) > 0) {
|
||||
for(int i = 0; i < num_polls; ++i) {
|
||||
if(polls[i].fd == socket_fd) {
|
||||
if(polls[i].revents & (POLLERR|POLLHUP)) {
|
||||
close(socket_fd);
|
||||
socket_fd = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if(line[line_len - 1] == '\n') {
|
||||
line[line_len - 1] = '\0';
|
||||
--line_len;
|
||||
const int client_fd = accept(socket_fd, NULL, NULL);
|
||||
if(num_polls >= GSR_RPC_MAX_POLLS) {
|
||||
if(errno != EWOULDBLOCK)
|
||||
fprintf(stderr, "Error: Rpc::poll: unable to accept more clients, error: %s\n", strerror(errno));
|
||||
} else {
|
||||
polls[num_polls].fd = client_fd;
|
||||
polls[num_polls].events = POLLIN;
|
||||
polls[num_polls].revents = 0;
|
||||
++num_polls;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if(polls[i].revents & POLLIN)
|
||||
handle_client_data(polls[i].fd, polls_data[i]);
|
||||
|
||||
if(polls[i].revents & (POLLERR|POLLHUP)) {
|
||||
close(polls[i].fd);
|
||||
polls[i] = polls[num_polls - 1];
|
||||
|
||||
memcpy(polls_data[i].buffer, polls_data[num_polls - 1].buffer, polls_data[num_polls - 1].buffer_size);
|
||||
polls_data[i].buffer_size = polls_data[num_polls - 1].buffer_size;
|
||||
|
||||
--num_polls;
|
||||
--i;
|
||||
}
|
||||
}
|
||||
|
||||
name = line;
|
||||
auto it = handlers_by_name.find(name);
|
||||
if(it != handlers_by_name.end())
|
||||
it->second(name);
|
||||
}
|
||||
}
|
||||
|
||||
void Rpc::handle_client_data(int client_fd, PollData &poll_data) {
|
||||
char *write_buffer = poll_data.buffer + poll_data.buffer_size;
|
||||
const ssize_t num_bytes_read = read(client_fd, write_buffer, sizeof(poll_data.buffer) - poll_data.buffer_size);
|
||||
if(num_bytes_read <= 0)
|
||||
return;
|
||||
|
||||
poll_data.buffer_size += num_bytes_read;
|
||||
const char *newline_p = (const char*)memchr(write_buffer, '\n', num_bytes_read);
|
||||
if(!newline_p)
|
||||
return;
|
||||
|
||||
const size_t command_size = newline_p - poll_data.buffer;
|
||||
std::string name;
|
||||
name.assign(poll_data.buffer, command_size);
|
||||
memmove(poll_data.buffer, newline_p + 1, poll_data.buffer_size - (command_size + 1));
|
||||
poll_data.buffer_size -= (command_size + 1);
|
||||
|
||||
auto it = handlers_by_name.find(name);
|
||||
if(it != handlers_by_name.end())
|
||||
it->second(name);
|
||||
}
|
||||
|
||||
bool Rpc::add_handler(const std::string &name, RpcCallback callback) {
|
||||
return handlers_by_name.insert(std::make_pair(name, std::move(callback))).second;
|
||||
}
|
||||
|
||||
@@ -120,6 +120,12 @@ namespace gsr {
|
||||
if(!theme->trash_texture.load_from_file((resources_path + "images/trash.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->masked_texture.load_from_file((resources_path + "images/masked.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->unmasked_texture.load_from_file((resources_path + "images/unmasked.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->ps4_home_texture.load_from_file((resources_path + "images/ps4_home.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
|
||||
@@ -32,6 +32,28 @@ namespace gsr {
|
||||
return str.size() >= len && memcmp(str.data() + str.size() - len, substr, len) == 0;
|
||||
}
|
||||
|
||||
std::string strip(const std::string &str) {
|
||||
int start_index = 0;
|
||||
int str_len = str.size();
|
||||
|
||||
for(int i = 0; i < str_len; ++i) {
|
||||
if(str[i] != ' ') {
|
||||
start_index += i;
|
||||
str_len -= i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for(int i = str_len - 1; i >= 0; --i) {
|
||||
if(str[i] != ' ') {
|
||||
str_len = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return str.substr(start_index, str_len);
|
||||
}
|
||||
|
||||
std::string get_home_dir() {
|
||||
const char *home_dir = getenv("HOME");
|
||||
if(!home_dir) {
|
||||
|
||||
236
src/WindowSelector.cpp
Normal file
236
src/WindowSelector.cpp
Normal file
@@ -0,0 +1,236 @@
|
||||
#include "../include/WindowSelector.hpp"
|
||||
#include "../include/WindowUtils.hpp"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/extensions/shape.h>
|
||||
#include <X11/cursorfont.h>
|
||||
#include <X11/keysym.h>
|
||||
|
||||
namespace gsr {
|
||||
static const int rectangle_border_size = 2;
|
||||
|
||||
static int max_int(int a, int b) {
|
||||
return a >= b ? a : b;
|
||||
}
|
||||
|
||||
static void set_region_rectangle(Display *dpy, Window window, int x, int y, int width, int height, int border_size) {
|
||||
if(width < 0) {
|
||||
x += width;
|
||||
width = abs(width);
|
||||
}
|
||||
|
||||
if(height < 0) {
|
||||
y += height;
|
||||
height = abs(height);
|
||||
}
|
||||
|
||||
XRectangle rectangles[] = {
|
||||
{
|
||||
(short)max_int(0, x), (short)max_int(0, y),
|
||||
(unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height)
|
||||
}, // Left
|
||||
{
|
||||
(short)max_int(0, x + width - border_size), (short)max_int(0, y),
|
||||
(unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height)
|
||||
}, // Right
|
||||
{
|
||||
(short)max_int(0, x + border_size), (short)max_int(0, y),
|
||||
(unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size)
|
||||
}, // Top
|
||||
{
|
||||
(short)max_int(0, x + border_size), (short)max_int(0, y + height - border_size),
|
||||
(unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size)
|
||||
}, // Bottom
|
||||
};
|
||||
XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 4, ShapeSet, Unsorted);
|
||||
XFlush(dpy);
|
||||
}
|
||||
|
||||
static unsigned long mgl_color_to_x11_color(mgl::Color color) {
|
||||
if(color.a == 0)
|
||||
return 0;
|
||||
return ((uint32_t)color.a << 24) | (((uint32_t)color.r * color.a / 0xFF) << 16) | (((uint32_t)color.g * color.a / 0xFF) << 8) | ((uint32_t)color.b * color.a / 0xFF);
|
||||
}
|
||||
|
||||
static Window get_cursor_window(Display *dpy) {
|
||||
Window root_window = None;
|
||||
Window window = None;
|
||||
int dummy_i;
|
||||
unsigned int dummy_u;
|
||||
mgl::vec2i root_pos;
|
||||
XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &root_pos.x, &root_pos.y, &dummy_i, &dummy_i, &dummy_u);
|
||||
return window;
|
||||
}
|
||||
|
||||
static void get_window_geometry(Display *dpy, Window window, mgl::vec2i &pos, mgl::vec2i &size) {
|
||||
Window root_window;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
unsigned int w = 0;
|
||||
unsigned int h = 0;
|
||||
unsigned int dummy_border, dummy_depth;
|
||||
XGetGeometry(dpy, window, &root_window, &x, &y, &w, &h, &dummy_border, &dummy_depth);
|
||||
pos.x = x;
|
||||
pos.y = y;
|
||||
size.x = w;
|
||||
size.y = h;
|
||||
}
|
||||
|
||||
WindowSelector::WindowSelector() {
|
||||
|
||||
}
|
||||
|
||||
WindowSelector::~WindowSelector() {
|
||||
stop();
|
||||
}
|
||||
|
||||
bool WindowSelector::start(mgl::Color border_color) {
|
||||
if(dpy)
|
||||
return false;
|
||||
|
||||
const unsigned long border_color_x11 = mgl_color_to_x11_color(border_color);
|
||||
dpy = XOpenDisplay(nullptr);
|
||||
if(!dpy) {
|
||||
fprintf(stderr, "Error: WindowSelector::start: failed to connect to the X11 server\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
const Window cursor_window = get_cursor_window(dpy);
|
||||
mgl::vec2i cursor_window_pos, cursor_window_size;
|
||||
get_window_geometry(dpy, cursor_window, cursor_window_pos, cursor_window_size);
|
||||
|
||||
XVisualInfo vinfo;
|
||||
memset(&vinfo, 0, sizeof(vinfo));
|
||||
XMatchVisualInfo(dpy, DefaultScreen(dpy), 32, TrueColor, &vinfo);
|
||||
border_window_colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), vinfo.visual, AllocNone);
|
||||
|
||||
XSetWindowAttributes window_attr;
|
||||
window_attr.background_pixel = border_color_x11;
|
||||
window_attr.border_pixel = 0;
|
||||
window_attr.override_redirect = true;
|
||||
window_attr.event_mask = StructureNotifyMask | PointerMotionMask | ButtonPressMask | ButtonReleaseMask;
|
||||
window_attr.colormap = border_window_colormap;
|
||||
|
||||
Screen *screen = XDefaultScreenOfDisplay(dpy);
|
||||
border_window = XCreateWindow(dpy, DefaultRootWindow(dpy), 0, 0, XWidthOfScreen(screen), XHeightOfScreen(screen), 0,
|
||||
vinfo.depth, InputOutput, vinfo.visual, CWBackPixel | CWBorderPixel | CWOverrideRedirect | CWEventMask | CWColormap, &window_attr);
|
||||
if(!border_window) {
|
||||
fprintf(stderr, "Error: WindowSelector::start: failed to create region window\n");
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
set_window_size_not_resizable(dpy, border_window, XWidthOfScreen(screen), XHeightOfScreen(screen));
|
||||
|
||||
unsigned char data = 2; // Prefer being composed to allow transparency. Do this to prevent the compositor from getting turned on/off when taking a screenshot
|
||||
XChangeProperty(dpy, border_window, XInternAtom(dpy, "_NET_WM_BYPASS_COMPOSITOR", False), XA_CARDINAL, 32, PropModeReplace, &data, 1);
|
||||
|
||||
if(cursor_window && cursor_window != DefaultRootWindow(dpy))
|
||||
set_region_rectangle(dpy, border_window, cursor_window_pos.x, cursor_window_pos.y, cursor_window_size.x, cursor_window_size.y, rectangle_border_size);
|
||||
else
|
||||
set_region_rectangle(dpy, border_window, 0, 0, 0, 0, 0);
|
||||
make_window_click_through(dpy, border_window);
|
||||
XMapWindow(dpy, border_window);
|
||||
|
||||
crosshair_cursor = XCreateFontCursor(dpy, XC_crosshair);
|
||||
XGrabPointer(dpy, DefaultRootWindow(dpy), True, PointerMotionMask | ButtonPressMask | ButtonReleaseMask | ButtonMotionMask, GrabModeAsync, GrabModeAsync, None, crosshair_cursor, CurrentTime);
|
||||
XGrabKeyboard(dpy, DefaultRootWindow(dpy), True, GrabModeAsync, GrabModeAsync, CurrentTime);
|
||||
XFlush(dpy);
|
||||
|
||||
selected = false;
|
||||
canceled = false;
|
||||
selected_window = None;
|
||||
return true;
|
||||
}
|
||||
|
||||
void WindowSelector::stop() {
|
||||
if(!dpy)
|
||||
return;
|
||||
|
||||
XUngrabPointer(dpy, CurrentTime);
|
||||
XUngrabKeyboard(dpy, CurrentTime);
|
||||
|
||||
if(border_window_colormap) {
|
||||
XFreeColormap(dpy, border_window_colormap);
|
||||
border_window_colormap = 0;
|
||||
}
|
||||
|
||||
if(border_window) {
|
||||
XDestroyWindow(dpy, border_window);
|
||||
border_window = 0;
|
||||
}
|
||||
|
||||
if(crosshair_cursor) {
|
||||
XFreeCursor(dpy, crosshair_cursor);
|
||||
crosshair_cursor = None;
|
||||
}
|
||||
|
||||
XFlush(dpy);
|
||||
XSync(dpy, False);
|
||||
|
||||
XCloseDisplay(dpy);
|
||||
dpy = nullptr;
|
||||
}
|
||||
|
||||
bool WindowSelector::is_started() const {
|
||||
return dpy != nullptr;
|
||||
}
|
||||
|
||||
bool WindowSelector::failed() const {
|
||||
return !dpy;
|
||||
}
|
||||
|
||||
bool WindowSelector::poll_events() {
|
||||
if(!dpy || selected)
|
||||
return false;
|
||||
|
||||
XEvent xev;
|
||||
while(XPending(dpy)) {
|
||||
XNextEvent(dpy, &xev);
|
||||
|
||||
if(xev.type == MotionNotify) {
|
||||
const Window motion_window = xev.xmotion.subwindow;
|
||||
mgl::vec2i motion_window_pos, motion_window_size;
|
||||
get_window_geometry(dpy, motion_window, motion_window_pos, motion_window_size);
|
||||
if(motion_window && motion_window != DefaultRootWindow(dpy))
|
||||
set_region_rectangle(dpy, border_window, motion_window_pos.x, motion_window_pos.y, motion_window_size.x, motion_window_size.y, rectangle_border_size);
|
||||
else
|
||||
set_region_rectangle(dpy, border_window, 0, 0, 0, 0, 0);
|
||||
XFlush(dpy);
|
||||
} else if(xev.type == ButtonRelease && xev.xbutton.button == Button1) {
|
||||
selected_window = xev.xbutton.subwindow;
|
||||
const Window clicked_window_real = window_get_target_window_child(dpy, selected_window);
|
||||
if(clicked_window_real)
|
||||
selected_window = clicked_window_real;
|
||||
selected = true;
|
||||
|
||||
stop();
|
||||
break;
|
||||
} else if(xev.type == KeyRelease && XKeycodeToKeysym(dpy, xev.xkey.keycode, 0) == XK_Escape) {
|
||||
canceled = true;
|
||||
selected = false;
|
||||
stop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WindowSelector::take_selection() {
|
||||
const bool result = selected;
|
||||
selected = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool WindowSelector::take_canceled() {
|
||||
const bool result = canceled;
|
||||
canceled = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
Window WindowSelector::get_selection() const {
|
||||
return selected_window;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "../include/WindowUtils.hpp"
|
||||
#include "../include/Utils.hpp"
|
||||
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/Xutil.h>
|
||||
@@ -62,7 +63,7 @@ namespace gsr {
|
||||
return window_has_atom(dpy, window, net_wm_state_atom) || window_has_atom(dpy, window, wm_state_atom);
|
||||
}
|
||||
|
||||
static Window window_get_target_window_child(Display *display, Window window) {
|
||||
Window window_get_target_window_child(Display *display, Window window) {
|
||||
if(window == None)
|
||||
return None;
|
||||
|
||||
@@ -212,28 +213,6 @@ namespace gsr {
|
||||
return result;
|
||||
}
|
||||
|
||||
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] != ' ') {
|
||||
start_index += i;
|
||||
str_len -= i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for(int i = str_len - 1; i >= 0; --i) {
|
||||
if(str[i] != ' ') {
|
||||
str_len = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return str.substr(start_index, str_len);
|
||||
}
|
||||
|
||||
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type) {
|
||||
std::string result;
|
||||
const Window focused_window = get_focused_window(dpy, window_capture_type);
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace gsr {
|
||||
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)) {
|
||||
if(mgl::FloatRect(item.position, item_size).contains(mouse_pos) && item.enabled) {
|
||||
const size_t prev_selected_item = selected_item;
|
||||
selected_item = i;
|
||||
show_dropdown = false;
|
||||
@@ -93,7 +93,7 @@ namespace gsr {
|
||||
void ComboBox::set_selected_item(const std::string &id, bool trigger_event, bool trigger_event_even_if_selection_not_changed) {
|
||||
for(size_t i = 0; i < items.size(); ++i) {
|
||||
auto &item = items[i];
|
||||
if(item.id == id) {
|
||||
if(item.id == id && item.enabled) {
|
||||
const size_t prev_selected_item = selected_item;
|
||||
selected_item = i;
|
||||
dirty = true;
|
||||
@@ -106,6 +106,22 @@ namespace gsr {
|
||||
}
|
||||
}
|
||||
|
||||
void ComboBox::set_item_enabled(const std::string &id, bool enabled) {
|
||||
for(size_t i = 0; i < items.size(); ++i) {
|
||||
auto &item = items[i];
|
||||
if(item.id == id) {
|
||||
item.enabled = enabled;
|
||||
item.text.set_color(item.enabled ? mgl::Color(255, 255, 255, 255) : mgl::Color(255, 255, 255, 80));
|
||||
if(selected_item == i) {
|
||||
selected_item = 0;
|
||||
show_dropdown = false;
|
||||
dirty = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::string& ComboBox::get_selected_id() const {
|
||||
if(items.empty()) {
|
||||
static std::string dummy;
|
||||
@@ -150,7 +166,7 @@ namespace gsr {
|
||||
Item &item = items[i];
|
||||
item_size.y = padding_top + item.text.get_bounds().size.y + padding_bottom;
|
||||
|
||||
if(!cursor_inside) {
|
||||
if(!cursor_inside && item.enabled) {
|
||||
cursor_inside = mgl::FloatRect(items_draw_pos, item_size).contains(mouse_pos);
|
||||
if(cursor_inside) {
|
||||
mgl::Rectangle item_background(items_draw_pos.floor(), item_size.floor());
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#include "../../include/gui/Entry.hpp"
|
||||
#include "../../include/gui/Utils.hpp"
|
||||
#include "../../include/Theme.hpp"
|
||||
#include <mglpp/graphics/Rectangle.hpp>
|
||||
#include <mglpp/window/Window.hpp>
|
||||
#include <mglpp/window/Event.hpp>
|
||||
#include <mglpp/system/FloatRect.hpp>
|
||||
#include <mglpp/system/Utf8.hpp>
|
||||
#include <optional>
|
||||
#include <string.h>
|
||||
|
||||
namespace gsr {
|
||||
static const float padding_top_scale = 0.004629f;
|
||||
@@ -16,8 +16,20 @@ namespace gsr {
|
||||
static const float border_scale = 0.0015f;
|
||||
static const float caret_width_scale = 0.001f;
|
||||
|
||||
Entry::Entry(mgl::Font *font, const char *text, float max_width) : text("", *font), max_width(max_width) {
|
||||
static void string_replace_all(std::string &str, char old_char, char new_char) {
|
||||
for(char &c : str) {
|
||||
if(c == old_char)
|
||||
c = new_char;
|
||||
}
|
||||
}
|
||||
|
||||
Entry::Entry(mgl::Font *font, const char *text, float max_width) :
|
||||
text(std::u32string(), *font),
|
||||
masked_text(std::u32string(), *font),
|
||||
max_width(max_width)
|
||||
{
|
||||
this->text.set_color(get_color_theme().text_color);
|
||||
this->masked_text.set_color(get_color_theme().text_color);
|
||||
set_text(text);
|
||||
}
|
||||
|
||||
@@ -25,25 +37,133 @@ namespace gsr {
|
||||
if(!visible)
|
||||
return true;
|
||||
|
||||
mgl::Text32 &active_text = masked ? masked_text : text;
|
||||
|
||||
if(event.type == mgl::Event::MouseButtonPressed && event.mouse_button.button == mgl::Mouse::Left) {
|
||||
selected = mgl::FloatRect(position + offset, get_size()).contains({ (float)event.mouse_button.x, (float)event.mouse_button.y });
|
||||
} else if(event.type == mgl::Event::KeyPressed && selected) {
|
||||
if(event.key.code == mgl::Keyboard::Backspace && !text.get_string().empty()) {
|
||||
std::string str = text.get_string();
|
||||
const size_t prev_index = mgl::utf8_get_start_of_codepoint((const unsigned char*)str.c_str(), str.size(), str.size());
|
||||
str.erase(prev_index, std::string::npos);
|
||||
set_text(std::move(str));
|
||||
} else if(event.key.code == mgl::Keyboard::V && event.key.control) {
|
||||
std::string clipboard_text = window.get_clipboard_string();
|
||||
std::string str = text.get_string();
|
||||
str += clipboard_text;
|
||||
set_text(std::move(str));
|
||||
const mgl::vec2f mouse_pos = { (float)event.mouse_button.x, (float)event.mouse_button.y };
|
||||
selected = mgl::FloatRect(position + offset, get_size()).contains(mouse_pos);
|
||||
if(selected) {
|
||||
selecting_text = true;
|
||||
|
||||
const auto caret_index_mouse = find_closest_caret_index_by_position(mouse_pos);
|
||||
caret.index = caret_index_mouse.index;
|
||||
caret.offset_x = caret_index_mouse.pos.x - active_text.get_position().x;
|
||||
selection_start_caret = caret;
|
||||
show_selection = true;
|
||||
} else {
|
||||
selecting_text = false;
|
||||
selecting_with_keyboard = false;
|
||||
show_selection = false;
|
||||
}
|
||||
} else if(event.type == mgl::Event::TextEntered && selected && event.text.codepoint >= 32) {
|
||||
std::string str = text.get_string();
|
||||
str.append(event.text.str, event.text.size);
|
||||
set_text(std::move(str));
|
||||
} else if(event.type == mgl::Event::MouseButtonReleased && event.mouse_button.button == mgl::Mouse::Left) {
|
||||
selecting_text = false;
|
||||
if(caret.index == selection_start_caret.index)
|
||||
show_selection = false;
|
||||
} else if(event.type == mgl::Event::MouseMoved && selected) {
|
||||
if(selecting_text) {
|
||||
const auto caret_index_mouse = find_closest_caret_index_by_position(mgl::vec2f(event.mouse_move.x, event.mouse_move.y));
|
||||
caret.index = caret_index_mouse.index;
|
||||
caret.offset_x = caret_index_mouse.pos.x - active_text.get_position().x;
|
||||
return false;
|
||||
}
|
||||
} else if(event.type == mgl::Event::KeyPressed && selected) {
|
||||
int selection_start_byte = caret.index;
|
||||
int selection_end_byte = caret.index;
|
||||
if(show_selection) {
|
||||
selection_start_byte = std::min(caret.index, selection_start_caret.index);
|
||||
selection_end_byte = std::max(caret.index, selection_start_caret.index);
|
||||
}
|
||||
|
||||
if(event.key.code == mgl::Keyboard::Backspace) {
|
||||
if(selection_start_byte == selection_end_byte && caret.index > 0)
|
||||
selection_start_byte -= 1;
|
||||
|
||||
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, std::u32string());
|
||||
} else if(event.key.code == mgl::Keyboard::Delete) {
|
||||
if(selection_start_byte == selection_end_byte && caret.index < (int)active_text.get_string().size())
|
||||
selection_end_byte += 1;
|
||||
|
||||
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, std::u32string());
|
||||
} else if(event.key.code == mgl::Keyboard::C && event.key.control) {
|
||||
const size_t selection_num_bytes = selection_end_byte - selection_start_byte;
|
||||
if(selection_num_bytes > 0)
|
||||
window.set_clipboard(mgl::utf32_to_utf8(text.get_string().substr(selection_start_byte, selection_num_bytes)));
|
||||
} else if(event.key.code == mgl::Keyboard::V && event.key.control) {
|
||||
std::string clipboard_string = window.get_clipboard_string();
|
||||
string_replace_all(clipboard_string, '\n', ' ');
|
||||
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, mgl::utf8_to_utf32(clipboard_string));
|
||||
} else if(event.key.code == mgl::Keyboard::A && event.key.control) {
|
||||
selection_start_caret.index = 0;
|
||||
selection_start_caret.offset_x = 0.0f;
|
||||
|
||||
caret.index = active_text.get_string().size();
|
||||
// TODO: Optimize
|
||||
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
|
||||
|
||||
show_selection = true;
|
||||
} else if(event.key.code == mgl::Keyboard::Left) {
|
||||
if(!selecting_with_keyboard && show_selection)
|
||||
show_selection = false;
|
||||
else
|
||||
move_caret_word(Direction::LEFT, event.key.control ? 999999 : 1);
|
||||
|
||||
if(!selecting_with_keyboard) {
|
||||
selection_start_caret = caret;
|
||||
show_selection = false;
|
||||
}
|
||||
} else if(event.key.code == mgl::Keyboard::Right) {
|
||||
if(!selecting_with_keyboard && show_selection)
|
||||
show_selection = false;
|
||||
else
|
||||
move_caret_word(Direction::RIGHT, event.key.control ? 999999 : 1);
|
||||
|
||||
if(!selecting_with_keyboard) {
|
||||
selection_start_caret = caret;
|
||||
show_selection = false;
|
||||
}
|
||||
} else if(event.key.code == mgl::Keyboard::Home) {
|
||||
caret.index = 0;
|
||||
caret.offset_x = 0.0f;
|
||||
|
||||
if(!selecting_with_keyboard) {
|
||||
selection_start_caret = caret;
|
||||
show_selection = false;
|
||||
}
|
||||
} else if(event.key.code == mgl::Keyboard::End) {
|
||||
caret.index = active_text.get_string().size();
|
||||
// TODO: Optimize
|
||||
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
|
||||
|
||||
if(!selecting_with_keyboard) {
|
||||
selection_start_caret = caret;
|
||||
show_selection = false;
|
||||
}
|
||||
} else if(event.key.code == mgl::Keyboard::LShift || event.key.code == mgl::Keyboard::RShift) {
|
||||
if(!show_selection)
|
||||
selection_start_caret = caret;
|
||||
selecting_with_keyboard = true;
|
||||
show_selection = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} else if(event.type == mgl::Event::KeyReleased && selected) {
|
||||
if(event.key.code == mgl::Keyboard::LShift || event.key.code == mgl::Keyboard::RShift) {
|
||||
selecting_with_keyboard = false;
|
||||
}
|
||||
|
||||
return false;
|
||||
} else if(event.type == mgl::Event::TextEntered && selected && event.text.codepoint >= 32 && event.text.codepoint != 127) {
|
||||
int selection_start_byte = caret.index;
|
||||
int selection_end_byte = caret.index;
|
||||
if(show_selection) {
|
||||
selection_start_byte = std::min(caret.index, selection_start_caret.index);
|
||||
selection_end_byte = std::max(caret.index, selection_start_caret.index);
|
||||
}
|
||||
|
||||
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, mgl::utf8_to_utf32((const unsigned char*)event.text.str, event.text.size));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -54,26 +174,91 @@ namespace gsr {
|
||||
const mgl::vec2f draw_pos = position + offset;
|
||||
|
||||
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;
|
||||
|
||||
mgl::Rectangle background(get_size());
|
||||
mgl::Text32 &active_text = masked ? masked_text : text;
|
||||
|
||||
background.set_size(get_size());
|
||||
background.set_position(draw_pos.floor());
|
||||
background.set_color(selected ? mgl::Color(0, 0, 0, 255) : mgl::Color(0, 0, 0, 120));
|
||||
window.draw(background);
|
||||
|
||||
const int caret_width = std::max(1.0f, caret_width_scale * get_theme().window_height);
|
||||
const mgl::vec2f caret_size = mgl::vec2f(caret_width, active_text.get_bounds().size.y).floor();
|
||||
|
||||
const float overflow_left = (caret.offset_x + padding_left) - (padding_left + text_overflow);
|
||||
if(overflow_left < 0.0f)
|
||||
text_overflow += overflow_left;
|
||||
|
||||
const float overflow_right = (caret.offset_x + padding_left) - (background.get_size().x - padding_right);
|
||||
if(overflow_right - text_overflow > 0.0f)
|
||||
text_overflow = overflow_right;
|
||||
|
||||
active_text.set_position((draw_pos + mgl::vec2f(padding_left, get_size().y * 0.5f - active_text.get_bounds().size.y * 0.5f) - mgl::vec2f(text_overflow, 0.0f)).floor());
|
||||
|
||||
const auto text_bounds = active_text.get_bounds();
|
||||
const bool text_larger_than_background = text_bounds.size.x > (background.get_size().x - padding_left - padding_right);
|
||||
const float text_overflow_right = (text_bounds.position.x + text_bounds.size.x) - (background.get_position().x + background.get_size().x - padding_right);
|
||||
if(text_larger_than_background) {
|
||||
if(text_overflow_right < 0.0f) {
|
||||
text_overflow += text_overflow_right;
|
||||
active_text.set_position(active_text.get_position() + mgl::vec2f(-text_overflow_right, 0.0f));
|
||||
}
|
||||
} else {
|
||||
active_text.set_position(active_text.get_position() + mgl::vec2f(-text_overflow, 0.0f));
|
||||
text_overflow = 0.0f;
|
||||
}
|
||||
|
||||
if(selected) {
|
||||
const int border_size = std::max(1.0f, border_scale * get_theme().window_height);
|
||||
draw_rectangle_outline(window, draw_pos.floor(), get_size().floor(), get_color_theme().tint_color, border_size);
|
||||
|
||||
const int caret_width = std::max(1.0f, caret_width_scale * get_theme().window_height);
|
||||
mgl::Rectangle caret({(float)caret_width, text.get_bounds().size.y});
|
||||
caret.set_position((draw_pos + mgl::vec2f(padding_left + caret_offset_x, padding_top)).floor());
|
||||
caret.set_color(mgl::Color(255, 255, 255));
|
||||
window.draw(caret);
|
||||
draw_caret(window, draw_pos, caret_size);
|
||||
}
|
||||
|
||||
text.set_position((draw_pos + mgl::vec2f(padding_left, get_size().y * 0.5f - text.get_bounds().size.y * 0.5f)).floor());
|
||||
window.draw(text);
|
||||
const mgl::Scissor parent_scissor = window.get_scissor();
|
||||
const mgl::Scissor scissor = scissor_get_sub_area(parent_scissor,
|
||||
mgl::Scissor{
|
||||
(background.get_position() + mgl::vec2f(padding_left, padding_top)).to_vec2i(),
|
||||
(background.get_size() - mgl::vec2f(padding_left + padding_right, padding_top + padding_bottom)).to_vec2i()
|
||||
});
|
||||
window.set_scissor(scissor);
|
||||
|
||||
window.draw(active_text);
|
||||
|
||||
if(show_selection)
|
||||
draw_caret_selection(window, draw_pos, caret_size);
|
||||
|
||||
window.set_scissor(parent_scissor);
|
||||
}
|
||||
|
||||
void Entry::draw_caret(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size) {
|
||||
const int padding_top = padding_top_scale * get_theme().window_height;
|
||||
const int padding_left = padding_left_scale * get_theme().window_height;
|
||||
|
||||
mgl::Rectangle caret_rect(caret_size);
|
||||
mgl::vec2f caret_draw_pos = draw_pos + mgl::vec2f(padding_left + caret.offset_x - text_overflow, padding_top);
|
||||
caret_rect.set_position(caret_draw_pos.floor());
|
||||
caret_rect.set_color(mgl::Color(255, 255, 255));
|
||||
window.draw(caret_rect);
|
||||
}
|
||||
|
||||
void Entry::draw_caret_selection(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size) {
|
||||
if(selection_start_caret.index == caret.index)
|
||||
return;
|
||||
|
||||
const int padding_top = padding_top_scale * get_theme().window_height;
|
||||
const int padding_left = padding_left_scale * get_theme().window_height;
|
||||
const int caret_width = std::max(1.0f, caret_width_scale * get_theme().window_height);
|
||||
const int offset = caret.index < selection_start_caret.index ? caret_width : 0;
|
||||
|
||||
mgl::Rectangle caret_selection_rect(mgl::vec2f(std::abs(selection_start_caret.offset_x - caret.offset_x) - offset, caret_size.y).floor());
|
||||
caret_selection_rect.set_position((draw_pos + mgl::vec2f(padding_left + std::min(caret.offset_x, selection_start_caret.offset_x) - text_overflow + offset, padding_top)).floor());
|
||||
mgl::Color caret_select_color = get_color_theme().tint_color;
|
||||
caret_select_color.a = 100;
|
||||
caret_selection_rect.set_color(caret_select_color);
|
||||
window.draw(caret_selection_rect);
|
||||
}
|
||||
|
||||
mgl::vec2f Entry::get_size() {
|
||||
@@ -85,24 +270,157 @@ namespace gsr {
|
||||
return { max_width, text.get_bounds().size.y + padding_top + padding_bottom };
|
||||
}
|
||||
|
||||
void Entry::set_text(std::string str) {
|
||||
if(!validate_handler || validate_handler(str)) {
|
||||
text.set_string(std::move(str));
|
||||
caret_offset_x = text.find_character_pos(99999).x - this->text.get_position().x;
|
||||
if(on_changed)
|
||||
on_changed(text.get_string());
|
||||
void Entry::move_caret_word(Direction direction, size_t max_codepoints) {
|
||||
mgl::Text32 &active_text = masked ? masked_text : text;
|
||||
const int dir_step = direction == Direction::LEFT ? -1 : 1;
|
||||
const int num_delimiter_chars = 15;
|
||||
const char delimiter_chars[num_delimiter_chars + 1] = " \t\n/.,:;\\[](){}";
|
||||
const char32_t *text_str = active_text.get_string().data();
|
||||
|
||||
int num_non_delimiter_chars_found = 0;
|
||||
|
||||
for(size_t i = 0; i < max_codepoints; ++i) {
|
||||
const uint32_t codepoint = text_str[caret.index];
|
||||
|
||||
const bool is_delimiter_char = codepoint < 127 && !!memchr(delimiter_chars, codepoint, num_delimiter_chars);
|
||||
if(is_delimiter_char) {
|
||||
if(num_non_delimiter_chars_found > 0)
|
||||
break;
|
||||
} else {
|
||||
++num_non_delimiter_chars_found;
|
||||
}
|
||||
|
||||
if(caret.index + dir_step < 0 || caret.index + dir_step > (int)active_text.get_string().size())
|
||||
break;
|
||||
|
||||
caret.index += dir_step;
|
||||
}
|
||||
|
||||
// TODO: Move right by some characters instead of calculating every character to caret index
|
||||
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
|
||||
}
|
||||
|
||||
const std::string& Entry::get_text() const {
|
||||
return text.get_string();
|
||||
EntryValidateHandlerResult Entry::set_text(const std::string &str) {
|
||||
EntryValidateHandlerResult validate_result = set_text_internal(mgl::utf8_to_utf32(str));
|
||||
if(validate_result == EntryValidateHandlerResult::ALLOW) {
|
||||
mgl::Text32 &active_text = masked ? masked_text : text;
|
||||
caret.index = active_text.get_string().size();
|
||||
// TODO: Optimize
|
||||
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
|
||||
selection_start_caret = caret;
|
||||
|
||||
selecting_text = false;
|
||||
selecting_with_keyboard = false;
|
||||
show_selection = false;
|
||||
}
|
||||
return validate_result;
|
||||
}
|
||||
|
||||
EntryValidateHandlerResult Entry::set_text_internal(std::u32string str) {
|
||||
EntryValidateHandlerResult validate_result = EntryValidateHandlerResult::ALLOW;
|
||||
if(validate_handler)
|
||||
validate_result = validate_handler(*this, str);
|
||||
|
||||
if(validate_result == EntryValidateHandlerResult::ALLOW) {
|
||||
text.set_string(std::move(str));
|
||||
if(masked)
|
||||
masked_text.set_string(std::u32string(text.get_string().size(), '*'));
|
||||
// TODO: Call callback with utf32 instead?
|
||||
if(on_changed)
|
||||
on_changed(mgl::utf32_to_utf8(text.get_string()));
|
||||
}
|
||||
|
||||
return validate_result;
|
||||
}
|
||||
|
||||
std::string Entry::get_text() const {
|
||||
return mgl::utf32_to_utf8(text.get_string());
|
||||
}
|
||||
|
||||
void Entry::set_masked(bool masked) {
|
||||
if(masked == this->masked)
|
||||
return;
|
||||
|
||||
this->masked = masked;
|
||||
|
||||
if(masked)
|
||||
masked_text.set_string(std::u32string(text.get_string().size(), '*'));
|
||||
else
|
||||
masked_text.set_string(std::u32string());
|
||||
|
||||
mgl::Text32 &active_text = masked ? masked_text : text;
|
||||
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
|
||||
selection_start_caret.offset_x = active_text.find_character_pos(selection_start_caret.index).x - active_text.get_position().x;
|
||||
}
|
||||
|
||||
bool Entry::is_masked() const {
|
||||
return masked;
|
||||
}
|
||||
|
||||
void Entry::replace_text(size_t index, size_t size, const std::u32string &replacement) {
|
||||
if(index + size > text.get_string().size())
|
||||
return;
|
||||
|
||||
const auto prev_caret = caret;
|
||||
|
||||
if((int)index >= caret.index)
|
||||
caret.index += replacement.size();
|
||||
else
|
||||
caret.index = caret.index - size + replacement.size();
|
||||
|
||||
std::u32string str = text.get_string();
|
||||
str.replace(index, size, replacement);
|
||||
const EntryValidateHandlerResult validate_result = set_text_internal(std::move(str));
|
||||
if(validate_result == EntryValidateHandlerResult::DENY) {
|
||||
caret = prev_caret;
|
||||
return;
|
||||
} else if(validate_result == EntryValidateHandlerResult::REPLACED) {
|
||||
return;
|
||||
}
|
||||
|
||||
mgl::Text32 &active_text = masked ? masked_text : text;
|
||||
// TODO: Optimize
|
||||
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
|
||||
selection_start_caret = caret;
|
||||
|
||||
selecting_text = false;
|
||||
selecting_with_keyboard = false;
|
||||
show_selection = false;
|
||||
}
|
||||
|
||||
CaretIndexPos Entry::find_closest_caret_index_by_position(mgl::vec2f position) {
|
||||
mgl::Text32 &active_text = masked ? masked_text : text;
|
||||
const std::u32string &str = active_text.get_string();
|
||||
mgl::Font *font = active_text.get_font();
|
||||
|
||||
CaretIndexPos result = {0, {active_text.get_position().x, active_text.get_position().y}};
|
||||
for(result.index = 0; result.index < (int)str.size(); ++result.index) {
|
||||
const uint32_t codepoint = str[result.index];
|
||||
|
||||
float glyph_width = 0.0f;
|
||||
if(codepoint == '\t') {
|
||||
const auto glyph = font->get_glyph(' ');
|
||||
const int tab_width = 4;
|
||||
glyph_width = glyph.advance * tab_width;
|
||||
} else {
|
||||
const auto glyph = font->get_glyph(codepoint);
|
||||
glyph_width = glyph.advance;
|
||||
}
|
||||
|
||||
if(result.pos.x + glyph_width * 0.5f >= position.x)
|
||||
break;
|
||||
|
||||
result.pos.x += glyph_width;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool is_number(uint8_t c) {
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
static std::optional<int> to_integer(const std::string &str) {
|
||||
static std::optional<int> to_integer(const std::u32string &str) {
|
||||
if(str.empty())
|
||||
return std::nullopt;
|
||||
|
||||
@@ -114,7 +432,7 @@ namespace gsr {
|
||||
int number = 0;
|
||||
for(; i < str.size(); ++i) {
|
||||
if(!is_number(str[i]))
|
||||
return false;
|
||||
return std::nullopt;
|
||||
|
||||
const int new_number = number * 10 + (str[i] - '0');
|
||||
if(new_number < number)
|
||||
@@ -129,19 +447,23 @@ namespace gsr {
|
||||
}
|
||||
|
||||
EntryValidateHandler create_entry_validator_integer_in_range(int min, int max) {
|
||||
return [min, max](std::string &str) {
|
||||
return [min, max](Entry &entry, const std::u32string &str) {
|
||||
if(str.empty())
|
||||
return true;
|
||||
return EntryValidateHandlerResult::ALLOW;
|
||||
|
||||
std::optional<int> number = to_integer(str);
|
||||
const std::optional<int> number = to_integer(str);
|
||||
if(!number)
|
||||
return false;
|
||||
return EntryValidateHandlerResult::DENY;
|
||||
|
||||
if(number.value() < min)
|
||||
str = std::to_string(min);
|
||||
else if(number.value() > max)
|
||||
str = std::to_string(max);
|
||||
return true;
|
||||
if(number.value() < min) {
|
||||
entry.set_text(std::to_string(min));
|
||||
return EntryValidateHandlerResult::REPLACED;
|
||||
} else if(number.value() > max) {
|
||||
entry.set_text(std::to_string(max));
|
||||
return EntryValidateHandlerResult::REPLACED;
|
||||
}
|
||||
|
||||
return EntryValidateHandlerResult::ALLOW;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -190,11 +190,12 @@ namespace gsr {
|
||||
}
|
||||
|
||||
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);
|
||||
auto enable_hotkeys_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::VERTICAL);
|
||||
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("Yes, but only grab virtual devices (supports some input remapping software)", "enable_hotkeys_virtual_devices");
|
||||
enable_hotkeys_radio_button->add_item("Yes, but don't grab devices (supports all input remapping software)", "enable_hotkeys_no_grab");
|
||||
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());
|
||||
@@ -348,6 +349,27 @@ namespace gsr {
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> GlobalSettingsPage::create_screenshot_window_hotkey_options() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
|
||||
char str[128];
|
||||
if(gsr_info->system_info.display_server == DisplayServer::X11)
|
||||
snprintf(str, sizeof(str), "Take a screenshot of a window:");
|
||||
else
|
||||
snprintf(str, sizeof(str), "Take a screenshot with desktop portal:");
|
||||
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color));
|
||||
auto take_screenshot_window_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
take_screenshot_window_button_ptr = take_screenshot_window_button.get();
|
||||
list->add_widget(std::move(take_screenshot_window_button));
|
||||
|
||||
take_screenshot_window_button_ptr->on_click = [this] {
|
||||
configure_hotkey_start(ConfigureHotkeyType::TAKE_SCREENSHOT_WINDOW);
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> GlobalSettingsPage::create_hotkey_control_buttons() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
|
||||
@@ -362,6 +384,7 @@ namespace gsr {
|
||||
config.replay_config.save_10_min_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.screenshot_config.take_screenshot_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.screenshot_config.take_screenshot_region_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.screenshot_config.take_screenshot_window_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.main_config.show_hide_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
load_hotkeys();
|
||||
overlay->rebind_all_keyboard_hotkeys();
|
||||
@@ -404,6 +427,7 @@ namespace gsr {
|
||||
list_ptr->add_widget(create_stream_hotkey_options());
|
||||
list_ptr->add_widget(create_screenshot_hotkey_options());
|
||||
list_ptr->add_widget(create_screenshot_region_hotkey_options());
|
||||
list_ptr->add_widget(create_screenshot_window_hotkey_options());
|
||||
list_ptr->add_widget(create_hotkey_control_buttons());
|
||||
return subsection;
|
||||
}
|
||||
@@ -444,13 +468,42 @@ namespace gsr {
|
||||
return exit_program_button;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> GlobalSettingsPage::create_notification_speed() {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Notification speed", get_color_theme().text_color));
|
||||
|
||||
auto radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL);
|
||||
notification_speed_button_ptr = radio_button.get();
|
||||
radio_button->add_item("Normal", "normal");
|
||||
radio_button->add_item("Fast", "fast");
|
||||
radio_button->on_selection_changed = [this](const std::string&, const std::string &id) {
|
||||
if(id == "normal")
|
||||
overlay->set_notification_speed(NotificationSpeed::NORMAL);
|
||||
else if(id == "fast")
|
||||
overlay->set_notification_speed(NotificationSpeed::FAST);
|
||||
return true;
|
||||
};
|
||||
list->add_widget(std::move(radio_button));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<Subsection> GlobalSettingsPage::create_application_options_subsection(ScrollablePage *parent_page) {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
List *list_ptr = list.get();
|
||||
auto subsection = std::make_unique<Subsection>("Application options", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
|
||||
|
||||
list_ptr->add_widget(create_notification_speed());
|
||||
list_ptr->add_widget(std::make_unique<LineSeparator>(LineSeparator::Orientation::HORIZONTAL, subsection->get_inner_size().x));
|
||||
|
||||
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL);
|
||||
list->add_widget(create_exit_program_button());
|
||||
auto navigate_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
|
||||
navigate_list->add_widget(create_exit_program_button());
|
||||
if(inside_flatpak)
|
||||
list->add_widget(create_go_back_to_old_ui_button());
|
||||
return std::make_unique<Subsection>("Application options", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
|
||||
navigate_list->add_widget(create_go_back_to_old_ui_button());
|
||||
list_ptr->add_widget(std::move(navigate_list));
|
||||
|
||||
return subsection;
|
||||
}
|
||||
|
||||
std::unique_ptr<Subsection> GlobalSettingsPage::create_application_info_subsection(ScrollablePage *parent_page) {
|
||||
@@ -476,6 +529,22 @@ namespace gsr {
|
||||
return std::make_unique<Subsection>("Application info", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
std::unique_ptr<Subsection> GlobalSettingsPage::create_donate_subsection(ScrollablePage *parent_page) {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "If you would like to donate you can do so by donating at https://buymeacoffee.com/dec05eba:", get_color_theme().text_color));
|
||||
|
||||
auto donate_button = std::make_unique<Button>(&get_theme().body_font, "Donate", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
donate_button->on_click = [this] {
|
||||
const char *args[] = { "xdg-open", "https://buymeacoffee.com/dec05eba", nullptr };
|
||||
exec_program_daemonized(args);
|
||||
overlay->hide_next_frame();
|
||||
};
|
||||
list->add_widget(std::move(donate_button));
|
||||
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "All donations go toward developing software (including GPU Screen Recorder)\nand buying hardware to test the software.", get_color_theme().text_color));
|
||||
return std::make_unique<Subsection>("Donate", 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());
|
||||
|
||||
@@ -487,6 +556,7 @@ namespace gsr {
|
||||
settings_list->add_widget(create_controller_hotkey_subsection(scrollable_page.get()));
|
||||
settings_list->add_widget(create_application_options_subsection(scrollable_page.get()));
|
||||
settings_list->add_widget(create_application_info_subsection(scrollable_page.get()));
|
||||
settings_list->add_widget(create_donate_subsection(scrollable_page.get()));
|
||||
scrollable_page->add_widget(std::move(settings_list));
|
||||
|
||||
content_page_ptr->add_widget(std::move(scrollable_page));
|
||||
@@ -512,6 +582,8 @@ namespace gsr {
|
||||
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);
|
||||
|
||||
notification_speed_button_ptr->set_selected_item(config.main_config.notification_speed);
|
||||
|
||||
load_hotkeys();
|
||||
}
|
||||
|
||||
@@ -528,6 +600,7 @@ namespace gsr {
|
||||
|
||||
take_screenshot_button_ptr->set_text(config.screenshot_config.take_screenshot_hotkey.to_string());
|
||||
take_screenshot_region_button_ptr->set_text(config.screenshot_config.take_screenshot_region_hotkey.to_string());
|
||||
take_screenshot_window_button_ptr->set_text(config.screenshot_config.take_screenshot_window_hotkey.to_string());
|
||||
|
||||
show_hide_button_ptr->set_text(config.main_config.show_hide_hotkey.to_string());
|
||||
}
|
||||
@@ -537,6 +610,7 @@ namespace gsr {
|
||||
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();
|
||||
config.main_config.notification_speed = notification_speed_button_ptr->get_selected_id();
|
||||
save_config(config);
|
||||
}
|
||||
|
||||
@@ -611,6 +685,8 @@ namespace gsr {
|
||||
return take_screenshot_button_ptr;
|
||||
case ConfigureHotkeyType::TAKE_SCREENSHOT_REGION:
|
||||
return take_screenshot_region_button_ptr;
|
||||
case ConfigureHotkeyType::TAKE_SCREENSHOT_WINDOW:
|
||||
return take_screenshot_window_button_ptr;
|
||||
case ConfigureHotkeyType::SHOW_HIDE:
|
||||
return show_hide_button_ptr;
|
||||
}
|
||||
@@ -639,6 +715,8 @@ namespace gsr {
|
||||
return &config.screenshot_config.take_screenshot_hotkey;
|
||||
case ConfigureHotkeyType::TAKE_SCREENSHOT_REGION:
|
||||
return &config.screenshot_config.take_screenshot_region_hotkey;
|
||||
case ConfigureHotkeyType::TAKE_SCREENSHOT_WINDOW:
|
||||
return &config.screenshot_config.take_screenshot_window_hotkey;
|
||||
case ConfigureHotkeyType::SHOW_HIDE:
|
||||
return &config.main_config.show_hide_hotkey;
|
||||
}
|
||||
@@ -654,6 +732,7 @@ namespace gsr {
|
||||
&config.streaming_config.start_stop_hotkey,
|
||||
&config.screenshot_config.take_screenshot_hotkey,
|
||||
&config.screenshot_config.take_screenshot_region_hotkey,
|
||||
&config.screenshot_config.take_screenshot_window_hotkey,
|
||||
&config.main_config.show_hide_hotkey
|
||||
};
|
||||
for(ConfigHotkey *config_hotkey : config_hotkeys) {
|
||||
@@ -702,6 +781,13 @@ namespace gsr {
|
||||
case ConfigureHotkeyType::TAKE_SCREENSHOT_REGION:
|
||||
hotkey_configure_action_name = "Take a screenshot of a region";
|
||||
break;
|
||||
case ConfigureHotkeyType::TAKE_SCREENSHOT_WINDOW: {
|
||||
if(gsr_info->system_info.display_server == DisplayServer::X11)
|
||||
hotkey_configure_action_name = "Take a screenshot of a window";
|
||||
else
|
||||
hotkey_configure_action_name = "Take a screenshot with desktop portal";
|
||||
break;
|
||||
}
|
||||
case ConfigureHotkeyType::SHOW_HIDE:
|
||||
hotkey_configure_action_name = "Show/hide UI";
|
||||
break;
|
||||
|
||||
@@ -35,13 +35,12 @@ namespace gsr {
|
||||
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");
|
||||
if(capture_options.window)
|
||||
record_area_box->add_item("Window", "window");
|
||||
if(capture_options.region)
|
||||
record_area_box->add_item("Region", "region");
|
||||
if(!capture_options.monitors.empty())
|
||||
record_area_box->add_item(gsr_info->system_info.display_server == DisplayServer::WAYLAND ? "Focused monitor (Experimental on Wayland)" : "Focused monitor", "focused_monitor");
|
||||
record_area_box->add_item("Focused monitor", "focused_monitor");
|
||||
for(const auto &monitor : capture_options.monitors) {
|
||||
char name[256];
|
||||
snprintf(name, sizeof(name), "Monitor %s (%dx%d)", monitor.name.c_str(), monitor.size.x, monitor.size.y);
|
||||
@@ -60,14 +59,6 @@ namespace gsr {
|
||||
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);
|
||||
@@ -124,13 +115,12 @@ namespace gsr {
|
||||
|
||||
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));
|
||||
return std::make_unique<Subsection>("Capture", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
std::unique_ptr<List> ScreenshotSettingsPage::create_image_quality_section() {
|
||||
@@ -212,7 +202,7 @@ namespace gsr {
|
||||
|
||||
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_save_directory("Directory to save screenshots:"));
|
||||
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));
|
||||
}
|
||||
@@ -225,15 +215,38 @@ namespace gsr {
|
||||
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<CheckBox> ScreenshotSettingsPage::create_save_screenshot_to_clipboard() {
|
||||
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, gsr_info->system_info.display_server == DisplayServer::X11 ? "Save screenshot to clipboard" : "Save screenshot to clipboard (Not supported properly by Wayland)");
|
||||
save_screenshot_to_clipboard_checkbox_ptr = checkbox.get();
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
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_notifications() {
|
||||
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show screenshot notifications");
|
||||
checkbox->set_checked(true);
|
||||
show_notification_checkbox_ptr = checkbox.get();
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_led_indicator() {
|
||||
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Blink scroll lock led when taking a screenshot");
|
||||
checkbox->set_checked(true);
|
||||
led_indicator_checkbox_ptr = checkbox.get();
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_general_section() {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
list->add_widget(create_save_screenshot_in_game_folder());
|
||||
list->add_widget(create_save_screenshot_to_clipboard());
|
||||
return std::make_unique<Subsection>("General", std::move(list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_screenshot_indicator_section() {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
list->add_widget(create_notifications());
|
||||
list->add_widget(create_led_indicator());
|
||||
return std::make_unique<Subsection>("Screenshot indicator", std::move(list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_settings() {
|
||||
@@ -249,7 +262,7 @@ namespace gsr {
|
||||
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_list->add_widget(create_screenshot_indicator_section());
|
||||
settings_scrollable_page_ptr->add_widget(std::move(settings_list));
|
||||
return page_list;
|
||||
}
|
||||
@@ -258,9 +271,7 @@ namespace gsr {
|
||||
content_page_ptr->add_widget(create_settings());
|
||||
|
||||
record_area_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
|
||||
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;
|
||||
@@ -293,7 +304,9 @@ namespace gsr {
|
||||
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);
|
||||
save_screenshot_to_clipboard_checkbox_ptr->set_checked(config.screenshot_config.save_screenshot_to_clipboard);
|
||||
show_notification_checkbox_ptr->set_checked(config.screenshot_config.show_notifications);
|
||||
led_indicator_checkbox_ptr->set_checked(config.screenshot_config.use_led_indicator);
|
||||
|
||||
if(config.screenshot_config.image_width == 0)
|
||||
config.screenshot_config.image_width = 1920;
|
||||
@@ -321,7 +334,9 @@ namespace gsr {
|
||||
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();
|
||||
config.screenshot_config.save_screenshot_to_clipboard = save_screenshot_to_clipboard_checkbox_ptr->is_checked();
|
||||
config.screenshot_config.show_notifications = show_notification_checkbox_ptr->is_checked();
|
||||
config.screenshot_config.use_led_indicator = led_indicator_checkbox_ptr->is_checked();
|
||||
|
||||
if(config.screenshot_config.image_width == 0)
|
||||
config.screenshot_config.image_width = 1920;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "../../include/gui/PageStack.hpp"
|
||||
#include "../../include/gui/FileChooser.hpp"
|
||||
#include "../../include/gui/Subsection.hpp"
|
||||
#include "../../include/gui/Image.hpp"
|
||||
#include "../../include/Theme.hpp"
|
||||
#include "../../include/GsrInfo.hpp"
|
||||
#include "../../include/Utils.hpp"
|
||||
@@ -65,15 +66,14 @@ namespace gsr {
|
||||
std::unique_ptr<ComboBox> SettingsPage::create_record_area_box() {
|
||||
auto record_area_box = std::make_unique<ComboBox>(&get_theme().body_font);
|
||||
// TODO: Show options not supported but disable them
|
||||
// TODO: Enable this
|
||||
//if(capture_options.window)
|
||||
// record_area_box->add_item("Window", "window");
|
||||
if(capture_options.region)
|
||||
record_area_box->add_item("Region", "region");
|
||||
if(capture_options.window)
|
||||
record_area_box->add_item("Window", "window");
|
||||
if(capture_options.focused)
|
||||
record_area_box->add_item("Follow focused window", "focused");
|
||||
if(capture_options.region)
|
||||
record_area_box->add_item("Region", "region");
|
||||
if(!capture_options.monitors.empty())
|
||||
record_area_box->add_item(gsr_info->system_info.display_server == DisplayServer::WAYLAND ? "Focused monitor (Experimental on Wayland)" : "Focused monitor", "focused_monitor");
|
||||
record_area_box->add_item("Focused monitor", "focused_monitor");
|
||||
for(const auto &monitor : capture_options.monitors) {
|
||||
char name[256];
|
||||
snprintf(name, sizeof(name), "Monitor %s (%dx%d)", monitor.name.c_str(), monitor.size.x, monitor.size.y);
|
||||
@@ -92,14 +92,6 @@ namespace gsr {
|
||||
return record_area_list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> SettingsPage::create_select_window() {
|
||||
auto select_window_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
select_window_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Select window:", get_color_theme().text_color));
|
||||
select_window_list->add_widget(std::make_unique<Button>(&get_theme().body_font, "Click here to select a window...", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)));
|
||||
select_window_list_ptr = select_window_list.get();
|
||||
return select_window_list;
|
||||
}
|
||||
|
||||
std::unique_ptr<Entry> SettingsPage::create_area_width_entry() {
|
||||
auto area_width_entry = std::make_unique<Entry>(&get_theme().body_font, "1920", get_theme().body_font.get_character_size() * 3);
|
||||
area_width_entry->validate_handler = create_entry_validator_integer_in_range(1, 1 << 15);
|
||||
@@ -186,14 +178,13 @@ namespace gsr {
|
||||
|
||||
auto capture_target_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
capture_target_list->add_widget(create_record_area());
|
||||
capture_target_list->add_widget(create_select_window());
|
||||
capture_target_list->add_widget(create_area_size_section());
|
||||
capture_target_list->add_widget(create_video_resolution_section());
|
||||
capture_target_list->add_widget(create_restore_portal_session_section());
|
||||
|
||||
ll->add_widget(std::move(capture_target_list));
|
||||
ll->add_widget(create_change_video_resolution_section());
|
||||
return std::make_unique<Subsection>("Record area", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
return std::make_unique<Subsection>("Capture", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
static bool audio_device_is_output(const std::string &audio_device_id) {
|
||||
@@ -451,13 +442,13 @@ namespace gsr {
|
||||
|
||||
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));
|
||||
auto video_bitrate_entry = std::make_unique<Entry>(&get_theme().body_font, "8000", (int)(get_theme().body_font.get_character_size() * 4.0f));
|
||||
video_bitrate_entry->validate_handler = create_entry_validator_integer_in_range(1, 500000);
|
||||
video_bitrate_entry_ptr = video_bitrate_entry.get();
|
||||
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.64MB", get_color_theme().text_color);
|
||||
auto size_mb_label = std::make_unique<Label>(&get_theme().body_font, "", get_color_theme().text_color);
|
||||
Label *size_mb_label_ptr = size_mb_label.get();
|
||||
list->add_widget(std::move(size_mb_label));
|
||||
|
||||
@@ -577,6 +568,10 @@ namespace gsr {
|
||||
framerate_mode_box->add_item("Auto (Recommended)", "auto");
|
||||
framerate_mode_box->add_item("Constant", "cfr");
|
||||
framerate_mode_box->add_item("Variable", "vfr");
|
||||
if(gsr_info->system_info.display_server == DisplayServer::X11)
|
||||
framerate_mode_box->add_item("Sync to content", "content");
|
||||
else
|
||||
framerate_mode_box->add_item("Sync to content (Only X11 or desktop portal capture)", "content");
|
||||
framerate_mode_box_ptr = framerate_mode_box.get();
|
||||
return framerate_mode_box;
|
||||
}
|
||||
@@ -634,10 +629,8 @@ namespace gsr {
|
||||
content_page_ptr->add_widget(create_settings());
|
||||
|
||||
record_area_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
|
||||
const bool window_selected = id == "window";
|
||||
const bool focused_selected = id == "focused";
|
||||
const bool portal_selected = id == "portal";
|
||||
select_window_list_ptr->set_visible(window_selected);
|
||||
area_size_list_ptr->set_visible(focused_selected);
|
||||
video_resolution_list_ptr->set_visible(!focused_selected && change_video_resolution_checkbox_ptr->is_checked());
|
||||
change_video_resolution_checkbox_ptr->set_visible(!focused_selected);
|
||||
@@ -828,16 +821,41 @@ namespace gsr {
|
||||
replay_time_label_ptr->set_text(buffer);
|
||||
}
|
||||
|
||||
void SettingsPage::view_changed(bool advanced_view, Subsection *notifications_subsection_ptr) {
|
||||
void SettingsPage::view_changed(bool advanced_view) {
|
||||
color_range_list_ptr->set_visible(advanced_view);
|
||||
audio_codec_ptr->set_visible(advanced_view);
|
||||
video_codec_ptr->set_visible(advanced_view);
|
||||
framerate_mode_list_ptr->set_visible(advanced_view);
|
||||
notifications_subsection_ptr->set_visible(advanced_view);
|
||||
set_application_audio_options_visible(audio_track_section_list_ptr, advanced_view, *gsr_info);
|
||||
settings_scrollable_page_ptr->reset_scroll();
|
||||
}
|
||||
|
||||
std::unique_ptr<CheckBox> SettingsPage::create_led_indicator(const char *type) {
|
||||
char label_str[256];
|
||||
snprintf(label_str, sizeof(label_str), "Show %s status with scroll lock led", type);
|
||||
|
||||
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, label_str);
|
||||
checkbox->set_checked(false);
|
||||
led_indicator_checkbox_ptr = checkbox.get();
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
std::unique_ptr<CheckBox> SettingsPage::create_notifications(const char *type) {
|
||||
char label_str[256];
|
||||
snprintf(label_str, sizeof(label_str), "Show %s notifications", type);
|
||||
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, label_str);
|
||||
checkbox->set_checked(true);
|
||||
show_notification_checkbox_ptr = checkbox.get();
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> SettingsPage::create_indicator(const char *type) {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
list->add_widget(create_notifications(type));
|
||||
list->add_widget(create_led_indicator(type));
|
||||
return list;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -853,33 +871,13 @@ namespace gsr {
|
||||
general_list->add_widget(create_save_replay_in_game_folder());
|
||||
if(gsr_info->system_info.gsr_version >= GsrVersion{5, 0, 3})
|
||||
general_list->add_widget(create_restart_replay_on_save());
|
||||
settings_list_ptr->add_widget(std::make_unique<Subsection>("General", std::move(general_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
|
||||
|
||||
settings_list_ptr->add_widget(std::make_unique<Subsection>("General", std::move(general_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
|
||||
settings_list_ptr->add_widget(std::make_unique<Subsection>("Replay indicator", create_indicator("replay"), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
|
||||
settings_list_ptr->add_widget(std::make_unique<Subsection>("Autostart", create_start_replay_automatically(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
|
||||
|
||||
auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
|
||||
auto show_replay_started_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show replay started notification");
|
||||
show_replay_started_notification_checkbox->set_checked(true);
|
||||
show_replay_started_notification_checkbox_ptr = show_replay_started_notification_checkbox.get();
|
||||
checkboxes_list->add_widget(std::move(show_replay_started_notification_checkbox));
|
||||
|
||||
auto show_replay_stopped_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show replay stopped notification");
|
||||
show_replay_stopped_notification_checkbox->set_checked(true);
|
||||
show_replay_stopped_notification_checkbox_ptr = show_replay_stopped_notification_checkbox.get();
|
||||
checkboxes_list->add_widget(std::move(show_replay_stopped_notification_checkbox));
|
||||
|
||||
auto show_replay_saved_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show replay saved notification");
|
||||
show_replay_saved_notification_checkbox->set_checked(true);
|
||||
show_replay_saved_notification_checkbox_ptr = show_replay_saved_notification_checkbox.get();
|
||||
checkboxes_list->add_widget(std::move(show_replay_saved_notification_checkbox));
|
||||
|
||||
auto notifications_subsection = std::make_unique<Subsection>("Notifications", std::move(checkboxes_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
Subsection *notifications_subsection_ptr = notifications_subsection.get();
|
||||
settings_list_ptr->add_widget(std::move(notifications_subsection));
|
||||
|
||||
view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string&, const std::string &id) {
|
||||
view_changed(id == "advanced", notifications_subsection_ptr);
|
||||
view_radio_button_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
|
||||
view_changed(id == "advanced");
|
||||
return true;
|
||||
};
|
||||
view_radio_button_ptr->on_selection_changed("Simple", "simple");
|
||||
@@ -920,37 +918,21 @@ namespace gsr {
|
||||
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_save_directory("Directory to save videos:"));
|
||||
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 general_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
general_list->add_widget(create_save_recording_in_game_folder());
|
||||
|
||||
auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
settings_list_ptr->add_widget(std::make_unique<Subsection>("General", std::move(general_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
|
||||
settings_list_ptr->add_widget(std::make_unique<Subsection>("Recording indicator", create_indicator("recording"), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
|
||||
|
||||
auto show_recording_started_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show recording started notification");
|
||||
show_recording_started_notification_checkbox->set_checked(true);
|
||||
show_recording_started_notification_checkbox_ptr = show_recording_started_notification_checkbox.get();
|
||||
checkboxes_list->add_widget(std::move(show_recording_started_notification_checkbox));
|
||||
|
||||
auto show_video_saved_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show video saved notification");
|
||||
show_video_saved_notification_checkbox->set_checked(true);
|
||||
show_video_saved_notification_checkbox_ptr = show_video_saved_notification_checkbox.get();
|
||||
checkboxes_list->add_widget(std::move(show_video_saved_notification_checkbox));
|
||||
|
||||
auto show_video_paused_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show video paused/unpaused notification");
|
||||
show_video_paused_notification_checkbox->set_checked(true);
|
||||
show_video_paused_notification_checkbox_ptr = show_video_paused_notification_checkbox.get();
|
||||
checkboxes_list->add_widget(std::move(show_video_paused_notification_checkbox));
|
||||
|
||||
auto notifications_subsection = std::make_unique<Subsection>("Notifications", std::move(checkboxes_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
Subsection *notifications_subsection_ptr = notifications_subsection.get();
|
||||
settings_list_ptr->add_widget(std::move(notifications_subsection));
|
||||
|
||||
view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string&, const std::string &id) {
|
||||
view_changed(id == "advanced", notifications_subsection_ptr);
|
||||
view_radio_button_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
|
||||
view_changed(id == "advanced");
|
||||
return true;
|
||||
};
|
||||
view_radio_button_ptr->on_selection_changed("Simple", "simple");
|
||||
@@ -964,6 +946,7 @@ namespace gsr {
|
||||
auto streaming_service_box = std::make_unique<ComboBox>(&get_theme().body_font);
|
||||
streaming_service_box->add_item("Twitch", "twitch");
|
||||
streaming_service_box->add_item("YouTube", "youtube");
|
||||
streaming_service_box->add_item("Rumble", "rumble");
|
||||
streaming_service_box->add_item("Custom", "custom");
|
||||
streaming_service_box_ptr = streaming_service_box.get();
|
||||
return streaming_service_box;
|
||||
@@ -976,32 +959,75 @@ namespace gsr {
|
||||
return streaming_service_list;
|
||||
}
|
||||
|
||||
static std::unique_ptr<Button> create_mask_toggle_button(Entry *entry_to_toggle, mgl::vec2f size) {
|
||||
auto button = std::make_unique<Button>(&get_theme().body_font, "", size, mgl::Color(0, 0, 0, 0));
|
||||
Button *button_ptr = button.get();
|
||||
button->set_icon(&get_theme().masked_texture);
|
||||
button->on_click = [entry_to_toggle, button_ptr]() {
|
||||
const bool is_masked = entry_to_toggle->is_masked();
|
||||
button_ptr->set_icon(is_masked ? &get_theme().unmasked_texture : &get_theme().masked_texture);
|
||||
entry_to_toggle->set_masked(!is_masked);
|
||||
};
|
||||
return button;
|
||||
}
|
||||
|
||||
static Entry* add_stream_key_entry_to_list(List *stream_key_list) {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
auto key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
|
||||
key_entry->set_masked(true);
|
||||
Entry *key_entry_ptr = key_entry.get();
|
||||
const float mask_icon_size = key_entry_ptr->get_size().y * 0.9f;
|
||||
list->add_widget(std::move(key_entry));
|
||||
list->add_widget(create_mask_toggle_button(key_entry_ptr, mgl::vec2f(mask_icon_size, mask_icon_size)));
|
||||
stream_key_list->add_widget(std::move(list));
|
||||
return key_entry_ptr;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> SettingsPage::create_stream_key_section() {
|
||||
auto stream_key_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
stream_key_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream key:", get_color_theme().text_color));
|
||||
|
||||
auto twitch_stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
|
||||
twitch_stream_key_entry_ptr = twitch_stream_key_entry.get();
|
||||
stream_key_list->add_widget(std::move(twitch_stream_key_entry));
|
||||
|
||||
auto youtube_stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
|
||||
youtube_stream_key_entry_ptr = youtube_stream_key_entry.get();
|
||||
stream_key_list->add_widget(std::move(youtube_stream_key_entry));
|
||||
twitch_stream_key_entry_ptr = add_stream_key_entry_to_list(stream_key_list.get());
|
||||
youtube_stream_key_entry_ptr = add_stream_key_entry_to_list(stream_key_list.get());
|
||||
rumble_stream_key_entry_ptr = add_stream_key_entry_to_list(stream_key_list.get());
|
||||
|
||||
stream_key_list_ptr = stream_key_list.get();
|
||||
return stream_key_list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> SettingsPage::create_stream_url_section() {
|
||||
auto stream_url_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
stream_url_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "URL:", get_color_theme().text_color));
|
||||
|
||||
std::unique_ptr<List> SettingsPage::create_stream_custom_url() {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
auto stream_url_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
|
||||
stream_url_entry_ptr = stream_url_entry.get();
|
||||
stream_url_list->add_widget(std::move(stream_url_entry));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream URL:", get_color_theme().text_color));
|
||||
list->add_widget(std::move(stream_url_entry));
|
||||
return list;
|
||||
}
|
||||
|
||||
stream_url_list_ptr = stream_url_list.get();
|
||||
return stream_url_list;
|
||||
std::unique_ptr<List> SettingsPage::create_stream_custom_key() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
auto stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
|
||||
stream_key_entry->set_masked(true);
|
||||
stream_key_entry_ptr = stream_key_entry.get();
|
||||
const float mask_icon_size = stream_key_entry_ptr->get_size().y * 0.9f;
|
||||
list->add_widget(std::move(stream_key_entry));
|
||||
list->add_widget(create_mask_toggle_button(stream_key_entry_ptr, mgl::vec2f(mask_icon_size, mask_icon_size)));
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> SettingsPage::create_stream_custom_section() {
|
||||
auto custom_stream_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
|
||||
auto stream_url_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
|
||||
stream_url_list->add_widget(create_stream_custom_url());
|
||||
stream_url_list->add_widget(create_stream_container());
|
||||
|
||||
custom_stream_list->add_widget(std::move(stream_url_list));
|
||||
custom_stream_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream key:", get_color_theme().text_color));
|
||||
custom_stream_list->add_widget(create_stream_custom_key());
|
||||
|
||||
custom_stream_list_ptr = custom_stream_list.get();
|
||||
return custom_stream_list;
|
||||
}
|
||||
|
||||
std::unique_ptr<ComboBox> SettingsPage::create_stream_container_box() {
|
||||
@@ -1014,11 +1040,10 @@ namespace gsr {
|
||||
return container_box;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> SettingsPage::create_stream_container_section() {
|
||||
std::unique_ptr<List> SettingsPage::create_stream_container() {
|
||||
auto container_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
container_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Container:", get_color_theme().text_color));
|
||||
container_list->add_widget(create_stream_container_box());
|
||||
container_list_ptr = container_list.get();
|
||||
return container_list;
|
||||
}
|
||||
|
||||
@@ -1026,41 +1051,32 @@ namespace gsr {
|
||||
auto streaming_info_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
|
||||
streaming_info_list->add_widget(create_streaming_service_section());
|
||||
streaming_info_list->add_widget(create_stream_key_section());
|
||||
streaming_info_list->add_widget(create_stream_url_section());
|
||||
streaming_info_list->add_widget(create_stream_container_section());
|
||||
streaming_info_list->add_widget(create_stream_custom_section());
|
||||
|
||||
settings_list_ptr->add_widget(std::make_unique<Subsection>("Streaming info", std::move(streaming_info_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
|
||||
|
||||
auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
auto general_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
general_list->add_widget(create_save_recording_in_game_folder());
|
||||
|
||||
auto show_streaming_started_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show streaming started notification");
|
||||
show_streaming_started_notification_checkbox->set_checked(true);
|
||||
show_streaming_started_notification_checkbox_ptr = show_streaming_started_notification_checkbox.get();
|
||||
checkboxes_list->add_widget(std::move(show_streaming_started_notification_checkbox));
|
||||
|
||||
auto show_streaming_stopped_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show streaming stopped notification");
|
||||
show_streaming_stopped_notification_checkbox->set_checked(true);
|
||||
show_streaming_stopped_notification_checkbox_ptr = show_streaming_stopped_notification_checkbox.get();
|
||||
checkboxes_list->add_widget(std::move(show_streaming_stopped_notification_checkbox));
|
||||
|
||||
auto notifications_subsection = std::make_unique<Subsection>("Notifications", std::move(checkboxes_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
Subsection *notifications_subsection_ptr = notifications_subsection.get();
|
||||
settings_list_ptr->add_widget(std::move(notifications_subsection));
|
||||
settings_list_ptr->add_widget(std::make_unique<Subsection>("General", std::move(general_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
|
||||
settings_list_ptr->add_widget(std::make_unique<Subsection>("Streaming indicator", create_indicator("streaming"), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
|
||||
|
||||
streaming_service_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
|
||||
const bool twitch_option = id == "twitch";
|
||||
const bool youtube_option = id == "youtube";
|
||||
const bool rumble_option = id == "rumble";
|
||||
const bool custom_option = id == "custom";
|
||||
stream_key_list_ptr->set_visible(!custom_option);
|
||||
stream_url_list_ptr->set_visible(custom_option);
|
||||
container_list_ptr->set_visible(custom_option);
|
||||
twitch_stream_key_entry_ptr->set_visible(twitch_option);
|
||||
youtube_stream_key_entry_ptr->set_visible(youtube_option);
|
||||
custom_stream_list_ptr->set_visible(custom_option);
|
||||
twitch_stream_key_entry_ptr->get_parent_widget()->set_visible(twitch_option);
|
||||
youtube_stream_key_entry_ptr->get_parent_widget()->set_visible(youtube_option);
|
||||
rumble_stream_key_entry_ptr->get_parent_widget()->set_visible(rumble_option);
|
||||
return true;
|
||||
};
|
||||
streaming_service_box_ptr->on_selection_changed("Twitch", "twitch");
|
||||
|
||||
view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string&, const std::string &id) {
|
||||
view_changed(id == "advanced", notifications_subsection_ptr);
|
||||
view_radio_button_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
|
||||
view_changed(id == "advanced");
|
||||
return true;
|
||||
};
|
||||
view_radio_button_ptr->on_selection_changed("Simple", "simple");
|
||||
@@ -1181,6 +1197,8 @@ namespace gsr {
|
||||
//record_options.overclock = false;
|
||||
record_cursor_checkbox_ptr->set_checked(record_options.record_cursor);
|
||||
restore_portal_session_checkbox_ptr->set_checked(record_options.restore_portal_session);
|
||||
show_notification_checkbox_ptr->set_checked(record_options.show_notifications);
|
||||
led_indicator_checkbox_ptr->set_checked(record_options.use_led_indicator);
|
||||
|
||||
if(record_options.record_area_width == 0)
|
||||
record_options.record_area_width = 1920;
|
||||
@@ -1226,9 +1244,7 @@ namespace gsr {
|
||||
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);
|
||||
|
||||
@@ -1242,21 +1258,18 @@ namespace gsr {
|
||||
void SettingsPage::load_record() {
|
||||
load_common(config.record_config.record_options);
|
||||
save_recording_in_game_folder_ptr->set_checked(config.record_config.save_video_in_game_folder);
|
||||
show_recording_started_notification_checkbox_ptr->set_checked(config.record_config.show_recording_started_notifications);
|
||||
show_video_saved_notification_checkbox_ptr->set_checked(config.record_config.show_video_saved_notifications);
|
||||
show_video_paused_notification_checkbox_ptr->set_checked(config.record_config.show_video_paused_notifications);
|
||||
save_directory_button_ptr->set_text(config.record_config.save_directory);
|
||||
container_box_ptr->set_selected_item(config.record_config.container);
|
||||
}
|
||||
|
||||
void SettingsPage::load_stream() {
|
||||
load_common(config.streaming_config.record_options);
|
||||
show_streaming_started_notification_checkbox_ptr->set_checked(config.streaming_config.show_streaming_started_notifications);
|
||||
show_streaming_stopped_notification_checkbox_ptr->set_checked(config.streaming_config.show_streaming_stopped_notifications);
|
||||
streaming_service_box_ptr->set_selected_item(config.streaming_config.streaming_service);
|
||||
youtube_stream_key_entry_ptr->set_text(config.streaming_config.youtube.stream_key);
|
||||
twitch_stream_key_entry_ptr->set_text(config.streaming_config.twitch.stream_key);
|
||||
rumble_stream_key_entry_ptr->set_text(config.streaming_config.rumble.stream_key);
|
||||
stream_url_entry_ptr->set_text(config.streaming_config.custom.url);
|
||||
stream_key_entry_ptr->set_text(config.streaming_config.custom.key);
|
||||
container_box_ptr->set_selected_item(config.streaming_config.custom.container);
|
||||
}
|
||||
|
||||
@@ -1316,6 +1329,8 @@ namespace gsr {
|
||||
//record_options.overclock = false;
|
||||
record_options.record_cursor = record_cursor_checkbox_ptr->is_checked();
|
||||
record_options.restore_portal_session = restore_portal_session_checkbox_ptr->is_checked();
|
||||
record_options.show_notifications = show_notification_checkbox_ptr->is_checked();
|
||||
record_options.use_led_indicator = led_indicator_checkbox_ptr->is_checked();
|
||||
|
||||
if(record_options.record_area_width == 0)
|
||||
record_options.record_area_width = 1920;
|
||||
@@ -1366,9 +1381,6 @@ namespace gsr {
|
||||
config.replay_config.save_video_in_game_folder = save_replay_in_game_folder_ptr->is_checked();
|
||||
if(restart_replay_on_save)
|
||||
config.replay_config.restart_replay_on_save = restart_replay_on_save->is_checked();
|
||||
config.replay_config.show_replay_started_notifications = show_replay_started_notification_checkbox_ptr->is_checked();
|
||||
config.replay_config.show_replay_stopped_notifications = show_replay_stopped_notification_checkbox_ptr->is_checked();
|
||||
config.replay_config.show_replay_saved_notifications = show_replay_saved_notification_checkbox_ptr->is_checked();
|
||||
config.replay_config.save_directory = save_directory_button_ptr->get_text();
|
||||
config.replay_config.container = container_box_ptr->get_selected_id();
|
||||
config.replay_config.replay_time = atoi(replay_time_entry_ptr->get_text().c_str());
|
||||
@@ -1383,21 +1395,18 @@ namespace gsr {
|
||||
void SettingsPage::save_record() {
|
||||
save_common(config.record_config.record_options);
|
||||
config.record_config.save_video_in_game_folder = save_recording_in_game_folder_ptr->is_checked();
|
||||
config.record_config.show_recording_started_notifications = show_recording_started_notification_checkbox_ptr->is_checked();
|
||||
config.record_config.show_video_saved_notifications = show_video_saved_notification_checkbox_ptr->is_checked();
|
||||
config.record_config.show_video_paused_notifications = show_video_paused_notification_checkbox_ptr->is_checked();
|
||||
config.record_config.save_directory = save_directory_button_ptr->get_text();
|
||||
config.record_config.container = container_box_ptr->get_selected_id();
|
||||
}
|
||||
|
||||
void SettingsPage::save_stream() {
|
||||
save_common(config.streaming_config.record_options);
|
||||
config.streaming_config.show_streaming_started_notifications = show_streaming_started_notification_checkbox_ptr->is_checked();
|
||||
config.streaming_config.show_streaming_stopped_notifications = show_streaming_stopped_notification_checkbox_ptr->is_checked();
|
||||
config.streaming_config.streaming_service = streaming_service_box_ptr->get_selected_id();
|
||||
config.streaming_config.youtube.stream_key = youtube_stream_key_entry_ptr->get_text();
|
||||
config.streaming_config.twitch.stream_key = twitch_stream_key_entry_ptr->get_text();
|
||||
config.streaming_config.rumble.stream_key = rumble_stream_key_entry_ptr->get_text();
|
||||
config.streaming_config.custom.url = stream_url_entry_ptr->get_text();
|
||||
config.streaming_config.custom.key = stream_key_entry_ptr->get_text();
|
||||
config.streaming_config.custom.container = container_box_ptr->get_selected_id();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,18 @@
|
||||
namespace gsr {
|
||||
static double frame_delta_seconds = 1.0;
|
||||
|
||||
static mgl::vec2i min_vec2i(mgl::vec2i a, mgl::vec2i b) {
|
||||
return { std::min(a.x, b.x), std::min(a.y, b.y) };
|
||||
}
|
||||
|
||||
static mgl::vec2i max_vec2i(mgl::vec2i a, mgl::vec2i b) {
|
||||
return { std::max(a.x, b.x), std::max(a.y, b.y) };
|
||||
}
|
||||
|
||||
static mgl::vec2i clamp_vec2i(mgl::vec2i value, mgl::vec2i min, mgl::vec2i max) {
|
||||
return min_vec2i(max, max_vec2i(value, min));
|
||||
}
|
||||
|
||||
// TODO: Use vertices to make it one draw call
|
||||
void draw_rectangle_outline(mgl::Window &window, mgl::vec2f pos, mgl::vec2f size, mgl::Color color, float border_size) {
|
||||
pos = pos.floor();
|
||||
@@ -74,4 +86,12 @@ namespace gsr {
|
||||
else
|
||||
return from;
|
||||
}
|
||||
|
||||
mgl::Scissor scissor_get_sub_area(mgl::Scissor parent, mgl::Scissor child) {
|
||||
const mgl::vec2i pos = clamp_vec2i(child.position, parent.position, parent.position + parent.size);
|
||||
return mgl::Scissor{
|
||||
pos,
|
||||
min_vec2i(child.size, parent.position + parent.size - pos)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,10 @@ namespace gsr {
|
||||
this->visible = visible;
|
||||
}
|
||||
|
||||
Widget* Widget::get_parent_widget() {
|
||||
return parent_widget;
|
||||
}
|
||||
|
||||
void add_widget_to_remove(std::unique_ptr<Widget> widget) {
|
||||
widgets_to_remove.push_back(std::move(widget));
|
||||
}
|
||||
|
||||
60
src/main.cpp
60
src/main.cpp
@@ -4,7 +4,6 @@
|
||||
#include "../include/Process.hpp"
|
||||
#include "../include/Rpc.hpp"
|
||||
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
#include <string.h>
|
||||
#include <limits.h>
|
||||
@@ -14,7 +13,6 @@
|
||||
#include <mglpp/system/Clock.hpp>
|
||||
|
||||
// TODO: Make keyboard/controller controllable for steam deck (and other controllers).
|
||||
// TODO: Keep track of gpu screen recorder run by other programs to not allow recording at the same time, or something.
|
||||
// TODO: Add systray by using org.kde.StatusNotifierWatcher/etc dbus directly.
|
||||
// TODO: Make sure the overlay always stays on top. Test with starting the overlay and then opening youtube in fullscreen.
|
||||
// This is done in Overlay::force_window_on_top, but it's not called right now. It cant be used because the overlay will be on top of
|
||||
@@ -97,24 +95,11 @@ static void rpc_add_commands(gsr::Rpc *rpc, gsr::Overlay *overlay) {
|
||||
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
|
||||
overlay->take_screenshot_region();
|
||||
});
|
||||
}
|
||||
|
||||
static bool is_gsr_ui_virtual_keyboard_running() {
|
||||
FILE *f = fopen("/proc/bus/input/devices", "rb");
|
||||
if(!f)
|
||||
return false;
|
||||
|
||||
bool virtual_keyboard_running = false;
|
||||
char line[1024];
|
||||
while(fgets(line, sizeof(line), f)) {
|
||||
if(strstr(line, "gsr-ui virtual keyboard")) {
|
||||
virtual_keyboard_running = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
return virtual_keyboard_running;
|
||||
rpc->add_handler("take-screenshot-window", [overlay](const std::string &name) {
|
||||
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
|
||||
overlay->take_screenshot_window();
|
||||
});
|
||||
}
|
||||
|
||||
static void install_flatpak_systemd_service() {
|
||||
@@ -192,7 +177,9 @@ enum class LaunchAction {
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
setlocale(LC_ALL, "C"); // Sigh... stupid C
|
||||
#ifdef __GLIBC__
|
||||
mallopt(M_MMAP_THRESHOLD, 65536);
|
||||
#endif
|
||||
|
||||
if(geteuid() == 0) {
|
||||
fprintf(stderr, "Error: don't run gsr-ui as the root user\n");
|
||||
@@ -220,17 +207,14 @@ int main(int argc, char **argv) {
|
||||
|
||||
set_display_server_environment_variables();
|
||||
|
||||
// TODO: This is a shitty method to detect if multiple instances of gsr-ui is running but this will work properly even in flatpak
|
||||
// that uses pid sandboxing. Replace this with a better method once we no longer rely on linux global hotkeys on some platform.
|
||||
// TODO: This method doesn't work when disabling hotkeys and the method below with pidof gsr-ui doesn't work in flatpak.
|
||||
// What do? creating a pid file doesn't work in flatpak either.
|
||||
// TODO: This doesn't work in flatpak when disabling hotkeys.
|
||||
if(is_gsr_ui_virtual_keyboard_running() || gsr::pidof("gsr-ui", getpid()) != -1) {
|
||||
auto rpc = std::make_unique<gsr::Rpc>();
|
||||
const gsr::RpcOpenResult rpc_open_result = rpc->open("gsr-ui");
|
||||
|
||||
if(rpc_open_result == gsr::RpcOpenResult::OK) {
|
||||
if(launch_action == LaunchAction::LAUNCH_DAEMON)
|
||||
return 1;
|
||||
|
||||
gsr::Rpc rpc;
|
||||
if(rpc.open("gsr-ui") && rpc.write("show_ui\n", 8)) {
|
||||
if(rpc->write("show_ui\n", 8)) {
|
||||
fprintf(stderr, "Error: another instance of gsr-ui is already running, opening that one instead\n");
|
||||
} else {
|
||||
fprintf(stderr, "Error: failed to send command to running gsr-ui instance, user will have to open the UI manually with Alt+Z\n");
|
||||
@@ -240,6 +224,19 @@ int main(int argc, char **argv) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(!rpc->create("gsr-ui"))
|
||||
fprintf(stderr, "Error: Failed to create rpc\n");
|
||||
|
||||
if(gsr::pidof("gpu-screen-recorder", -1) != -1) {
|
||||
const char *args[] = { "gsr-notify", "--text", "GPU Screen Recorder is already running in another process.\nPlease close it before using GPU Screen Recorder UI.", "--timeout", "5.0", "--icon-color", "ff0000", "--bg-color", "ff0000", nullptr };
|
||||
gsr::exec_program_daemonized(args);
|
||||
}
|
||||
|
||||
if(mgl_init(MGL_WINDOW_SYSTEM_X11) != 0) {
|
||||
fprintf(stderr, "Error: failed to initialize mgl. Failed to either connect to the X11 server or setup opengl\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(is_flatpak())
|
||||
install_flatpak_systemd_service();
|
||||
else
|
||||
@@ -283,11 +280,6 @@ int main(int argc, char **argv) {
|
||||
disable_prime_run();
|
||||
}
|
||||
|
||||
if(mgl_init(MGL_WINDOW_SYSTEM_X11) != 0) {
|
||||
fprintf(stderr, "Error: failed to initialize mgl. Failed to either connect to the X11 server or setup opengl\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
gsr::SupportedCaptureOptions capture_options = gsr::get_supported_capture_options(gsr_info);
|
||||
|
||||
std::string resources_path;
|
||||
@@ -320,10 +312,6 @@ int main(int argc, char **argv) {
|
||||
if(launch_action == LaunchAction::LAUNCH_SHOW)
|
||||
overlay->show();
|
||||
|
||||
auto rpc = std::make_unique<gsr::Rpc>();
|
||||
if(!rpc->create("gsr-ui"))
|
||||
fprintf(stderr, "Error: Failed to create rpc, commands won't be received\n");
|
||||
|
||||
rpc_add_commands(rpc.get(), overlay.get());
|
||||
|
||||
// TODO: Add hotkeys in Overlay when using x11 global hotkeys. The hotkeys in Overlay should duplicate each key that is used for x11 global hotkeys.
|
||||
|
||||
@@ -24,4 +24,9 @@ To close gsr-global-hotkeys send `exit<newline>` to the programs stdin, for exam
|
||||
```
|
||||
exit
|
||||
|
||||
```
|
||||
```
|
||||
# Conflict with other keyboard software
|
||||
Some keyboard remapping software such as keyd may conflict with gsr-global-hotkeys if configured incorrect (if it's configured to grab all devices, including gsr-ui virtual keyboard).
|
||||
If that happens it may grab gsr-ui-virtual keyboard while gsr-global-hotkeys will grab the keyboard remapping software virtual device, leading to a circular lock, making it not possible
|
||||
to use your keyboard. gsr-global-hotkeys detects this and outputs `gsr-ui-virtual-keyboard-grabbed` to stdout. You can listen to this and stop gsr-global-hotkeys or restart it with `--no-grab`
|
||||
option to only listen to devices, not grabbing them.
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "keyboard_event.h"
|
||||
#include "keys.h"
|
||||
#include "leds.h"
|
||||
|
||||
/* C stdlib */
|
||||
#include <stdio.h>
|
||||
@@ -7,6 +8,7 @@
|
||||
#include <errno.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
|
||||
/* POSIX */
|
||||
#include <fcntl.h>
|
||||
@@ -17,6 +19,7 @@
|
||||
/* LINUX */
|
||||
#include <linux/input.h>
|
||||
#include <linux/uinput.h>
|
||||
#include <sys/timerfd.h>
|
||||
|
||||
#define GSR_UI_VIRTUAL_KEYBOARD_NAME "gsr-ui virtual keyboard"
|
||||
|
||||
@@ -26,6 +29,14 @@
|
||||
|
||||
#define KEY_STATES_SIZE (KEY_MAX/8 + 1)
|
||||
|
||||
static double clock_get_monotonic_seconds(void) {
|
||||
struct timespec ts;
|
||||
ts.tv_sec = 0;
|
||||
ts.tv_nsec = 0;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (double)ts.tv_sec + (double)ts.tv_nsec * 0.000000001;
|
||||
}
|
||||
|
||||
static inline int count_num_bits_set(unsigned char c) {
|
||||
int n = 0;
|
||||
n += (c & 1);
|
||||
@@ -183,6 +194,12 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
|
||||
return;
|
||||
}
|
||||
|
||||
if(extra_data->gsr_ui_virtual_keyboard) {
|
||||
if(event.type == EV_KEY || event.type == EV_MSC)
|
||||
self->check_grab_lock = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if(event.type == EV_SYN && event.code == SYN_DROPPED) {
|
||||
/* TODO: Don't do this on every SYN_DROPPED to prevent spamming this, instead wait until the next event or wait for timeout */
|
||||
keyboard_event_fetch_update_key_states(self, extra_data, fd);
|
||||
@@ -211,11 +228,27 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
|
||||
}
|
||||
|
||||
if(extra_data->grabbed) {
|
||||
if(!self->check_grab_lock && (event.type == EV_KEY || event.type == EV_MSC)) {
|
||||
self->uinput_written_time_seconds = clock_get_monotonic_seconds();
|
||||
self->check_grab_lock = true;
|
||||
}
|
||||
|
||||
/* TODO: What to do on error? */
|
||||
if(write(self->uinput_fd, &event, sizeof(event)) != sizeof(event))
|
||||
fprintf(stderr, "Error: failed to write event data to virtual keyboard for exclusively grabbed device\n");
|
||||
}
|
||||
|
||||
if(event.type == EV_LED) {
|
||||
write(fd, &event, sizeof(event));
|
||||
|
||||
const struct input_event syn_event = {
|
||||
.type = EV_SYN,
|
||||
.code = 0,
|
||||
.value = 0
|
||||
};
|
||||
write(fd, &syn_event, sizeof(syn_event));
|
||||
}
|
||||
|
||||
if(!extra_data->is_possibly_non_keyboard_device)
|
||||
return;
|
||||
|
||||
@@ -234,14 +267,23 @@ 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;
|
||||
int fds_to_close_now[MAX_CLOSE_FDS];
|
||||
int num_fds_to_close_now = 0;
|
||||
|
||||
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]);
|
||||
fds_to_close_now[i] = self->close_fds[i];
|
||||
}
|
||||
num_fds_to_close_now = self->num_close_fds;
|
||||
self->num_close_fds = 0;
|
||||
pthread_mutex_unlock(&self->close_dev_input_mutex);
|
||||
|
||||
for(int i = 0; i < num_fds_to_close_now; ++i) {
|
||||
close(fds_to_close_now[i]);
|
||||
}
|
||||
num_fds_to_close_now = 0;
|
||||
|
||||
usleep(100 * 1000); /* 100 milliseconds */
|
||||
}
|
||||
return NULL;
|
||||
@@ -359,7 +401,7 @@ static bool keyboard_event_try_add_device_if_keyboard(keyboard_event *self, cons
|
||||
if(keyboard_event_has_event_with_dev_input_fd(self, dev_input_id))
|
||||
return false;
|
||||
|
||||
const int fd = open(dev_input_filepath, O_RDONLY);
|
||||
const int fd = open(dev_input_filepath, O_RDWR);
|
||||
if(fd == -1)
|
||||
return false;
|
||||
|
||||
@@ -371,7 +413,29 @@ static bool keyboard_event_try_add_device_if_keyboard(keyboard_event *self, cons
|
||||
ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);
|
||||
const bool is_keyboard = (evbit & (1 << EV_SYN)) && (evbit & (1 << EV_KEY));
|
||||
|
||||
if(is_keyboard && strcmp(device_name, GSR_UI_VIRTUAL_KEYBOARD_NAME) != 0) {
|
||||
if(strcmp(device_name, GSR_UI_VIRTUAL_KEYBOARD_NAME) == 0) {
|
||||
if(self->num_event_polls < MAX_EVENT_POLLS) {
|
||||
self->event_polls[self->num_event_polls] = (struct pollfd) {
|
||||
.fd = fd,
|
||||
.events = POLLIN,
|
||||
.revents = 0
|
||||
};
|
||||
|
||||
self->event_extra_data[self->num_event_polls] = (event_extra_data) {
|
||||
.dev_input_id = dev_input_id,
|
||||
.grabbed = false,
|
||||
.key_states = NULL,
|
||||
.key_presses_grabbed = NULL,
|
||||
.num_keys_pressed = 0,
|
||||
.gsr_ui_virtual_keyboard = true
|
||||
};
|
||||
|
||||
++self->num_event_polls;
|
||||
return true;
|
||||
} else {
|
||||
fprintf(stderr, "Error: failed to listen to gsr-ui virtual keyboard\n");
|
||||
}
|
||||
} else if(is_keyboard) {
|
||||
unsigned char key_bits[KEY_MAX/8 + 1] = {0};
|
||||
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(key_bits)), &key_bits);
|
||||
|
||||
@@ -456,9 +520,13 @@ static void keyboard_event_remove_event(keyboard_event *self, int index) {
|
||||
if(index < 0 || index >= self->num_event_polls)
|
||||
return;
|
||||
|
||||
if(self->event_polls[index].fd > 0) {
|
||||
ioctl(self->event_polls[index].fd, EVIOCGRAB, 0);
|
||||
close(self->event_polls[index].fd);
|
||||
const int poll_fd = self->event_polls[index].fd;
|
||||
if(poll_fd > 0) {
|
||||
ioctl(poll_fd, EVIOCGRAB, 0);
|
||||
if(!keyboard_event_try_add_close_fd(self, poll_fd)) {
|
||||
fprintf(stderr, "Error: failed to add immediately, closing now\n");
|
||||
close(poll_fd);
|
||||
}
|
||||
}
|
||||
free(self->event_extra_data[index].key_states);
|
||||
free(self->event_extra_data[index].key_presses_grabbed);
|
||||
@@ -497,10 +565,12 @@ static int setup_virtual_keyboard_input(const char *name) {
|
||||
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) {
|
||||
|
||||
// for(int i = 0; i <= LED_CHARGING; ++i) {
|
||||
// success &= (ioctl(fd, UI_SET_LEDBIT, i) != -1);
|
||||
// }
|
||||
|
||||
@@ -539,7 +609,6 @@ static int setup_virtual_keyboard_input(const char *name) {
|
||||
|
||||
bool keyboard_event_init(keyboard_event *self, bool exclusive_grab, keyboard_grab_type grab_type) {
|
||||
memset(self, 0, sizeof(*self));
|
||||
self->stdin_event_index = -1;
|
||||
self->hotplug_event_index = -1;
|
||||
self->grab_type = grab_type;
|
||||
self->running = true;
|
||||
@@ -571,9 +640,51 @@ bool keyboard_event_init(keyboard_event *self, bool exclusive_grab, keyboard_gra
|
||||
.num_keys_pressed = 0
|
||||
};
|
||||
|
||||
self->stdin_event_index = self->num_event_polls;
|
||||
++self->num_event_polls;
|
||||
|
||||
self->timer_fd = timerfd_create(CLOCK_MONOTONIC, 0);
|
||||
if(self->timer_fd <= 0) {
|
||||
fprintf(stderr, "Error: timerfd_create failed\n");
|
||||
keyboard_event_deinit(self);
|
||||
return false;
|
||||
}
|
||||
|
||||
self->event_polls[self->num_event_polls] = (struct pollfd) {
|
||||
.fd = self->timer_fd,
|
||||
.events = POLLIN,
|
||||
.revents = 0
|
||||
};
|
||||
|
||||
self->event_extra_data[self->num_event_polls] = (event_extra_data) {
|
||||
.dev_input_id = -1,
|
||||
.grabbed = false,
|
||||
.key_states = NULL,
|
||||
.key_presses_grabbed = NULL,
|
||||
.num_keys_pressed = 0
|
||||
};
|
||||
|
||||
++self->num_event_polls;
|
||||
|
||||
/* 0.5 seconds */
|
||||
const struct itimerspec timer_value = {
|
||||
.it_value = (struct timespec) {
|
||||
.tv_sec = 0,
|
||||
.tv_nsec = 500000000ULL,
|
||||
},
|
||||
.it_interval = (struct timespec) {
|
||||
.tv_sec = 0,
|
||||
.tv_nsec = 500000000ULL
|
||||
}
|
||||
};
|
||||
|
||||
if(timerfd_settime(self->timer_fd, 0, &timer_value, NULL) < 0) {
|
||||
fprintf(stderr, "Error: timerfd_settime failed\n");
|
||||
keyboard_event_deinit(self);
|
||||
return false;
|
||||
}
|
||||
|
||||
self->uinput_written_time_seconds = clock_get_monotonic_seconds();
|
||||
|
||||
if(hotplug_event_init(&self->hotplug_ev)) {
|
||||
self->event_polls[self->num_event_polls] = (struct pollfd) {
|
||||
.fd = hotplug_event_steal_fd(&self->hotplug_ev),
|
||||
@@ -606,6 +717,41 @@ bool keyboard_event_init(keyboard_event *self, bool exclusive_grab, keyboard_gra
|
||||
return true;
|
||||
}
|
||||
|
||||
static void write_led_data_to_device(int fd, uint16_t led, int value) {
|
||||
struct input_event led_data = {
|
||||
.type = EV_LED,
|
||||
.code = led,
|
||||
.value = value
|
||||
};
|
||||
write(fd, &led_data, sizeof(led_data));
|
||||
|
||||
struct input_event syn_data = {
|
||||
.type = EV_SYN,
|
||||
.code = 0,
|
||||
.value = 0
|
||||
};
|
||||
write(fd, &syn_data, sizeof(syn_data));
|
||||
}
|
||||
|
||||
/* When the device is ungrabbed the leds are unset for some reason. Set them back to their previous brightness */
|
||||
static void keyboard_event_device_deinit(int fd, event_extra_data *extra_data) {
|
||||
ggh_leds leds;
|
||||
const bool got_leds = get_leds(extra_data->dev_input_id, &leds);
|
||||
|
||||
ioctl(fd, EVIOCGRAB, 0);
|
||||
if(got_leds) {
|
||||
if(leds.scroll_lock_brightness >= 0)
|
||||
write_led_data_to_device(fd, LED_SCROLLL, leds.scroll_lock_brightness);
|
||||
|
||||
if(leds.num_lock_brightness >= 0)
|
||||
write_led_data_to_device(fd, LED_NUML, leds.num_lock_brightness);
|
||||
|
||||
if(leds.caps_lock_brightness >= 0)
|
||||
write_led_data_to_device(fd, LED_CAPSL, leds.caps_lock_brightness);
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
|
||||
void keyboard_event_deinit(keyboard_event *self) {
|
||||
self->running = false;
|
||||
|
||||
@@ -622,8 +768,10 @@ void keyboard_event_deinit(keyboard_event *self) {
|
||||
|
||||
for(int i = 0; i < self->num_event_polls; ++i) {
|
||||
if(self->event_polls[i].fd > 0) {
|
||||
ioctl(self->event_polls[i].fd, EVIOCGRAB, 0);
|
||||
close(self->event_polls[i].fd);
|
||||
if(self->event_extra_data[i].dev_input_id > 0 && !self->event_extra_data[i].gsr_ui_virtual_keyboard)
|
||||
keyboard_event_device_deinit(self->event_polls[i].fd, &self->event_extra_data[i]);
|
||||
else
|
||||
close(self->event_polls[i].fd);
|
||||
}
|
||||
free(self->event_extra_data[i].key_states);
|
||||
free(self->event_extra_data[i].key_presses_grabbed);
|
||||
@@ -817,7 +965,7 @@ void keyboard_event_poll_events(keyboard_event *self, int timeout_milliseconds)
|
||||
return;
|
||||
|
||||
for(int i = 0; i < self->num_event_polls; ++i) {
|
||||
if(i == self->stdin_event_index && (self->event_polls[i].revents & (POLLHUP|POLLERR)))
|
||||
if(self->event_polls[i].fd == STDIN_FILENO && (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? */
|
||||
@@ -832,8 +980,17 @@ 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->stdin_event_index) {
|
||||
} else if(self->event_polls[i].fd == STDIN_FILENO) {
|
||||
keyboard_event_process_stdin_command_data(self, self->event_polls[i].fd);
|
||||
} else if(self->event_polls[i].fd == self->timer_fd) {
|
||||
uint64_t timers_elapsed = 0;
|
||||
read(self->timer_fd, &timers_elapsed, sizeof(timers_elapsed));
|
||||
|
||||
if(self->grab_type != KEYBOARD_GRAB_TYPE_NO_GRAB && self->check_grab_lock && clock_get_monotonic_seconds() - self->uinput_written_time_seconds >= 1.5) {
|
||||
self->check_grab_lock = false;
|
||||
puts("gsr-ui-virtual-keyboard-grabbed");
|
||||
fflush(stdout);
|
||||
}
|
||||
} else {
|
||||
keyboard_event_process_input_event_data(self, &self->event_extra_data[i], self->event_polls[i].fd);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
#define MAX_EVENT_POLLS 32
|
||||
#define MAX_CLOSE_FDS 256
|
||||
#define MAX_GLOBAL_HOTKEYS 32
|
||||
#define MAX_GLOBAL_HOTKEYS 40
|
||||
|
||||
typedef enum {
|
||||
KEYBOARD_MODKEY_LALT = 1 << 0,
|
||||
@@ -41,6 +41,7 @@ typedef struct {
|
||||
bool grabbed;
|
||||
bool is_non_keyboard_device;
|
||||
bool is_possibly_non_keyboard_device;
|
||||
bool gsr_ui_virtual_keyboard;
|
||||
unsigned char *key_states;
|
||||
unsigned char *key_presses_grabbed;
|
||||
int num_keys_pressed;
|
||||
@@ -48,7 +49,8 @@ typedef struct {
|
||||
|
||||
typedef enum {
|
||||
KEYBOARD_GRAB_TYPE_ALL,
|
||||
KEYBOARD_GRAB_TYPE_VIRTUAL
|
||||
KEYBOARD_GRAB_TYPE_VIRTUAL,
|
||||
KEYBOARD_GRAB_TYPE_NO_GRAB
|
||||
} keyboard_grab_type;
|
||||
|
||||
typedef struct {
|
||||
@@ -62,10 +64,12 @@ typedef struct {
|
||||
event_extra_data event_extra_data[MAX_EVENT_POLLS]; /* Current size is |num_event_polls| */
|
||||
int num_event_polls;
|
||||
|
||||
int stdin_event_index;
|
||||
int hotplug_event_index;
|
||||
int uinput_fd;
|
||||
int timer_fd;
|
||||
bool stdin_failed;
|
||||
bool check_grab_lock;
|
||||
double uinput_written_time_seconds;
|
||||
keyboard_grab_type grab_type;
|
||||
|
||||
pthread_t close_dev_input_fds_thread;
|
||||
|
||||
181
tools/gsr-global-hotkeys/leds.c
Normal file
181
tools/gsr-global-hotkeys/leds.c
Normal file
@@ -0,0 +1,181 @@
|
||||
#include "leds.h"
|
||||
|
||||
/* C stdlib */
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <limits.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* POSIX */
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
|
||||
/* LINUX */
|
||||
#include <linux/input.h>
|
||||
|
||||
/* Returns -1 on error */
|
||||
static int read_int_from_file(const char *filepath) {
|
||||
const int fd = open(filepath, O_RDONLY);
|
||||
if(fd == -1) {
|
||||
fprintf(stderr, "Warning: get_max_brightness open error: %s\n", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
int value = 0;
|
||||
char buffer[32];
|
||||
const ssize_t num_bytes_read = read(fd, buffer, sizeof(buffer));
|
||||
if(num_bytes_read > 0) {
|
||||
buffer[num_bytes_read] = '\0';
|
||||
success = sscanf(buffer, "%d", &value) == 1;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
return success ? value : -1;
|
||||
}
|
||||
|
||||
static int get_max_brightness(const char *sys_class_path, const char *filename) {
|
||||
char path[PATH_MAX];
|
||||
snprintf(path, sizeof(path), "%s/%s/max_brightness", sys_class_path, filename);
|
||||
return read_int_from_file(path);
|
||||
}
|
||||
|
||||
static int get_brightness(const char *sys_class_path, const char *filename) {
|
||||
char path[PATH_MAX];
|
||||
snprintf(path, sizeof(path), "%s/%s/brightness", sys_class_path, filename);
|
||||
return read_int_from_file(path);
|
||||
}
|
||||
|
||||
static bool string_starts_with(const char *str, const char *sub) {
|
||||
const int str_len = strlen(str);
|
||||
const int sub_len = strlen(sub);
|
||||
return str_len >= sub_len && memcmp(str, sub, sub_len) == 0;
|
||||
}
|
||||
|
||||
static bool string_ends_with(const char *str, const char *sub) {
|
||||
const int str_len = strlen(str);
|
||||
const int sub_len = strlen(sub);
|
||||
return str_len >= sub_len && memcmp(str + str_len - sub_len, sub, sub_len) == 0;
|
||||
}
|
||||
|
||||
static int sys_class_led_path_get_event_number(const char *sys_class_path, const char *filename) {
|
||||
char path[PATH_MAX];
|
||||
snprintf(path, sizeof(path), "%s/%s/device", sys_class_path, filename);
|
||||
|
||||
DIR *dir = opendir(path);
|
||||
if(!dir)
|
||||
return -1;
|
||||
|
||||
int event_number = -1;
|
||||
struct dirent *entry;
|
||||
while((entry = readdir(dir)) != NULL) {
|
||||
int v = -1;
|
||||
if(sscanf(entry->d_name, "event%d", &v) == 1 && v >= 0) {
|
||||
event_number = v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return event_number;
|
||||
}
|
||||
|
||||
/*
|
||||
We have to do this retardation instead of setting /sys/class/leds brightness since it doesn't work with /dev/uinput
|
||||
and we cant loop all /dev/input devices and open and write to them either since closing a /dev/input is very slow on linux.
|
||||
So we instead check which devices have the led before opening it.
|
||||
*/
|
||||
static bool set_device_leds(const char *led_name_path, bool enabled) {
|
||||
DIR *dir = opendir("/sys/class/leds");
|
||||
if(!dir)
|
||||
return false;
|
||||
|
||||
char dev_input_filepath[1024];
|
||||
struct dirent *entry;
|
||||
while((entry = readdir(dir)) != NULL) {
|
||||
if(entry->d_name[0] == '.')
|
||||
continue;
|
||||
|
||||
if(!string_starts_with(entry->d_name, "input") || !string_ends_with(entry->d_name, led_name_path))
|
||||
continue;
|
||||
|
||||
const int event_number = sys_class_led_path_get_event_number("/sys/class/leds", entry->d_name);
|
||||
if(event_number == -1)
|
||||
continue;
|
||||
|
||||
snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/input/event%d", event_number);
|
||||
|
||||
const int device_fd = open(dev_input_filepath, O_WRONLY);
|
||||
if(device_fd == -1)
|
||||
continue;
|
||||
|
||||
int brightness = 0;
|
||||
if(enabled) {
|
||||
brightness = get_max_brightness("/sys/class/leds", entry->d_name);
|
||||
if(brightness < 0)
|
||||
brightness = 1;
|
||||
}
|
||||
|
||||
struct input_event led_data = {
|
||||
.type = EV_LED,
|
||||
.code = LED_SCROLLL,
|
||||
.value = brightness
|
||||
};
|
||||
write(device_fd, &led_data, sizeof(led_data));
|
||||
|
||||
struct input_event syn_data = {
|
||||
.type = EV_SYN,
|
||||
.code = 0,
|
||||
.value = 0
|
||||
};
|
||||
write(device_fd, &syn_data, sizeof(syn_data));
|
||||
|
||||
close(device_fd);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool set_leds(const char *led_name, bool enabled) {
|
||||
if(strcmp(led_name, "Scroll Lock") == 0) {
|
||||
return set_device_leds("::scrolllock", enabled);
|
||||
} else {
|
||||
fprintf(stderr, "Error: invalid led: \"%s\", expected \"Scroll Lock\"\n", led_name);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool get_leds(int event_number, ggh_leds *leds) {
|
||||
leds->scroll_lock_brightness = -1;
|
||||
leds->num_lock_brightness = -1;
|
||||
leds->caps_lock_brightness = -1;
|
||||
|
||||
char path[PATH_MAX];
|
||||
snprintf(path, sizeof(path), "/sys/class/input/event%d/device", event_number);
|
||||
|
||||
DIR *dir = opendir(path);
|
||||
if(!dir)
|
||||
return false;
|
||||
|
||||
struct dirent *entry;
|
||||
while((entry = readdir(dir)) != NULL) {
|
||||
if(entry->d_name[0] == '.')
|
||||
continue;
|
||||
|
||||
if(!string_starts_with(entry->d_name, "input"))
|
||||
continue;
|
||||
|
||||
if(string_ends_with(entry->d_name, "::scrolllock"))
|
||||
leds->scroll_lock_brightness = get_brightness(path, entry->d_name);
|
||||
else if(string_ends_with(entry->d_name, "::numlock"))
|
||||
leds->num_lock_brightness = get_brightness(path, entry->d_name);
|
||||
else if(string_ends_with(entry->d_name, "::capslock"))
|
||||
leds->caps_lock_brightness = get_brightness(path, entry->d_name);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return true;
|
||||
}
|
||||
17
tools/gsr-global-hotkeys/leds.h
Normal file
17
tools/gsr-global-hotkeys/leds.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#ifndef LEDS_H
|
||||
#define LEDS_H
|
||||
|
||||
/* C stdlib */
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef struct {
|
||||
/* These are set to -1 if not supported */
|
||||
int scroll_lock_brightness;
|
||||
int num_lock_brightness;
|
||||
int caps_lock_brightness;
|
||||
} ggh_leds;
|
||||
|
||||
bool set_leds(const char *led_name, bool enabled);
|
||||
bool get_leds(int event_number, ggh_leds *leds);
|
||||
|
||||
#endif /* LEDS_H */
|
||||
@@ -1,18 +1,26 @@
|
||||
#include "keyboard_event.h"
|
||||
#include "leds.h"
|
||||
|
||||
/* C stdlib */
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <locale.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/* POSIX */
|
||||
#include <unistd.h>
|
||||
|
||||
static void usage(void) {
|
||||
fprintf(stderr, "usage: gsr-global-hotkeys [--all|--virtual]\n");
|
||||
fprintf(stderr, "usage: gsr-global-hotkeys [--all|--virtual|--no-grab|--set-led] [Scroll Lock] [on|off]\n");
|
||||
fprintf(stderr, "OPTIONS:\n");
|
||||
fprintf(stderr, " --all Grab all devices.\n");
|
||||
fprintf(stderr, " --virtual Grab all virtual devices only.\n");
|
||||
fprintf(stderr, " --no-grab Don't grab devices, only listen to them.\n");
|
||||
fprintf(stderr, " --set-led Turn device led on/off.\n");
|
||||
fprintf(stderr, "EXAMPLES:\n");
|
||||
fprintf(stderr, " gsr-global-hotkeys --all\n");
|
||||
fprintf(stderr, " gsr-global-hotkeys --set-led \"Scroll Lock\" on\n");
|
||||
fprintf(stderr, " gsr-global-hotkeys --set-led \"Scroll Lock\" off\n");
|
||||
}
|
||||
|
||||
static bool is_gsr_global_hotkeys_already_running(void) {
|
||||
@@ -37,49 +45,83 @@ int main(int argc, char **argv) {
|
||||
setlocale(LC_ALL, "C"); /* Sigh... stupid C */
|
||||
|
||||
keyboard_grab_type grab_type = KEYBOARD_GRAB_TYPE_ALL;
|
||||
const uid_t user_id = getuid();
|
||||
|
||||
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 if(strcmp(grab_type_arg, "--no-grab") == 0) {
|
||||
grab_type = KEYBOARD_GRAB_TYPE_NO_GRAB;
|
||||
} else if(strcmp(grab_type_arg, "--set-led") == 0) {
|
||||
fprintf(stderr, "Error: missing led name and on/off argument to --set-led\n");
|
||||
usage();
|
||||
return 1;
|
||||
} else {
|
||||
fprintf(stderr, "Error: expected --all or --virtual, got %s\n", grab_type_arg);
|
||||
fprintf(stderr, "gsr-global-hotkeys error: expected --all, --virtual, --no-grab or --set-led, got %s\n", grab_type_arg);
|
||||
usage();
|
||||
return 1;
|
||||
}
|
||||
} else if(argc == 4) {
|
||||
/* It's not ideal to use gsr-global-hotkeys for leds, but we do that for now because it's a mess to create another binary for flatpak and distros */
|
||||
const char *led_name = argv[2];
|
||||
const char *led_enabled_str = argv[3];
|
||||
bool led_enabled = false;
|
||||
|
||||
if(strcmp(led_enabled_str, "on") == 0) {
|
||||
led_enabled = true;
|
||||
} else if(strcmp(led_enabled_str, "off") == 0) {
|
||||
led_enabled = false;
|
||||
} else {
|
||||
fprintf(stderr, "Error: expected \"on\" or \"off\" for --set-led option, got: \"%s\"", led_enabled_str);
|
||||
usage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(geteuid() != 0) {
|
||||
if(setuid(0) == -1) {
|
||||
fprintf(stderr, "gsr-global-hotkeys error: failed to change user to root, global hotkeys will not work. Make sure to set the correct capability on gsr-global-hotkeys\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
const bool success = set_leds(led_name, led_enabled);
|
||||
setuid(user_id);
|
||||
|
||||
return success ? 0 : 1;
|
||||
} else if(argc != 1) {
|
||||
fprintf(stderr, "Error: expected 0 or 1 arguments, got %d argument(s)\n", argc);
|
||||
fprintf(stderr, "gsr-global-hotkeys 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");
|
||||
fprintf(stderr, "gsr-global-hotkeys error: gsr-global-hotkeys is already running\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const uid_t user_id = getuid();
|
||||
if(geteuid() != 0) {
|
||||
if(setuid(0) == -1) {
|
||||
fprintf(stderr, "Error: failed to change user to root\n");
|
||||
fprintf(stderr, "gsr-global-hotkeys error: failed to change user to root, global hotkeys will not work. Make sure to set the correct capability on gsr-global-hotkeys\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
keyboard_event keyboard_ev;
|
||||
if(!keyboard_event_init(&keyboard_ev, true, grab_type)) {
|
||||
fprintf(stderr, "Error: failed to setup hotplugging and no keyboard input devices were found\n");
|
||||
if(!keyboard_event_init(&keyboard_ev, grab_type != KEYBOARD_GRAB_TYPE_NO_GRAB, grab_type)) {
|
||||
fprintf(stderr, "gsr-global-hotkeys error: failed to setup hotplugging and no keyboard input devices were found\n");
|
||||
setuid(user_id);
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "Info: global hotkeys setup, waiting for hotkeys to be pressed\n");
|
||||
fprintf(stderr, "gsr-global-hotkeys info: global hotkeys setup, waiting for hotkeys to be pressed\n");
|
||||
|
||||
for(;;) {
|
||||
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");
|
||||
fprintf(stderr, "gsr-global-hotkeys info: stdin closed (parent process likely closed this process), exiting...\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
static void get_runtime_filepath(char *buffer, size_t buffer_size, const char *filename) {
|
||||
static void get_socket_filepath(char *buffer, size_t buffer_size, const char *filename) {
|
||||
char dir[PATH_MAX];
|
||||
|
||||
const char *runtime_dir = getenv("XDG_RUNTIME_DIR");
|
||||
@@ -23,7 +25,7 @@ static void get_runtime_filepath(char *buffer, size_t buffer_size, const char *f
|
||||
}
|
||||
|
||||
/* Assumes |str| size is less than 256 */
|
||||
static void fifo_write_all(int file_fd, const char *str) {
|
||||
static void file_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)) {
|
||||
@@ -33,7 +35,7 @@ static void fifo_write_all(int file_fd, const char *str) {
|
||||
|
||||
ssize_t offset = 0;
|
||||
while(offset < (ssize_t)command_size) {
|
||||
const ssize_t bytes_written = write(file_fd, str + offset, command_size - offset);
|
||||
const ssize_t bytes_written = write(file_fd, command + offset, command_size - offset);
|
||||
if(bytes_written > 0)
|
||||
offset += bytes_written;
|
||||
}
|
||||
@@ -64,6 +66,8 @@ static void usage(void) {
|
||||
printf(" Take a screenshot.\n");
|
||||
printf(" take-screenshot-region\n");
|
||||
printf(" Take a screenshot of a region.\n");
|
||||
printf(" take-screenshot-window\n");
|
||||
printf(" Take a screenshot of a window (or desktop portal on Wayland).\n");
|
||||
printf("\n");
|
||||
printf("EXAMPLES:\n");
|
||||
printf(" gsr-ui-cli toggle-show\n");
|
||||
@@ -83,6 +87,7 @@ static bool is_valid_command(const char *command) {
|
||||
"replay-save-10-min",
|
||||
"take-screenshot",
|
||||
"take-screenshot-region",
|
||||
"take-screenshot-window",
|
||||
NULL
|
||||
};
|
||||
|
||||
@@ -109,15 +114,34 @@ int main(int argc, char **argv) {
|
||||
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);
|
||||
char socket_filepath[PATH_MAX];
|
||||
get_socket_filepath(socket_filepath, sizeof(socket_filepath), "gsr-ui");
|
||||
|
||||
const int socket_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0);
|
||||
if(socket_fd <= 0) {
|
||||
fprintf(stderr, "Error: failed to create socket\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
fifo_write_all(fifo_fd, command);
|
||||
close(fifo_fd);
|
||||
struct sockaddr_un addr = {0};
|
||||
addr.sun_family = AF_UNIX;
|
||||
snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", socket_filepath);
|
||||
|
||||
for(;;) {
|
||||
if(connect(socket_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
|
||||
const int err = errno;
|
||||
if(err == EWOULDBLOCK) {
|
||||
usleep(10 * 1000);
|
||||
} else {
|
||||
fprintf(stderr, "Error: failed to connect, error: %s. Maybe gsr-ui is not running?\n", strerror(err));
|
||||
exit(2);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
file_write_all(socket_fd, command);
|
||||
close(socket_fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user