Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bf6e533c5 | ||
|
|
902fc7f6a9 | ||
|
|
794064a8b8 | ||
|
|
e44b2ec528 | ||
|
|
5f484bd82c | ||
|
|
0269387b9a | ||
|
|
5c4ebbab59 | ||
|
|
40b2af5668 | ||
|
|
aa717a95ec | ||
|
|
86424607b7 | ||
|
|
74bb6f0070 | ||
|
|
61bbaf3728 | ||
|
|
fed47000ce | ||
|
|
7f43adfbd5 | ||
|
|
1f6251baf3 | ||
|
|
d1220b013e | ||
|
|
93a55b6bdf | ||
|
|
974e760136 | ||
|
|
387141d36f | ||
|
|
3713d3d59e | ||
|
|
2bb6754523 | ||
|
|
df1610431d | ||
|
|
1ea9615584 | ||
|
|
45ae7c95cf | ||
|
|
f1b6df4d56 | ||
|
|
202c0b2415 | ||
|
|
fd5026489c | ||
|
|
8032cb2cf0 | ||
|
|
13562d2aa1 | ||
|
|
1971d4a288 | ||
|
|
c039b79174 | ||
|
|
245dcf5730 | ||
|
|
e68a342b81 | ||
|
|
11aa237821 | ||
|
|
d7be9b38b1 | ||
|
|
4717c64b03 | ||
|
|
2c2633ec58 | ||
|
|
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 |
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
|
||||
|
||||
25
README.md
@@ -4,18 +4,18 @@
|
||||
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) 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.
|
||||
|
||||
@@ -37,14 +37,14 @@ 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.
|
||||
|
||||
# License
|
||||
This software is licensed under GPL3.0-only. Files under `fonts/` directory belong to the Noto Sans Google fonts project and they are licensed under `SIL Open Font License`.\
|
||||
This software is licensed under GPL-3.0-only, see the LICENSE file for more information. Files under `fonts/` directory belong to the Noto Sans Google fonts project and they are licensed under `SIL Open Font License`.\
|
||||
`images/default.cur` it part of the [Adwaita icon theme](https://gitlab.gnome.org/GNOME/adwaita-icon-theme/-/tree/master) which is licensed under `CC BY-SA 3.0`.\
|
||||
The controller buttons under `images/` were created by [Julio Cacko](https://juliocacko.itch.io/free-input-prompts) and they are licensed under `CC0 1.0 Universal`.\
|
||||
The PlayStation logo under `images/` was created by [ArksDigital](https://arks.itch.io/ps4-buttons) and it's licensed under `CC BY 4.0`.
|
||||
@@ -71,3 +71,8 @@ I'm looking for somebody that can create sound effects for the notifications.
|
||||
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.
|
||||
|
||||
67
TODO
@@ -25,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).
|
||||
@@ -86,9 +84,6 @@ 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.
|
||||
Fix this by writing 0 or 1 to /sys/class/leds/input2::numlock/brightness.
|
||||
|
||||
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.
|
||||
@@ -132,8 +127,6 @@ Add option to do screen-direct recording. But make it clear that it should not b
|
||||
|
||||
Add systray for recording status.
|
||||
|
||||
Add a desktop icon when gsr-ui has a window mode option (which should be the default launch option).
|
||||
|
||||
Verify if cursor tracker monitor name is always correct. It uses the wayland monitor name for recording, but gpu screen recorder uses a custom name created from the drm connector name.
|
||||
|
||||
Notification with the focused monitor (with CursorTrackerWayland) assumes that the x11 monitor name is the same as the drm monitor name. Same for find_monitor_by_name.
|
||||
@@ -173,8 +166,6 @@ 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.
|
||||
@@ -210,3 +201,61 @@ 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.
|
||||
|
||||
Sometimes when opening gpu screen recorder ui gsr-global-hotkeys incorrectly detects that keyboard input is locked.
|
||||
|
||||
When running replay for a long time and then stopping it it takes a while. Improve this.
|
||||
|
||||
Make it possible to resize webcam box from top left, top right and bottom left as well.
|
||||
|
||||
The flatpak version can for some get stuck at shutdown when instant replay is running. It only happens in the flatpak version and only when instant replay is running and it happens always. Manual SIGINT on gsr-ui stops gsr-ui properly, so why does it fail when shutting down the computer when the systemd stop signal is SIGINT? Maybe its related to the flatpak version being launched through gsr-gtk. I cant personally reproduce it.
|
||||
|
||||
Redesign the UI to allow capturing multiple video sources. Move webcam to capture sources as well then. Maybe design the UI to work more like obs studio then, where you start recording and then add sources at capture time, with a preview.
|
||||
|
||||
Add option to choose video container (either flv or mpegts) for youtube livestreaming.
|
||||
|
||||
Get wayland cursor position for region selector, otherwise the start position before the cursor moves is off.
|
||||
10
gpu-screen-recorder.desktop
Normal file
@@ -0,0 +1,10 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=GPU Screen Recorder
|
||||
GenericName=Screen recorder
|
||||
Comment=A ShadowPlay-like screen recorder for Linux
|
||||
Icon=gpu-screen-recorder
|
||||
Exec=gsr-ui launch-hide-announce
|
||||
Terminal=false
|
||||
Keywords=gpu-screen-recorder;gsr-ui;screen recorder;streaming;twitch;replay;shadowplay;
|
||||
Categories=AudioVideo;Recorder;
|
||||
BIN
icons/hicolor/128x128/apps/gpu-screen-recorder.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
icons/hicolor/32x32/apps/gpu-screen-recorder.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
icons/hicolor/64x64/apps/gpu-screen-recorder.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
images/info.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
images/masked.png
Normal file
|
After Width: | Height: | Size: 930 B |
BIN
images/question_mark.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
images/unmasked.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
images/warning.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
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;
|
||||
};
|
||||
}
|
||||
@@ -60,14 +60,31 @@ namespace gsr {
|
||||
bool overclock = false;
|
||||
bool record_cursor = true;
|
||||
bool restore_portal_session = true;
|
||||
bool low_power_mode = false;
|
||||
|
||||
std::string webcam_source = "";
|
||||
bool webcam_flip_horizontally = false;
|
||||
std::string webcam_video_format = "auto";
|
||||
int32_t webcam_camera_width = 0;
|
||||
int32_t webcam_camera_height = 0;
|
||||
int32_t webcam_camera_fps = 0;
|
||||
int32_t webcam_x = 0; // A value between 0 and 100 (percentage)
|
||||
int32_t webcam_y = 0; // A value between 0 and 100 (percentage)
|
||||
int32_t webcam_width = 30; // A value between 0 and 100 (percentage), 0 = Don't scale it
|
||||
int32_t webcam_height = 30; // A value between 0 and 100 (percentage), 0 = Don't scale it
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -83,6 +100,11 @@ namespace gsr {
|
||||
std::string stream_key;
|
||||
};
|
||||
|
||||
struct KickStreamConfig {
|
||||
std::string stream_url;
|
||||
std::string stream_key;
|
||||
};
|
||||
|
||||
struct CustomStreamConfig {
|
||||
std::string url;
|
||||
std::string key;
|
||||
@@ -91,12 +113,11 @@ namespace gsr {
|
||||
|
||||
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;
|
||||
KickStreamConfig kick;
|
||||
CustomStreamConfig custom;
|
||||
ConfigHotkey start_stop_hotkey;
|
||||
};
|
||||
@@ -104,13 +125,12 @@ 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;
|
||||
ConfigHotkey pause_unpause_hotkey;
|
||||
ConfigHotkey start_stop_region_hotkey;
|
||||
ConfigHotkey start_stop_window_hotkey;
|
||||
};
|
||||
|
||||
struct ReplayConfig {
|
||||
@@ -118,9 +138,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;
|
||||
@@ -142,11 +159,16 @@ 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 save_screenshot_to_disk = true;
|
||||
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
|
||||
|
||||
std::string custom_script;
|
||||
};
|
||||
|
||||
struct Config {
|
||||
@@ -165,4 +187,4 @@ namespace gsr {
|
||||
|
||||
std::optional<Config> read_config(const SupportedCaptureOptions &capture_options);
|
||||
void save_config(Config &config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "CursorTracker.hpp"
|
||||
#include <stdint.h>
|
||||
#include <vector>
|
||||
|
||||
struct wl_display;
|
||||
struct wl_registry;
|
||||
struct wl_output;
|
||||
struct zxdg_output_manager_v1;
|
||||
struct zxdg_output_v1;
|
||||
|
||||
namespace gsr {
|
||||
struct WaylandOutput {
|
||||
uint32_t wl_name;
|
||||
struct wl_output *output;
|
||||
struct zxdg_output_v1 *xdg_output;
|
||||
mgl::vec2i pos;
|
||||
mgl::vec2i size;
|
||||
int32_t transform;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
class CursorTrackerWayland : public CursorTracker {
|
||||
public:
|
||||
CursorTrackerWayland(const char *card_path);
|
||||
CursorTrackerWayland(const char *card_path, struct wl_display *wayland_dpy);
|
||||
CursorTrackerWayland(const CursorTrackerWayland&) = delete;
|
||||
CursorTrackerWayland& operator=(const CursorTrackerWayland&) = delete;
|
||||
~CursorTrackerWayland();
|
||||
|
||||
void update() override;
|
||||
std::optional<CursorInfo> get_latest_cursor_info() override;
|
||||
|
||||
std::vector<WaylandOutput> monitors;
|
||||
struct zxdg_output_manager_v1 *xdg_output_manager = nullptr;
|
||||
private:
|
||||
void set_monitor_outputs_from_xdg_output(struct wl_display *dpy);
|
||||
private:
|
||||
int drm_fd = -1;
|
||||
mgl::vec2i latest_cursor_position; // Position of the cursor within the monitor
|
||||
int latest_crtc_id = -1;
|
||||
struct wl_display *wayland_dpy = nullptr;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
@@ -56,8 +65,6 @@ namespace gsr {
|
||||
bool down_pressed = false;
|
||||
bool left_pressed = false;
|
||||
bool right_pressed = false;
|
||||
bool l3_button_pressed = false;
|
||||
bool r3_button_pressed = false;
|
||||
|
||||
bool save_replay = false;
|
||||
bool save_1_min_replay = false;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -25,11 +25,28 @@ namespace gsr {
|
||||
bool png = false;
|
||||
};
|
||||
|
||||
enum GsrCameraPixelFormat {
|
||||
YUYV,
|
||||
MJPEG
|
||||
};
|
||||
|
||||
struct GsrMonitor {
|
||||
std::string name;
|
||||
mgl::vec2i size;
|
||||
};
|
||||
|
||||
struct GsrCameraSetup {
|
||||
mgl::vec2i resolution;
|
||||
int fps;
|
||||
//GsrCameraPixelFormat pixel_format;
|
||||
};
|
||||
|
||||
struct GsrCamera {
|
||||
std::string path;
|
||||
std::vector<GsrCameraSetup> yuyv_setups;
|
||||
std::vector<GsrCameraSetup> mjpeg_setups;
|
||||
};
|
||||
|
||||
struct GsrVersion {
|
||||
uint8_t major = 0;
|
||||
uint8_t minor = 0;
|
||||
@@ -51,6 +68,7 @@ namespace gsr {
|
||||
bool focused = false;
|
||||
bool portal = false;
|
||||
std::vector<GsrMonitor> monitors;
|
||||
std::vector<GsrCamera> cameras;
|
||||
};
|
||||
|
||||
enum class DisplayServer {
|
||||
@@ -103,4 +121,5 @@ namespace gsr {
|
||||
std::vector<AudioDevice> get_audio_devices();
|
||||
std::vector<std::string> get_application_audio();
|
||||
SupportedCaptureOptions get_supported_capture_options(const GsrInfo &gsr_info);
|
||||
std::vector<GsrCamera> get_v4l2_devices();
|
||||
}
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -10,6 +10,8 @@
|
||||
#include "AudioPlayer.hpp"
|
||||
#include "RegionSelector.hpp"
|
||||
#include "WindowSelector.hpp"
|
||||
#include "ClipboardFile.hpp"
|
||||
#include "LedIndicator.hpp"
|
||||
#include "CursorTracker/CursorTracker.hpp"
|
||||
|
||||
#include <mglpp/window/Window.hpp>
|
||||
@@ -22,6 +24,8 @@
|
||||
|
||||
#include <array>
|
||||
|
||||
struct wl_display;
|
||||
|
||||
namespace gsr {
|
||||
class DropdownButton;
|
||||
class GlobalHotkeys;
|
||||
@@ -38,7 +42,19 @@ namespace gsr {
|
||||
RECORD,
|
||||
REPLAY,
|
||||
STREAM,
|
||||
SCREENSHOT
|
||||
SCREENSHOT,
|
||||
NOTICE
|
||||
};
|
||||
|
||||
enum class NotificationLevel {
|
||||
INFO,
|
||||
ERROR,
|
||||
};
|
||||
|
||||
enum class RecordForceType {
|
||||
NONE,
|
||||
REGION,
|
||||
WINDOW
|
||||
};
|
||||
|
||||
enum class ScreenshotForceType {
|
||||
@@ -47,6 +63,11 @@ namespace gsr {
|
||||
WINDOW
|
||||
};
|
||||
|
||||
enum class NotificationSpeed {
|
||||
NORMAL,
|
||||
FAST
|
||||
};
|
||||
|
||||
class Overlay {
|
||||
public:
|
||||
Overlay(std::string resources_path, GsrInfo gsr_info, SupportedCaptureOptions capture_options, egl_functions egl_funcs);
|
||||
@@ -59,9 +80,9 @@ namespace gsr {
|
||||
bool draw();
|
||||
|
||||
void show();
|
||||
void hide();
|
||||
void hide_next_frame();
|
||||
void toggle_show();
|
||||
void toggle_record();
|
||||
void toggle_record(RecordForceType force_type);
|
||||
void toggle_pause();
|
||||
void toggle_stream();
|
||||
void toggle_replay();
|
||||
@@ -71,7 +92,7 @@ namespace gsr {
|
||||
void take_screenshot();
|
||||
void take_screenshot_region();
|
||||
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);
|
||||
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();
|
||||
@@ -81,10 +102,21 @@ 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:
|
||||
const char* notification_type_to_string(NotificationType notification_type);
|
||||
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 update_led_indicator_after_settings_change();
|
||||
void create_frontpage_ui_components();
|
||||
void xi_setup();
|
||||
void handle_xi_events();
|
||||
@@ -96,7 +128,7 @@ namespace gsr {
|
||||
|
||||
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 save_video_in_current_game_directory(std::string &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);
|
||||
@@ -108,7 +140,7 @@ namespace gsr {
|
||||
void update_power_supply_status();
|
||||
void update_system_startup_status();
|
||||
|
||||
void on_stop_recording(int exit_code, const std::string &video_filepath);
|
||||
void on_stop_recording(int exit_code, std::string &video_filepath);
|
||||
|
||||
void update_ui_recording_paused();
|
||||
void update_ui_recording_unpaused();
|
||||
@@ -127,11 +159,14 @@ namespace gsr {
|
||||
void on_press_save_replay_1_min_replay();
|
||||
void on_press_save_replay_10_min_replay();
|
||||
bool on_press_start_replay(bool disable_notification, bool finished_selection);
|
||||
void on_press_start_record(bool finished_selection);
|
||||
void on_press_start_record(bool finished_selection, RecordForceType force_type);
|
||||
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);
|
||||
|
||||
void add_region_command(std::vector<const char*> &args, char *region_str, int region_str_size);
|
||||
void add_common_gpu_screen_recorder_args(std::vector<const char*> &args, const RecordOptions &record_options, const std::vector<std::string> &audio_tracks, const std::string &video_bitrate, const char *region, char *region_str, int region_str_size, const std::string ®ion_area_option);
|
||||
|
||||
std::string get_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options);
|
||||
|
||||
void force_window_on_top();
|
||||
@@ -148,6 +183,9 @@ namespace gsr {
|
||||
GsrInfo gsr_info;
|
||||
egl_functions egl_funcs;
|
||||
Config config;
|
||||
Config current_recording_config;
|
||||
|
||||
std::string gsr_icon_path;
|
||||
|
||||
bool visible = false;
|
||||
|
||||
@@ -184,6 +222,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;
|
||||
@@ -212,16 +252,17 @@ 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;
|
||||
|
||||
struct wl_display *wayland_dpy = nullptr;
|
||||
|
||||
mgl::Clock replay_save_clock;
|
||||
bool replay_save_show_notification = false;
|
||||
ReplayStartupMode replay_startup_mode = ReplayStartupMode::TURN_ON_AT_SYSTEM_STARTUP;
|
||||
bool try_replay_startup = true;
|
||||
bool replay_recording = false;
|
||||
int replay_save_duration_min = 0;
|
||||
double replay_buffer_save_duration_sec = 0.0;
|
||||
mgl::Clock replay_duration_clock;
|
||||
double replay_saved_duration_sec = 0.0;
|
||||
bool replay_restart_on_save = false;
|
||||
@@ -245,5 +286,9 @@ namespace gsr {
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -13,13 +13,17 @@ namespace gsr {
|
||||
|
||||
// Arguments ending with NULL
|
||||
bool exec_program_daemonized(const char **args, bool debug = true);
|
||||
// Arguments ending with NULL.
|
||||
// This works the same as |exec_program_get_stdout|, except on flatpak where this runs the program on the
|
||||
// host machine with flatpak-spawn --host.
|
||||
bool exec_program_on_host_daemonized(const char **args, bool debug = true);
|
||||
// Arguments ending with NULL. |read_fd| can be NULL
|
||||
pid_t exec_program(const char **args, int *read_fd, bool debug = true);
|
||||
// Arguments ending with NULL. Returns the exit status of the program or -1 on error
|
||||
int exec_program_get_stdout(const char **args, std::string &result, bool debug = true);
|
||||
// Arguments ending with NULL. Returns the exit status of the program or -1 on error.
|
||||
// This works the same as |exec_program_get_stdout|, except on flatpak where this runs the program on the
|
||||
// host machine with flatpak-spawn --host
|
||||
// host machine with flatpak-spawn --host.
|
||||
int exec_program_on_host_get_stdout(const char **args, std::string &result, bool debug = true);
|
||||
pid_t pidof(const char *process_name, pid_t ignore_pid);
|
||||
}
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
struct wl_display;
|
||||
|
||||
namespace gsr {
|
||||
struct Region {
|
||||
mgl::vec2i pos;
|
||||
@@ -28,7 +30,7 @@ namespace gsr {
|
||||
bool poll_events();
|
||||
bool take_selection();
|
||||
bool take_canceled();
|
||||
Region get_selection() const;
|
||||
Region get_selection(Display *x11_dpy, struct wl_display *wayland_dpy) const;
|
||||
private:
|
||||
void on_button_press(const void *de);
|
||||
void on_button_release(const void *de);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -24,6 +24,7 @@ namespace gsr {
|
||||
mgl::Font body_font;
|
||||
mgl::Font title_font;
|
||||
mgl::Font top_bar_font;
|
||||
mgl::Font camera_setup_font;
|
||||
|
||||
mgl::Texture combobox_arrow_texture;
|
||||
mgl::Texture settings_texture;
|
||||
@@ -44,6 +45,11 @@ namespace gsr {
|
||||
mgl::Texture save_texture;
|
||||
mgl::Texture screenshot_texture;
|
||||
mgl::Texture trash_texture;
|
||||
mgl::Texture masked_texture;
|
||||
mgl::Texture unmasked_texture;
|
||||
mgl::Texture warning_texture;
|
||||
mgl::Texture info_texture;
|
||||
mgl::Texture question_mark_texture;
|
||||
|
||||
mgl::Texture ps4_home_texture;
|
||||
mgl::Texture ps4_options_texture;
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <optional>
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
struct wl_display;
|
||||
|
||||
namespace gsr {
|
||||
enum class WindowCaptureType {
|
||||
FOCUSED,
|
||||
@@ -13,14 +15,14 @@ namespace gsr {
|
||||
};
|
||||
|
||||
struct Monitor {
|
||||
mgl::vec2i position;
|
||||
mgl::vec2i size;
|
||||
mgl::vec2i position; // Logical position on Wayland
|
||||
mgl::vec2i size; // Logical size on Wayland
|
||||
std::string name;
|
||||
};
|
||||
|
||||
std::optional<std::string> get_window_title(Display *dpy, Window window);
|
||||
Window get_focused_window(Display *dpy, WindowCaptureType cap_type);
|
||||
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type);
|
||||
Window get_focused_window(Display *dpy, WindowCaptureType cap_type, bool fallback_cursor_focused = true);
|
||||
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type, bool fallback_cursor_focused = true);
|
||||
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);
|
||||
@@ -30,6 +32,7 @@ namespace gsr {
|
||||
std::string get_window_manager_name(Display *display);
|
||||
bool is_compositor_running(Display *dpy, int screen);
|
||||
std::vector<Monitor> get_monitors(Display *dpy);
|
||||
std::vector<Monitor> get_monitors_wayland(struct wl_display *dpy);
|
||||
void xi_grab_all_mouse_devices(Display *dpy);
|
||||
void xi_ungrab_all_mouse_devices(Display *dpy);
|
||||
void xi_warp_all_mouse_devices(Display *dpy, mgl::vec2i position);
|
||||
|
||||
@@ -18,8 +18,12 @@ namespace gsr {
|
||||
bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override;
|
||||
void draw(mgl::Window &window, mgl::vec2f offset) override;
|
||||
|
||||
void add_item(const std::string &text, const std::string &id);
|
||||
void add_item(const std::string &text, const std::string &id, bool allow_duplicate = true);
|
||||
void clear_items();
|
||||
|
||||
// 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 +40,7 @@ namespace gsr {
|
||||
mgl::Text text;
|
||||
std::string id;
|
||||
mgl::vec2f position;
|
||||
bool enabled = true;
|
||||
};
|
||||
|
||||
mgl::vec2f max_size;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <functional>
|
||||
|
||||
#include <mglpp/graphics/Color.hpp>
|
||||
#include <mglpp/graphics/Text.hpp>
|
||||
#include <mglpp/graphics/Text32.hpp>
|
||||
#include <mglpp/graphics/Rectangle.hpp>
|
||||
|
||||
namespace gsr {
|
||||
@@ -15,10 +15,20 @@ namespace gsr {
|
||||
ALLOW,
|
||||
REPLACED
|
||||
};
|
||||
using EntryValidateHandler = std::function<EntryValidateHandlerResult(Entry &entry, const std::string &str)>;
|
||||
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;
|
||||
@@ -28,11 +38,11 @@ namespace gsr {
|
||||
|
||||
mgl::vec2f get_size() override;
|
||||
|
||||
EntryValidateHandlerResult set_text(std::string str);
|
||||
const std::string& get_text() const;
|
||||
EntryValidateHandlerResult set_text(const std::string &str);
|
||||
std::string get_text() const;
|
||||
|
||||
// Also updates the cursor position
|
||||
void replace_text(size_t index, size_t size, const std::string &replacement);
|
||||
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.
|
||||
@@ -40,24 +50,28 @@ namespace gsr {
|
||||
|
||||
std::function<void(const std::string &text)> on_changed;
|
||||
private:
|
||||
EntryValidateHandlerResult set_text_internal(std::string str);
|
||||
// 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);
|
||||
mgl_index_codepoint_pair find_closest_caret_index_by_position(mgl::vec2f position);
|
||||
CaretIndexPos find_closest_caret_index_by_position(mgl::vec2f position);
|
||||
private:
|
||||
struct Caret {
|
||||
float offset_x = 0.0f;
|
||||
int utf8_index = 0;
|
||||
int byte_index = 0;
|
||||
int index = 0;
|
||||
};
|
||||
|
||||
mgl::Rectangle background;
|
||||
mgl::Text text;
|
||||
mgl::Text32 text;
|
||||
mgl::Text32 masked_text;
|
||||
float max_width;
|
||||
bool selected = false;
|
||||
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;
|
||||
|
||||
@@ -26,6 +26,8 @@ namespace gsr {
|
||||
REPLAY_SAVE_10_MIN,
|
||||
RECORD_START_STOP,
|
||||
RECORD_PAUSE_UNPAUSE,
|
||||
RECORD_START_STOP_REGION,
|
||||
RECORD_START_STOP_WINDOW,
|
||||
STREAM_START_STOP,
|
||||
TAKE_SCREENSHOT,
|
||||
TAKE_SCREENSHOT_REGION,
|
||||
@@ -61,6 +63,7 @@ namespace gsr {
|
||||
std::unique_ptr<List> create_replay_hotkey_options();
|
||||
std::unique_ptr<List> create_replay_partial_save_hotkey_options();
|
||||
std::unique_ptr<List> create_record_hotkey_options();
|
||||
std::unique_ptr<List> create_record_hotkey_window_region_options();
|
||||
std::unique_ptr<List> create_stream_hotkey_options();
|
||||
std::unique_ptr<List> create_screenshot_hotkey_options();
|
||||
std::unique_ptr<List> create_screenshot_region_hotkey_options();
|
||||
@@ -70,8 +73,10 @@ namespace gsr {
|
||||
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();
|
||||
@@ -98,11 +103,14 @@ namespace gsr {
|
||||
Button *save_replay_10_min_button_ptr = nullptr;
|
||||
Button *start_stop_recording_button_ptr = nullptr;
|
||||
Button *pause_unpause_recording_button_ptr = nullptr;
|
||||
Button *start_stop_recording_region_button_ptr = nullptr;
|
||||
Button *start_stop_recording_window_button_ptr = nullptr;
|
||||
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;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "Widget.hpp"
|
||||
|
||||
#include <mglpp/graphics/Sprite.hpp>
|
||||
#include <functional>
|
||||
|
||||
namespace gsr {
|
||||
class Image : public Widget {
|
||||
@@ -21,6 +22,8 @@ namespace gsr {
|
||||
void draw(mgl::Window &window, mgl::vec2f offset) override;
|
||||
|
||||
mgl::vec2f get_size() override;
|
||||
|
||||
std::function<void(bool inside)> on_mouse_move;
|
||||
private:
|
||||
mgl::Sprite sprite;
|
||||
mgl::vec2f size;
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace gsr {
|
||||
void load();
|
||||
void save();
|
||||
void on_navigate_away_from_page() override;
|
||||
|
||||
std::function<void()> on_config_changed;
|
||||
private:
|
||||
std::unique_ptr<ComboBox> create_record_area_box();
|
||||
std::unique_ptr<Widget> create_record_area();
|
||||
@@ -42,8 +44,15 @@ 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<CheckBox> create_save_screenshot_to_disk();
|
||||
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_custom_script_screenshot_section();
|
||||
std::unique_ptr<List> create_custom_script_screenshot_entry();
|
||||
std::unique_ptr<List> create_custom_script_screenshot();
|
||||
std::unique_ptr<Widget> create_settings();
|
||||
void add_widgets();
|
||||
|
||||
@@ -69,8 +78,12 @@ 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 *save_screenshot_to_disk_checkbox_ptr = nullptr;
|
||||
CheckBox *show_notification_checkbox_ptr = nullptr;
|
||||
CheckBox *led_indicator_checkbox_ptr = nullptr;
|
||||
Entry *create_custom_script_screenshot_entry_ptr = nullptr;
|
||||
|
||||
PageStack *page_stack = nullptr;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,14 @@ namespace gsr {
|
||||
INPUT
|
||||
};
|
||||
|
||||
enum class WebcamBoxResizeCorner {
|
||||
NONE,
|
||||
//TOP_LEFT,
|
||||
//TOP_RIGHT,
|
||||
//BOTTOM_LEFT,
|
||||
BOTTOM_RIGHT
|
||||
};
|
||||
|
||||
class SettingsPage : public StaticPage {
|
||||
public:
|
||||
enum class Type {
|
||||
@@ -58,10 +66,19 @@ namespace gsr {
|
||||
std::unique_ptr<List> create_restore_portal_session_section();
|
||||
std::unique_ptr<Widget> create_change_video_resolution_section();
|
||||
std::unique_ptr<Widget> create_capture_target_section();
|
||||
std::unique_ptr<List> create_webcam_sources();
|
||||
std::unique_ptr<List> create_webcam_video_setups();
|
||||
std::unique_ptr<List> create_webcam_video_format();
|
||||
std::unique_ptr<List> create_webcam_video_setup_list();
|
||||
std::unique_ptr<Widget> create_webcam_location_widget();
|
||||
std::unique_ptr<CheckBox> create_flip_camera_checkbox();
|
||||
std::unique_ptr<List> create_webcam_body();
|
||||
std::unique_ptr<Widget> create_webcam_section();
|
||||
std::unique_ptr<ComboBox> create_audio_device_selection_combobox(AudioDeviceType device_type);
|
||||
std::unique_ptr<Button> create_remove_audio_device_button(List *audio_input_list_ptr, List *audio_device_list_ptr);
|
||||
std::unique_ptr<List> create_audio_device(AudioDeviceType device_type, List *audio_input_list_ptr);
|
||||
std::unique_ptr<Button> create_add_audio_track_button();
|
||||
void update_application_audio_warning_visibility();
|
||||
std::unique_ptr<Button> create_add_audio_output_device_button(List *audio_input_list_ptr);
|
||||
std::unique_ptr<Button> create_add_audio_input_device_button(List *audio_input_list_ptr);
|
||||
std::unique_ptr<ComboBox> create_application_audio_selection_combobox(List *application_audio_row);
|
||||
@@ -71,6 +88,7 @@ namespace gsr {
|
||||
std::unique_ptr<List> create_add_audio_buttons(List *audio_input_list_ptr);
|
||||
std::unique_ptr<List> create_audio_input_section();
|
||||
std::unique_ptr<CheckBox> create_application_audio_invert_checkbox();
|
||||
std::unique_ptr<Widget> create_application_audio_warning();
|
||||
std::unique_ptr<List> create_audio_track_title_and_remove(Subsection *audio_track_subsection, const char *title);
|
||||
std::unique_ptr<Subsection> create_audio_track_section(Widget *parent_widget);
|
||||
std::unique_ptr<List> create_audio_track_section_list();
|
||||
@@ -112,15 +130,21 @@ 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);
|
||||
std::unique_ptr<Widget> create_low_power_mode();
|
||||
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_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);
|
||||
@@ -134,7 +158,9 @@ namespace gsr {
|
||||
void save_record();
|
||||
void save_stream();
|
||||
|
||||
void view_changed(bool advanced_view, Subsection *notifications_subsection_ptr);
|
||||
void view_changed(bool advanced_view);
|
||||
|
||||
RecordOptions& get_current_record_options();
|
||||
private:
|
||||
Type type;
|
||||
Config &config;
|
||||
@@ -173,24 +199,17 @@ 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 *kick_stream_url_entry_ptr = nullptr;
|
||||
Entry *kick_stream_key_entry_ptr = nullptr;
|
||||
Entry *stream_url_entry_ptr = nullptr;
|
||||
Entry *stream_key_entry_ptr = nullptr;
|
||||
Entry *replay_time_entry_ptr = nullptr;
|
||||
@@ -199,7 +218,34 @@ namespace gsr {
|
||||
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;
|
||||
ComboBox *webcam_sources_box_ptr = nullptr;
|
||||
ComboBox *webcam_video_setup_box_ptr = nullptr;
|
||||
ComboBox *webcam_video_format_box_ptr = nullptr;
|
||||
List *webcam_body_list_ptr = nullptr;
|
||||
CheckBox *flip_camera_horizontally_checkbox_ptr = nullptr;
|
||||
CheckBox *low_power_mode_checkbox_ptr = nullptr;
|
||||
|
||||
PageStack *page_stack = nullptr;
|
||||
|
||||
mgl::vec2f webcam_box_pos;
|
||||
mgl::vec2f webcam_box_size;
|
||||
|
||||
mgl::vec2f webcam_box_drawn_pos;
|
||||
mgl::vec2f webcam_box_drawn_size;
|
||||
mgl::vec2f webcam_box_grab_offset;
|
||||
|
||||
mgl::vec2f camera_screen_size;
|
||||
mgl::vec2f screen_inner_size;
|
||||
bool moving_webcam_box = false;
|
||||
|
||||
WebcamBoxResizeCorner webcam_resize_corner = WebcamBoxResizeCorner::NONE;
|
||||
mgl::vec2f webcam_resize_start_pos;
|
||||
mgl::vec2f webcam_box_pos_resize_start;
|
||||
mgl::vec2f webcam_box_size_resize_start;
|
||||
|
||||
std::optional<GsrCamera> selected_camera;
|
||||
std::optional<GsrCameraSetup> selected_camera_setup;
|
||||
};
|
||||
}
|
||||
22
include/gui/Tooltip.hpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "Widget.hpp"
|
||||
#include <mglpp/graphics/Text.hpp>
|
||||
|
||||
namespace gsr {
|
||||
class Tooltip : public Widget {
|
||||
public:
|
||||
Tooltip(mgl::Font *font);
|
||||
Tooltip(const Tooltip&) = delete;
|
||||
Tooltip& operator=(const Tooltip&) = delete;
|
||||
|
||||
bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override;
|
||||
void draw(mgl::Window &window, mgl::vec2f offset) override;
|
||||
|
||||
mgl::vec2f get_size() override;
|
||||
|
||||
void set_text(std::string text);
|
||||
private:
|
||||
mgl::Text label;
|
||||
};
|
||||
}
|
||||
@@ -9,6 +9,10 @@ namespace mgl {
|
||||
}
|
||||
|
||||
namespace gsr {
|
||||
mgl::vec2i min_vec2i(mgl::vec2i a, mgl::vec2i b);
|
||||
mgl::vec2i max_vec2i(mgl::vec2i a, mgl::vec2i b);
|
||||
mgl::vec2i clamp_vec2i(mgl::vec2i value, mgl::vec2i min, mgl::vec2i max);
|
||||
|
||||
// Inner border
|
||||
void draw_rectangle_outline(mgl::Window &window, mgl::vec2f pos, mgl::vec2f size, mgl::Color color, float border_size);
|
||||
double get_frame_delta_seconds();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <mglpp/system/vec.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace mgl {
|
||||
class Event;
|
||||
@@ -44,6 +45,13 @@ namespace gsr {
|
||||
Alignment get_vertical_alignment() const;
|
||||
|
||||
void set_visible(bool visible);
|
||||
bool is_visible() const;
|
||||
|
||||
Widget* get_parent_widget();
|
||||
|
||||
void set_tooltip_text(std::string text);
|
||||
const std::string& get_tooltip_text() const;
|
||||
void handle_tooltip_event(mgl::Event &event, mgl::vec2f position, mgl::vec2f size);
|
||||
|
||||
void *userdata = nullptr;
|
||||
protected:
|
||||
@@ -59,8 +67,13 @@ namespace gsr {
|
||||
Alignment vertical_aligment = Alignment::START;
|
||||
|
||||
bool visible = true;
|
||||
std::string tooltip_text;
|
||||
};
|
||||
|
||||
void add_widget_to_remove(std::unique_ptr<Widget> widget);
|
||||
void remove_widgets_to_be_removed();
|
||||
|
||||
void set_current_tooltip(Widget *widget);
|
||||
void remove_as_current_tooltip(Widget *widget);
|
||||
void draw_tooltip(mgl::Window &window);
|
||||
}
|
||||
19
meson.build
@@ -1,4 +1,6 @@
|
||||
project('gsr-ui', ['c', 'cpp'], version : '1.7.2', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
|
||||
project('gsr-ui', ['c', 'cpp'], version : '1.10.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'])
|
||||
@@ -32,6 +34,7 @@ src = [
|
||||
'src/gui/GlobalSettingsPage.cpp',
|
||||
'src/gui/GsrPage.cpp',
|
||||
'src/gui/Subsection.cpp',
|
||||
'src/gui/Tooltip.cpp',
|
||||
'src/GlobalHotkeys/GlobalHotkeysX11.cpp',
|
||||
'src/GlobalHotkeys/GlobalHotkeysLinux.cpp',
|
||||
'src/GlobalHotkeys/GlobalHotkeysJoystick.cpp',
|
||||
@@ -47,6 +50,8 @@ src = [
|
||||
'src/Overlay.cpp',
|
||||
'src/AudioPlayer.cpp',
|
||||
'src/Hotplug.cpp',
|
||||
'src/ClipboardFile.cpp',
|
||||
'src/LedIndicator.cpp',
|
||||
'src/Rpc.cpp',
|
||||
'src/main.cpp',
|
||||
]
|
||||
@@ -60,9 +65,10 @@ mglpp_dep = mglpp_proj.get_variable('mglpp_dep')
|
||||
prefix = get_option('prefix')
|
||||
datadir = get_option('datadir')
|
||||
gsr_ui_resources_path = join_paths(prefix, datadir, 'gsr-ui')
|
||||
icons_path = join_paths(prefix, datadir, 'icons')
|
||||
|
||||
add_project_arguments('-DGSR_UI_VERSION="' + meson.project_version() + '"', language: ['c', 'cpp'])
|
||||
add_project_arguments('-DGSR_FLATPAK_VERSION="5.7.5"', language: ['c', 'cpp'])
|
||||
add_project_arguments('-DGSR_FLATPAK_VERSION="5.12.0"', language: ['c', 'cpp'])
|
||||
|
||||
executable(
|
||||
meson.project_name(),
|
||||
@@ -90,6 +96,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',
|
||||
@@ -107,6 +114,14 @@ executable(
|
||||
install_subdir('images', install_dir : gsr_ui_resources_path)
|
||||
install_subdir('fonts', install_dir : gsr_ui_resources_path)
|
||||
|
||||
if get_option('desktop-files') == true
|
||||
install_data(files('gpu-screen-recorder.desktop'), install_dir : join_paths(prefix, datadir, 'applications'))
|
||||
install_subdir('icons/hicolor', install_dir : icons_path)
|
||||
|
||||
gnome = import('gnome')
|
||||
gnome.post_install(update_desktop_database : true)
|
||||
endif
|
||||
|
||||
if get_option('systemd') == true
|
||||
install_data(files('extra/gpu-screen-recorder-ui.service'), install_dir : 'lib/systemd/user')
|
||||
endif
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
option('systemd', type : 'boolean', value : true, description : 'Install systemd service file')
|
||||
option('capabilities', type : 'boolean', value : true, description : 'Set binary setuid capability on gsr-global-hotkeys binary to allow global hotkeys')
|
||||
option('capabilities', type : 'boolean', value : true, description : 'Set binary setuid capability on gsr-global-hotkeys binary to allow global hotkeys')
|
||||
option('desktop-files', type : 'boolean', value : true, description : 'Install desktop files')
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "gsr-ui"
|
||||
type = "executable"
|
||||
version = "1.7.2"
|
||||
version = "1.10.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"
|
||||
|
||||
315
src/ClipboardFile.cpp
Normal file
@@ -0,0 +1,315 @@
|
||||
#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: {
|
||||
bool clear_current_file = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
should_clear_selection = true;
|
||||
if(clipboard_copies.empty()) {
|
||||
should_clear_selection = false;
|
||||
clear_current_file = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(clear_current_file)
|
||||
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(file_fd <= 0)
|
||||
return;
|
||||
|
||||
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(XGetSelectionOwner(dpy, clipboard_atom) == clipboard_window) {
|
||||
XSetSelectionOwner(dpy, clipboard_atom, None, CurrentTime);
|
||||
XFlush(dpy);
|
||||
}
|
||||
|
||||
if(filepath.empty()) {
|
||||
// TODO: Cancel transfer
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -145,6 +145,8 @@ namespace gsr {
|
||||
|
||||
record_config.start_stop_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LALT};
|
||||
record_config.pause_unpause_hotkey = {mgl::Keyboard::F7, HOTKEY_MOD_LALT};
|
||||
record_config.start_stop_region_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LCTRL};
|
||||
record_config.start_stop_window_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LSHIFT};
|
||||
|
||||
replay_config.start_stop_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT | HOTKEY_MOD_LSHIFT};
|
||||
replay_config.save_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT};
|
||||
@@ -171,9 +173,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},
|
||||
@@ -197,12 +201,25 @@ 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.low_power_mode", &config.streaming_config.record_options.low_power_mode},
|
||||
{"streaming.record_options.webcam_source", &config.streaming_config.record_options.webcam_source},
|
||||
{"streaming.record_options.webcam_flip_horizontally", &config.streaming_config.record_options.webcam_flip_horizontally},
|
||||
{"streaming.record_options.webcam_video_format", &config.streaming_config.record_options.webcam_video_format},
|
||||
{"streaming.record_options.webcam_camera_width", &config.streaming_config.record_options.webcam_camera_width},
|
||||
{"streaming.record_options.webcam_camera_height", &config.streaming_config.record_options.webcam_camera_height},
|
||||
{"streaming.record_options.webcam_camera_fps", &config.streaming_config.record_options.webcam_camera_fps},
|
||||
{"streaming.record_options.webcam_x", &config.streaming_config.record_options.webcam_x},
|
||||
{"streaming.record_options.webcam_y", &config.streaming_config.record_options.webcam_y},
|
||||
{"streaming.record_options.webcam_width", &config.streaming_config.record_options.webcam_width},
|
||||
{"streaming.record_options.webcam_height", &config.streaming_config.record_options.webcam_height},
|
||||
{"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.kick.url", &config.streaming_config.kick.stream_url},
|
||||
{"streaming.kick.key", &config.streaming_config.kick.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},
|
||||
@@ -229,14 +246,26 @@ 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.low_power_mode", &config.record_config.record_options.low_power_mode},
|
||||
{"record.record_options.webcam_source", &config.record_config.record_options.webcam_source},
|
||||
{"record.record_options.webcam_flip_horizontally", &config.record_config.record_options.webcam_flip_horizontally},
|
||||
{"record.record_options.webcam_video_format", &config.record_config.record_options.webcam_video_format},
|
||||
{"record.record_options.webcam_camera_width", &config.record_config.record_options.webcam_camera_width},
|
||||
{"record.record_options.webcam_camera_height", &config.record_config.record_options.webcam_camera_height},
|
||||
{"record.record_options.webcam_camera_fps", &config.record_config.record_options.webcam_camera_fps},
|
||||
{"record.record_options.webcam_x", &config.record_config.record_options.webcam_x},
|
||||
{"record.record_options.webcam_y", &config.record_config.record_options.webcam_y},
|
||||
{"record.record_options.webcam_width", &config.record_config.record_options.webcam_width},
|
||||
{"record.record_options.webcam_height", &config.record_config.record_options.webcam_height},
|
||||
{"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},
|
||||
{"record.pause_unpause_hotkey", &config.record_config.pause_unpause_hotkey},
|
||||
{"record.start_stop_region_hotkey", &config.record_config.start_stop_region_hotkey},
|
||||
{"record.start_stop_window_hotkey", &config.record_config.start_stop_window_hotkey},
|
||||
|
||||
{"replay.record_options.record_area_option", &config.replay_config.record_options.record_area_option},
|
||||
{"replay.record_options.record_area_width", &config.replay_config.record_options.record_area_width},
|
||||
@@ -259,12 +288,22 @@ 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.low_power_mode", &config.replay_config.record_options.low_power_mode},
|
||||
{"replay.record_options.webcam_source", &config.replay_config.record_options.webcam_source},
|
||||
{"replay.record_options.webcam_flip_horizontally", &config.replay_config.record_options.webcam_flip_horizontally},
|
||||
{"replay.record_options.webcam_video_format", &config.replay_config.record_options.webcam_video_format},
|
||||
{"replay.record_options.webcam_camera_width", &config.replay_config.record_options.webcam_camera_width},
|
||||
{"replay.record_options.webcam_camera_height", &config.replay_config.record_options.webcam_camera_height},
|
||||
{"replay.record_options.webcam_camera_fps", &config.replay_config.record_options.webcam_camera_fps},
|
||||
{"replay.record_options.webcam_x", &config.replay_config.record_options.webcam_x},
|
||||
{"replay.record_options.webcam_y", &config.replay_config.record_options.webcam_y},
|
||||
{"replay.record_options.webcam_width", &config.replay_config.record_options.webcam_width},
|
||||
{"replay.record_options.webcam_height", &config.replay_config.record_options.webcam_height},
|
||||
{"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},
|
||||
@@ -283,11 +322,15 @@ 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.save_screenshot_to_disk", &config.screenshot_config.save_screenshot_to_disk},
|
||||
{"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_window_hotkey", &config.screenshot_config.take_screenshot_window_hotkey}
|
||||
{"screenshot.take_screenshot_window_hotkey", &config.screenshot_config.take_screenshot_window_hotkey},
|
||||
{"screenshot.custom_script", &config.screenshot_config.custom_script},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -485,4 +528,4 @@ namespace gsr {
|
||||
|
||||
fclose(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
#include "../../include/CursorTracker/CursorTrackerWayland.hpp"
|
||||
#include "../../include/WindowUtils.hpp"
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <xf86drm.h>
|
||||
#include <xf86drmMode.h>
|
||||
#include <wayland-client.h>
|
||||
#include "xdg-output-unstable-v1-client-protocol.h"
|
||||
|
||||
namespace gsr {
|
||||
static const int MAX_CONNECTORS = 32;
|
||||
@@ -136,177 +135,14 @@ namespace gsr {
|
||||
}
|
||||
|
||||
// Name is the crtc name. TODO: verify if this works on all wayland compositors
|
||||
static const WaylandOutput* get_wayland_monitor_by_name(const std::vector<WaylandOutput> &monitors, const std::string &name) {
|
||||
for(const WaylandOutput &monitor : monitors) {
|
||||
static const Monitor* get_wayland_monitor_by_name(const std::vector<Monitor> &monitors, const std::string &name) {
|
||||
for(const Monitor &monitor : monitors) {
|
||||
if(monitor.name == name)
|
||||
return &monitor;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static WaylandOutput* get_wayland_monitor_by_output(CursorTrackerWayland &cursor_tracker_wayland, struct wl_output *output) {
|
||||
for(WaylandOutput &monitor : cursor_tracker_wayland.monitors) {
|
||||
if(monitor.output == output)
|
||||
return &monitor;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void output_handle_geometry(void *data, struct wl_output *wl_output,
|
||||
int32_t x, int32_t y, int32_t phys_width, int32_t phys_height,
|
||||
int32_t subpixel, const char *make, const char *model,
|
||||
int32_t transform) {
|
||||
(void)wl_output;
|
||||
(void)phys_width;
|
||||
(void)phys_height;
|
||||
(void)subpixel;
|
||||
(void)make;
|
||||
(void)model;
|
||||
CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data;
|
||||
WaylandOutput *monitor = get_wayland_monitor_by_output(*cursor_tracker_wayland, wl_output);
|
||||
if(!monitor)
|
||||
return;
|
||||
|
||||
monitor->pos.x = x;
|
||||
monitor->pos.y = y;
|
||||
monitor->transform = transform;
|
||||
}
|
||||
|
||||
static void output_handle_mode(void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) {
|
||||
(void)wl_output;
|
||||
(void)flags;
|
||||
(void)refresh;
|
||||
CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data;
|
||||
WaylandOutput *monitor = get_wayland_monitor_by_output(*cursor_tracker_wayland, wl_output);
|
||||
if(!monitor)
|
||||
return;
|
||||
|
||||
monitor->size.x = width;
|
||||
monitor->size.y = height;
|
||||
}
|
||||
|
||||
static void output_handle_done(void *data, struct wl_output *wl_output) {
|
||||
(void)data;
|
||||
(void)wl_output;
|
||||
}
|
||||
|
||||
static void output_handle_scale(void* data, struct wl_output *wl_output, int32_t factor) {
|
||||
(void)data;
|
||||
(void)wl_output;
|
||||
(void)factor;
|
||||
}
|
||||
|
||||
static void output_handle_name(void *data, struct wl_output *wl_output, const char *name) {
|
||||
(void)wl_output;
|
||||
CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data;
|
||||
WaylandOutput *monitor = get_wayland_monitor_by_output(*cursor_tracker_wayland, wl_output);
|
||||
if(!monitor)
|
||||
return;
|
||||
|
||||
monitor->name = name;
|
||||
}
|
||||
|
||||
static void output_handle_description(void *data, struct wl_output *wl_output, const char *description) {
|
||||
(void)data;
|
||||
(void)wl_output;
|
||||
(void)description;
|
||||
}
|
||||
|
||||
static const struct wl_output_listener output_listener = {
|
||||
output_handle_geometry,
|
||||
output_handle_mode,
|
||||
output_handle_done,
|
||||
output_handle_scale,
|
||||
output_handle_name,
|
||||
output_handle_description,
|
||||
};
|
||||
|
||||
static void registry_add_object(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) {
|
||||
(void)version;
|
||||
CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data;
|
||||
if(strcmp(interface, wl_output_interface.name) == 0) {
|
||||
if(version < 4) {
|
||||
fprintf(stderr, "Warning: wl output interface version is < 4, expected >= 4\n");
|
||||
return;
|
||||
}
|
||||
|
||||
struct wl_output *output = (struct wl_output*)wl_registry_bind(registry, name, &wl_output_interface, 4);
|
||||
cursor_tracker_wayland->monitors.push_back(
|
||||
WaylandOutput{
|
||||
name,
|
||||
output,
|
||||
nullptr,
|
||||
mgl::vec2i{0, 0},
|
||||
mgl::vec2i{0, 0},
|
||||
0,
|
||||
""
|
||||
});
|
||||
wl_output_add_listener(output, &output_listener, cursor_tracker_wayland);
|
||||
} else if(strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) {
|
||||
if(version < 1) {
|
||||
fprintf(stderr, "Warning: xdg output interface version is < 1, expected >= 1\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if(cursor_tracker_wayland->xdg_output_manager) {
|
||||
zxdg_output_manager_v1_destroy(cursor_tracker_wayland->xdg_output_manager);
|
||||
cursor_tracker_wayland->xdg_output_manager = NULL;
|
||||
}
|
||||
cursor_tracker_wayland->xdg_output_manager = (struct zxdg_output_manager_v1*)wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, 1);
|
||||
}
|
||||
}
|
||||
|
||||
static void registry_remove_object(void *data, struct wl_registry *registry, uint32_t name) {
|
||||
(void)data;
|
||||
(void)registry;
|
||||
(void)name;
|
||||
// TODO: Remove output
|
||||
}
|
||||
|
||||
static struct wl_registry_listener registry_listener = {
|
||||
registry_add_object,
|
||||
registry_remove_object,
|
||||
};
|
||||
|
||||
static void xdg_output_logical_position(void *data, struct zxdg_output_v1 *zxdg_output_v1, int32_t x, int32_t y) {
|
||||
(void)zxdg_output_v1;
|
||||
WaylandOutput *monitor = (WaylandOutput*)data;
|
||||
monitor->pos.x = x;
|
||||
monitor->pos.y = y;
|
||||
}
|
||||
|
||||
static void xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, int32_t width, int32_t height) {
|
||||
(void)data;
|
||||
(void)xdg_output;
|
||||
(void)width;
|
||||
(void)height;
|
||||
}
|
||||
|
||||
static void xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) {
|
||||
(void)data;
|
||||
(void)xdg_output;
|
||||
}
|
||||
|
||||
static void xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, const char *name) {
|
||||
(void)data;
|
||||
(void)xdg_output;
|
||||
(void)name;
|
||||
}
|
||||
|
||||
static void xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, const char *description) {
|
||||
(void)data;
|
||||
(void)xdg_output;
|
||||
(void)description;
|
||||
}
|
||||
|
||||
static const struct zxdg_output_v1_listener xdg_output_listener = {
|
||||
xdg_output_logical_position,
|
||||
xdg_output_handle_logical_size,
|
||||
xdg_output_handle_done,
|
||||
xdg_output_handle_name,
|
||||
xdg_output_handle_description,
|
||||
};
|
||||
|
||||
/* Returns nullptr if not found */
|
||||
static drm_connector* get_drm_connector_by_crtc_id(drm_connectors *connectors, uint32_t crtc_id) {
|
||||
for(int i = 0; i < connectors->num_connectors; ++i) {
|
||||
@@ -368,8 +204,7 @@ namespace gsr {
|
||||
if(!properties)
|
||||
goto next_crtc;
|
||||
|
||||
if(!get_drm_property_by_name(drm_fd, properties, "VRR_ENABLED", &vrr_enabled))
|
||||
goto next_crtc;
|
||||
get_drm_property_by_name(drm_fd, properties, "VRR_ENABLED", &vrr_enabled);
|
||||
|
||||
connector = get_drm_connector_by_crtc_id(drm_connectors, crtc->crtc_id);
|
||||
if(!connector)
|
||||
@@ -391,7 +226,7 @@ namespace gsr {
|
||||
drmModeFreeResources(resources);
|
||||
}
|
||||
|
||||
CursorTrackerWayland::CursorTrackerWayland(const char *card_path) {
|
||||
CursorTrackerWayland::CursorTrackerWayland(const char *card_path, struct wl_display *wayland_dpy) : wayland_dpy(wayland_dpy) {
|
||||
drm_fd = open(card_path, O_RDONLY);
|
||||
if(drm_fd <= 0) {
|
||||
fprintf(stderr, "Error: CursorTrackerWayland: failed to open %s\n", card_path);
|
||||
@@ -465,74 +300,19 @@ namespace gsr {
|
||||
drmModeFreePlaneResources(planes);
|
||||
}
|
||||
|
||||
void CursorTrackerWayland::set_monitor_outputs_from_xdg_output(struct wl_display *dpy) {
|
||||
if(!xdg_output_manager) {
|
||||
fprintf(stderr, "Warning: CursorTrackerWayland::set_monitor_outputs_from_xdg_output: zxdg_output_manager not found. Registered monitor positions might be incorrect\n");
|
||||
return;
|
||||
}
|
||||
|
||||
for(WaylandOutput &monitor : monitors) {
|
||||
monitor.xdg_output = zxdg_output_manager_v1_get_xdg_output(xdg_output_manager, monitor.output);
|
||||
zxdg_output_v1_add_listener(monitor.xdg_output, &xdg_output_listener, &monitor);
|
||||
}
|
||||
|
||||
// Fetch xdg_output
|
||||
wl_display_roundtrip(dpy);
|
||||
}
|
||||
|
||||
std::optional<CursorInfo> CursorTrackerWayland::get_latest_cursor_info() {
|
||||
if(drm_fd <= 0 || latest_crtc_id == -1)
|
||||
if(drm_fd <= 0 || latest_crtc_id == -1 || !wayland_dpy)
|
||||
return std::nullopt;
|
||||
|
||||
std::string monitor_name = get_monitor_name_from_crtc_id(drm_fd, latest_crtc_id);
|
||||
if(monitor_name.empty())
|
||||
return std::nullopt;
|
||||
|
||||
struct wl_display *dpy = wl_display_connect(nullptr);
|
||||
if(!dpy) {
|
||||
fprintf(stderr, "Error: CursorTrackerWayland::get_latest_cursor_info: failed to connect to the wayland server\n");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
monitors.clear();
|
||||
struct wl_registry *registry = wl_display_get_registry(dpy);
|
||||
wl_registry_add_listener(registry, ®istry_listener, this);
|
||||
|
||||
// Fetch globals
|
||||
wl_display_roundtrip(dpy);
|
||||
|
||||
// Fetch wl_output
|
||||
wl_display_roundtrip(dpy);
|
||||
|
||||
set_monitor_outputs_from_xdg_output(dpy);
|
||||
|
||||
mgl::vec2i cursor_position = latest_cursor_position;
|
||||
const WaylandOutput *wayland_monitor = get_wayland_monitor_by_name(monitors, monitor_name);
|
||||
const std::vector<Monitor> wayland_monitors = get_monitors_wayland(wayland_dpy);
|
||||
const Monitor *wayland_monitor = get_wayland_monitor_by_name(wayland_monitors, monitor_name);
|
||||
if(!wayland_monitor)
|
||||
return std::nullopt;
|
||||
|
||||
cursor_position = wayland_monitor->pos + latest_cursor_position;
|
||||
for(WaylandOutput &monitor : monitors) {
|
||||
if(monitor.output) {
|
||||
wl_output_destroy(monitor.output);
|
||||
monitor.output = nullptr;
|
||||
}
|
||||
|
||||
if(monitor.xdg_output) {
|
||||
zxdg_output_v1_destroy(monitor.xdg_output);
|
||||
monitor.xdg_output = nullptr;
|
||||
}
|
||||
}
|
||||
monitors.clear();
|
||||
|
||||
if(xdg_output_manager) {
|
||||
zxdg_output_manager_v1_destroy(xdg_output_manager);
|
||||
xdg_output_manager = nullptr;
|
||||
}
|
||||
|
||||
wl_registry_destroy(registry);
|
||||
wl_display_disconnect(dpy);
|
||||
|
||||
return CursorInfo{ cursor_position, std::move(monitor_name) };
|
||||
return CursorInfo{ wayland_monitor->position + latest_cursor_position, std::move(monitor_name) };
|
||||
}
|
||||
}
|
||||
@@ -3,92 +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 l3_button = 11;
|
||||
static constexpr int r3_button = 12;
|
||||
static constexpr int axis_up_down = 7;
|
||||
static constexpr int axis_left_right = 6;
|
||||
|
||||
struct DeviceId {
|
||||
uint16_t vendor;
|
||||
uint16_t product;
|
||||
};
|
||||
|
||||
static bool read_file_hex_number(const char *path, unsigned int *value) {
|
||||
*value = 0;
|
||||
FILE *f = fopen(path, "rb");
|
||||
if(!f)
|
||||
return false;
|
||||
|
||||
fscanf(f, "%x", value);
|
||||
fclose(f);
|
||||
return true;
|
||||
}
|
||||
|
||||
static DeviceId joystick_get_device_id(const char *path) {
|
||||
DeviceId device_id;
|
||||
device_id.vendor = 0;
|
||||
device_id.product = 0;
|
||||
|
||||
const char *js_path_id = nullptr;
|
||||
const int len = strlen(path);
|
||||
for(int i = len - 1; i >= 0; --i) {
|
||||
if(path[i] == '/') {
|
||||
js_path_id = path + i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!js_path_id)
|
||||
return device_id;
|
||||
|
||||
unsigned int vendor = 0;
|
||||
unsigned int product = 0;
|
||||
char path_buf[1024];
|
||||
|
||||
snprintf(path_buf, sizeof(path_buf), "/sys/class/input/%s/device/id/vendor", js_path_id);
|
||||
if(!read_file_hex_number(path_buf, &vendor))
|
||||
return device_id;
|
||||
|
||||
snprintf(path_buf, sizeof(path_buf), "/sys/class/input/%s/device/id/product", js_path_id);
|
||||
if(!read_file_hex_number(path_buf, &product))
|
||||
return device_id;
|
||||
|
||||
device_id.vendor = vendor;
|
||||
device_id.product = product;
|
||||
return device_id;
|
||||
}
|
||||
|
||||
static bool is_ps4_controller(DeviceId device_id) {
|
||||
return device_id.vendor == 0x054C && (device_id.product == 0x09CC || device_id.product == 0x0BA0 || device_id.product == 0x05C4);
|
||||
}
|
||||
|
||||
static bool is_ps5_controller(DeviceId device_id) {
|
||||
return device_id.vendor == 0x054C && (device_id.product == 0x0DF2 || device_id.product == 0x0CE6);
|
||||
}
|
||||
|
||||
static bool is_stadia_controller(DeviceId device_id) {
|
||||
return device_id.vendor == 0x18D1 && (device_id.product == 0x9400);
|
||||
}
|
||||
|
||||
// Returns -1 on error
|
||||
static int get_js_dev_input_id_from_filepath(const char *dev_input_filepath) {
|
||||
if(strncmp(dev_input_filepath, "/dev/input/js", 13) != 0)
|
||||
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;
|
||||
@@ -98,8 +54,18 @@ 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) {
|
||||
if(poll_fd[i].fd > 0)
|
||||
@@ -141,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;
|
||||
}
|
||||
|
||||
@@ -214,8 +174,30 @@ 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)) {
|
||||
@@ -223,7 +205,7 @@ namespace gsr {
|
||||
goto done;
|
||||
|
||||
char dev_input_filepath[256];
|
||||
snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/input/js%d", extra_data[i].dev_input_id);
|
||||
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
|
||||
@@ -240,7 +222,7 @@ namespace gsr {
|
||||
hotplug.process_event_data(poll_fd[i].fd, [&](HotplugAction hotplug_action, const char *devname) {
|
||||
switch(hotplug_action) {
|
||||
case HotplugAction::ADD: {
|
||||
add_device(devname);
|
||||
add_device(devname, false);
|
||||
break;
|
||||
}
|
||||
case HotplugAction::REMOVE: {
|
||||
@@ -251,7 +233,7 @@ namespace gsr {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
process_js_event(poll_fd[i].fd, event);
|
||||
process_input_event(poll_fd[i].fd, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,54 +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: {
|
||||
// Workaround weird steam input (in-game) behavior where steam triggers playstation button + options when pressing both l3 and r3 at the same time
|
||||
playstation_button_pressed = (event.value == button_pressed) && !l3_button_pressed && !r3_button_pressed;
|
||||
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;
|
||||
}
|
||||
case l3_button: {
|
||||
l3_button_pressed = event.value == button_pressed;
|
||||
break;
|
||||
}
|
||||
case r3_button: {
|
||||
r3_button_pressed = event.value == button_pressed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if((event.type & JS_EVENT_AXIS) == JS_EVENT_AXIS && playstation_button_pressed) {
|
||||
const int trigger_threshold = 16383;
|
||||
} 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)
|
||||
@@ -321,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;
|
||||
|
||||
@@ -338,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,
|
||||
@@ -356,7 +360,7 @@ namespace gsr {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -372,8 +376,13 @@ namespace gsr {
|
||||
if(index < 0 || index >= num_poll_fd)
|
||||
return false;
|
||||
|
||||
if(poll_fd[index].fd > 0)
|
||||
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];
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
113
src/GsrInfo.cpp
@@ -288,6 +288,58 @@ namespace gsr {
|
||||
return application_audio;
|
||||
}
|
||||
|
||||
struct KeyValue3 {
|
||||
std::string_view value1;
|
||||
std::string_view value2;
|
||||
std::string_view value3;
|
||||
};
|
||||
|
||||
static std::optional<KeyValue3> parse_3(std::string_view line) {
|
||||
const size_t space_index1 = line.find('|');
|
||||
if(space_index1 == std::string_view::npos)
|
||||
return std::nullopt;
|
||||
|
||||
const size_t space_index2 = line.find('|', space_index1 + 1);
|
||||
if(space_index2 == std::string_view::npos)
|
||||
return std::nullopt;
|
||||
|
||||
return KeyValue3{
|
||||
line.substr(0, space_index1),
|
||||
line.substr(space_index1 + 1, space_index2 - (space_index1 + 1)),
|
||||
line.substr(space_index2 + 1),
|
||||
};
|
||||
}
|
||||
|
||||
static bool parse_camera_pixel_format(std::string_view line, GsrCameraPixelFormat &pixel_format) {
|
||||
if(line == "yuyv") {
|
||||
pixel_format = YUYV;
|
||||
return true;
|
||||
} else if(line == "mjpeg") {
|
||||
pixel_format = MJPEG;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool capture_option_line_to_camera(std::string_view line, std::string &path, GsrCameraSetup &camera_setup, GsrCameraPixelFormat &pixel_format) {
|
||||
const std::optional<KeyValue3> key_value3 = parse_3(line);
|
||||
if(!key_value3)
|
||||
return false;
|
||||
|
||||
path = key_value3->value1;
|
||||
|
||||
char value_buffer[256];
|
||||
snprintf(value_buffer, sizeof(value_buffer), "%.*s", (int)key_value3->value2.size(), key_value3->value2.data());
|
||||
if(sscanf(value_buffer, "%dx%d@%dhz", &camera_setup.resolution.x, &camera_setup.resolution.y, &camera_setup.fps) != 3)
|
||||
return false;
|
||||
|
||||
if(!parse_camera_pixel_format(key_value3->value3, pixel_format))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::optional<GsrMonitor> capture_option_line_to_monitor(std::string_view line) {
|
||||
std::optional<GsrMonitor> monitor;
|
||||
const std::optional<KeyValue> key_value = parse_key_value(line);
|
||||
@@ -304,16 +356,49 @@ namespace gsr {
|
||||
return monitor;
|
||||
}
|
||||
|
||||
static GsrCamera* get_gsr_camera_by_path(std::vector<GsrCamera> &cameras, const std::string &path) {
|
||||
for(GsrCamera &camera : cameras) {
|
||||
if(camera.path == path)
|
||||
return &camera;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void parse_camera_line(std::string_view line, std::vector<GsrCamera> &cameras) {
|
||||
std::string camera_path;
|
||||
GsrCameraSetup camera_setup;
|
||||
GsrCameraPixelFormat pixel_format;
|
||||
if(!capture_option_line_to_camera(line, camera_path, camera_setup, pixel_format))
|
||||
return;
|
||||
|
||||
GsrCamera *existing_camera = get_gsr_camera_by_path(cameras, camera_path);
|
||||
if(!existing_camera) {
|
||||
cameras.push_back(GsrCamera{camera_path, std::vector<GsrCameraSetup>{}, std::vector<GsrCameraSetup>{}});
|
||||
existing_camera = &cameras.back();
|
||||
}
|
||||
|
||||
switch(pixel_format) {
|
||||
case YUYV:
|
||||
existing_camera->yuyv_setups.push_back(camera_setup);
|
||||
break;
|
||||
case MJPEG:
|
||||
existing_camera->mjpeg_setups.push_back(camera_setup);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void parse_capture_options_line(SupportedCaptureOptions &capture_options, std::string_view line) {
|
||||
if(line == "window")
|
||||
if(line == "window") {
|
||||
capture_options.window = true;
|
||||
else if(line == "region")
|
||||
} else if(line == "region") {
|
||||
capture_options.region = true;
|
||||
else if(line == "focused")
|
||||
} else if(line == "focused") {
|
||||
capture_options.focused = true;
|
||||
else if(line == "portal")
|
||||
} else if(line == "portal") {
|
||||
capture_options.portal = true;
|
||||
else {
|
||||
} else if(!line.empty() && line[0] == '/') {
|
||||
parse_camera_line(line, capture_options.cameras);
|
||||
} else {
|
||||
std::optional<GsrMonitor> monitor = capture_option_line_to_monitor(line);
|
||||
if(monitor)
|
||||
capture_options.monitors.push_back(std::move(monitor.value()));
|
||||
@@ -348,4 +433,22 @@ namespace gsr {
|
||||
|
||||
return capture_options;
|
||||
}
|
||||
|
||||
std::vector<GsrCamera> get_v4l2_devices() {
|
||||
std::vector<GsrCamera> cameras;
|
||||
|
||||
std::string stdout_str;
|
||||
const char *args[] = { "gpu-screen-recorder", "--list-v4l2-devices", nullptr };
|
||||
if(exec_program_get_stdout(args, stdout_str) != 0) {
|
||||
fprintf(stderr, "error: 'gpu-screen-recorder --list-v4l2-devices' failed\n");
|
||||
return cameras;
|
||||
}
|
||||
|
||||
string_split_char(stdout_str, '\n', [&](std::string_view line) {
|
||||
parse_camera_line(line, cameras);
|
||||
return true;
|
||||
});
|
||||
|
||||
return cameras;
|
||||
}
|
||||
}
|
||||
|
||||
175
src/LedIndicator.cpp
Normal file
@@ -0,0 +1,175 @@
|
||||
#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 any_keyboard_with_led_enabled = false;
|
||||
bool any_keyboard_with_led_disabled = 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')
|
||||
any_keyboard_with_led_disabled = true;
|
||||
else
|
||||
any_keyboard_with_led_enabled = true;
|
||||
lseek(led_brightness_file_fd, 0, SEEK_SET);
|
||||
}
|
||||
}
|
||||
|
||||
if(led_enabled && any_keyboard_with_led_disabled)
|
||||
run_gsr_global_hotkeys_set_leds(true);
|
||||
else if(!led_enabled && any_keyboard_with_led_enabled)
|
||||
run_gsr_global_hotkeys_set_leds(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
690
src/Overlay.cpp
@@ -73,6 +73,28 @@ namespace gsr {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool exec_program_on_host_daemonized(const char **args, bool debug) {
|
||||
if(count_num_args(args) > 64 - 3) {
|
||||
fprintf(stderr, "Error: too many arguments when trying to launch \"%s\"\n", args[0]);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
|
||||
if(inside_flatpak) {
|
||||
// Assumes programs wont need more than 64 - 3 args
|
||||
const char *modified_args[64] = { "flatpak-spawn", "--host", "--" };
|
||||
for(int i = 3; i < 64; ++i) {
|
||||
const char *arg = args[i - 3];
|
||||
modified_args[i] = arg;
|
||||
if(!arg)
|
||||
break;
|
||||
}
|
||||
return exec_program_daemonized(modified_args, debug);
|
||||
} else {
|
||||
return exec_program_daemonized(args, debug);
|
||||
}
|
||||
}
|
||||
|
||||
pid_t exec_program(const char **args, int *read_fd, bool debug) {
|
||||
if(read_fd)
|
||||
*read_fd = -1;
|
||||
@@ -164,11 +186,9 @@ namespace gsr {
|
||||
const char *modified_args[64] = { "flatpak-spawn", "--host", "--" };
|
||||
for(int i = 3; i < 64; ++i) {
|
||||
const char *arg = args[i - 3];
|
||||
if(!arg) {
|
||||
modified_args[i] = nullptr;
|
||||
break;
|
||||
}
|
||||
modified_args[i] = arg;
|
||||
if(!arg)
|
||||
break;
|
||||
}
|
||||
return exec_program_get_stdout(modified_args, result, debug);
|
||||
} else {
|
||||
|
||||
@@ -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>
|
||||
@@ -165,6 +166,62 @@ namespace gsr {
|
||||
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 const Monitor* get_monitor_by_region_center(const std::vector<Monitor> &monitors, Region region) {
|
||||
const mgl::vec2i center = {region.pos.x + region.size.x / 2, region.pos.y + region.size.y / 2};
|
||||
for(const Monitor &monitor : monitors) {
|
||||
if(center.x >= monitor.position.x && center.x <= monitor.position.x + monitor.size.x
|
||||
&& center.y >= monitor.position.y && center.y <= monitor.position.y + monitor.size.y)
|
||||
{
|
||||
return &monitor;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Name is the x11 name. TODO: verify if this works on all wayland compositors
|
||||
static const Monitor* get_wayland_monitor_by_name(const std::vector<Monitor> &monitors, const std::string &name) {
|
||||
for(const Monitor &monitor : monitors) {
|
||||
if(monitor.name == name)
|
||||
return &monitor;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static mgl::vec2d to_vec2d(mgl::vec2i v) {
|
||||
return { (double)v.x, (double)v.y };
|
||||
}
|
||||
|
||||
static Region x11_region_to_wayland_region(Display *dpy, struct wl_display *wayland_dpy, Region x11_region) {
|
||||
const std::vector<Monitor> x11_monitors = get_monitors(dpy);
|
||||
const Monitor *x11_selected_monitor = get_monitor_by_region_center(x11_monitors, x11_region);
|
||||
if(!x11_selected_monitor) {
|
||||
fprintf(stderr, "Warning: RegionSelector: failed to get x11 monitor\n");
|
||||
return x11_region;
|
||||
}
|
||||
|
||||
const std::vector<Monitor> wayland_monitors = get_monitors_wayland(wayland_dpy);
|
||||
const Monitor *wayland_monitor = get_wayland_monitor_by_name(wayland_monitors, x11_selected_monitor->name);
|
||||
if(!wayland_monitor) {
|
||||
fprintf(stderr, "Warning: RegionSelector: failed to get wayland monitor\n");
|
||||
return x11_region;
|
||||
}
|
||||
|
||||
const mgl::vec2d region_relative_pos = {
|
||||
(double)(x11_region.pos.x - x11_selected_monitor->position.x) / (double)x11_selected_monitor->size.x,
|
||||
(double)(x11_region.pos.y - x11_selected_monitor->position.y) / (double)x11_selected_monitor->size.y,
|
||||
};
|
||||
|
||||
const mgl::vec2d region_relative_size = {
|
||||
(double)x11_region.size.x / (double)x11_selected_monitor->size.x,
|
||||
(double)x11_region.size.y / (double)x11_selected_monitor->size.y,
|
||||
};
|
||||
|
||||
return Region {
|
||||
wayland_monitor->position + (region_relative_pos * to_vec2d(wayland_monitor->size)).to_vec2i(),
|
||||
(region_relative_size * to_vec2d(wayland_monitor->size)).to_vec2i(),
|
||||
};
|
||||
}
|
||||
|
||||
RegionSelector::RegionSelector() {
|
||||
|
||||
}
|
||||
@@ -221,6 +278,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 +369,9 @@ namespace gsr {
|
||||
region_window = 0;
|
||||
}
|
||||
|
||||
XFlush(dpy);
|
||||
XSync(dpy, False);
|
||||
|
||||
XCloseDisplay(dpy);
|
||||
dpy = nullptr;
|
||||
selecting_region = false;
|
||||
@@ -378,8 +441,11 @@ namespace gsr {
|
||||
return result;
|
||||
}
|
||||
|
||||
Region RegionSelector::get_selection() const {
|
||||
return region;
|
||||
Region RegionSelector::get_selection(Display *x11_dpy, struct wl_display *wayland_dpy) const {
|
||||
Region returned_region = region;
|
||||
if(is_wayland && x11_dpy && wayland_dpy)
|
||||
returned_region = x11_region_to_wayland_region(x11_dpy, wayland_dpy, returned_region);
|
||||
return returned_region;
|
||||
}
|
||||
|
||||
void RegionSelector::on_button_press(const void *de) {
|
||||
|
||||
204
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,79 +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())
|
||||
unlink(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;
|
||||
unlink(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());
|
||||
fifo_filepath.clear();
|
||||
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())) {
|
||||
unlink(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;
|
||||
}
|
||||
@@ -104,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;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,9 @@ namespace gsr {
|
||||
if(!theme->body_font.load_from_file(theme->body_font_file, std::max(13.0f, window_size.y * 0.015f)))
|
||||
return false;
|
||||
|
||||
if(!theme->camera_setup_font.load_from_file(theme->body_font_file, 24))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -120,6 +123,21 @@ 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->warning_texture.load_from_file((resources_path + "images/warning.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->question_mark_texture.load_from_file((resources_path + "images/question_mark.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->info_texture.load_from_file((resources_path + "images/info.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;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/extensions/shape.h>
|
||||
#include <X11/cursorfont.h>
|
||||
#include <X11/keysym.h>
|
||||
@@ -122,6 +123,10 @@ namespace gsr {
|
||||
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
|
||||
@@ -163,6 +168,8 @@ namespace gsr {
|
||||
}
|
||||
|
||||
XFlush(dpy);
|
||||
XSync(dpy, False);
|
||||
|
||||
XCloseDisplay(dpy);
|
||||
dpy = nullptr;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
#include <X11/extensions/shapeconst.h>
|
||||
#include <X11/extensions/Xrandr.h>
|
||||
|
||||
#include <wayland-client.h>
|
||||
#include "xdg-output-unstable-v1-client-protocol.h"
|
||||
|
||||
#include <mglpp/system/Utf8.hpp>
|
||||
|
||||
extern "C" {
|
||||
@@ -23,6 +26,209 @@ extern "C" {
|
||||
#define MAX_PROPERTY_VALUE_LEN 4096
|
||||
|
||||
namespace gsr {
|
||||
struct WaylandOutput {
|
||||
uint32_t wl_name;
|
||||
struct wl_output *output;
|
||||
struct zxdg_output_v1 *xdg_output;
|
||||
mgl::vec2i pos;
|
||||
mgl::vec2i size;
|
||||
int32_t transform;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
struct Wayland {
|
||||
std::vector<WaylandOutput> outputs;
|
||||
struct zxdg_output_manager_v1 *xdg_output_manager = nullptr;
|
||||
};
|
||||
|
||||
static WaylandOutput* get_wayland_monitor_by_output(Wayland &wayland, struct wl_output *output) {
|
||||
for(WaylandOutput &monitor : wayland.outputs) {
|
||||
if(monitor.output == output)
|
||||
return &monitor;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void output_handle_geometry(void *data, struct wl_output *wl_output,
|
||||
int32_t x, int32_t y, int32_t phys_width, int32_t phys_height,
|
||||
int32_t subpixel, const char *make, const char *model,
|
||||
int32_t transform) {
|
||||
(void)wl_output;
|
||||
(void)phys_width;
|
||||
(void)phys_height;
|
||||
(void)subpixel;
|
||||
(void)make;
|
||||
(void)model;
|
||||
Wayland *wayland = (Wayland*)data;
|
||||
WaylandOutput *monitor = get_wayland_monitor_by_output(*wayland, wl_output);
|
||||
if(!monitor)
|
||||
return;
|
||||
|
||||
monitor->pos.x = x;
|
||||
monitor->pos.y = y;
|
||||
monitor->transform = transform;
|
||||
}
|
||||
|
||||
static void output_handle_mode(void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) {
|
||||
(void)wl_output;
|
||||
(void)flags;
|
||||
(void)refresh;
|
||||
Wayland *wayland = (Wayland*)data;
|
||||
WaylandOutput *monitor = get_wayland_monitor_by_output(*wayland, wl_output);
|
||||
if(!monitor)
|
||||
return;
|
||||
|
||||
monitor->size.x = width;
|
||||
monitor->size.y = height;
|
||||
}
|
||||
|
||||
static void output_handle_done(void *data, struct wl_output *wl_output) {
|
||||
(void)data;
|
||||
(void)wl_output;
|
||||
}
|
||||
|
||||
static void output_handle_scale(void* data, struct wl_output *wl_output, int32_t factor) {
|
||||
(void)data;
|
||||
(void)wl_output;
|
||||
(void)factor;
|
||||
}
|
||||
|
||||
static void output_handle_name(void *data, struct wl_output *wl_output, const char *name) {
|
||||
(void)wl_output;
|
||||
Wayland *wayland = (Wayland*)data;
|
||||
WaylandOutput *monitor = get_wayland_monitor_by_output(*wayland, wl_output);
|
||||
if(!monitor)
|
||||
return;
|
||||
|
||||
monitor->name = name;
|
||||
}
|
||||
|
||||
static void output_handle_description(void *data, struct wl_output *wl_output, const char *description) {
|
||||
(void)data;
|
||||
(void)wl_output;
|
||||
(void)description;
|
||||
}
|
||||
|
||||
static const struct wl_output_listener output_listener = {
|
||||
output_handle_geometry,
|
||||
output_handle_mode,
|
||||
output_handle_done,
|
||||
output_handle_scale,
|
||||
output_handle_name,
|
||||
output_handle_description,
|
||||
};
|
||||
|
||||
static void registry_add_object(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) {
|
||||
(void)version;
|
||||
Wayland *wayland = (Wayland*)data;
|
||||
if(strcmp(interface, wl_output_interface.name) == 0) {
|
||||
if(version < 4) {
|
||||
fprintf(stderr, "Warning: wl output interface version is < 4, expected >= 4\n");
|
||||
return;
|
||||
}
|
||||
|
||||
struct wl_output *output = (struct wl_output*)wl_registry_bind(registry, name, &wl_output_interface, 4);
|
||||
wayland->outputs.push_back(
|
||||
WaylandOutput{
|
||||
name,
|
||||
output,
|
||||
nullptr,
|
||||
mgl::vec2i{0, 0},
|
||||
mgl::vec2i{0, 0},
|
||||
0,
|
||||
""
|
||||
});
|
||||
wl_output_add_listener(output, &output_listener, wayland);
|
||||
} else if(strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) {
|
||||
if(version < 1) {
|
||||
fprintf(stderr, "Warning: xdg output interface version is < 1, expected >= 1\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if(wayland->xdg_output_manager) {
|
||||
zxdg_output_manager_v1_destroy(wayland->xdg_output_manager);
|
||||
wayland->xdg_output_manager = NULL;
|
||||
}
|
||||
wayland->xdg_output_manager = (struct zxdg_output_manager_v1*)wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, 1);
|
||||
}
|
||||
}
|
||||
|
||||
static void registry_remove_object(void *data, struct wl_registry *registry, uint32_t name) {
|
||||
(void)data;
|
||||
(void)registry;
|
||||
(void)name;
|
||||
// TODO: Remove output
|
||||
}
|
||||
|
||||
static struct wl_registry_listener registry_listener = {
|
||||
registry_add_object,
|
||||
registry_remove_object,
|
||||
};
|
||||
|
||||
static void xdg_output_logical_position(void *data, struct zxdg_output_v1 *zxdg_output_v1, int32_t x, int32_t y) {
|
||||
(void)zxdg_output_v1;
|
||||
WaylandOutput *monitor = (WaylandOutput*)data;
|
||||
monitor->pos.x = x;
|
||||
monitor->pos.y = y;
|
||||
}
|
||||
|
||||
static void xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, int32_t width, int32_t height) {
|
||||
(void)xdg_output;
|
||||
WaylandOutput *monitor = (WaylandOutput*)data;
|
||||
monitor->size.x = width;
|
||||
monitor->size.y = height;
|
||||
}
|
||||
|
||||
static void xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) {
|
||||
(void)data;
|
||||
(void)xdg_output;
|
||||
}
|
||||
|
||||
static void xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, const char *name) {
|
||||
(void)data;
|
||||
(void)xdg_output;
|
||||
(void)name;
|
||||
}
|
||||
|
||||
static void xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, const char *description) {
|
||||
(void)data;
|
||||
(void)xdg_output;
|
||||
(void)description;
|
||||
}
|
||||
|
||||
static const struct zxdg_output_v1_listener xdg_output_listener = {
|
||||
xdg_output_logical_position,
|
||||
xdg_output_handle_logical_size,
|
||||
xdg_output_handle_done,
|
||||
xdg_output_handle_name,
|
||||
xdg_output_handle_description,
|
||||
};
|
||||
|
||||
static const int transform_90 = 1;
|
||||
static const int transform_270 = 3;
|
||||
|
||||
static void transform_monitors(Wayland &wayland) {
|
||||
for(WaylandOutput &output : wayland.outputs) {
|
||||
if(output.transform == transform_90 || output.transform == transform_270)
|
||||
std::swap(output.size.x, output.size.y);
|
||||
}
|
||||
}
|
||||
|
||||
static void set_monitor_outputs_from_xdg_output(Wayland &wayland, struct wl_display *dpy) {
|
||||
if(!wayland.xdg_output_manager) {
|
||||
fprintf(stderr, "Warning: WindowUtils::set_monitor_outputs_from_xdg_output: zxdg_output_manager not found. Registered monitor positions might be incorrect\n");
|
||||
return;
|
||||
}
|
||||
|
||||
for(WaylandOutput &monitor : wayland.outputs) {
|
||||
monitor.xdg_output = zxdg_output_manager_v1_get_xdg_output(wayland.xdg_output_manager, monitor.output);
|
||||
zxdg_output_v1_add_listener(monitor.xdg_output, &xdg_output_listener, &monitor);
|
||||
}
|
||||
|
||||
// Fetch xdg_output
|
||||
wl_display_roundtrip(dpy);
|
||||
}
|
||||
|
||||
static unsigned char* window_get_property(Display *dpy, Window window, Atom property_type, const char *property_name, unsigned int *property_size) {
|
||||
Atom ret_property_type = None;
|
||||
int ret_format = 0;
|
||||
@@ -121,7 +327,7 @@ namespace gsr {
|
||||
return root_pos;
|
||||
}
|
||||
|
||||
Window get_focused_window(Display *dpy, WindowCaptureType cap_type) {
|
||||
Window get_focused_window(Display *dpy, WindowCaptureType cap_type, bool fallback_cursor_focused) {
|
||||
//const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False);
|
||||
Window focused_window = None;
|
||||
|
||||
@@ -146,6 +352,9 @@ namespace gsr {
|
||||
XGetInputFocus(dpy, &focused_window, &revert_to);
|
||||
if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window))
|
||||
return focused_window;
|
||||
|
||||
if(!fallback_cursor_focused)
|
||||
return None;
|
||||
}
|
||||
|
||||
get_cursor_position(dpy, &focused_window);
|
||||
@@ -213,9 +422,9 @@ namespace gsr {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type) {
|
||||
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type, bool fallback_cursor_focused) {
|
||||
std::string result;
|
||||
const Window focused_window = get_focused_window(dpy, window_capture_type);
|
||||
const Window focused_window = get_focused_window(dpy, window_capture_type, fallback_cursor_focused);
|
||||
if(focused_window == None)
|
||||
return result;
|
||||
|
||||
@@ -516,6 +725,47 @@ namespace gsr {
|
||||
return monitors;
|
||||
}
|
||||
|
||||
std::vector<Monitor> get_monitors_wayland(struct wl_display *dpy) {
|
||||
Wayland wayland;
|
||||
|
||||
struct wl_registry *registry = wl_display_get_registry(dpy);
|
||||
wl_registry_add_listener(registry, ®istry_listener, &wayland);
|
||||
|
||||
// Fetch globals
|
||||
wl_display_roundtrip(dpy);
|
||||
|
||||
// Fetch wl_output
|
||||
wl_display_roundtrip(dpy);
|
||||
|
||||
transform_monitors(wayland);
|
||||
set_monitor_outputs_from_xdg_output(wayland, dpy);
|
||||
|
||||
std::vector<Monitor> monitors;
|
||||
for(WaylandOutput &output : wayland.outputs) {
|
||||
monitors.push_back(Monitor{output.pos, output.size, std::move(output.name)});
|
||||
|
||||
if(output.output) {
|
||||
wl_output_destroy(output.output);
|
||||
output.output = nullptr;
|
||||
}
|
||||
|
||||
if(output.xdg_output) {
|
||||
zxdg_output_v1_destroy(output.xdg_output);
|
||||
output.xdg_output = nullptr;
|
||||
}
|
||||
}
|
||||
wayland.outputs.clear();
|
||||
|
||||
if(wayland.xdg_output_manager) {
|
||||
zxdg_output_manager_v1_destroy(wayland.xdg_output_manager);
|
||||
wayland.xdg_output_manager = nullptr;
|
||||
}
|
||||
|
||||
wl_registry_destroy(registry);
|
||||
|
||||
return monitors;
|
||||
}
|
||||
|
||||
static bool device_is_mouse(const XIDeviceInfo *dev) {
|
||||
for(int i = 0; i < dev->num_classes; ++i) {
|
||||
if(dev->classes[i]->type == XIMasterPointer || dev->classes[i]->type == XISlavePointer)
|
||||
|
||||
@@ -40,6 +40,8 @@ namespace gsr {
|
||||
if(!visible)
|
||||
return true;
|
||||
|
||||
handle_tooltip_event(event, position + offset, get_size());
|
||||
|
||||
if(event.type == mgl::Event::MouseButtonPressed && event.mouse_button.button == mgl::Mouse::Left) {
|
||||
const bool clicked_inside = mgl::FloatRect(position + offset, get_size()).contains({ (float)event.mouse_button.x, (float)event.mouse_button.y });
|
||||
if(clicked_inside) {
|
||||
|
||||
@@ -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;
|
||||
@@ -83,17 +83,31 @@ namespace gsr {
|
||||
draw_unselected(window, draw_pos);
|
||||
}
|
||||
|
||||
void ComboBox::add_item(const std::string &text, const std::string &id) {
|
||||
void ComboBox::add_item(const std::string &text, const std::string &id, bool allow_duplicate) {
|
||||
if(!allow_duplicate) {
|
||||
for(const auto &item : items) {
|
||||
if(item.id == id)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
items.push_back({mgl::Text(text, *font), id, {0.0f, 0.0f}});
|
||||
items.back().text.set_max_width(font->get_character_size() * 20); // TODO: Make a proper solution
|
||||
//items.back().text.set_max_rows(1);
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void ComboBox::clear_items() {
|
||||
items.clear();
|
||||
selected_item = 0;
|
||||
show_dropdown = false;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
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 +120,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 +180,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,4 +1,5 @@
|
||||
#include "../../include/gui/CustomRendererWidget.hpp"
|
||||
#include "../../include/gui/Utils.hpp"
|
||||
|
||||
#include <mglpp/window/Window.hpp>
|
||||
|
||||
@@ -17,11 +18,14 @@ namespace gsr {
|
||||
|
||||
const mgl::vec2f draw_pos = position + offset;
|
||||
|
||||
const mgl::Scissor prev_scissor = window.get_scissor();
|
||||
window.set_scissor({draw_pos.to_vec2i(), size.to_vec2i()});
|
||||
const mgl::Scissor parent_scissor = window.get_scissor();
|
||||
const mgl::Scissor scissor = scissor_get_sub_area(parent_scissor, {draw_pos.to_vec2i(), size.to_vec2i()});
|
||||
window.set_scissor(scissor);
|
||||
|
||||
if(draw_handler)
|
||||
draw_handler(window, draw_pos, size);
|
||||
window.set_scissor(prev_scissor);
|
||||
|
||||
window.set_scissor(parent_scissor);
|
||||
}
|
||||
|
||||
mgl::vec2f CustomRendererWidget::get_size() {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#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;
|
||||
@@ -22,8 +23,13 @@ namespace gsr {
|
||||
}
|
||||
}
|
||||
|
||||
Entry::Entry(mgl::Font *font, const char *text, float max_width) : text("", *font), max_width(max_width) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -31,6 +37,8 @@ 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) {
|
||||
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);
|
||||
@@ -38,9 +46,8 @@ namespace gsr {
|
||||
selecting_text = true;
|
||||
|
||||
const auto caret_index_mouse = find_closest_caret_index_by_position(mouse_pos);
|
||||
caret.byte_index = caret_index_mouse.byte_index;
|
||||
caret.utf8_index = caret_index_mouse.utf8_index;
|
||||
caret.offset_x = caret_index_mouse.pos.x - this->text.get_position().x;
|
||||
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 {
|
||||
@@ -50,89 +57,72 @@ namespace gsr {
|
||||
}
|
||||
} else if(event.type == mgl::Event::MouseButtonReleased && event.mouse_button.button == mgl::Mouse::Left) {
|
||||
selecting_text = false;
|
||||
if(caret.byte_index == selection_start_caret.byte_index)
|
||||
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.byte_index = caret_index_mouse.byte_index;
|
||||
caret.utf8_index = caret_index_mouse.utf8_index;
|
||||
caret.offset_x = caret_index_mouse.pos.x - this->text.get_position().x;
|
||||
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.byte_index;
|
||||
int selection_end_byte = caret.byte_index;
|
||||
int selection_start_byte = caret.index;
|
||||
int selection_end_byte = caret.index;
|
||||
if(show_selection) {
|
||||
selection_start_byte = std::min(caret.byte_index, selection_start_caret.byte_index);
|
||||
selection_end_byte = std::max(caret.byte_index, selection_start_caret.byte_index);
|
||||
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.byte_index > 0)
|
||||
selection_start_byte = mgl::utf8_get_start_of_codepoint((const unsigned char*)text.get_string().c_str(), text.get_string().size(), caret.byte_index - 1);
|
||||
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, "");
|
||||
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.byte_index < (int)text.get_string().size()) {
|
||||
size_t codepoint_length = 1;
|
||||
mgl::utf8_get_codepoint_length(((const unsigned char*)text.get_string().c_str())[caret.byte_index], &codepoint_length);
|
||||
selection_end_byte = selection_start_byte + codepoint_length;
|
||||
}
|
||||
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, "");
|
||||
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(text.get_string().substr(selection_start_byte, selection_num_bytes));
|
||||
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, std::move(clipboard_string));
|
||||
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.byte_index = 0;
|
||||
selection_start_caret.utf8_index = 0;
|
||||
selection_start_caret.index = 0;
|
||||
selection_start_caret.offset_x = 0.0f;
|
||||
|
||||
caret.byte_index = text.get_string().size();
|
||||
caret.utf8_index = mgl::utf8_get_character_count((const unsigned char*)text.get_string().data(), text.get_string().size());
|
||||
caret.index = active_text.get_string().size();
|
||||
// TODO: Optimize
|
||||
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
|
||||
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 && caret.byte_index > 0) {
|
||||
if(!selecting_with_keyboard && show_selection) {
|
||||
} else if(event.key.code == mgl::Keyboard::Left) {
|
||||
if(!selecting_with_keyboard && show_selection)
|
||||
show_selection = false;
|
||||
} else {
|
||||
caret.byte_index = mgl::utf8_get_start_of_codepoint((const unsigned char*)text.get_string().data(), text.get_string().size(), caret.byte_index - 1);
|
||||
caret.utf8_index -= 1;
|
||||
// TODO: Move left by one character instead of calculating every character to caret index
|
||||
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
|
||||
}
|
||||
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) {
|
||||
if(!selecting_with_keyboard && show_selection)
|
||||
show_selection = false;
|
||||
} else {
|
||||
const int caret_byte_index_before = caret.byte_index;
|
||||
caret.byte_index = mgl::utf8_index_to_byte_index((const unsigned char*)text.get_string().data(), text.get_string().size(), caret.utf8_index + 1);
|
||||
if(caret.byte_index != caret_byte_index_before)
|
||||
caret.utf8_index += 1;
|
||||
// TODO: Move right by one character instead of calculating every character to caret index
|
||||
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
|
||||
}
|
||||
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.byte_index = 0;
|
||||
caret.utf8_index = 0;
|
||||
caret.index = 0;
|
||||
caret.offset_x = 0.0f;
|
||||
|
||||
if(!selecting_with_keyboard) {
|
||||
@@ -140,10 +130,9 @@ namespace gsr {
|
||||
show_selection = false;
|
||||
}
|
||||
} else if(event.key.code == mgl::Keyboard::End) {
|
||||
caret.byte_index = text.get_string().size();
|
||||
caret.utf8_index = mgl::utf8_get_character_count((const unsigned char*)text.get_string().data(), text.get_string().size());
|
||||
caret.index = active_text.get_string().size();
|
||||
// TODO: Optimize
|
||||
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
|
||||
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
|
||||
|
||||
if(!selecting_with_keyboard) {
|
||||
selection_start_caret = caret;
|
||||
@@ -164,14 +153,14 @@ namespace gsr {
|
||||
|
||||
return false;
|
||||
} else if(event.type == mgl::Event::TextEntered && selected && event.text.codepoint >= 32 && event.text.codepoint != 127) {
|
||||
int selection_start_byte = caret.byte_index;
|
||||
int selection_end_byte = caret.byte_index;
|
||||
int selection_start_byte = caret.index;
|
||||
int selection_end_byte = caret.index;
|
||||
if(show_selection) {
|
||||
selection_start_byte = std::min(caret.byte_index, selection_start_caret.byte_index);
|
||||
selection_end_byte = std::max(caret.byte_index, selection_start_caret.byte_index);
|
||||
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, std::string(event.text.str, event.text.size));
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -189,13 +178,15 @@ namespace gsr {
|
||||
const int padding_left = padding_left_scale * get_theme().window_height;
|
||||
const int padding_right = padding_right_scale * get_theme().window_height;
|
||||
|
||||
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, text.get_bounds().size.y).floor();
|
||||
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)
|
||||
@@ -205,18 +196,18 @@ namespace gsr {
|
||||
if(overflow_right - text_overflow > 0.0f)
|
||||
text_overflow = overflow_right;
|
||||
|
||||
text.set_position((draw_pos + mgl::vec2f(padding_left, get_size().y * 0.5f - text.get_bounds().size.y * 0.5f) - mgl::vec2f(text_overflow, 0.0f)).floor());
|
||||
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 = text.get_bounds();
|
||||
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;
|
||||
text.set_position(text.get_position() + mgl::vec2f(-text_overflow_right, 0.0f));
|
||||
active_text.set_position(active_text.get_position() + mgl::vec2f(-text_overflow_right, 0.0f));
|
||||
}
|
||||
} else {
|
||||
text.set_position(text.get_position() + mgl::vec2f(-text_overflow, 0.0f));
|
||||
active_text.set_position(active_text.get_position() + mgl::vec2f(-text_overflow, 0.0f));
|
||||
text_overflow = 0.0f;
|
||||
}
|
||||
|
||||
@@ -234,7 +225,7 @@ namespace gsr {
|
||||
});
|
||||
window.set_scissor(scissor);
|
||||
|
||||
window.draw(text);
|
||||
window.draw(active_text);
|
||||
|
||||
if(show_selection)
|
||||
draw_caret_selection(window, draw_pos, caret_size);
|
||||
@@ -254,11 +245,16 @@ namespace gsr {
|
||||
}
|
||||
|
||||
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), 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, padding_top)).floor());
|
||||
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);
|
||||
@@ -274,13 +270,43 @@ namespace gsr {
|
||||
return { max_width, text.get_bounds().size.y + padding_top + padding_bottom };
|
||||
}
|
||||
|
||||
EntryValidateHandlerResult Entry::set_text(std::string str) {
|
||||
EntryValidateHandlerResult validate_result = set_text_internal(std::move(str));
|
||||
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;
|
||||
}
|
||||
|
||||
EntryValidateHandlerResult Entry::set_text(const std::string &str) {
|
||||
EntryValidateHandlerResult validate_result = set_text_internal(mgl::utf8_to_utf32(str));
|
||||
if(validate_result == EntryValidateHandlerResult::ALLOW) {
|
||||
caret.byte_index = text.get_string().size();
|
||||
caret.utf8_index = mgl::utf8_get_character_count((const unsigned char*)text.get_string().data(), text.get_string().size());
|
||||
mgl::Text32 &active_text = masked ? masked_text : text;
|
||||
caret.index = active_text.get_string().size();
|
||||
// TODO: Optimize
|
||||
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
|
||||
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
|
||||
selection_start_caret = caret;
|
||||
|
||||
selecting_text = false;
|
||||
@@ -290,40 +316,59 @@ namespace gsr {
|
||||
return validate_result;
|
||||
}
|
||||
|
||||
EntryValidateHandlerResult Entry::set_text_internal(std::string str) {
|
||||
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(text.get_string());
|
||||
on_changed(mgl::utf32_to_utf8(text.get_string()));
|
||||
}
|
||||
|
||||
return validate_result;
|
||||
}
|
||||
|
||||
const std::string& Entry::get_text() const {
|
||||
return text.get_string();
|
||||
std::string Entry::get_text() const {
|
||||
return mgl::utf32_to_utf8(text.get_string());
|
||||
}
|
||||
|
||||
void Entry::replace_text(size_t index, size_t size, const std::string &replacement) {
|
||||
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.byte_index) {
|
||||
caret.utf8_index += mgl::utf8_get_character_count((const unsigned char*)replacement.c_str(), replacement.size());
|
||||
caret.byte_index += replacement.size();
|
||||
} else {
|
||||
caret.utf8_index -= mgl::utf8_get_character_count((const unsigned char*)(text.get_string().c_str() + caret.byte_index - size), size);
|
||||
caret.utf8_index += mgl::utf8_get_character_count((const unsigned char*)replacement.c_str(), replacement.size());
|
||||
caret.byte_index = caret.byte_index - size + replacement.size();
|
||||
}
|
||||
if((int)index >= caret.index)
|
||||
caret.index += replacement.size();
|
||||
else
|
||||
caret.index = caret.index - size + replacement.size();
|
||||
|
||||
std::string str = text.get_string();
|
||||
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) {
|
||||
@@ -333,8 +378,9 @@ namespace gsr {
|
||||
return;
|
||||
}
|
||||
|
||||
mgl::Text32 &active_text = masked ? masked_text : text;
|
||||
// TODO: Optimize
|
||||
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
|
||||
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
|
||||
selection_start_caret = caret;
|
||||
|
||||
selecting_text = false;
|
||||
@@ -342,17 +388,14 @@ namespace gsr {
|
||||
show_selection = false;
|
||||
}
|
||||
|
||||
mgl_index_codepoint_pair Entry::find_closest_caret_index_by_position(mgl::vec2f position) {
|
||||
const std::string &str = text.get_string();
|
||||
mgl::Font *font = text.get_font();
|
||||
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();
|
||||
|
||||
mgl_index_codepoint_pair result = {0, 0, {text.get_position().x, text.get_position().y}};
|
||||
|
||||
for(; result.byte_index < str.size();) {
|
||||
uint32_t codepoint = ' ';
|
||||
size_t clen = 1;
|
||||
if(!mgl::utf8_decode((const unsigned char*)&str[result.byte_index], str.size() - result.byte_index, &codepoint, &clen))
|
||||
clen = 1;
|
||||
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') {
|
||||
@@ -368,8 +411,6 @@ namespace gsr {
|
||||
break;
|
||||
|
||||
result.pos.x += glyph_width;
|
||||
result.byte_index += clen;
|
||||
result.utf8_index += 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -379,7 +420,7 @@ namespace gsr {
|
||||
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;
|
||||
|
||||
@@ -406,7 +447,7 @@ namespace gsr {
|
||||
}
|
||||
|
||||
EntryValidateHandler create_entry_validator_integer_in_range(int min, int max) {
|
||||
return [min, max](Entry &entry, const std::string &str) {
|
||||
return [min, max](Entry &entry, const std::u32string &str) {
|
||||
if(str.empty())
|
||||
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());
|
||||
@@ -303,6 +304,36 @@ namespace gsr {
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> GlobalSettingsPage::create_record_hotkey_window_region_options() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Start/stop recording a region:", get_color_theme().text_color));
|
||||
auto start_stop_recording_region_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
start_stop_recording_region_button_ptr = start_stop_recording_region_button.get();
|
||||
list->add_widget(std::move(start_stop_recording_region_button));
|
||||
|
||||
char str[128];
|
||||
if(gsr_info->system_info.display_server == DisplayServer::X11)
|
||||
snprintf(str, sizeof(str), "Start/stop recording a window:");
|
||||
else
|
||||
snprintf(str, sizeof(str), "Start/stop recording with desktop portal:");
|
||||
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color));
|
||||
auto start_stop_recording_window_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
start_stop_recording_window_button_ptr = start_stop_recording_window_button.get();
|
||||
list->add_widget(std::move(start_stop_recording_window_button));
|
||||
|
||||
start_stop_recording_region_button_ptr->on_click = [this] {
|
||||
configure_hotkey_start(ConfigureHotkeyType::RECORD_START_STOP_REGION);
|
||||
};
|
||||
|
||||
start_stop_recording_window_button_ptr->on_click = [this] {
|
||||
configure_hotkey_start(ConfigureHotkeyType::RECORD_START_STOP_WINDOW);
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> GlobalSettingsPage::create_stream_hotkey_options() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
|
||||
@@ -374,17 +405,9 @@ namespace gsr {
|
||||
|
||||
auto clear_hotkeys_button = std::make_unique<Button>(&get_theme().body_font, "Clear hotkeys", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
clear_hotkeys_button->on_click = [this] {
|
||||
config.streaming_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.record_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.record_config.pause_unpause_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.replay_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.replay_config.save_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.replay_config.save_1_min_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.replay_config.save_10_min_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.screenshot_config.take_screenshot_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.screenshot_config.take_screenshot_region_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.screenshot_config.take_screenshot_window_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.main_config.show_hide_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
for_each_config_hotkey([&](ConfigHotkey *config_hotkey_item) {
|
||||
*config_hotkey_item = {mgl::Keyboard::Unknown, 0};
|
||||
});
|
||||
load_hotkeys();
|
||||
overlay->rebind_all_keyboard_hotkeys();
|
||||
};
|
||||
@@ -423,6 +446,7 @@ namespace gsr {
|
||||
list_ptr->add_widget(create_replay_hotkey_options());
|
||||
list_ptr->add_widget(create_replay_partial_save_hotkey_options());
|
||||
list_ptr->add_widget(create_record_hotkey_options());
|
||||
list_ptr->add_widget(create_record_hotkey_window_region_options());
|
||||
list_ptr->add_widget(create_stream_hotkey_options());
|
||||
list_ptr->add_widget(create_screenshot_hotkey_options());
|
||||
list_ptr->add_widget(create_screenshot_region_hotkey_options());
|
||||
@@ -467,13 +491,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) {
|
||||
@@ -499,6 +552,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());
|
||||
|
||||
@@ -510,6 +579,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));
|
||||
@@ -535,6 +605,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();
|
||||
}
|
||||
|
||||
@@ -546,6 +618,8 @@ namespace gsr {
|
||||
|
||||
start_stop_recording_button_ptr->set_text(config.record_config.start_stop_hotkey.to_string());
|
||||
pause_unpause_recording_button_ptr->set_text(config.record_config.pause_unpause_hotkey.to_string());
|
||||
start_stop_recording_region_button_ptr->set_text(config.record_config.start_stop_region_hotkey.to_string());
|
||||
start_stop_recording_window_button_ptr->set_text(config.record_config.start_stop_window_hotkey.to_string());
|
||||
|
||||
start_stop_streaming_button_ptr->set_text(config.streaming_config.start_stop_hotkey.to_string());
|
||||
|
||||
@@ -561,6 +635,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);
|
||||
}
|
||||
|
||||
@@ -629,6 +704,10 @@ namespace gsr {
|
||||
return start_stop_recording_button_ptr;
|
||||
case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE:
|
||||
return pause_unpause_recording_button_ptr;
|
||||
case ConfigureHotkeyType::RECORD_START_STOP_REGION:
|
||||
return start_stop_recording_region_button_ptr;
|
||||
case ConfigureHotkeyType::RECORD_START_STOP_WINDOW:
|
||||
return start_stop_recording_window_button_ptr;
|
||||
case ConfigureHotkeyType::STREAM_START_STOP:
|
||||
return start_stop_streaming_button_ptr;
|
||||
case ConfigureHotkeyType::TAKE_SCREENSHOT:
|
||||
@@ -659,6 +738,10 @@ namespace gsr {
|
||||
return &config.record_config.start_stop_hotkey;
|
||||
case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE:
|
||||
return &config.record_config.pause_unpause_hotkey;
|
||||
case ConfigureHotkeyType::RECORD_START_STOP_REGION:
|
||||
return &config.record_config.start_stop_region_hotkey;
|
||||
case ConfigureHotkeyType::RECORD_START_STOP_WINDOW:
|
||||
return &config.record_config.start_stop_window_hotkey;
|
||||
case ConfigureHotkeyType::STREAM_START_STOP:
|
||||
return &config.streaming_config.start_stop_hotkey;
|
||||
case ConfigureHotkeyType::TAKE_SCREENSHOT:
|
||||
@@ -677,8 +760,12 @@ namespace gsr {
|
||||
ConfigHotkey *config_hotkeys[] = {
|
||||
&config.replay_config.start_stop_hotkey,
|
||||
&config.replay_config.save_hotkey,
|
||||
&config.replay_config.save_1_min_hotkey,
|
||||
&config.replay_config.save_10_min_hotkey,
|
||||
&config.record_config.start_stop_hotkey,
|
||||
&config.record_config.pause_unpause_hotkey,
|
||||
&config.record_config.start_stop_region_hotkey,
|
||||
&config.record_config.start_stop_window_hotkey,
|
||||
&config.streaming_config.start_stop_hotkey,
|
||||
&config.screenshot_config.take_screenshot_hotkey,
|
||||
&config.screenshot_config.take_screenshot_region_hotkey,
|
||||
@@ -722,6 +809,15 @@ namespace gsr {
|
||||
case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE:
|
||||
hotkey_configure_action_name = "Pause/unpause recording";
|
||||
break;
|
||||
case ConfigureHotkeyType::RECORD_START_STOP_REGION:
|
||||
hotkey_configure_action_name = "Start/stop recording a region";
|
||||
break;
|
||||
case ConfigureHotkeyType::RECORD_START_STOP_WINDOW:
|
||||
if(gsr_info->system_info.display_server == DisplayServer::X11)
|
||||
hotkey_configure_action_name = "Start/stop recording a window";
|
||||
else
|
||||
hotkey_configure_action_name = "Start/stop recording with desktop portal";
|
||||
break;
|
||||
case ConfigureHotkeyType::STREAM_START_STOP:
|
||||
hotkey_configure_action_name = "Start/stop streaming";
|
||||
break;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#include "../../include/gui/Utils.hpp"
|
||||
|
||||
#include <mglpp/window/Window.hpp>
|
||||
#include <mglpp/window/Event.hpp>
|
||||
#include <mglpp/system/FloatRect.hpp>
|
||||
#include <mglpp/graphics/Texture.hpp>
|
||||
|
||||
namespace gsr {
|
||||
@@ -19,8 +21,15 @@ namespace gsr {
|
||||
if(!visible)
|
||||
return;
|
||||
|
||||
const mgl::vec2f draw_pos = (position + offset).floor();
|
||||
|
||||
if(on_mouse_move) {
|
||||
const bool mouse_inside = mgl::FloatRect(draw_pos, get_size()).contains(window.get_mouse_position().to_vec2f());
|
||||
on_mouse_move(mouse_inside);
|
||||
}
|
||||
|
||||
sprite.set_size(get_size());
|
||||
sprite.set_position((position + offset).floor());
|
||||
sprite.set_position(draw_pos);
|
||||
window.draw(sprite);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace gsr {
|
||||
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);
|
||||
@@ -54,7 +54,7 @@ namespace gsr {
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_record_area() {
|
||||
auto record_area_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
record_area_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Capture target:", get_color_theme().text_color));
|
||||
record_area_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Capture source:", get_color_theme().text_color));
|
||||
record_area_list->add_widget(create_record_area_box());
|
||||
return record_area_list;
|
||||
}
|
||||
@@ -139,7 +139,7 @@ namespace gsr {
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_record_cursor_section() {
|
||||
auto record_cursor_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Record cursor");
|
||||
record_cursor_checkbox->set_checked(true);
|
||||
@@ -163,7 +163,7 @@ namespace gsr {
|
||||
auto select_directory_page = std::make_unique<GsrPage>("File", "Settings");
|
||||
select_directory_page->add_button("Save", "save", get_color_theme().tint_color);
|
||||
select_directory_page->add_button("Cancel", "cancel", get_color_theme().page_bg_color);
|
||||
|
||||
|
||||
auto file_chooser = std::make_unique<FileChooser>(save_directory_button_ptr->get_text().c_str(), select_directory_page->get_inner_size());
|
||||
FileChooser *file_chooser_ptr = file_chooser.get();
|
||||
select_directory_page->add_widget(std::move(file_chooser));
|
||||
@@ -202,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));
|
||||
}
|
||||
@@ -215,21 +215,73 @@ 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<CheckBox> ScreenshotSettingsPage::create_save_screenshot_to_disk() {
|
||||
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Save screenshot to disk");
|
||||
save_screenshot_to_disk_checkbox_ptr = checkbox.get();
|
||||
checkbox->set_checked(true);
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
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());
|
||||
list->add_widget(create_save_screenshot_to_disk());
|
||||
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<List> ScreenshotSettingsPage::create_custom_script_screenshot_entry() {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL, List::Alignment::CENTER);
|
||||
|
||||
auto create_custom_script_screenshot_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
|
||||
create_custom_script_screenshot_entry_ptr = create_custom_script_screenshot_entry.get();
|
||||
list->add_widget(std::move(create_custom_script_screenshot_entry));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> ScreenshotSettingsPage::create_custom_script_screenshot() {
|
||||
auto custom_script_screenshot_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
custom_script_screenshot_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Command to open the screenshot with:", get_color_theme().text_color));
|
||||
custom_script_screenshot_list->add_widget(create_custom_script_screenshot_entry());
|
||||
return custom_script_screenshot_list;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_custom_script_screenshot_section() {
|
||||
return std::make_unique<Subsection>("Script", create_custom_script_screenshot(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_settings() {
|
||||
auto page_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
page_list->set_spacing(0.018f);
|
||||
auto scrollable_page = std::make_unique<ScrollablePage>(content_page_ptr->get_inner_size() - mgl::vec2f(0.0f, page_list->get_size().y + 0.018f * get_theme().window_height));
|
||||
auto scrollable_page = std::make_unique<ScrollablePage>(content_page_ptr->get_inner_size() - mgl::vec2f(0.0f, page_list->get_size().y));
|
||||
settings_scrollable_page_ptr = scrollable_page.get();
|
||||
page_list->add_widget(std::move(scrollable_page));
|
||||
|
||||
@@ -239,7 +291,8 @@ 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_list->add_widget(create_custom_script_screenshot_section());
|
||||
settings_scrollable_page_ptr->add_widget(std::move(settings_list));
|
||||
return page_list;
|
||||
}
|
||||
@@ -281,7 +334,10 @@ 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);
|
||||
save_screenshot_to_disk_checkbox_ptr->set_checked(config.screenshot_config.save_screenshot_to_disk);
|
||||
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;
|
||||
@@ -296,9 +352,13 @@ namespace gsr {
|
||||
if(config.screenshot_config.image_height < 32)
|
||||
config.screenshot_config.image_height = 32;
|
||||
image_height_entry_ptr->set_text(std::to_string(config.screenshot_config.image_height));
|
||||
|
||||
create_custom_script_screenshot_entry_ptr->set_text(config.screenshot_config.custom_script);
|
||||
}
|
||||
|
||||
void ScreenshotSettingsPage::save() {
|
||||
Config prev_config = config;
|
||||
|
||||
config.screenshot_config.record_area_option = record_area_box_ptr->get_selected_id();
|
||||
config.screenshot_config.image_width = atoi(image_width_entry_ptr->get_text().c_str());
|
||||
config.screenshot_config.image_height = atoi(image_height_entry_ptr->get_text().c_str());
|
||||
@@ -309,7 +369,11 @@ 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.save_screenshot_to_disk = save_screenshot_to_disk_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();
|
||||
config.screenshot_config.custom_script = create_custom_script_screenshot_entry_ptr->get_text();
|
||||
|
||||
if(config.screenshot_config.image_width == 0)
|
||||
config.screenshot_config.image_width = 1920;
|
||||
@@ -328,5 +392,8 @@ namespace gsr {
|
||||
}
|
||||
|
||||
save_config(config);
|
||||
|
||||
if(on_config_changed && config != prev_config)
|
||||
on_config_changed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,9 @@ namespace gsr {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(event.type == mgl::Event::MouseButtonPressed || event.type == mgl::Event::MouseButtonReleased) {
|
||||
// Pass release to children even if outside area, because we want to be able to release mouse when moved outside,
|
||||
// for example in Entry when selecting text
|
||||
if(event.type == mgl::Event::MouseButtonPressed/* || event.type == mgl::Event::MouseButtonReleased*/) {
|
||||
if(!mgl::IntRect(scissor_pos, scissor_size).contains({event.mouse_button.x, event.mouse_button.y}))
|
||||
return true;
|
||||
} else if(event.type == mgl::Event::MouseMoved) {
|
||||
|
||||
@@ -4,10 +4,17 @@
|
||||
#include "../../include/gui/PageStack.hpp"
|
||||
#include "../../include/gui/FileChooser.hpp"
|
||||
#include "../../include/gui/Subsection.hpp"
|
||||
#include "../../include/gui/CustomRendererWidget.hpp"
|
||||
#include "../../include/gui/Image.hpp"
|
||||
#include "../../include/gui/Utils.hpp"
|
||||
#include "../../include/Theme.hpp"
|
||||
#include "../../include/GsrInfo.hpp"
|
||||
#include "../../include/Utils.hpp"
|
||||
#include "mglpp/window/Window.hpp"
|
||||
#include "mglpp/window/Event.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <string.h>
|
||||
|
||||
namespace gsr {
|
||||
@@ -72,7 +79,7 @@ namespace gsr {
|
||||
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);
|
||||
@@ -86,7 +93,7 @@ namespace gsr {
|
||||
|
||||
std::unique_ptr<Widget> SettingsPage::create_record_area() {
|
||||
auto record_area_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
record_area_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Capture target:", get_color_theme().text_color));
|
||||
record_area_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Capture source:", get_color_theme().text_color));
|
||||
record_area_list->add_widget(create_record_area_box());
|
||||
return record_area_list;
|
||||
}
|
||||
@@ -186,6 +193,291 @@ namespace gsr {
|
||||
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> SettingsPage::create_webcam_sources() {
|
||||
auto ll = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
ll->add_widget(std::make_unique<Label>(&get_theme().body_font, "Webcam source:", get_color_theme().text_color));
|
||||
|
||||
auto combobox = std::make_unique<ComboBox>(&get_theme().body_font);
|
||||
combobox->add_item("None", "");
|
||||
for(const GsrCamera &camera : capture_options.cameras) {
|
||||
combobox->add_item(camera.path, camera.path);
|
||||
}
|
||||
webcam_sources_box_ptr = combobox.get();
|
||||
|
||||
webcam_sources_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
|
||||
selected_camera = std::nullopt;
|
||||
selected_camera_setup = std::nullopt;
|
||||
webcam_video_format_box_ptr->clear_items();
|
||||
if(id == "") {
|
||||
webcam_body_list_ptr->set_visible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = std::find_if(capture_options.cameras.begin(), capture_options.cameras.end(), [&](const GsrCamera &camera) {
|
||||
return camera.path == id;
|
||||
});
|
||||
if(it == capture_options.cameras.end())
|
||||
return;
|
||||
|
||||
webcam_body_list_ptr->set_visible(true);
|
||||
webcam_video_format_box_ptr->add_item("Auto (recommended)", "auto");
|
||||
|
||||
if(!it->yuyv_setups.empty())
|
||||
webcam_video_format_box_ptr->add_item("YUYV", "yuyv");
|
||||
|
||||
if(!it->mjpeg_setups.empty())
|
||||
webcam_video_format_box_ptr->add_item("Motion-JPEG", "mjpeg");
|
||||
|
||||
webcam_video_format_box_ptr->set_selected_item("auto");
|
||||
webcam_video_format_box_ptr->set_selected_item(get_current_record_options().webcam_video_format);
|
||||
selected_camera = *it;
|
||||
|
||||
// TODO: Set from config
|
||||
if(webcam_video_format_box_ptr->get_selected_id() == "yuyv" && !it->yuyv_setups.empty())
|
||||
selected_camera_setup = selected_camera->yuyv_setups.front();
|
||||
else if(webcam_video_format_box_ptr->get_selected_id() == "mjpeg" && !it->mjpeg_setups.empty())
|
||||
selected_camera_setup = selected_camera->mjpeg_setups.front();
|
||||
else if(webcam_video_format_box_ptr->get_selected_id() == "auto") {
|
||||
if(!it->mjpeg_setups.empty())
|
||||
selected_camera_setup = selected_camera->mjpeg_setups.front();
|
||||
else if(!it->yuyv_setups.empty())
|
||||
selected_camera_setup = selected_camera->yuyv_setups.front();
|
||||
}
|
||||
};
|
||||
|
||||
ll->add_widget(std::move(combobox));
|
||||
return ll;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> SettingsPage::create_webcam_video_setups() {
|
||||
auto ll = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
ll->add_widget(std::make_unique<Label>(&get_theme().body_font, "Video setup:", get_color_theme().text_color));
|
||||
|
||||
auto combobox = std::make_unique<ComboBox>(&get_theme().body_font);
|
||||
webcam_video_setup_box_ptr = combobox.get();
|
||||
|
||||
ll->add_widget(std::move(combobox));
|
||||
return ll;
|
||||
}
|
||||
|
||||
static std::vector<GsrCameraSetup> sort_camera_setup(const std::vector<GsrCameraSetup> &setups) {
|
||||
auto result = setups;
|
||||
std::sort(result.begin(), result.end(), [](const auto &a, const auto &b) {
|
||||
const uint64_t score_a = (uint64_t)a.resolution.x * (uint64_t)a.resolution.y * (uint64_t)a.fps;
|
||||
const uint64_t score_b = (uint64_t)b.resolution.x * (uint64_t)b.resolution.y * (uint64_t)b.fps;
|
||||
return score_a > score_b;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> SettingsPage::create_webcam_video_format() {
|
||||
auto ll = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
ll->add_widget(std::make_unique<Label>(&get_theme().body_font, "Video format:", get_color_theme().text_color));
|
||||
|
||||
auto combobox = std::make_unique<ComboBox>(&get_theme().body_font);
|
||||
webcam_video_format_box_ptr = combobox.get();
|
||||
|
||||
webcam_video_format_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
|
||||
get_current_record_options().webcam_video_format = id;
|
||||
|
||||
auto it = std::find_if(capture_options.cameras.begin(), capture_options.cameras.end(), [&](const GsrCamera &camera) {
|
||||
return camera.path == webcam_sources_box_ptr->get_selected_id();
|
||||
});
|
||||
if(it == capture_options.cameras.end())
|
||||
return;
|
||||
|
||||
webcam_video_setup_box_ptr->clear_items();
|
||||
if(id == "yuyv") {
|
||||
const auto setups = sort_camera_setup(it->yuyv_setups);
|
||||
for(const auto &setup : setups) {
|
||||
char setup_str[256];
|
||||
snprintf(setup_str, sizeof(setup_str), "%dx%d@%dhz", setup.resolution.x, setup.resolution.y, setup.fps);
|
||||
webcam_video_setup_box_ptr->add_item(setup_str, setup_str, false);
|
||||
}
|
||||
} else if(id == "mjpeg") {
|
||||
const auto setups = sort_camera_setup(it->mjpeg_setups);
|
||||
for(const auto &setup : setups) {
|
||||
char setup_str[256];
|
||||
snprintf(setup_str, sizeof(setup_str), "%dx%d@%dhz", setup.resolution.x, setup.resolution.y, setup.fps);
|
||||
webcam_video_setup_box_ptr->add_item(setup_str, setup_str, false);
|
||||
}
|
||||
} else if(id == "auto") {
|
||||
auto setups = it->yuyv_setups;
|
||||
setups.insert(setups.end(), it->mjpeg_setups.begin(), it->mjpeg_setups.end());
|
||||
setups = sort_camera_setup(setups);
|
||||
|
||||
for(const auto &setup : setups) {
|
||||
char setup_str[256];
|
||||
snprintf(setup_str, sizeof(setup_str), "%dx%d@%dhz", setup.resolution.x, setup.resolution.y, setup.fps);
|
||||
webcam_video_setup_box_ptr->add_item(setup_str, setup_str, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ll->add_widget(std::move(combobox));
|
||||
return ll;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> SettingsPage::create_webcam_video_setup_list() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL);
|
||||
list->add_widget(create_webcam_video_format());
|
||||
list->add_widget(create_webcam_video_setups());
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> SettingsPage::create_webcam_location_widget() {
|
||||
const float camera_screen_width = std::min(400.0f, (float)settings_scrollable_page_ptr->get_inner_size().x * 0.90f);
|
||||
camera_screen_size = mgl::vec2f(camera_screen_width, camera_screen_width * 0.5625);
|
||||
|
||||
const float screen_border = 2.0f;
|
||||
const mgl::vec2f screen_border_size(screen_border, screen_border);
|
||||
screen_inner_size = mgl::vec2f(camera_screen_size - screen_border_size*2.0f);
|
||||
|
||||
const mgl::vec2f bounding_box_size(30.0f, 30.0f);
|
||||
|
||||
auto camera_location_widget = std::make_unique<CustomRendererWidget>(camera_screen_size);
|
||||
camera_location_widget->draw_handler = [this, screen_border_size, screen_border](mgl::Window &window, mgl::vec2f pos, mgl::vec2f size) {
|
||||
if(!selected_camera.has_value() || !selected_camera_setup.has_value())
|
||||
return;
|
||||
|
||||
pos = pos.floor();
|
||||
size = size.floor();
|
||||
const mgl::vec2i mouse_pos = window.get_mouse_position();
|
||||
const mgl::vec2f webcam_box_min_size = clamp_keep_aspect_ratio(selected_camera_setup->resolution.to_vec2f(), screen_inner_size * 0.2f);
|
||||
|
||||
if(moving_webcam_box) {
|
||||
webcam_box_pos = mouse_pos.to_vec2f() - screen_border_size - webcam_box_grab_offset - pos;
|
||||
} else if(webcam_resize_corner == WebcamBoxResizeCorner::BOTTOM_RIGHT) {
|
||||
const mgl::vec2f mouse_diff = mouse_pos.to_vec2f() - webcam_resize_start_pos;
|
||||
webcam_box_size = webcam_box_size_resize_start + mouse_diff;
|
||||
}
|
||||
|
||||
webcam_box_size = clamp_keep_aspect_ratio(selected_camera_setup->resolution.to_vec2f(), webcam_box_size);
|
||||
|
||||
if(webcam_box_pos.x < 0.0f)
|
||||
webcam_box_pos.x = 0.0f;
|
||||
else if(webcam_box_pos.x + webcam_box_size.x > screen_inner_size.x)
|
||||
webcam_box_pos.x = screen_inner_size.x - webcam_box_size.x;
|
||||
|
||||
if(webcam_box_pos.y < 0.0f)
|
||||
webcam_box_pos.y = 0.0f;
|
||||
else if(webcam_box_pos.y + webcam_box_size.y > screen_inner_size.y)
|
||||
webcam_box_pos.y = screen_inner_size.y - webcam_box_size.y;
|
||||
|
||||
if(webcam_box_size.x < webcam_box_min_size.x)
|
||||
webcam_box_size.x = webcam_box_min_size.x;
|
||||
else if(webcam_box_pos.x + webcam_box_size.x > screen_inner_size.x)
|
||||
webcam_box_size.x = screen_inner_size.x - webcam_box_pos.x;
|
||||
|
||||
//webcam_box_size = clamp_keep_aspect_ratio(selected_camera->size.to_vec2f(), webcam_box_size);
|
||||
|
||||
if(webcam_box_size.y < webcam_box_min_size.y)
|
||||
webcam_box_size.y = webcam_box_min_size.y;
|
||||
else if(webcam_box_pos.y + webcam_box_size.y > screen_inner_size.y)
|
||||
webcam_box_size.y = screen_inner_size.y - webcam_box_pos.y;
|
||||
|
||||
webcam_box_size = clamp_keep_aspect_ratio(selected_camera_setup->resolution.to_vec2f(), webcam_box_size);
|
||||
|
||||
{
|
||||
draw_rectangle_outline(window, pos, size, mgl::Color(255, 0, 0, 255), screen_border);
|
||||
mgl::Text screen_text("Screen", get_theme().camera_setup_font);
|
||||
screen_text.set_position((pos + size * 0.5f - screen_text.get_bounds().size * 0.5f).floor());
|
||||
window.draw(screen_text);
|
||||
}
|
||||
|
||||
{
|
||||
webcam_box_drawn_size = clamp_keep_aspect_ratio(selected_camera_setup->resolution.to_vec2f(), webcam_box_size);
|
||||
webcam_box_drawn_pos = (pos + screen_border_size + webcam_box_pos).floor();
|
||||
|
||||
draw_rectangle_outline(window, webcam_box_drawn_pos, webcam_box_drawn_size, mgl::Color(0, 255, 0, 255), screen_border);
|
||||
|
||||
// mgl::Rectangle resize_area(webcam_box_drawn_pos + webcam_box_drawn_size - bounding_box_size*0.5f - screen_border_size*0.5f, bounding_box_size);
|
||||
// resize_area.set_color(mgl::Color(0, 0, 255, 255));
|
||||
// window.draw(resize_area);
|
||||
|
||||
mgl::Text webcam_text("Webcam", get_theme().camera_setup_font);
|
||||
webcam_text.set_position((webcam_box_drawn_pos + webcam_box_drawn_size * 0.5f - webcam_text.get_bounds().size * 0.5f).floor());
|
||||
window.draw(webcam_text);
|
||||
}
|
||||
};
|
||||
|
||||
camera_location_widget->event_handler = [this, screen_border_size, bounding_box_size](mgl::Event &event, mgl::Window&, mgl::vec2f, mgl::vec2f) {
|
||||
switch(event.type) {
|
||||
case mgl::Event::MouseButtonPressed: {
|
||||
if(event.mouse_button.button == mgl::Mouse::Left && webcam_resize_corner == WebcamBoxResizeCorner::NONE) {
|
||||
const mgl::vec2f mouse_button_pos(event.mouse_button.x, event.mouse_button.y);
|
||||
if(mgl::FloatRect(webcam_box_drawn_pos, webcam_box_drawn_size).contains(mouse_button_pos)) {
|
||||
moving_webcam_box = true;
|
||||
webcam_box_grab_offset = mouse_button_pos - webcam_box_drawn_pos;
|
||||
} else {
|
||||
moving_webcam_box = false;
|
||||
}
|
||||
} else if(event.mouse_button.button == mgl::Mouse::Right && !moving_webcam_box) {
|
||||
const mgl::vec2f mouse_button_pos(event.mouse_button.x, event.mouse_button.y);
|
||||
webcam_resize_start_pos = mouse_button_pos;
|
||||
webcam_box_pos_resize_start = webcam_box_pos;
|
||||
webcam_box_size_resize_start = webcam_box_size;
|
||||
webcam_box_grab_offset = mouse_button_pos - webcam_box_drawn_pos;
|
||||
|
||||
/*if(mgl::FloatRect(webcam_box_drawn_pos - bounding_box_size*0.5f, bounding_box_size).contains(mouse_button_pos)) {
|
||||
webcam_resize_corner = WebcamBoxResizeCorner::TOP_LEFT;
|
||||
fprintf(stderr, "top left\n");
|
||||
} else if(mgl::FloatRect(webcam_box_drawn_pos + mgl::vec2f(webcam_box_drawn_size.x, 0.0f) - bounding_box_size*0.5f, bounding_box_size).contains(mouse_button_pos)) {
|
||||
webcam_resize_corner = WebcamBoxResizeCorner::TOP_RIGHT;
|
||||
fprintf(stderr, "top right\n");
|
||||
} else if(mgl::FloatRect(webcam_box_drawn_pos + mgl::vec2f(0.0f, webcam_box_drawn_size.y) - bounding_box_size*0.5f, bounding_box_size).contains(mouse_button_pos)) {
|
||||
webcam_resize_corner = WebcamBoxResizeCorner::BOTTOM_LEFT;
|
||||
fprintf(stderr, "bottom left\n");
|
||||
} else */if(mgl::FloatRect(webcam_box_drawn_pos + webcam_box_drawn_size - bounding_box_size*0.5f - screen_border_size*0.5f, bounding_box_size).contains(mouse_button_pos)) {
|
||||
webcam_resize_corner = WebcamBoxResizeCorner::BOTTOM_RIGHT;
|
||||
} else {
|
||||
webcam_resize_corner = WebcamBoxResizeCorner::NONE;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case mgl::Event::MouseButtonReleased: {
|
||||
if(event.mouse_button.button == mgl::Mouse::Left && webcam_resize_corner == WebcamBoxResizeCorner::NONE) {
|
||||
moving_webcam_box = false;
|
||||
} else if(event.mouse_button.button == mgl::Mouse::Right && !moving_webcam_box) {
|
||||
webcam_resize_corner = WebcamBoxResizeCorner::NONE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return camera_location_widget;
|
||||
}
|
||||
|
||||
std::unique_ptr<CheckBox> SettingsPage::create_flip_camera_checkbox() {
|
||||
auto flip_camera_horizontally_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Flip camera horizontally");
|
||||
flip_camera_horizontally_checkbox_ptr = flip_camera_horizontally_checkbox.get();
|
||||
return flip_camera_horizontally_checkbox;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> SettingsPage::create_webcam_body() {
|
||||
auto body_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
webcam_body_list_ptr = body_list.get();
|
||||
body_list->set_visible(false);
|
||||
body_list->add_widget(create_webcam_location_widget());
|
||||
body_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "* Right click in the bottom right corner to resize the webcam", get_color_theme().text_color));
|
||||
body_list->add_widget(create_flip_camera_checkbox());
|
||||
body_list->add_widget(create_webcam_video_setup_list());
|
||||
return body_list;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> SettingsPage::create_webcam_section() {
|
||||
auto ll = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
ll->add_widget(create_webcam_sources());
|
||||
ll->add_widget(create_webcam_body());
|
||||
return std::make_unique<Subsection>("Webcam", 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) {
|
||||
return audio_device_id == "default_output" || ends_with(audio_device_id, ".monitor");
|
||||
}
|
||||
@@ -230,8 +522,9 @@ namespace gsr {
|
||||
auto remove_audio_track_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 0));
|
||||
remove_audio_track_button->set_icon(&get_theme().trash_texture);
|
||||
remove_audio_track_button->set_icon_padding_scale(0.75f);
|
||||
remove_audio_track_button->on_click = [audio_input_list_ptr, audio_device_list_ptr]() {
|
||||
remove_audio_track_button->on_click = [this, audio_input_list_ptr, audio_device_list_ptr]() {
|
||||
audio_input_list_ptr->remove_widget(audio_device_list_ptr);
|
||||
update_application_audio_warning_visibility();
|
||||
};
|
||||
return remove_audio_track_button;
|
||||
}
|
||||
@@ -254,11 +547,48 @@ namespace gsr {
|
||||
return button;
|
||||
}
|
||||
|
||||
void SettingsPage::update_application_audio_warning_visibility() {
|
||||
audio_track_section_list_ptr->for_each_child_widget([](std::unique_ptr<Widget> &child_widget) {
|
||||
Subsection *audio_subsection = dynamic_cast<Subsection*>(child_widget.get());
|
||||
List *audio_track_section_items_list_ptr = dynamic_cast<List*>(audio_subsection->get_inner_widget());
|
||||
List *audio_input_list_ptr = dynamic_cast<List*>(audio_track_section_items_list_ptr->get_child_widget_by_index(2));
|
||||
CheckBox *application_audio_invert_checkbox_ptr = dynamic_cast<CheckBox*>(audio_track_section_items_list_ptr->get_child_widget_by_index(3));
|
||||
List *application_audio_warning_list_ptr = dynamic_cast<List*>(audio_track_section_items_list_ptr->get_child_widget_by_index(4));
|
||||
|
||||
int num_output_devices = 0;
|
||||
int num_application_audio = 0;
|
||||
|
||||
audio_input_list_ptr->for_each_child_widget([&num_output_devices, &num_application_audio](std::unique_ptr<Widget> &child_widget){
|
||||
List *audio_track_line = dynamic_cast<List*>(child_widget.get());
|
||||
const AudioTrackType audio_track_type = (AudioTrackType)(uintptr_t)audio_track_line->userdata;
|
||||
switch(audio_track_type) {
|
||||
case AudioTrackType::DEVICE: {
|
||||
Label *label = dynamic_cast<Label*>(audio_track_line->get_child_widget_by_index(0));
|
||||
const bool is_output_device = starts_with(label->get_text().c_str(), "Output device");
|
||||
if(is_output_device)
|
||||
num_output_devices++;
|
||||
break;
|
||||
}
|
||||
case AudioTrackType::APPLICATION:
|
||||
case AudioTrackType::APPLICATION_CUSTOM: {
|
||||
num_application_audio++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
application_audio_warning_list_ptr->set_visible(num_output_devices > 0 && (num_application_audio > 0 || application_audio_invert_checkbox_ptr->is_checked()));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
std::unique_ptr<Button> SettingsPage::create_add_audio_output_device_button(List *audio_input_list_ptr) {
|
||||
auto button = std::make_unique<Button>(&get_theme().body_font, "Add output device", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
button->on_click = [this, audio_input_list_ptr]() {
|
||||
audio_devices = get_audio_devices();
|
||||
audio_input_list_ptr->add_widget(create_audio_device(AudioDeviceType::OUTPUT, audio_input_list_ptr));
|
||||
update_application_audio_warning_visibility();
|
||||
};
|
||||
return button;
|
||||
}
|
||||
@@ -317,6 +647,8 @@ namespace gsr {
|
||||
audio_input_list_ptr->add_widget(create_custom_application_audio(audio_input_list_ptr));
|
||||
else
|
||||
audio_input_list_ptr->add_widget(create_application_audio(audio_input_list_ptr));
|
||||
|
||||
update_application_audio_warning_visibility();
|
||||
};
|
||||
return add_audio_track_button;
|
||||
}
|
||||
@@ -338,9 +670,24 @@ namespace gsr {
|
||||
std::unique_ptr<CheckBox> SettingsPage::create_application_audio_invert_checkbox() {
|
||||
auto application_audio_invert_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Record audio from all applications except the selected ones");
|
||||
application_audio_invert_checkbox->set_checked(false);
|
||||
application_audio_invert_checkbox->on_changed = [this](bool) {
|
||||
update_application_audio_warning_visibility();
|
||||
};
|
||||
return application_audio_invert_checkbox;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> SettingsPage::create_application_audio_warning() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL);
|
||||
list->set_spacing(0.003f);
|
||||
list->set_visible(false);
|
||||
|
||||
const int font_character_size = get_theme().body_font.get_character_size();
|
||||
list->add_widget(std::make_unique<Image>(&get_theme().warning_texture, mgl::vec2f(font_character_size, font_character_size), Image::ScaleBehavior::SCALE));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Recording output devices and application audio may record all output audio, which is likely\nnot what you want to do. Remove the output devices.", get_color_theme().text_color));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
static void update_audio_track_titles(List *audio_track_section_list_ptr) {
|
||||
int index = 0;
|
||||
audio_track_section_list_ptr->for_each_child_widget([&index](std::unique_ptr<Widget> &widget) {
|
||||
@@ -388,6 +735,7 @@ namespace gsr {
|
||||
list_ptr->add_widget(create_add_audio_buttons(audio_input_section_ptr));
|
||||
list_ptr->add_widget(std::move(audio_input_section));
|
||||
list_ptr->add_widget(create_application_audio_invert_checkbox());
|
||||
list_ptr->add_widget(create_application_audio_warning());
|
||||
|
||||
set_application_audio_options_visible(subsection.get(), view_radio_button_ptr->get_selected_id() == "advanced", *gsr_info);
|
||||
return subsection;
|
||||
@@ -567,6 +915,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;
|
||||
}
|
||||
@@ -613,6 +965,7 @@ namespace gsr {
|
||||
auto settings_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
settings_list->set_spacing(0.018f);
|
||||
settings_list->add_widget(create_capture_target_section());
|
||||
settings_list->add_widget(create_webcam_section());
|
||||
settings_list->add_widget(create_audio_section());
|
||||
settings_list->add_widget(create_video_section());
|
||||
settings_list_ptr = settings_list.get();
|
||||
@@ -816,16 +1169,78 @@ 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();
|
||||
}
|
||||
|
||||
RecordOptions& SettingsPage::get_current_record_options() {
|
||||
switch(type) {
|
||||
default:
|
||||
assert(false);
|
||||
case Type::REPLAY: return config.replay_config.record_options;
|
||||
case Type::RECORD: return config.record_config.record_options;
|
||||
case Type::STREAM: return config.streaming_config.record_options;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> SettingsPage::create_low_power_mode() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
|
||||
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Record in low-power mode");
|
||||
checkbox->set_visible(gsr_info->gpu_info.vendor == GpuVendor::AMD);
|
||||
low_power_mode_checkbox_ptr = checkbox.get();
|
||||
|
||||
list->add_widget(std::move(checkbox));
|
||||
|
||||
auto info = std::make_unique<Image>(&get_theme().question_mark_texture, low_power_mode_checkbox_ptr->get_size(), Image::ScaleBehavior::SCALE);
|
||||
info->set_tooltip_text(
|
||||
"Do not force the GPU to go into high performance mode when recording.\n"
|
||||
"May affect recording performance, especially when playing a video at the same time.\n"
|
||||
"If enabled then it's recommended to use sync to content frame rate mode to reduce power usage when idle."
|
||||
);
|
||||
Image *info_ptr = info.get();
|
||||
info->on_mouse_move = [info_ptr](bool inside) {
|
||||
if(inside)
|
||||
set_current_tooltip(info_ptr);
|
||||
else
|
||||
remove_as_current_tooltip(info_ptr);
|
||||
};
|
||||
list->add_widget(std::move(info));
|
||||
|
||||
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);
|
||||
@@ -841,33 +1256,14 @@ 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)));
|
||||
general_list->add_widget(create_low_power_mode());
|
||||
|
||||
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");
|
||||
@@ -908,37 +1304,22 @@ 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());
|
||||
general_list->add_widget(create_low_power_mode());
|
||||
|
||||
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");
|
||||
@@ -953,6 +1334,7 @@ namespace gsr {
|
||||
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("Kick", "kick");
|
||||
streaming_service_box->add_item("Custom", "custom");
|
||||
streaming_service_box_ptr = streaming_service_box.get();
|
||||
return streaming_service_box;
|
||||
@@ -965,42 +1347,77 @@ 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));
|
||||
|
||||
auto rumble_stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
|
||||
rumble_stream_key_entry_ptr = rumble_stream_key_entry.get();
|
||||
stream_key_list->add_widget(std::move(rumble_stream_key_entry));
|
||||
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());
|
||||
kick_stream_url_entry_ptr = add_stream_key_entry_to_list(stream_key_list.get());
|
||||
kick_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_custom_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, "Stream 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));
|
||||
|
||||
stream_url_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream key:", get_color_theme().text_color));
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
stream_url_list->add_widget(std::move(stream_key_entry));
|
||||
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;
|
||||
}
|
||||
|
||||
stream_url_list_ptr = stream_url_list.get();
|
||||
return stream_url_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() {
|
||||
@@ -1013,11 +1430,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,42 +1442,34 @@ namespace gsr {
|
||||
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_custom_section());
|
||||
streaming_info_list->add_widget(create_stream_container_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_low_power_mode());
|
||||
|
||||
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 kick_option = id == "kick";
|
||||
const bool custom_option = id == "custom";
|
||||
stream_key_list_ptr->set_visible(!custom_option);
|
||||
stream_url_list_ptr->set_visible(custom_option);
|
||||
container_list_ptr->set_visible(custom_option);
|
||||
twitch_stream_key_entry_ptr->set_visible(twitch_option);
|
||||
youtube_stream_key_entry_ptr->set_visible(youtube_option);
|
||||
rumble_stream_key_entry_ptr->set_visible(rumble_option);
|
||||
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);
|
||||
kick_stream_url_entry_ptr->get_parent_widget()->set_visible(kick_option);
|
||||
kick_stream_key_entry_ptr->get_parent_widget()->set_visible(kick_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");
|
||||
@@ -1166,6 +1574,8 @@ namespace gsr {
|
||||
auto audio_track_section = create_audio_track_section(audio_section_ptr);
|
||||
audio_track_section_list_ptr->add_widget(std::move(audio_track_section));
|
||||
}
|
||||
|
||||
update_application_audio_warning_visibility();
|
||||
}
|
||||
|
||||
void SettingsPage::load_common(RecordOptions &record_options) {
|
||||
@@ -1182,6 +1592,24 @@ 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);
|
||||
low_power_mode_checkbox_ptr->set_checked(record_options.low_power_mode);
|
||||
|
||||
char webcam_setup_str[256];
|
||||
snprintf(webcam_setup_str, sizeof(webcam_setup_str), "%dx%d@%dhz", record_options.webcam_camera_width, record_options.webcam_camera_height, record_options.webcam_camera_fps);
|
||||
|
||||
webcam_sources_box_ptr->set_selected_item(record_options.webcam_source);
|
||||
flip_camera_horizontally_checkbox_ptr->set_checked(record_options.webcam_flip_horizontally);
|
||||
webcam_video_format_box_ptr->set_selected_item(record_options.webcam_video_format);
|
||||
webcam_video_setup_box_ptr->set_selected_item(webcam_setup_str);
|
||||
webcam_box_pos.x = ((float)record_options.webcam_x / 100.0f) * screen_inner_size.x;
|
||||
webcam_box_pos.y = ((float)record_options.webcam_y / 100.0f) * screen_inner_size.y;
|
||||
webcam_box_size.x = ((float)record_options.webcam_width / 100.0f * screen_inner_size.x);
|
||||
webcam_box_size.y = ((float)record_options.webcam_height / 100.0f * screen_inner_size.y);
|
||||
|
||||
if(selected_camera_setup.has_value())
|
||||
webcam_box_size = clamp_keep_aspect_ratio(selected_camera_setup->resolution.to_vec2f(), webcam_box_size);
|
||||
|
||||
if(record_options.record_area_width == 0)
|
||||
record_options.record_area_width = 1920;
|
||||
@@ -1227,9 +1655,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);
|
||||
|
||||
@@ -1243,21 +1669,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);
|
||||
kick_stream_url_entry_ptr->set_text(config.streaming_config.kick.stream_url);
|
||||
kick_stream_key_entry_ptr->set_text(config.streaming_config.kick.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);
|
||||
@@ -1319,6 +1742,30 @@ 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();
|
||||
record_options.low_power_mode = low_power_mode_checkbox_ptr->is_checked();
|
||||
|
||||
// TODO: Set selected_camera_setup properly when updating and shit
|
||||
|
||||
if(selected_camera_setup.has_value())
|
||||
webcam_box_size = clamp_keep_aspect_ratio(selected_camera_setup->resolution.to_vec2f(), webcam_box_size);
|
||||
|
||||
int camera_width = 0;
|
||||
int camera_height = 0;
|
||||
int camera_fps = 0;
|
||||
sscanf(webcam_video_setup_box_ptr->get_selected_id().c_str(), "%dx%d@%dhz", &camera_width, &camera_height, &camera_fps);
|
||||
|
||||
record_options.webcam_source = webcam_sources_box_ptr->get_selected_id();
|
||||
record_options.webcam_flip_horizontally = flip_camera_horizontally_checkbox_ptr->is_checked();
|
||||
record_options.webcam_video_format = webcam_video_format_box_ptr->get_selected_id();
|
||||
record_options.webcam_camera_width = camera_width;
|
||||
record_options.webcam_camera_height = camera_height;
|
||||
record_options.webcam_camera_fps = camera_fps;
|
||||
record_options.webcam_x = std::round((webcam_box_pos.x / screen_inner_size.x) * 100.0f);
|
||||
record_options.webcam_y = std::round((webcam_box_pos.y / screen_inner_size.y) * 100.0f);
|
||||
record_options.webcam_width = std::round((webcam_box_size.x / screen_inner_size.x) * 100.0f);
|
||||
record_options.webcam_height = std::round((webcam_box_size.y / screen_inner_size.y) * 100.0f);
|
||||
|
||||
if(record_options.record_area_width == 0)
|
||||
record_options.record_area_width = 1920;
|
||||
@@ -1369,9 +1816,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());
|
||||
@@ -1386,21 +1830,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.kick.stream_url = kick_stream_url_entry_ptr->get_text();
|
||||
config.streaming_config.kick.stream_key = kick_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();
|
||||
|
||||
67
src/gui/Tooltip.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#include "../../include/gui/Tooltip.hpp"
|
||||
#include "../../include/Theme.hpp"
|
||||
|
||||
#include <mglpp/graphics/Rectangle.hpp>
|
||||
#include <mglpp/graphics/Sprite.hpp>
|
||||
#include <mglpp/window/Window.hpp>
|
||||
|
||||
namespace gsr {
|
||||
static const float padding_top_scale = 0.008f;
|
||||
static const float padding_bottom_scale = 0.008f;
|
||||
static const float padding_left_scale = 0.008f;
|
||||
static const float padding_right_scale = 0.008f;
|
||||
static const float accent_scale = 0.0025f;
|
||||
|
||||
Tooltip::Tooltip(mgl::Font *font) : label("", *font) {}
|
||||
|
||||
bool Tooltip::on_event(mgl::Event&, mgl::Window&, mgl::vec2f) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void Tooltip::draw(mgl::Window &window, mgl::vec2f offset) {
|
||||
if(!visible)
|
||||
return;
|
||||
|
||||
const mgl::vec2f draw_pos = (window.get_mouse_position().to_vec2f() + offset).floor();
|
||||
|
||||
const int padding_top = get_theme().window_height * padding_top_scale;
|
||||
const int padding_left = get_theme().window_height * padding_left_scale;
|
||||
const int accent_height = get_theme().window_height * accent_scale;
|
||||
const int icon_height = label.get_font()->get_character_size();
|
||||
|
||||
mgl::Rectangle background(get_size());
|
||||
background.set_position(draw_pos - mgl::vec2f(0.0f, background.get_size().y));
|
||||
background.set_color(mgl::Color(0, 0, 0));
|
||||
window.draw(background);
|
||||
|
||||
mgl::Rectangle accent(background.get_position(), mgl::vec2f(background.get_size().x, accent_height).floor());
|
||||
accent.set_color(get_color_theme().tint_color);
|
||||
window.draw(accent);
|
||||
|
||||
mgl::Sprite icon_sprite(&get_theme().info_texture, background.get_position() + mgl::vec2f(padding_left, accent_height + padding_top).floor());
|
||||
icon_sprite.set_height(icon_height);
|
||||
window.draw(icon_sprite);
|
||||
|
||||
label.set_position(background.get_position() + mgl::vec2f(padding_left, accent_height + padding_top + icon_sprite.get_size().y).floor());
|
||||
window.draw(label);
|
||||
}
|
||||
|
||||
mgl::vec2f Tooltip::get_size() {
|
||||
if(!visible)
|
||||
return {0.0f, 0.0f};
|
||||
|
||||
const int padding_top = get_theme().window_height * padding_top_scale;
|
||||
const int padding_bottom = get_theme().window_height * padding_bottom_scale;
|
||||
const int padding_left = get_theme().window_height * padding_left_scale;
|
||||
const int padding_right = get_theme().window_height * padding_right_scale;
|
||||
const int accent_height = get_theme().window_height * accent_scale;
|
||||
const mgl::vec2f text_size = label.get_bounds().size.floor();
|
||||
const int icon_height = label.get_font()->get_character_size();
|
||||
|
||||
return mgl::vec2f(padding_left + text_size.x + padding_right, accent_height + padding_top + icon_height + text_size.y + padding_bottom).floor();
|
||||
}
|
||||
|
||||
void Tooltip::set_text(std::string text) {
|
||||
label.set_string(std::move(text));
|
||||
}
|
||||
}
|
||||
@@ -5,15 +5,15 @@
|
||||
namespace gsr {
|
||||
static double frame_delta_seconds = 1.0;
|
||||
|
||||
static mgl::vec2i min_vec2i(mgl::vec2i a, mgl::vec2i b) {
|
||||
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) {
|
||||
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) {
|
||||
mgl::vec2i clamp_vec2i(mgl::vec2i value, mgl::vec2i min, mgl::vec2i max) {
|
||||
return min_vec2i(max, max_vec2i(value, min));
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace gsr {
|
||||
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)
|
||||
max_vec2i(mgl::vec2i(0, 0), min_vec2i(child.position + child.size - pos, parent.position + parent.size - pos))
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,16 @@
|
||||
#include "../../include/gui/Widget.hpp"
|
||||
#include "../../include/gui/Tooltip.hpp"
|
||||
#include "../../include/Theme.hpp"
|
||||
#include <vector>
|
||||
|
||||
#include <mglpp/window/Event.hpp>
|
||||
|
||||
namespace gsr {
|
||||
static std::vector<std::unique_ptr<Widget>> widgets_to_remove;
|
||||
static Widget *current_tooltip_widget = nullptr;
|
||||
static std::unique_ptr<Tooltip> tooltip;
|
||||
|
||||
static void set_current_tooltip_text(Widget *widget);
|
||||
|
||||
Widget::Widget() {
|
||||
|
||||
@@ -10,6 +18,7 @@ namespace gsr {
|
||||
|
||||
Widget::~Widget() {
|
||||
remove_widget_as_selected_in_parent();
|
||||
remove_as_current_tooltip(this);
|
||||
}
|
||||
|
||||
void Widget::set_position(mgl::vec2f position) {
|
||||
@@ -64,6 +73,34 @@ namespace gsr {
|
||||
this->visible = visible;
|
||||
}
|
||||
|
||||
bool Widget::is_visible() const {
|
||||
return visible;
|
||||
}
|
||||
|
||||
Widget* Widget::get_parent_widget() {
|
||||
return parent_widget;
|
||||
}
|
||||
|
||||
void Widget::set_tooltip_text(std::string text) {
|
||||
tooltip_text = std::move(text);
|
||||
if(current_tooltip_widget == this)
|
||||
set_current_tooltip_text(current_tooltip_widget);
|
||||
}
|
||||
|
||||
const std::string& Widget::get_tooltip_text() const {
|
||||
return tooltip_text;
|
||||
}
|
||||
|
||||
void Widget::handle_tooltip_event(mgl::Event &event, mgl::vec2f position, mgl::vec2f size) {
|
||||
if(event.type == mgl::Event::MouseMoved) {
|
||||
if(mgl::FloatRect(position, size).contains(mgl::vec2f(event.mouse_move.x, event.mouse_move.y))) {
|
||||
set_current_tooltip(this);
|
||||
} else {
|
||||
remove_as_current_tooltip(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void add_widget_to_remove(std::unique_ptr<Widget> widget) {
|
||||
widgets_to_remove.push_back(std::move(widget));
|
||||
}
|
||||
@@ -74,4 +111,40 @@ namespace gsr {
|
||||
}
|
||||
widgets_to_remove.clear();
|
||||
}
|
||||
|
||||
void set_current_tooltip(Widget *widget) {
|
||||
if(current_tooltip_widget == widget)
|
||||
return;
|
||||
|
||||
set_current_tooltip_text(widget);
|
||||
}
|
||||
|
||||
void remove_as_current_tooltip(Widget *widget) {
|
||||
if(current_tooltip_widget == widget)
|
||||
set_current_tooltip_text(nullptr);
|
||||
}
|
||||
|
||||
void set_current_tooltip_text(Widget *widget) {
|
||||
if(widget && !widget->get_tooltip_text().empty()) {
|
||||
current_tooltip_widget = widget;
|
||||
if(!tooltip)
|
||||
tooltip = std::make_unique<Tooltip>(&get_theme().body_font);
|
||||
tooltip->set_text(current_tooltip_widget->get_tooltip_text());
|
||||
} else {
|
||||
current_tooltip_widget = nullptr;
|
||||
tooltip.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void draw_tooltip(mgl::Window &window) {
|
||||
if(!tooltip)
|
||||
return;
|
||||
|
||||
if(!current_tooltip_widget->is_visible()) {
|
||||
set_current_tooltip(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
tooltip->draw(window, mgl::vec2f(0.0f, 0.0f));
|
||||
}
|
||||
}
|
||||
103
src/main.cpp
@@ -3,6 +3,7 @@
|
||||
#include "../include/gui/Utils.hpp"
|
||||
#include "../include/Process.hpp"
|
||||
#include "../include/Rpc.hpp"
|
||||
#include "../include/Theme.hpp"
|
||||
|
||||
#include <signal.h>
|
||||
#include <string.h>
|
||||
@@ -23,8 +24,10 @@ extern "C" {
|
||||
}
|
||||
|
||||
static sig_atomic_t running = 1;
|
||||
static sig_atomic_t killed = 0;
|
||||
static void sigint_handler(int signal) {
|
||||
(void)signal;
|
||||
killed = 1;
|
||||
running = 0;
|
||||
}
|
||||
|
||||
@@ -53,7 +56,7 @@ static void rpc_add_commands(gsr::Rpc *rpc, gsr::Overlay *overlay) {
|
||||
|
||||
rpc->add_handler("toggle-record", [overlay](const std::string &name) {
|
||||
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
|
||||
overlay->toggle_record();
|
||||
overlay->toggle_record(gsr::RecordForceType::NONE);
|
||||
});
|
||||
|
||||
rpc->add_handler("toggle-pause", [overlay](const std::string &name) {
|
||||
@@ -61,6 +64,16 @@ static void rpc_add_commands(gsr::Rpc *rpc, gsr::Overlay *overlay) {
|
||||
overlay->toggle_pause();
|
||||
});
|
||||
|
||||
rpc->add_handler("toggle-record-region", [overlay](const std::string &name) {
|
||||
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
|
||||
overlay->toggle_record(gsr::RecordForceType::REGION);
|
||||
});
|
||||
|
||||
rpc->add_handler("toggle-record-window", [overlay](const std::string &name) {
|
||||
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
|
||||
overlay->toggle_record(gsr::RecordForceType::WINDOW);
|
||||
});
|
||||
|
||||
rpc->add_handler("toggle-stream", [overlay](const std::string &name) {
|
||||
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
|
||||
overlay->toggle_stream();
|
||||
@@ -102,24 +115,6 @@ static void rpc_add_commands(gsr::Rpc *rpc, gsr::Overlay *overlay) {
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static void install_flatpak_systemd_service() {
|
||||
const bool systemd_service_exists = system(
|
||||
"data_home=$(flatpak-spawn --host -- /bin/sh -c 'echo \"${XDG_DATA_HOME:-$HOME/.local/share}\"') && "
|
||||
@@ -180,9 +175,10 @@ static void set_display_server_environment_variables() {
|
||||
static void usage() {
|
||||
printf("usage: gsr-ui [action]\n");
|
||||
printf("OPTIONS:\n");
|
||||
printf(" action The launch action. Should be either \"launch-show\", \"launch-hide\" or \"launch-daemon\". Optional, defaults to \"launch-hide\".\n");
|
||||
printf(" action The launch action. Should be either \"launch-show\", \"launch-hide\", \"launch-hide-announce\" or \"launch-daemon\". Optional, defaults to \"launch-hide\".\n");
|
||||
printf(" If \"launch-show\" is used then the program starts and the UI is immediately opened and can be shown/hidden with Alt+Z.\n");
|
||||
printf(" If \"launch-hide\" is used then the program starts but the UI is not opened until Alt+Z is pressed. The UI will be opened if the program is already running in another process.\n");
|
||||
printf(" If \"launch-hide-announce\" is used then the program starts but the UI is not opened until Alt+Z is pressed and a notification tells the user to press Alt+Z. The UI will be opened if the program is already running in another process.\n");
|
||||
printf(" If \"launch-daemon\" is used then the program starts but the UI is not opened until Alt+Z is pressed. The UI will not be opened if the program is already running in another process.\n");
|
||||
exit(1);
|
||||
}
|
||||
@@ -190,12 +186,15 @@ static void usage() {
|
||||
enum class LaunchAction {
|
||||
LAUNCH_SHOW,
|
||||
LAUNCH_HIDE,
|
||||
LAUNCH_HIDE_ANNOUNCE,
|
||||
LAUNCH_DAEMON
|
||||
};
|
||||
|
||||
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");
|
||||
@@ -211,10 +210,12 @@ int main(int argc, char **argv) {
|
||||
launch_action = LaunchAction::LAUNCH_SHOW;
|
||||
} else if(strcmp(launch_action_opt, "launch-hide") == 0) {
|
||||
launch_action = LaunchAction::LAUNCH_HIDE;
|
||||
} else if(strcmp(launch_action_opt, "launch-hide-announce") == 0) {
|
||||
launch_action = LaunchAction::LAUNCH_HIDE_ANNOUNCE;
|
||||
} else if(strcmp(launch_action_opt, "launch-daemon") == 0) {
|
||||
launch_action = LaunchAction::LAUNCH_DAEMON;
|
||||
} else {
|
||||
printf("error: invalid action \"%s\", expected \"launch-show\", \"launch-hide\" or \"launch-daemon\".\n", launch_action_opt);
|
||||
printf("error: invalid action \"%s\", expected \"launch-show\", \"launch-hide\", \"launch-hide-announce\" or \"launch-daemon\".\n", launch_action_opt);
|
||||
usage();
|
||||
}
|
||||
} else {
|
||||
@@ -223,28 +224,47 @@ int main(int argc, char **argv) {
|
||||
|
||||
set_display_server_environment_variables();
|
||||
|
||||
auto rpc = std::make_unique<gsr::Rpc>();
|
||||
const bool rpc_created = rpc->create("gsr-ui");
|
||||
if(!rpc_created)
|
||||
fprintf(stderr, "Error: Failed to create rpc\n");
|
||||
std::string resources_path;
|
||||
if(access("sibs-build/linux_x86_64/debug/gsr-ui", F_OK) == 0) {
|
||||
resources_path = "./";
|
||||
} else {
|
||||
#ifdef GSR_UI_RESOURCES_PATH
|
||||
resources_path = GSR_UI_RESOURCES_PATH "/";
|
||||
#else
|
||||
resources_path = "/usr/share/gsr-ui/";
|
||||
#endif
|
||||
}
|
||||
|
||||
if(is_gsr_ui_virtual_keyboard_running() || !rpc_created) {
|
||||
const std::string gsr_icon_path = resources_path + "images/gpu_screen_recorder_logo.png";
|
||||
|
||||
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;
|
||||
|
||||
rpc = std::make_unique<gsr::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");
|
||||
const char *args[] = { "gsr-notify", "--text", "Another instance of GPU Screen Recorder UI is already running.\nPress Alt+Z to open the UI.", "--timeout", "5.0", "--icon-color", "ff0000", "--bg-color", "ff0000", nullptr };
|
||||
const char *args[] = {
|
||||
"gsr-notify", "--text", "Another instance of GPU Screen Recorder UI is already running.\nPress Alt+Z to open the UI.", "--timeout", "5.0",
|
||||
"--icon-color", "ffffff", "--icon", gsr_icon_path.c_str(), "--bg-color", "ff0000", nullptr
|
||||
};
|
||||
gsr::exec_program_daemonized(args);
|
||||
}
|
||||
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 };
|
||||
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", "ffffff", "--icon", gsr_icon_path.c_str(), "--bg-color", "ff0000", nullptr
|
||||
};
|
||||
gsr::exec_program_daemonized(args);
|
||||
}
|
||||
|
||||
@@ -298,17 +318,6 @@ int main(int argc, char **argv) {
|
||||
|
||||
gsr::SupportedCaptureOptions capture_options = gsr::get_supported_capture_options(gsr_info);
|
||||
|
||||
std::string resources_path;
|
||||
if(access("sibs-build/linux_x86_64/debug/gsr-ui", F_OK) == 0) {
|
||||
resources_path = "./";
|
||||
} else {
|
||||
#ifdef GSR_UI_RESOURCES_PATH
|
||||
resources_path = GSR_UI_RESOURCES_PATH "/";
|
||||
#else
|
||||
resources_path = "/usr/share/gsr-ui/";
|
||||
#endif
|
||||
}
|
||||
|
||||
mgl_context *context = mgl_get_context();
|
||||
|
||||
egl_functions egl_funcs;
|
||||
@@ -327,6 +336,8 @@ int main(int argc, char **argv) {
|
||||
auto overlay = std::make_unique<gsr::Overlay>(resources_path, std::move(gsr_info), std::move(capture_options), egl_funcs);
|
||||
if(launch_action == LaunchAction::LAUNCH_SHOW)
|
||||
overlay->show();
|
||||
else if(launch_action == LaunchAction::LAUNCH_HIDE_ANNOUNCE)
|
||||
overlay->show_notification("Press Alt+Z to open the GPU Screen Recorder UI", 5.0, mgl::Color(255, 255, 255), gsr::get_color_theme().tint_color, gsr::NotificationType::NOTICE, nullptr, gsr::NotificationLevel::ERROR);
|
||||
|
||||
rpc_add_commands(rpc.get(), overlay.get());
|
||||
|
||||
@@ -347,6 +358,8 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
}
|
||||
|
||||
const bool connected_to_display_server = mgl_is_connected_to_display_server();
|
||||
|
||||
fprintf(stderr, "Info: shutting down!\n");
|
||||
rpc.reset();
|
||||
overlay.reset();
|
||||
@@ -360,5 +373,11 @@ int main(int argc, char **argv) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return mgl_is_connected_to_display_server() ? 0 : 1;
|
||||
if(killed)
|
||||
return 0;
|
||||
|
||||
if(connected_to_display_server)
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -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,15 @@ 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(extra_data->is_non_keyboard_device)
|
||||
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);
|
||||
@@ -193,6 +213,8 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
|
||||
//fprintf(stderr, "fd: %d, type: %d, pressed %d, value: %d\n", fd, event.type, event.code, event.value);
|
||||
//}
|
||||
|
||||
const bool prev_grabbed = extra_data->grabbed;
|
||||
|
||||
const bool keyboard_key = is_keyboard_key(event.code);
|
||||
if(event.type == EV_KEY && keyboard_key) {
|
||||
keyboard_event_process_key_state_change(self, &event, extra_data, fd);
|
||||
@@ -211,6 +233,11 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
|
||||
}
|
||||
|
||||
if(extra_data->grabbed) {
|
||||
if(prev_grabbed && !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");
|
||||
@@ -234,14 +261,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 +395,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 +407,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 +514,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 +559,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 +603,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 +634,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 +711,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 +762,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 +959,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 +974,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
@@ -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
@@ -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,17 +45,52 @@ 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, "gsr-global-hotkeys 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, "gsr-global-hotkeys error: expected 0 or 1 arguments, got %d argument(s)\n", argc);
|
||||
usage();
|
||||
@@ -59,7 +102,6 @@ int main(int argc, char **argv) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const uid_t user_id = getuid();
|
||||
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");
|
||||
@@ -68,7 +110,7 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
|
||||
keyboard_event keyboard_ev;
|
||||
if(!keyboard_event_init(&keyboard_ev, true, grab_type)) {
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -50,6 +52,10 @@ static void usage(void) {
|
||||
printf(" Start/stop recording.\n");
|
||||
printf(" toggle-pause\n");
|
||||
printf(" Pause/unpause recording. Only applies to regular recording.\n");
|
||||
printf(" toggle-record-region\n");
|
||||
printf(" Start/stop recording a region.\n");
|
||||
printf(" toggle-record-window\n");
|
||||
printf(" Start/stop recording a window (or desktop portal on Wayland).\n");
|
||||
printf(" toggle-stream\n");
|
||||
printf(" Start/stop streaming.\n");
|
||||
printf(" toggle-replay\n");
|
||||
@@ -78,6 +84,8 @@ static bool is_valid_command(const char *command) {
|
||||
"toggle-show",
|
||||
"toggle-record",
|
||||
"toggle-pause",
|
||||
"toggle-record-region",
|
||||
"toggle-record-window",
|
||||
"toggle-stream",
|
||||
"toggle-replay",
|
||||
"replay-save",
|
||||
@@ -112,15 +120,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;
|
||||
}
|
||||
|
||||