Compare commits

...

48 Commits
1.6.6 ... 1.7.5

Author SHA1 Message Date
dec05eba
609a3e54fd 1.7.5 2025-09-06 19:16:09 +02:00
dec05eba
4e62d12e8c Allow 'sync to content' framerate mode option on wayland (only desktop portal) 2025-09-06 01:58:28 +02:00
dec05eba
b4e003c8f7 Only use mallopt M_MMAP_THRESHOLD if glibc 2025-09-03 18:21:58 +02:00
dec05eba
9efe9d3c91 Add content framerate mode (for x11) 2025-09-01 18:11:16 +02:00
dec05eba
ef4a0fe7cb Update flatpak version reference 2025-08-25 22:30:10 +02:00
dec05eba
dacf6126bf Screenshot: add option to save screenshot to clipboard 2025-08-25 22:26:54 +02:00
dec05eba
9bbec944de GlobalSettings: Add notification speed setting, change recording start notification speed 2025-08-24 22:12:34 +02:00
dec05eba
6a55338b12 Entry: update selection caret when changing masked state 2025-08-07 21:20:05 +02:00
dec05eba
2d3abace0e Fix build 2025-08-07 20:47:14 +02:00
dec05eba
47c02fc6c8 1.7.3 2025-08-07 20:22:20 +02:00
dec05eba
5f8c366b43 Show video codec error messages from gsr
let gsr choose video codec automatically for us when using auto (prefer h264, then hevc and then av1),
fallback to software encoding in gsr ui if none of them are available.
2025-08-07 20:21:22 +02:00
dec05eba
f4ed622510 Entry: add more delimiter for moving 2025-08-07 02:51:55 +02:00
dec05eba
f1ee19d014 Mask stream keys, add button to unmask it 2025-08-07 02:00:35 +02:00
dec05eba
67a8040e57 Entry: use text32 (utf32) instead of text (utf8). This simplifies text editing and other features such as text masking (password) 2025-08-07 00:13:59 +02:00
dec05eba
ff00be30df Entry: implement moving care by word with ctrl+arrow keys 2025-08-06 14:54:25 +02:00
dec05eba
cf282bc225 Minor entry improvements 2025-08-06 03:33:50 +02:00
dec05eba
c05a8290b7 1.7.2 2025-08-06 02:04:23 +02:00
dec05eba
a9e118ea8f Improve entry with cutting off text, vertical scroll, text selection, caret movement, copy, etc 2025-08-06 02:03:48 +02:00
dec05eba
8ed1fe4799 Make custom streaming url backwards compatible 2025-08-03 22:02:49 +02:00
dec05eba
c1d76b5169 Use a separate field for stream key in custom streaming 2025-08-03 16:18:24 +02:00
dec05eba
b1e650c7ec Update flatpak version reference 2025-07-29 23:39:50 +02:00
dec05eba
2e0dc48f3e Fix controller hotplug not always working 2025-07-22 03:08:06 +02:00
dec05eba
d64e698eb1 Show recording/replay duration in notification 2025-07-22 01:13:42 +02:00
dec05eba
315fab99a8 1.7.0 2025-07-21 02:46:07 +02:00
dec05eba
8ffd8de74a Update TODO 2025-07-20 01:56:17 +02:00
dec05eba
ad94cff59e Add lshift + printscreen hotkey to take a screenshot of a window (or desktop portal on wayland) 2025-07-20 01:55:02 +02:00
dec05eba
b64cb6a3fd 1.6.10 2025-07-18 23:46:01 +02:00
dec05eba
182c96d8e9 Hide UI when starting desktop portal capture (because the desktop portal selection needs to be clicked on) 2025-07-18 23:45:34 +02:00
dec05eba
9192b3eba1 1.6.9 2025-07-09 02:48:37 +02:00
dec05eba
dd7aae3191 Fix window capture not working in replay (thanks crosscoder) 2025-07-07 03:59:59 +02:00
dec05eba
3fee07ad4c 1.6.8 2025-07-06 23:06:21 +02:00
dec05eba
2daa8ba4aa gsr running shouldn't be an error condition 2025-07-06 23:05:43 +02:00
dec05eba
a78cefc65b Add better single instance detection (use rpc fifo file existence with unlink to detect process instead of pidof gsr-ui) 2025-07-06 20:38:18 +02:00
dec05eba
a0d1de55d7 Add ellipsis at end of title in notification 2025-06-28 18:56:08 +02:00
dec05eba
c0cd6337fc Remove launch-daemon from flatpak service file 2025-06-24 01:11:39 +02:00
dec05eba
0b8a3815b4 Update flatpak version reference 2025-06-23 13:00:02 +02:00
dec05eba
fc82d73728 Show better desktop portal error message (failed to start, canceled) 2025-06-15 00:55:41 +02:00
dec05eba
0dfcb004e4 Record all applications when selecting 'Record audio from all applications except the selected ones' without selecting any application to exclude 2025-06-11 21:38:22 +02:00
dec05eba
644d3f36d1 Update flatpak version 2025-06-10 11:01:41 +02:00
dec05eba
6607aba30b Update README and TODO 2025-06-10 11:00:45 +02:00
dec05eba
aa62c1bb9a Update flatpak version 2025-06-07 00:59:09 +02:00
dec05eba
9eab194c5f 1.6.7 2025-06-05 00:30:39 +02:00
dec05eba
abeaf5cb61 Better window behavior when wayland application is focused on hyprland 2025-06-04 22:29:32 +02:00
dec05eba
575592a12d Hyprland: fix background of ui not rendering, if waybar is running and it's running in dock mode 2025-06-04 22:13:15 +02:00
dec05eba
d72ce588fb Update flatpak version reference 2025-06-04 20:14:37 +02:00
dec05eba
7d2f2e9b47 Show error notification if another gpu screen recorder process is running when starting the ui 2025-06-04 01:32:30 +02:00
dec05eba
636150ef08 Fallback to cpu encoding if auto video encoder is selected and gpu encoding is not supported. Automatically use correct mp4/webm container depending on video codec 2025-06-03 00:05:34 +02:00
dec05eba
612fe6a9c2 Workaround weird steam input (in-game) behavior where steam triggers playstation button + options when pressing both l3 and r3 at the same time 2025-06-01 00:48:49 +02:00
37 changed files with 1530 additions and 228 deletions

View File

@@ -61,11 +61,13 @@ I'm looking for somebody that can create sound effects for the notifications.
![](https://dec05eba.com/images/settings_page.jpg)
# Known issues
* When the UI is open the wallpaper is shown instead of the game on Hyprland. This is an issue with Hyprland. It cant be fixed until the UI is redesigned to not be a fullscreen overlay.
* Opening the UI when a game is fullscreen can mess up the game window a bit on Hyprland. This is an issue with Hyprland.
* When the UI is open the wallpaper is shown instead of the game on Hyprland. This is an issue with Hyprland. It cant be fixed until the UI is redesigned to not be a fullscreen overlay. Change your waybar dock mode to "dock" in its config to fix this.
* Opening the UI when a game is fullscreen can mess up the game window a bit on Hyprland. This is an issue with Hyprland. Change your waybar dock mode to "dock" in its config to fix this.
* The background of the UI is black when opening the UI while a Wayland application is focused on COSMIC. This is an issue with COSMIC.
* Unable to close the region selection with escape key while a Wayland application is focused on COSMIC. This is an issue with COSMIC.
# FAQ
## I get an error when trying to start the gpu-screen-recorder-ui.service systemd service
If you have previously used the flatpak version of GPU Screen Recorder with the new UI then the non-flatpak version of the systemd service will conflict with that. Run `gsr-ui` to fix that.
## I use a non-qwerty keyboard layout and I have an issue with incorrect keys registered in the software
This is a KDE Plasma Wayland issue. Use `setxkbmap <language>` command, for example `setxkbmap se` to make sure X11 applications (such as this one) gets updated to use your languages keyboard layout.

40
TODO
View File

@@ -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).
@@ -87,6 +85,7 @@ 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 "".
@@ -172,10 +171,43 @@ Add a bug report page that automatically includes system info (make this clear t
Make it possible to change controller hotkeys. Also read from /dev/input/eventN instead of /dev/input/jsN. This is readable for controllers.
Add option to copy screenshot to clipboard. Does it work properly on Wayland compositors? Maybe need to wait until the application becomes Wayland native instead of XWayland.
Show message that replay/streaming has to be restarted if recording settings are changed while replay/streaming is ongoing.
Support vector graphics. Maybe support svg, rendering it to a texture for better performance.
Support freetype for text rendering. Maybe load freetype as runtime (with dlopen) and use that when available and fallback to stb_freetype if not available.
Show .webm container option. It's currently chosen automatically if vp8/vp9 is chosen. The available containers should automatically switch depending on the video codec.
In settings show audio levels for each audio. Maybe show audio level image beside the audio name in the dropdown box and switch to a different image (have 3-4 different images for each level) depending on the volume.
Only use fake cursor on wayland if the focused x11 window is fullscreen.
Create window as a real overlay window, using layer shell protocol, when possible. This will however minimize windows on floating wms. Check if this can be fixed somehow, or only use layer shell in tiling wms.
Add timeout option for screenshots.
Add a window that shows a warning for wayland users, that wayland doesn't support this software and if they experience issues then they should use x11 instead.
Add a window that shows a warning if gpu video encoding isn't supported.
Disable system notifications when recording. Does the notification dbus interface support pausing notifications?
Disable hotkeys if virtual keyboard is found (either at startup or after running), if grab type if not virtual. Show a notification if that happens that hotkeys have been disabled.
Detect if keyboard is locked by listening to gsr-ui virtual keyboard events and if no event is received after pressing a key (when writing to it after receiving input from another keyboard)
then remove the keyboard grab and show a message or something.
This can happen if the gsr-ui virtual keyboard is grabbed by some other software.
Maybe this can be fixed automatically by grabbing gsr-ui virtual keyboard and releasing it just before we write to it and then release it again.
But wont keyboard remapping software grab the keyboard first if they detect it quickly?
If we fail to grab it because some other software did then dont grab any keyboards nor gsr-ui virtual keyboards, just listen to them.
Support localization.
Add option to not capture cursor in screenshot when doing region/window capture.
Window selection doesn't work when a window is fullscreen on x11.
Make it possible to change replay duration of the "save 1 min" and "save 10 min" by adding them to the replay settings as options.
If replay duration is set below the "save 1 min" or "save 10 min" then gray them out and when hovering over those buttons
show a tooltip that says that those buttons cant be used because the replay duration in replay settings is set to a lower value than that (and display the replay duration there).

View File

@@ -2,7 +2,7 @@
Description=GPU Screen Recorder UI Service
[Service]
ExecStart=flatpak run com.dec05eba.gpu_screen_recorder gsr-ui launch-daemon
ExecStart=flatpak run com.dec05eba.gpu_screen_recorder gsr-ui
KillSignal=SIGINT
Restart=on-failure
RestartSec=5s

BIN
images/masked.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

BIN
images/unmasked.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

60
include/ClipboardFile.hpp Normal file
View 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;
};
}

View File

@@ -68,6 +68,7 @@ namespace gsr {
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;
};
@@ -85,6 +86,7 @@ namespace gsr {
struct CustomStreamConfig {
std::string url;
std::string key;
std::string container = "flv";
};
@@ -141,10 +143,12 @@ namespace gsr {
bool restore_portal_session = true;
bool save_screenshot_in_game_folder = false;
bool save_screenshot_to_clipboard = false;
bool show_screenshot_saved_notifications = true;
std::string save_directory;
ConfigHotkey take_screenshot_hotkey;
ConfigHotkey take_screenshot_region_hotkey;
ConfigHotkey take_screenshot_window_hotkey; // Or desktop portal, on wayland
};
struct Config {

View File

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

View File

@@ -10,6 +10,7 @@
#include "AudioPlayer.hpp"
#include "RegionSelector.hpp"
#include "WindowSelector.hpp"
#include "ClipboardFile.hpp"
#include "CursorTracker/CursorTracker.hpp"
#include <mglpp/window/Window.hpp>
@@ -41,6 +42,17 @@ namespace gsr {
SCREENSHOT
};
enum class ScreenshotForceType {
NONE,
REGION,
WINDOW
};
enum class NotificationSpeed {
NORMAL,
FAST
};
class Overlay {
public:
Overlay(std::string resources_path, GsrInfo gsr_info, SupportedCaptureOptions capture_options, egl_functions egl_funcs);
@@ -64,6 +76,7 @@ namespace gsr {
void save_replay_10_min();
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);
bool is_open() const;
bool should_exit(std::string &reason) const;
@@ -74,6 +87,8 @@ namespace gsr {
void unbind_all_keyboard_hotkeys();
void rebind_all_keyboard_hotkeys();
void set_notification_speed(NotificationSpeed notification_speed);
private:
void handle_keyboard_mapping_event();
void on_event(mgl::Event &event);
@@ -87,10 +102,12 @@ namespace gsr {
void close_gpu_screen_recorder_output();
double get_time_passed_in_replay_buffer_seconds();
void update_notification_process_status();
void save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type);
void on_replay_saved(const char *replay_saved_filepath);
void process_gsr_output();
void on_gsr_process_error(int exit_code, NotificationType notification_type);
void update_gsr_process_status();
void update_gsr_screenshot_process_status();
@@ -120,7 +137,7 @@ namespace gsr {
bool on_press_start_replay(bool disable_notification, bool finished_selection);
void on_press_start_record(bool finished_selection);
void on_press_start_stream(bool finished_selection);
void on_press_take_screenshot(bool finished_selection, bool force_region_capture);
void on_press_take_screenshot(bool finished_selection, ScreenshotForceType force_type);
bool update_compositor_texture(const Monitor &monitor);
std::string get_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options);
@@ -212,6 +229,12 @@ namespace gsr {
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;
mgl::Clock recording_duration_clock;
AudioPlayer audio_player;
@@ -228,5 +251,9 @@ namespace gsr {
std::unique_ptr<CursorTracker> cursor_tracker;
mgl::Clock cursor_tracker_update_clock;
bool hide_ui = false;
double notification_duration_multiplier = 1.0;
ClipboardFile clipboard_file;
};
}

View File

@@ -44,6 +44,8 @@ namespace gsr {
mgl::Texture save_texture;
mgl::Texture screenshot_texture;
mgl::Texture trash_texture;
mgl::Texture masked_texture;
mgl::Texture unmasked_texture;
mgl::Texture ps4_home_texture;
mgl::Texture ps4_options_texture;

View File

@@ -19,7 +19,9 @@ namespace gsr {
void draw(mgl::Window &window, mgl::vec2f offset) override;
void add_item(const std::string &text, const std::string &id);
// The item can only be selected if it's enabled
void set_selected_item(const std::string &id, bool trigger_event = true, bool trigger_event_even_if_selection_not_changed = true);
void set_item_enabled(const std::string &id, bool enabled);
const std::string& get_selected_id() const;
mgl::vec2f get_size() override;
@@ -36,6 +38,7 @@ namespace gsr {
mgl::Text text;
std::string id;
mgl::vec2f position;
bool enabled = true;
};
mgl::vec2f max_size;

View File

@@ -4,13 +4,31 @@
#include <functional>
#include <mglpp/graphics/Color.hpp>
#include <mglpp/graphics/Text.hpp>
#include <mglpp/graphics/Text32.hpp>
#include <mglpp/graphics/Rectangle.hpp>
namespace gsr {
using EntryValidateHandler = std::function<bool(std::string &str)>;
class Entry;
enum class EntryValidateHandlerResult {
DENY,
ALLOW,
REPLACED
};
using EntryValidateHandler = std::function<EntryValidateHandlerResult(Entry &entry, const std::u32string &str)>;
struct CaretIndexPos {
int index;
mgl::vec2f pos;
};
class Entry : public Widget {
public:
enum class Direction {
LEFT,
RIGHT
};
Entry(mgl::Font *font, const char *text, float max_width);
Entry(const Entry&) = delete;
Entry& operator=(const Entry&) = delete;
@@ -20,8 +38,11 @@ namespace gsr {
mgl::vec2f get_size() override;
void set_text(std::string str);
const std::string& get_text() const;
EntryValidateHandlerResult set_text(const std::string &str);
std::string get_text() const;
void set_masked(bool masked);
bool is_masked() const;
// Return false to specify that the string should not be accepted. This reverts the string back to its previous value.
// The input can be changed by changing the input parameter and returning true.
@@ -29,10 +50,31 @@ namespace gsr {
std::function<void(const std::string &text)> on_changed;
private:
mgl::Text text;
// Also updates the cursor position
void replace_text(size_t index, size_t size, const std::u32string &replacement);
void move_caret_word(Direction direction, size_t max_codepoints);
EntryValidateHandlerResult set_text_internal(std::u32string str);
void draw_caret(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size);
void draw_caret_selection(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size);
CaretIndexPos find_closest_caret_index_by_position(mgl::vec2f position);
private:
struct Caret {
float offset_x = 0.0f;
int index = 0;
};
mgl::Rectangle background;
mgl::Text32 text;
mgl::Text32 masked_text;
float max_width;
bool selected = false;
float caret_offset_x = 0.0f;
bool selecting_text = false;
bool selecting_with_keyboard = false;
bool show_selection = false;
bool masked = false;
Caret caret;
Caret selection_start_caret;
float text_overflow = 0.0f;
};
EntryValidateHandler create_entry_validator_integer_in_range(int min, int max);

View File

@@ -29,6 +29,7 @@ namespace gsr {
STREAM_START_STOP,
TAKE_SCREENSHOT,
TAKE_SCREENSHOT_REGION,
TAKE_SCREENSHOT_WINDOW,
SHOW_HIDE
};
@@ -63,11 +64,13 @@ namespace gsr {
std::unique_ptr<List> create_stream_hotkey_options();
std::unique_ptr<List> create_screenshot_hotkey_options();
std::unique_ptr<List> create_screenshot_region_hotkey_options();
std::unique_ptr<List> create_screenshot_window_hotkey_options();
std::unique_ptr<List> create_hotkey_control_buttons();
std::unique_ptr<Subsection> create_keyboard_hotkey_subsection(ScrollablePage *parent_page);
std::unique_ptr<Subsection> create_controller_hotkey_subsection(ScrollablePage *parent_page);
std::unique_ptr<Button> create_exit_program_button();
std::unique_ptr<Button> create_go_back_to_old_ui_button();
std::unique_ptr<List> create_notification_speed();
std::unique_ptr<Subsection> create_application_options_subsection(ScrollablePage *parent_page);
std::unique_ptr<Subsection> create_application_info_subsection(ScrollablePage *parent_page);
void add_widgets();
@@ -99,7 +102,9 @@ namespace gsr {
Button *start_stop_streaming_button_ptr = nullptr;
Button *take_screenshot_button_ptr = nullptr;
Button *take_screenshot_region_button_ptr = nullptr;
Button *take_screenshot_window_button_ptr = nullptr;
Button *show_hide_button_ptr = nullptr;
RadioButton *notification_speed_button_ptr = nullptr;
ConfigHotkey configure_config_hotkey;
ConfigureHotkeyType configure_hotkey_type = ConfigureHotkeyType::NONE;

View File

@@ -42,6 +42,7 @@ namespace gsr {
std::unique_ptr<List> create_image_format_section();
std::unique_ptr<Widget> create_file_info_section();
std::unique_ptr<CheckBox> create_save_screenshot_in_game_folder();
std::unique_ptr<CheckBox> create_save_screenshot_to_clipboard();
std::unique_ptr<Widget> create_general_section();
std::unique_ptr<Widget> create_notifications_section();
std::unique_ptr<Widget> create_settings();
@@ -69,6 +70,7 @@ namespace gsr {
ComboBox *image_format_box_ptr = nullptr;
Button *save_directory_button_ptr = nullptr;
CheckBox *save_screenshot_in_game_folder_checkbox_ptr = nullptr;
CheckBox *save_screenshot_to_clipboard_checkbox_ptr = nullptr;
CheckBox *show_screenshot_saved_notification_checkbox_ptr = nullptr;
PageStack *page_stack = nullptr;

View File

@@ -118,9 +118,11 @@ namespace gsr {
std::unique_ptr<ComboBox> create_streaming_service_box();
std::unique_ptr<List> create_streaming_service_section();
std::unique_ptr<List> create_stream_key_section();
std::unique_ptr<List> create_stream_url_section();
std::unique_ptr<List> create_stream_custom_url();
std::unique_ptr<List> create_stream_custom_key();
std::unique_ptr<List> create_stream_custom_section();
std::unique_ptr<ComboBox> create_stream_container_box();
std::unique_ptr<List> create_stream_container_section();
std::unique_ptr<List> create_stream_container();
void add_stream_widgets();
void load_audio_tracks(const RecordOptions &record_options);
@@ -173,8 +175,7 @@ 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;
@@ -192,6 +193,7 @@ namespace gsr {
Entry *youtube_stream_key_entry_ptr = nullptr;
Entry *rumble_stream_key_entry_ptr = nullptr;
Entry *stream_url_entry_ptr = nullptr;
Entry *stream_key_entry_ptr = nullptr;
Entry *replay_time_entry_ptr = nullptr;
RadioButton *replay_storage_button_ptr = nullptr;
Label *replay_time_label_ptr = nullptr;

View File

@@ -2,6 +2,7 @@
#include <mglpp/system/vec.hpp>
#include <mglpp/graphics/Color.hpp>
#include <mglpp/window/Window.hpp>
namespace mgl {
class Window;
@@ -14,4 +15,5 @@ namespace gsr {
void set_frame_delta_seconds(double frame_delta);
mgl::vec2f scale_keep_aspect_ratio(mgl::vec2f from, mgl::vec2f to);
mgl::vec2f clamp_keep_aspect_ratio(mgl::vec2f from, mgl::vec2f to);
mgl::Scissor scissor_get_sub_area(mgl::Scissor parent, mgl::Scissor child);
}

View File

@@ -45,6 +45,8 @@ namespace gsr {
void set_visible(bool visible);
Widget* get_parent_widget();
void *userdata = nullptr;
protected:
void set_widget_as_selected_in_parent();

View File

@@ -1,4 +1,6 @@
project('gsr-ui', ['c', 'cpp'], version : '1.6.6', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
project('gsr-ui', ['c', 'cpp'], version : '1.7.5', 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'])
@@ -47,6 +49,7 @@ src = [
'src/Overlay.cpp',
'src/AudioPlayer.cpp',
'src/Hotplug.cpp',
'src/ClipboardFile.cpp',
'src/Rpc.cpp',
'src/main.cpp',
]
@@ -62,7 +65,7 @@ datadir = get_option('datadir')
gsr_ui_resources_path = join_paths(prefix, datadir, 'gsr-ui')
add_project_arguments('-DGSR_UI_VERSION="' + meson.project_version() + '"', language: ['c', 'cpp'])
add_project_arguments('-DGSR_FLATPAK_VERSION="5.6.0"', language: ['c', 'cpp'])
add_project_arguments('-DGSR_FLATPAK_VERSION="5.7.8"', language: ['c', 'cpp'])
executable(
meson.project_name(),

View File

@@ -1,7 +1,7 @@
[package]
name = "gsr-ui"
type = "executable"
version = "1.6.6"
version = "1.7.5"
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"

294
src/ClipboardFile.cpp Normal file
View File

@@ -0,0 +1,294 @@
#include "../include/ClipboardFile.hpp"
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <inttypes.h>
#include <poll.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <X11/Xatom.h>
#define FORMAT_I64 "%" PRIi64
#define FORMAT_U64 "%" PRIu64
namespace gsr {
ClipboardFile::ClipboardFile() {
dpy = XOpenDisplay(nullptr);
if(!dpy) {
fprintf(stderr, "gsr ui: error: ClipboardFile: failed to connect to the X11 server\n");
return;
}
clipboard_window = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0, 0, 8, 8, 0, 0, 0);
if(!clipboard_window) {
fprintf(stderr, "gsr ui: error: ClipboardFile: failed to create clipboard window\n");
XCloseDisplay(dpy);
dpy = nullptr;
return;
}
incr_atom = XInternAtom(dpy, "INCR", False);
targets_atom = XInternAtom(dpy, "TARGETS", False);
clipboard_atom = XInternAtom(dpy, "CLIPBOARD", False);
image_jpg_atom = XInternAtom(dpy, "image/jpg", False);
image_jpeg_atom = XInternAtom(dpy, "image/jpeg", False);
image_png_atom = XInternAtom(dpy, "image/png", False);
event_thread = std::thread([&]() {
pollfd poll_fds[1];
poll_fds[0].fd = ConnectionNumber(dpy);
poll_fds[0].events = POLLIN;
poll_fds[0].revents = 0;
XEvent xev;
while(running) {
poll(poll_fds, 1, 100);
while(XPending(dpy)) {
XNextEvent(dpy, &xev);
switch(xev.type) {
case SelectionClear: {
should_clear_selection = true;
if(clipboard_copies.empty()) {
should_clear_selection = false;
set_current_file("", file_type);
}
break;
}
case SelectionRequest:
send_clipboard_start(&xev.xselectionrequest);
break;
case PropertyNotify: {
if(xev.xproperty.state == PropertyDelete) {
std::lock_guard<std::mutex> lock(mutex);
ClipboardCopy *clipboard_copy = get_clipboard_copy_by_requestor(xev.xproperty.window);
if(!clipboard_copy || xev.xproperty.atom != clipboard_copy->property)
return;
XSelectionRequestEvent xselectionrequest;
xselectionrequest.display = xev.xproperty.display;;
xselectionrequest.requestor = xev.xproperty.window;
xselectionrequest.selection = clipboard_atom;
xselectionrequest.target = clipboard_copy->requestor_target;
xselectionrequest.property = clipboard_copy->property;
xselectionrequest.time = xev.xproperty.time;
transfer_clipboard_data(&xselectionrequest, clipboard_copy);
}
break;
}
}
}
}
});
}
ClipboardFile::~ClipboardFile() {
running = false;
if(event_thread.joinable())
event_thread.join();
if(file_fd > 0)
close(file_fd);
if(dpy) {
XDestroyWindow(dpy, clipboard_window);
XCloseDisplay(dpy);
}
}
bool ClipboardFile::file_type_matches_request_atom(FileType file_type, Atom request_atom) {
switch(file_type) {
case FileType::JPG:
return request_atom == image_jpg_atom || request_atom == image_jpeg_atom;
case FileType::PNG:
return request_atom == image_png_atom;
}
return false;
}
const char* ClipboardFile::file_type_clipboard_get_name(Atom request_atom) {
if(request_atom == image_jpg_atom)
return "image/jpg";
else if(request_atom == image_jpeg_atom)
return "image/jpeg";
else if(request_atom == image_png_atom)
return "image/png";
return "Unknown";
}
const char* ClipboardFile::file_type_get_name(FileType file_type) {
switch(file_type) {
case FileType::JPG:
return "image/jpeg";
case FileType::PNG:
return "image/png";
}
return "Unknown";
}
void ClipboardFile::send_clipboard_start(XSelectionRequestEvent *xselectionrequest) {
std::lock_guard<std::mutex> lock(mutex);
if(file_fd <= 0) {
fprintf(stderr, "gsr ui: warning: ClipboardFile::send_clipboard: requestor window " FORMAT_I64 " tried to get clipboard from us but we don't have any clipboard file open\n", (int64_t)xselectionrequest->requestor);
return;
}
if(xselectionrequest->selection != clipboard_atom) {
fprintf(stderr, "gsr ui: warning: ClipboardFile::send_clipboard: requestor window " FORMAT_I64 " tried to non-clipboard selection from us\n", (int64_t)xselectionrequest->requestor);
return;
}
XSelectionEvent selection_event;
selection_event.type = SelectionNotify;
selection_event.display = xselectionrequest->display;
selection_event.requestor = xselectionrequest->requestor;
selection_event.selection = xselectionrequest->selection;
selection_event.property = xselectionrequest->property;
selection_event.time = xselectionrequest->time;
selection_event.target = xselectionrequest->target;
if(xselectionrequest->target == targets_atom) {
int num_targets = 1;
Atom targets[3];
targets[0] = targets_atom;
switch(file_type) {
case FileType::JPG:
num_targets = 3;
targets[1] = image_jpg_atom;
targets[2] = image_jpeg_atom;
break;
case FileType::PNG:
num_targets = 2;
targets[1] = image_png_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) {
if(!file_type_matches_request_atom(file_type, xselectionrequest->target)) {
fprintf(stderr, "gsr ui: warning: ClipboardFile::send_clipboard: requestor window " FORMAT_I64 " tried to request clipboard of type %s, but %s was expected\n", (int64_t)xselectionrequest->requestor, file_type_clipboard_get_name(xselectionrequest->target), file_type_get_name(file_type));
return;
}
ClipboardCopy *clipboard_copy = get_clipboard_copy_by_requestor(xselectionrequest->requestor);
if(!clipboard_copy) {
clipboard_copies.push_back({ xselectionrequest->requestor, 0 });
clipboard_copy = &clipboard_copies.back();
}
*clipboard_copy = { xselectionrequest->requestor, 0 };
clipboard_copy->property = selection_event.property;
clipboard_copy->requestor_target = selection_event.target;
XSelectInput(dpy, selection_event.requestor, PropertyChangeMask);
const long lower_bound = std::min((uint64_t)1<<16, file_size);
XChangeProperty(dpy, selection_event.requestor, selection_event.property, incr_atom, 32, PropModeReplace, (const unsigned char*)&lower_bound, 1);
} else {
char *target_clipboard_name = XGetAtomName(dpy, xselectionrequest->target);
fprintf(stderr, "gsr ui: warning: ClipboardFile::send_clipboard: requestor window " FORMAT_I64 " tried to request clipboard of type %s, expected TARGETS, image/jpg, image/jpeg or image/png\n", (int64_t)xselectionrequest->requestor, target_clipboard_name ? target_clipboard_name : "Unknown");
if(target_clipboard_name)
XFree(target_clipboard_name);
selection_event.property = None;
}
XSendEvent(dpy, selection_event.requestor, False, NoEventMask, (XEvent*)&selection_event);
XFlush(dpy);
}
void ClipboardFile::transfer_clipboard_data(XSelectionRequestEvent *xselectionrequest, ClipboardCopy *clipboard_copy) {
uint8_t file_buffer[1<<16];
ssize_t file_bytes_read = 0;
if(lseek(file_fd, clipboard_copy->file_offset, SEEK_SET) == -1) {
fprintf(stderr, "gsr ui: error: ClipboardFile::send_clipboard: failed to seek in clipboard file to offset " FORMAT_U64 " for requestor window " FORMAT_I64 ", error: %s\n", (uint64_t)clipboard_copy->file_offset, (int64_t)xselectionrequest->requestor, strerror(errno));
clipboard_copy->file_offset = 0;
// TODO: Cancel transfer
return;
}
file_bytes_read = read(file_fd, file_buffer, sizeof(file_buffer));
if(file_bytes_read < 0) {
fprintf(stderr, "gsr ui: error: ClipbaordFile::send_clipboard: failed to read data from offset " FORMAT_U64 " for requestor window " FORMAT_I64 ", error: %s\n", (uint64_t)clipboard_copy->file_offset, (int64_t)xselectionrequest->requestor, strerror(errno));
clipboard_copy->file_offset = 0;
// TODO: Cancel transfer
return;
}
XChangeProperty(dpy, xselectionrequest->requestor, xselectionrequest->property, xselectionrequest->target, 8, PropModeReplace, (const unsigned char*)file_buffer, file_bytes_read);
XSendEvent(dpy, xselectionrequest->requestor, False, NoEventMask, (XEvent*)xselectionrequest);
XFlush(dpy);
clipboard_copy->file_offset += file_bytes_read;
if(file_bytes_read == 0)
remove_clipboard_copy(clipboard_copy->requestor);
}
ClipboardCopy* ClipboardFile::get_clipboard_copy_by_requestor(Window requestor) {
for(ClipboardCopy &clipboard_copy : clipboard_copies) {
if(clipboard_copy.requestor == requestor)
return &clipboard_copy;
}
return nullptr;
}
void ClipboardFile::remove_clipboard_copy(Window requestor) {
for(auto it = clipboard_copies.begin(), end = clipboard_copies.end(); it != end; ++it) {
if(it->requestor == requestor) {
clipboard_copies.erase(it);
XSelectInput(dpy, requestor, 0);
if(clipboard_copies.empty() && should_clear_selection) {
should_clear_selection = false;
set_current_file("", file_type);
}
return;
}
}
}
void ClipboardFile::set_current_file(const std::string &filepath, FileType file_type) {
if(!dpy)
return;
std::lock_guard<std::mutex> lock(mutex);
for(ClipboardCopy &clipboard_copy : clipboard_copies) {
XSelectInput(dpy, clipboard_copy.requestor, 0);
}
clipboard_copies.clear();
if(filepath.empty()) {
if(file_fd > 0) {
close(file_fd);
file_fd = -1;
}
file_size = 0;
return;
}
if(file_fd > 0) {
close(file_fd);
file_fd = -1;
file_size = 0;
}
file_fd = open(filepath.c_str(), O_RDONLY);
if(file_fd <= 0) {
fprintf(stderr, "gsr ui: error: ClipboardFile::set_current_file: failed to open file %s, error: %s\n", filepath.c_str(), strerror(errno));
return;
}
struct stat64 stat;
if(fstat64(file_fd, &stat) == -1) {
fprintf(stderr, "gsr ui: error: ClipboardFile::set_current_file: failed to get file size for file %s, error: %s\n", filepath.c_str(), strerror(errno));
close(file_fd);
file_fd = -1;
return;
}
file_size = stat.st_size;
this->file_type = file_type;
XSetSelectionOwner(dpy, clipboard_atom, clipboard_window, CurrentTime);
XFlush(dpy);
}
}

View File

@@ -153,6 +153,7 @@ namespace gsr {
screenshot_config.take_screenshot_hotkey = {mgl::Keyboard::Printscreen, 0};
screenshot_config.take_screenshot_region_hotkey = {mgl::Keyboard::Printscreen, HOTKEY_MOD_LCTRL};
screenshot_config.take_screenshot_window_hotkey = {mgl::Keyboard::Printscreen, HOTKEY_MOD_LSHIFT};
main_config.show_hide_hotkey = {mgl::Keyboard::Z, HOTKEY_MOD_LALT};
}
@@ -173,6 +174,7 @@ namespace gsr {
{"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},
@@ -203,6 +205,7 @@ namespace gsr {
{"streaming.twitch.key", &config.streaming_config.twitch.stream_key},
{"streaming.rumble.key", &config.streaming_config.rumble.stream_key},
{"streaming.custom.url", &config.streaming_config.custom.url},
{"streaming.custom.key", &config.streaming_config.custom.key},
{"streaming.custom.container", &config.streaming_config.custom.container},
{"streaming.start_stop_hotkey", &config.streaming_config.start_stop_hotkey},
@@ -281,10 +284,12 @@ 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.save_screenshot_to_clipboard", &config.screenshot_config.save_screenshot_to_clipboard},
{"screenshot.show_screenshot_saved_notifications", &config.screenshot_config.show_screenshot_saved_notifications},
{"screenshot.save_directory", &config.screenshot_config.save_directory},
{"screenshot.take_screenshot_hotkey", &config.screenshot_config.take_screenshot_hotkey},
{"screenshot.take_screenshot_region_hotkey", &config.screenshot_config.take_screenshot_region_hotkey}
{"screenshot.take_screenshot_region_hotkey", &config.screenshot_config.take_screenshot_region_hotkey},
{"screenshot.take_screenshot_window_hotkey", &config.screenshot_config.take_screenshot_window_hotkey}
};
}

View File

@@ -11,6 +11,8 @@ namespace gsr {
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;
@@ -100,7 +102,8 @@ namespace gsr {
close(event_fd);
for(int i = 0; i < num_poll_fd; ++i) {
close(poll_fd[i].fd);
if(poll_fd[i].fd > 0)
close(poll_fd[i].fd);
}
}
@@ -219,6 +222,9 @@ namespace gsr {
if(i == event_index)
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);
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
@@ -232,18 +238,13 @@ namespace gsr {
goto done;
} else if(i == hotplug_poll_index) {
hotplug.process_event_data(poll_fd[i].fd, [&](HotplugAction hotplug_action, const char *devname) {
char dev_input_filepath[1024];
snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/%s", devname);
switch(hotplug_action) {
case HotplugAction::ADD: {
// Cant open the /dev/input device immediately or it fails.
// TODO: Remove this hack when a better solution is found.
usleep(50 * 1000);
add_device(dev_input_filepath);
add_device(devname);
break;
}
case HotplugAction::REMOVE: {
if(remove_device(dev_input_filepath))
if(remove_device(devname))
--i; // This item was removed so we want to repeat the same index to continue to the next item
break;
}
@@ -266,7 +267,8 @@ namespace gsr {
if((event.type & JS_EVENT_BUTTON) == JS_EVENT_BUTTON) {
switch(event.number) {
case playstation_button: {
playstation_button_pressed = event.value == button_pressed;
// Workaround weird steam input (in-game) behavior where steam triggers playstation button + options when pressing both l3 and r3 at the same time
playstation_button_pressed = (event.value == button_pressed) && !l3_button_pressed && !r3_button_pressed;
break;
}
case options_button: {
@@ -284,6 +286,14 @@ namespace gsr {
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;
@@ -362,7 +372,9 @@ namespace gsr {
if(index < 0 || index >= num_poll_fd)
return false;
close(poll_fd[index].fd);
if(poll_fd[index].fd > 0)
close(poll_fd[index].fd);
for(int i = index + 1; i < num_poll_fd; ++i) {
poll_fd[i - 1] = poll_fd[i];
extra_data[i - 1] = extra_data[i];

View File

@@ -59,10 +59,9 @@ namespace gsr {
/* TODO: This assumes SUBSYSTEM= is output before DEVNAME=, is that always true? */
void Hotplug::parse_netlink_data(const char *line, const HotplugEventCallback &callback) {
const char *at_symbol = strchr(line, '@');
if(at_symbol) {
event_is_add = strncmp(line, "add@", 4) == 0;
event_is_remove = strncmp(line, "remove@", 7) == 0;
if(strncmp(line, "ACTION=", 7) == 0) {
event_is_add = strncmp(line+7, "add", 3) == 0;
event_is_remove = strncmp(line+7, "remove", 6) == 0;
subsystem_is_input = false;
} else if(event_is_add || event_is_remove) {
if(strcmp(line, "SUBSYSTEM=input") == 0)

View File

@@ -27,6 +27,7 @@
#include <stdexcept>
#include <algorithm>
#include <inttypes.h>
#include <math.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
@@ -49,7 +50,8 @@ namespace gsr {
static const double force_window_on_top_timeout_seconds = 1.0;
static const double replay_status_update_check_timeout_seconds = 1.5;
static const double replay_saving_notification_timeout_seconds = 0.5;
static const double notification_timeout_seconds = 2.5;
static const double short_notification_timeout_seconds = 2.0;
static const double notification_timeout_seconds = 3.0;
static const double notification_error_timeout_seconds = 5.0;
static const double cursor_tracker_update_timeout_sec = 0.1;
@@ -273,6 +275,33 @@ namespace gsr {
return true;
}
static bool is_hyprland_waybar_running_as_dock() {
const char *args[] = { "hyprctl", "layers", nullptr };
std::string stdout_str;
if(exec_program_on_host_get_stdout(args, stdout_str) != 0)
return false;
int waybar_layer_level = -1;
int current_layer_level = 0;
string_split_char(stdout_str, '\n', [&](const std::string_view line) {
if(line.find("Layer level 0") != std::string_view::npos)
current_layer_level = 0;
else if(line.find("Layer level 1") != std::string_view::npos)
current_layer_level = 1;
else if(line.find("Layer level 2") != std::string_view::npos)
current_layer_level = 2;
else if(line.find("Layer level 3") != std::string_view::npos)
current_layer_level = 3;
else if(line.find("namespace: waybar") != std::string_view::npos) {
waybar_layer_level = current_layer_level;
return false;
}
return true;
});
return waybar_layer_level >= 0 && waybar_layer_level <= 1;
}
static Hotkey config_hotkey_to_hotkey(ConfigHotkey config_hotkey) {
return {
(uint32_t)mgl::Keyboard::key_to_x11_keysym((mgl::Keyboard::Key)config_hotkey.key),
@@ -351,6 +380,13 @@ namespace gsr {
overlay->take_screenshot_region();
});
global_hotkeys->bind_key_press(
config_hotkey_to_hotkey(overlay->get_config().screenshot_config.take_screenshot_window_hotkey),
"take_screenshot_window", [overlay](const std::string &id) {
fprintf(stderr, "pressed %s\n", id.c_str());
overlay->take_screenshot_window();
});
global_hotkeys->bind_key_press(
config_hotkey_to_hotkey(ConfigHotkey{ mgl::Keyboard::Key::Escape, HOTKEY_MOD_LCTRL | HOTKEY_MOD_LSHIFT | HOTKEY_MOD_LALT }),
"exit", [overlay](const std::string &id) {
@@ -411,6 +447,17 @@ namespace gsr {
return global_hotkeys_js;
}
static NotificationSpeed to_notification_speed(const std::string &notification_speed_str) {
if(notification_speed_str == "normal")
return NotificationSpeed::NORMAL;
else if(notification_speed_str == "fast")
return NotificationSpeed::FAST;
else {
assert(false);
return NotificationSpeed::NORMAL;
}
}
Overlay::Overlay(std::string resources_path, GsrInfo gsr_info, SupportedCaptureOptions capture_options, egl_functions egl_funcs) :
resources_path(std::move(resources_path)),
gsr_info(std::move(gsr_info)),
@@ -439,6 +486,7 @@ namespace gsr {
power_supply_online_filepath = get_power_supply_online_filepath();
replay_startup_mode = replay_startup_string_to_type(config.replay_config.turn_on_replay_automatically_mode.c_str());
set_notification_speed(to_notification_speed(config.main_config.notification_speed));
if(config.main_config.hotkeys_enable_option == "enable_hotkeys")
global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::ALL);
@@ -731,6 +779,12 @@ namespace gsr {
update_gsr_screenshot_process_status();
replay_status_update_status();
if(hide_ui) {
hide_ui = false;
hide();
return false;
}
if(start_region_capture) {
start_region_capture = false;
hide();
@@ -904,6 +958,8 @@ namespace gsr {
const std::string wm_name = get_window_manager_name(display);
const bool is_kwin = wm_name == "KWin";
const bool is_wlroots = wm_name.find("wlroots") != std::string::npos;
const bool is_hyprland = wm_name.find("Hyprland") != std::string::npos;
const bool hyprland_waybar_is_dock = is_hyprland && is_hyprland_waybar_running_as_dock();
std::optional<CursorInfo> cursor_info;
if(cursor_tracker) {
@@ -931,7 +987,7 @@ namespace gsr {
// If the focused window is a wayland application then don't use override redirect and instead create
// a fullscreen window for the ui.
// TODO: (x11_cursor_window && is_window_fullscreen_on_monitor(display, x11_cursor_window, *focused_monitor))
const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || x11_cursor_window || is_wlroots;
const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || x11_cursor_window || is_wlroots || is_hyprland;
if(prevent_game_minimizing) {
window_pos = focused_monitor->position;
@@ -1035,7 +1091,7 @@ namespace gsr {
// Owlboy seems to use xi events and XGrabPointer doesn't prevent owlboy from receiving events.
xi_grab_all_mouse_devices(xi_display);
if(!is_wlroots)
if(!is_wlroots && !hyprland_waybar_is_dock)
window->set_fullscreen(true);
visible = true;
@@ -1309,6 +1365,8 @@ namespace gsr {
if(!visible)
return;
hide_ui = false;
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
@@ -1444,11 +1502,15 @@ namespace gsr {
}
void Overlay::take_screenshot() {
on_press_take_screenshot(false, false);
on_press_take_screenshot(false, ScreenshotForceType::NONE);
}
void Overlay::take_screenshot_region() {
on_press_take_screenshot(false, true);
on_press_take_screenshot(false, ScreenshotForceType::REGION);
}
void Overlay::take_screenshot_window() {
on_press_take_screenshot(false, ScreenshotForceType::WINDOW);
}
static const char* notification_type_to_string(NotificationType notification_type) {
@@ -1477,7 +1539,10 @@ namespace gsr {
byte_index += codepoint_length;
}
str.erase(byte_index);
if(byte_index < str.size()) {
str.erase(byte_index);
str += "...";
}
}
static bool is_hex_num(char c) {
@@ -1522,7 +1587,7 @@ namespace gsr {
return strcmp(capture_target, "window") != 0 && strcmp(capture_target, "focused") != 0 && strcmp(capture_target, "region") != 0 && strcmp(capture_target, "portal") != 0 && contains_non_hex_number(capture_target);
}
static std::string capture_target_get_notification_name(const char *capture_target) {
static std::string capture_target_get_notification_name(const char *capture_target, bool save) {
std::string result;
if(is_capture_target_monitor(capture_target)) {
result = "this monitor";
@@ -1534,9 +1599,11 @@ namespace gsr {
sscanf(capture_target, "%" PRIi64, &window_id);
const std::optional<std::string> window_title = get_window_title(display, window_id);
if(window_title) {
if(save) {
result = "window";
} else if(window_title) {
result = strip(window_title.value());
truncate_string(result, 20);
truncate_string(result, 30);
result = "window \"" + result + "\"";
} else {
result = std::string("window ") + capture_target;
@@ -1591,6 +1658,8 @@ namespace gsr {
}
void Overlay::show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type, const char *capture_target) {
timeout_seconds *= notification_duration_multiplier;
char timeout_seconds_str[32];
snprintf(timeout_seconds_str, sizeof(timeout_seconds_str), "%f", timeout_seconds);
@@ -1683,6 +1752,17 @@ namespace gsr {
bind_linux_hotkeys(static_cast<GlobalHotkeysLinux*>(global_hotkeys.get()), this);
}
void Overlay::set_notification_speed(NotificationSpeed notification_speed) {
switch(notification_speed) {
case NotificationSpeed::NORMAL:
notification_duration_multiplier = 1.0;
break;
case NotificationSpeed::FAST:
notification_duration_multiplier = 0.3;
break;
}
}
void Overlay::update_notification_process_status() {
if(notification_process <= 0)
return;
@@ -1723,6 +1803,54 @@ namespace gsr {
return result;
}
static std::string to_duration_string(double duration_sec) {
int seconds = ceil(duration_sec);
const int hours = seconds / 60 / 60;
seconds -= (hours * 60 * 60);
const int minutes = seconds / 60;
seconds -= (minutes * 60);
std::string result;
if(hours > 0)
result += std::to_string(hours) + " hour" + (hours == 1 ? "" : "s");
if(minutes > 0) {
if(!result.empty())
result += " ";
result += std::to_string(minutes) + " minute" + (minutes == 1 ? "" : "s");
}
if(seconds > 0 || (hours == 0 && minutes == 0)) {
if(!result.empty())
result += " ";
result += std::to_string(seconds) + " second" + (seconds == 1 ? "" : "s");
}
fprintf(stderr, "to duration string: %f, %d, %d, %d\n", duration_sec, seconds, minutes, hours);
return result;
}
double Overlay::get_time_passed_in_replay_buffer_seconds() {
double replay_duration_sec = replay_saved_duration_sec;
if(replay_duration_sec > replay_buffer_save_duration_sec)
replay_duration_sec = replay_buffer_save_duration_sec;
if(replay_save_duration_min > 0 && replay_duration_sec > replay_save_duration_min * 60)
replay_duration_sec = replay_save_duration_min * 60;
return replay_duration_sec;
}
static ClipboardFile::FileType filename_to_clipboard_file_type(const std::string &filename) {
if(ends_with(filename, ".jpg") || ends_with(filename, ".jpeg"))
return ClipboardFile::FileType::JPG;
else if(ends_with(filename, ".png"))
return ClipboardFile::FileType::PNG;
assert(false);
return ClipboardFile::FileType::PNG;
}
void Overlay::save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type) {
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
@@ -1743,7 +1871,7 @@ namespace gsr {
const std::string new_video_filepath = video_directory + "/" + video_filename;
rename(video_filepath, new_video_filepath.c_str());
truncate_string(focused_window_name, 20);
truncate_string(focused_window_name, 40);
const char *capture_target = nullptr;
char msg[512];
@@ -1752,7 +1880,10 @@ namespace gsr {
if(!config.record_config.show_video_saved_notifications)
return;
snprintf(msg, sizeof(msg), "Saved a recording of %s to \"%s\"", capture_target_get_notification_name(recording_capture_target.c_str()).c_str(), focused_window_name.c_str());
const std::string duration_str = to_duration_string(recording_duration_clock.get_elapsed_time_seconds());
snprintf(msg, sizeof(msg), "Saved a %s recording of %s\nto \"%s\"",
duration_str.c_str(),
capture_target_get_notification_name(recording_capture_target.c_str(), true).c_str(), focused_window_name.c_str());
capture_target = recording_capture_target.c_str();
break;
}
@@ -1760,13 +1891,10 @@ namespace gsr {
if(!config.replay_config.show_replay_saved_notifications)
return;
char duration[32];
if(replay_save_duration_min > 0)
snprintf(duration, sizeof(duration), " %d minute ", replay_save_duration_min);
else
snprintf(duration, sizeof(duration), " ");
snprintf(msg, sizeof(msg), "Saved a%sreplay of %s to \"%s\"", duration, capture_target_get_notification_name(recording_capture_target.c_str()).c_str(), focused_window_name.c_str());
const std::string duration_str = to_duration_string(get_time_passed_in_replay_buffer_seconds());
snprintf(msg, sizeof(msg), "Saved a %s replay of %s\nto \"%s\"",
duration_str.c_str(),
capture_target_get_notification_name(recording_capture_target.c_str(), true).c_str(), focused_window_name.c_str());
capture_target = recording_capture_target.c_str();
break;
}
@@ -1774,8 +1902,12 @@ namespace gsr {
if(!config.screenshot_config.show_screenshot_saved_notifications)
return;
snprintf(msg, sizeof(msg), "Saved a screenshot of %s to \"%s\"", capture_target_get_notification_name(screenshot_capture_target.c_str()).c_str(), focused_window_name.c_str());
snprintf(msg, sizeof(msg), "Saved a screenshot of %s\nto \"%s\"",
capture_target_get_notification_name(screenshot_capture_target.c_str(), true).c_str(), focused_window_name.c_str());
capture_target = screenshot_capture_target.c_str();
if(config.screenshot_config.save_screenshot_to_clipboard)
clipboard_file.set_current_file(new_video_filepath, filename_to_clipboard_file_type(new_video_filepath));
break;
}
case NotificationType::NONE:
@@ -1800,14 +1932,12 @@ namespace gsr {
if(config.replay_config.save_video_in_game_folder) {
save_video_in_current_game_directory(replay_saved_filepath, NotificationType::REPLAY);
} else if(config.replay_config.show_replay_saved_notifications) {
char duration[32];
if(replay_save_duration_min > 0)
snprintf(duration, sizeof(duration), " %d minute ", replay_save_duration_min);
else
snprintf(duration, sizeof(duration), " ");
const std::string duration_str = to_duration_string(get_time_passed_in_replay_buffer_seconds());
char msg[512];
snprintf(msg, sizeof(msg), "Saved a%sreplay of %s", duration, capture_target_get_notification_name(recording_capture_target.c_str()).c_str());
snprintf(msg, sizeof(msg), "Saved a %s replay of %s",
duration_str.c_str(),
capture_target_get_notification_name(recording_capture_target.c_str(), true).c_str());
show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY, recording_capture_target.c_str());
}
}
@@ -1856,6 +1986,45 @@ namespace gsr {
}
}
void Overlay::on_gsr_process_error(int exit_code, NotificationType notification_type) {
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
if(exit_code == 50) {
show_notification("Desktop portal capture failed.\nEither you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture\nor it's incorrectly setup on your system.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else if(exit_code == 51) {
show_notification("Monitor capture failed.\nThe monitor you are trying to capture is invalid.\nPlease validate your capture settings.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else if(exit_code == 52) {
show_notification("Capture failed. Neither H264, HEVC nor AV1 video codecs are supported\non your system or you are trying to capture at a resolution higher than your\nsystem supports for each video codec.", 10.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else if(exit_code == 53) {
show_notification("Capture failed. Your system doesn't support the resolution you are trying to\nrecord at with the video codec you have chosen.\nChange capture resolution or video codec and try again.\nNote: AV1 supports the highest resolution, then HEVC and then H264.", 10.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else if(exit_code == 54) {
show_notification("Capture failed. Your system doesn't support the video codec you have chosen.\nChange video codec and try again.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else if(exit_code == 60) {
show_notification("Stopped capture because the user canceled the desktop portal", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else {
const char *prefix = "";
switch(notification_type) {
case NotificationType::NONE:
break;
case NotificationType::SCREENSHOT:
prefix = "Failed to take a screenshot";
break;
case NotificationType::RECORD:
prefix = "Failed to start/save recording";
break;
case NotificationType::REPLAY:
prefix = "Replay stopped because of an error";
break;
case NotificationType::STREAM:
prefix = "Streaming stopped because of an error";
break;
}
char msg[256];
snprintf(msg, sizeof(msg), "%s. Verify if settings are correct", prefix);
show_notification(msg, notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
}
}
void Overlay::update_gsr_process_status() {
if(gpu_screen_recorder_process <= 0)
return;
@@ -1880,10 +2049,9 @@ namespace gsr {
update_ui_replay_stopped();
if(exit_code == 0) {
if(config.replay_config.show_replay_stopped_notifications)
show_notification("Replay stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
show_notification("Replay stopped", short_notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
} else {
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
show_notification("Replay stopped because of an error. Verify if settings are correct", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY);
on_gsr_process_error(exit_code, NotificationType::REPLAY);
}
break;
}
@@ -1896,10 +2064,9 @@ namespace gsr {
update_ui_streaming_stopped();
if(exit_code == 0) {
if(config.streaming_config.show_streaming_stopped_notifications)
show_notification("Streaming has stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
show_notification("Streaming has stopped", short_notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
} else {
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
show_notification("Streaming stopped because of an error. Verify if settings are correct", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM);
on_gsr_process_error(exit_code, NotificationType::STREAM);
}
break;
}
@@ -1928,8 +2095,12 @@ namespace gsr {
save_video_in_current_game_directory(screenshot_filepath.c_str(), NotificationType::SCREENSHOT);
} else if(config.screenshot_config.show_screenshot_saved_notifications) {
char msg[512];
snprintf(msg, sizeof(msg), "Saved a screenshot of %s", capture_target_get_notification_name(screenshot_capture_target.c_str()).c_str());
snprintf(msg, sizeof(msg), "Saved a screenshot of %s",
capture_target_get_notification_name(screenshot_capture_target.c_str(), true).c_str());
show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::SCREENSHOT, screenshot_capture_target.c_str());
if(config.screenshot_config.save_screenshot_to_clipboard)
clipboard_file.set_current_file(screenshot_filepath, filename_to_clipboard_file_type(screenshot_filepath));
}
} else {
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_screenshot_process, exit_code);
@@ -2026,13 +2197,16 @@ namespace gsr {
if(config.record_config.save_video_in_game_folder) {
save_video_in_current_game_directory(video_filepath.c_str(), NotificationType::RECORD);
} else if(config.record_config.show_video_saved_notifications) {
const std::string duration_str = to_duration_string(recording_duration_clock.get_elapsed_time_seconds());
char msg[512];
snprintf(msg, sizeof(msg), "Saved a recording of %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str());
snprintf(msg, sizeof(msg), "Saved a %s recording of %s",
duration_str.c_str(),
capture_target_get_notification_name(recording_capture_target.c_str(), true).c_str());
show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str());
}
} else {
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
show_notification("Failed to start/save recording. Verify if settings are correct", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD);
on_gsr_process_error(exit_code, NotificationType::RECORD);
}
update_ui_recording_stopped();
replay_recording = false;
@@ -2156,6 +2330,8 @@ namespace gsr {
for(const AudioTrack &audio_track : audio_tracks) {
std::string audio_track_merged;
int num_app_audio = 0;
for(const std::string &audio_input_name : audio_track.audio_inputs) {
std::string new_audio_input_name = audio_input_name;
const bool is_app_audio = starts_with(new_audio_input_name, "app:");
@@ -2165,12 +2341,22 @@ namespace gsr {
if(is_app_audio && audio_track.application_audio_invert)
new_audio_input_name.replace(0, 4, "app-inverse:");
if(is_app_audio)
++num_app_audio;
if(!audio_track_merged.empty())
audio_track_merged += "|";
audio_track_merged += new_audio_input_name;
}
if(num_app_audio == 0 && audio_track.application_audio_invert) {
if(!audio_track_merged.empty())
audio_track_merged += "|";
audio_track_merged += "app-inverse:";
}
if(!audio_track_merged.empty())
result.push_back(std::move(audio_track_merged));
}
@@ -2305,6 +2491,9 @@ namespace gsr {
replay_save_duration_min = 0;
replay_save_show_notification = true;
replay_save_clock.restart();
replay_saved_duration_sec = replay_duration_clock.get_elapsed_time_seconds();
if(replay_restart_on_save)
replay_duration_clock.restart();
kill(gpu_screen_recorder_process, SIGUSR1);
}
@@ -2315,6 +2504,7 @@ namespace gsr {
replay_save_duration_min = 1;
replay_save_show_notification = true;
replay_save_clock.restart();
replay_saved_duration_sec = replay_duration_clock.get_elapsed_time_seconds();
kill(gpu_screen_recorder_process, SIGRTMIN+3);
}
@@ -2325,9 +2515,59 @@ namespace gsr {
replay_save_duration_min = 10;
replay_save_show_notification = true;
replay_save_clock.restart();
replay_saved_duration_sec = replay_duration_clock.get_elapsed_time_seconds();
kill(gpu_screen_recorder_process, SIGRTMIN+5);
}
static const char* get_first_usable_hardware_video_codec_name(const GsrInfo &gsr_info) {
if(gsr_info.supported_video_codecs.h264)
return "h264";
else if(gsr_info.supported_video_codecs.hevc)
return "hevc";
else if(gsr_info.supported_video_codecs.av1)
return "av1";
else if(gsr_info.supported_video_codecs.vp8)
return "vp8";
else if(gsr_info.supported_video_codecs.vp9)
return "vp9";
return nullptr;
}
static const char* change_container_if_codec_not_supported(const char *video_codec, const char *container) {
if(strcmp(video_codec, "vp8") == 0 || strcmp(video_codec, "vp9") == 0) {
if(strcmp(container, "webm") != 0 && strcmp(container, "matroska") != 0) {
fprintf(stderr, "Warning: container '%s' is not compatible with video codec '%s', using webm container instead\n", container, video_codec);
return "webm";
}
} else if(strcmp(container, "webm") == 0) {
fprintf(stderr, "Warning: container webm is not compatible with video codec '%s', using mp4 container instead\n", video_codec);
return "mp4";
}
return container;
}
static void choose_video_codec_and_container_with_fallback(const GsrInfo &gsr_info, const char **video_codec, const char **container, const char **encoder) {
*encoder = "gpu";
if(strcmp(*video_codec, "h264_software") == 0) {
*video_codec = "h264";
*encoder = "cpu";
} else if(strcmp(*video_codec, "auto") == 0) {
if(!get_first_usable_hardware_video_codec_name(gsr_info)) {
*video_codec = "h264";
*encoder = "cpu";
}
}
*container = change_container_if_codec_not_supported(*video_codec, *container);
}
static std::string get_framerate_mode_validate(const RecordOptions &record_options, const GsrInfo &gsr_info) {
(void)gsr_info;
std::string framerate_mode = record_options.framerate_mode;
if(framerate_mode == "auto")
framerate_mode = "vfr";
return framerate_mode;
}
bool Overlay::on_press_start_replay(bool disable_notification, bool finished_selection) {
if(region_selector.is_started() || window_selector.is_started())
return false;
@@ -2372,9 +2612,9 @@ namespace gsr {
const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info);
recording_capture_target = get_capture_target(config.replay_config.record_options.record_area_option, capture_options);
if(!validate_capture_target(recording_capture_target, capture_options)) {
if(!validate_capture_target(config.replay_config.record_options.record_area_option, capture_options)) {
char err_msg[256];
snprintf(err_msg, sizeof(err_msg), "Failed to start replay, capture target \"%s\" is invalid. Please change capture target in settings", recording_capture_target.c_str());
snprintf(err_msg, sizeof(err_msg), "Failed to start replay, capture target \"%s\" is invalid.\nPlease change capture target in settings", recording_capture_target.c_str());
show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY);
return false;
}
@@ -2400,14 +2640,12 @@ namespace gsr {
const std::string video_bitrate = std::to_string(config.replay_config.record_options.video_bitrate);
const std::string output_directory = config.replay_config.save_directory;
const std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.replay_config.record_options.audio_tracks_list, gsr_info);
const std::string framerate_mode = config.replay_config.record_options.framerate_mode == "auto" ? "vfr" : config.replay_config.record_options.framerate_mode;
const std::string framerate_mode = get_framerate_mode_validate(config.replay_config.record_options, gsr_info);
const std::string replay_time = std::to_string(config.replay_config.replay_time);
const char *container = config.replay_config.container.c_str();
const char *video_codec = config.replay_config.record_options.video_codec.c_str();
const char *encoder = "gpu";
if(strcmp(video_codec, "h264_software") == 0) {
video_codec = "h264";
encoder = "cpu";
}
choose_video_codec_and_container_with_fallback(gsr_info, &video_codec, &container, &encoder);
char size[64];
size[0] = '\0';
@@ -2419,7 +2657,7 @@ namespace gsr {
std::vector<const char*> args = {
"gpu-screen-recorder", "-w", recording_capture_target.c_str(),
"-c", config.replay_config.container.c_str(),
"-c", container,
"-ac", config.replay_config.record_options.audio_codec.c_str(),
"-cursor", config.replay_config.record_options.record_cursor ? "yes" : "no",
"-cr", config.replay_config.record_options.color_range.c_str(),
@@ -2435,6 +2673,9 @@ namespace gsr {
if(config.replay_config.restart_replay_on_save && gsr_info.system_info.gsr_version >= GsrVersion{5, 0, 3}) {
args.push_back("-restart-replay-on-save");
args.push_back("yes");
replay_restart_on_save = true;
} else {
replay_restart_on_save = false;
}
if(gsr_info.system_info.gsr_version >= GsrVersion{5, 5, 0}) {
@@ -2474,10 +2715,17 @@ namespace gsr {
// to see when the program has exit.
if(!disable_notification && config.replay_config.show_replay_started_notifications) {
char msg[256];
snprintf(msg, sizeof(msg), "Started replaying %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str());
show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::REPLAY, recording_capture_target.c_str());
snprintf(msg, sizeof(msg), "Started replaying %s", capture_target_get_notification_name(recording_capture_target.c_str(), false).c_str());
show_notification(msg, short_notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::REPLAY, recording_capture_target.c_str());
}
if(config.replay_config.record_options.record_area_option == "portal")
hide_ui = true;
// TODO: This will be incorrect if the user uses portal capture, as capture wont start until the user has
// selected what to capture and accepted it.
replay_duration_clock.restart();
replay_buffer_save_duration_sec = config.replay_config.replay_time;
return true;
}
@@ -2496,8 +2744,12 @@ namespace gsr {
if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) {
if(!replay_recording) {
if(config.record_config.show_recording_started_notifications)
show_notification("Started recording in the replay session", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD);
show_notification("Started recording in the replay session", short_notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD);
update_ui_recording_started();
// TODO: This will be incorrect if the user uses portal capture, as capture wont start until the user has
// selected what to capture and accepted it.
recording_duration_clock.restart();
}
replay_recording = true;
kill(gpu_screen_recorder_process, SIGRTMIN);
@@ -2513,8 +2765,12 @@ namespace gsr {
if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) {
if(!replay_recording) {
if(config.record_config.show_recording_started_notifications)
show_notification("Started recording in the streaming session", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD);
show_notification("Started recording in the streaming session", short_notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD);
update_ui_recording_started();
// TODO: This will be incorrect if the user uses portal capture, as capture wont start until the user has
// selected what to capture and accepted it.
recording_duration_clock.restart();
}
replay_recording = true;
kill(gpu_screen_recorder_process, SIGRTMIN);
@@ -2553,7 +2809,7 @@ namespace gsr {
recording_capture_target = get_capture_target(config.record_config.record_options.record_area_option, capture_options);
if(!validate_capture_target(config.record_config.record_options.record_area_option, capture_options)) {
char err_msg[256];
snprintf(err_msg, sizeof(err_msg), "Failed to start recording, capture target \"%s\" is invalid. Please change capture target in settings", recording_capture_target.c_str());
snprintf(err_msg, sizeof(err_msg), "Failed to start recording, capture target \"%s\" is invalid.\nPlease change capture target in settings", recording_capture_target.c_str());
show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD);
return;
}
@@ -2581,13 +2837,11 @@ namespace gsr {
const std::string video_bitrate = std::to_string(config.record_config.record_options.video_bitrate);
const std::string output_file = config.record_config.save_directory + "/Video_" + get_date_str() + "." + container_to_file_extension(config.record_config.container.c_str());
const std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.record_config.record_options.audio_tracks_list, gsr_info);
const std::string framerate_mode = config.record_config.record_options.framerate_mode == "auto" ? "vfr" : config.record_config.record_options.framerate_mode;
const std::string framerate_mode = get_framerate_mode_validate(config.record_config.record_options, gsr_info);
const char *container = config.record_config.container.c_str();
const char *video_codec = config.record_config.record_options.video_codec.c_str();
const char *encoder = "gpu";
if(strcmp(video_codec, "h264_software") == 0) {
video_codec = "h264";
encoder = "cpu";
}
choose_video_codec_and_container_with_fallback(gsr_info, &video_codec, &container, &encoder);
char size[64];
size[0] = '\0';
@@ -2599,7 +2853,7 @@ namespace gsr {
std::vector<const char*> args = {
"gpu-screen-recorder", "-w", recording_capture_target.c_str(),
"-c", config.record_config.container.c_str(),
"-c", container,
"-ac", config.record_config.record_options.audio_codec.c_str(),
"-cursor", config.record_config.record_options.record_cursor ? "yes" : "no",
"-cr", config.record_config.record_options.color_range.c_str(),
@@ -2636,9 +2890,16 @@ namespace gsr {
// 1...
if(config.record_config.show_recording_started_notifications) {
char msg[256];
snprintf(msg, sizeof(msg), "Started recording %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str());
show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str());
snprintf(msg, sizeof(msg), "Started recording %s", capture_target_get_notification_name(recording_capture_target.c_str(), false).c_str());
show_notification(msg, short_notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str());
}
if(config.record_config.record_options.record_area_option == "portal")
hide_ui = true;
// TODO: This will be incorrect if the user uses portal capture, as capture wont start until the user has
// selected what to capture and accepted it.
recording_duration_clock.restart();
}
static std::string streaming_get_url(const Config &config) {
@@ -2672,6 +2933,11 @@ namespace gsr {
{}
else
url = "rtmp://" + url;
if(!url.empty() && url.back() != '/' && url.back() != '=' && !config.streaming_config.custom.key.empty())
url += "/";
url += config.streaming_config.custom.key;
}
return url;
}
@@ -2718,7 +2984,7 @@ namespace gsr {
recording_capture_target = get_capture_target(config.streaming_config.record_options.record_area_option, capture_options);
if(!validate_capture_target(config.streaming_config.record_options.record_area_option, capture_options)) {
char err_msg[256];
snprintf(err_msg, sizeof(err_msg), "Failed to start streaming, capture target \"%s\" is invalid. Please change capture target in settings", recording_capture_target.c_str());
snprintf(err_msg, sizeof(err_msg), "Failed to start streaming, capture target \"%s\" is invalid.\nPlease change capture target in settings", recording_capture_target.c_str());
show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM);
return;
}
@@ -2747,17 +3013,13 @@ namespace gsr {
// But we check it anyways as streaming on some sites can fail if there is more than one audio track
if(audio_tracks.size() > 1)
audio_tracks.resize(1);
const std::string framerate_mode = config.streaming_config.record_options.framerate_mode == "auto" ? "vfr" : config.streaming_config.record_options.framerate_mode;
const std::string framerate_mode = get_framerate_mode_validate(config.streaming_config.record_options, gsr_info);
const char *container = "flv";
if(config.streaming_config.streaming_service == "custom")
container = config.streaming_config.custom.container.c_str();
const char *video_codec = config.streaming_config.record_options.video_codec.c_str();
const char *encoder = "gpu";
if(strcmp(video_codec, "h264_software") == 0) {
video_codec = "h264";
encoder = "cpu";
}
std::string container = "flv";
if(config.streaming_config.streaming_service == "custom")
container = config.streaming_config.custom.container;
choose_video_codec_and_container_with_fallback(gsr_info, &video_codec, &container, &encoder);
const std::string url = streaming_get_url(config);
@@ -2771,7 +3033,7 @@ namespace gsr {
std::vector<const char*> args = {
"gpu-screen-recorder", "-w", recording_capture_target.c_str(),
"-c", container.c_str(),
"-c", container,
"-ac", config.streaming_config.record_options.audio_codec.c_str(),
"-cursor", config.streaming_config.record_options.record_cursor ? "yes" : "no",
"-cr", config.streaming_config.record_options.color_range.c_str(),
@@ -2814,12 +3076,15 @@ namespace gsr {
// to see when the program has exit.
if(config.streaming_config.show_streaming_started_notifications) {
char msg[256];
snprintf(msg, sizeof(msg), "Started streaming %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str());
show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM, recording_capture_target.c_str());
snprintf(msg, sizeof(msg), "Started streaming %s", capture_target_get_notification_name(recording_capture_target.c_str(), false).c_str());
show_notification(msg, short_notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM, recording_capture_target.c_str());
}
if(config.streaming_config.record_options.record_area_option == "portal")
hide_ui = true;
}
void Overlay::on_press_take_screenshot(bool finished_selection, bool force_region_capture) {
void Overlay::on_press_take_screenshot(bool finished_selection, ScreenshotForceType force_type) {
if(region_selector.is_started() || window_selector.is_started())
return;
@@ -2828,30 +3093,42 @@ namespace gsr {
return;
}
const bool region_capture = config.screenshot_config.record_area_option == "region" || force_region_capture;
const char *record_area_option = region_capture ? "region" : config.screenshot_config.record_area_option.c_str();
bool hotkey_window_capture = false;
std::string record_area_option;
switch(force_type) {
case ScreenshotForceType::NONE:
record_area_option = config.screenshot_config.record_area_option;
break;
case ScreenshotForceType::REGION:
record_area_option = "region";
break;
case ScreenshotForceType::WINDOW:
record_area_option = gsr_info.system_info.display_server == DisplayServer::X11 ? "window" : "portal";
hotkey_window_capture = true;
break;
}
const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info);
screenshot_capture_target = get_capture_target(record_area_option, capture_options);
if(!validate_capture_target(record_area_option, capture_options)) {
char err_msg[256];
snprintf(err_msg, sizeof(err_msg), "Failed to take a screenshot, capture target \"%s\" is invalid. Please change capture target in settings", screenshot_capture_target.c_str());
snprintf(err_msg, sizeof(err_msg), "Failed to take a screenshot, capture target \"%s\" is invalid.\nPlease change capture target in settings", screenshot_capture_target.c_str());
show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::SCREENSHOT);
return;
}
if(region_capture && !finished_selection) {
if(record_area_option == "region" && !finished_selection) {
start_region_capture = true;
on_region_selected = [this, force_region_capture]() {
usleep(200 * 1000); // Hack: wait 0.2 seconds before taking a screenshot to allow user to move cursor away. TODO: Remove this
on_press_take_screenshot(true, force_region_capture);
on_region_selected = [this, force_type]() {
on_press_take_screenshot(true, force_type);
};
return;
}
if(config.screenshot_config.record_area_option == "window" && !finished_selection) {
if(record_area_option == "window" && !finished_selection) {
start_window_capture = true;
on_window_selected = [this, force_region_capture]() {
on_press_take_screenshot(true, force_region_capture);
on_window_selected = [this, force_type]() {
on_press_take_screenshot(true, force_type);
};
return;
}
@@ -2875,13 +3152,22 @@ namespace gsr {
args.push_back(size);
}
if(config.screenshot_config.restore_portal_session) {
if(config.screenshot_config.restore_portal_session && !hotkey_window_capture) {
args.push_back("-restore-portal-session");
args.push_back("yes");
}
const std::string hotkey_window_capture_portal_session_token_filepath = get_config_dir() + "/gpu-screen-recorder/gsr-ui-window-capture-token";
if(record_area_option == "portal") {
hide_ui = true;
if(hotkey_window_capture) {
args.push_back("-portal-session-token-filepath");
args.push_back(hotkey_window_capture_portal_session_token_filepath.c_str());
}
}
char region_str[128];
if(region_capture)
if(record_area_option == "region")
add_region_command(args, region_str, sizeof(region_str), region_selector);
args.push_back(nullptr);

View File

@@ -176,11 +176,21 @@ namespace gsr {
}
}
static const char *get_basename(const char *path, int size) {
for(int i = size - 1; i >= 0; --i) {
if(path[i] == '/')
return path + i + 1;
}
return path;
}
// |output_buffer| should be at least PATH_MAX in size
bool read_cmdline_arg0(const char *filepath, char *output_buffer, int output_buffer_size) {
output_buffer[0] = '\0';
const char *arg0_start = NULL;
const char *arg0_end = NULL;
int arg0_size = 0;
int fd = open(filepath, O_RDONLY);
if(fd == -1)
return false;
@@ -190,13 +200,16 @@ namespace gsr {
if(bytes_read == -1)
goto err;
arg0_end = (const char*)memchr(buffer, '\0', bytes_read);
arg0_start = buffer;
arg0_end = (const char*)memchr(arg0_start, '\0', bytes_read);
if(!arg0_end)
goto err;
if((arg0_end - buffer) + 1 <= output_buffer_size) {
memcpy(output_buffer, buffer, arg0_end - buffer);
output_buffer[arg0_end - buffer] = '\0';
arg0_start = get_basename(arg0_start, arg0_end - arg0_start);
arg0_size = arg0_end - arg0_start;
if(arg0_size + 1 <= output_buffer_size) {
memcpy(output_buffer, arg0_start, arg0_size);
output_buffer[arg0_size] = '\0';
close(fd);
return true;
}

View File

@@ -32,7 +32,7 @@ namespace gsr {
fclose(file);
if(!fifo_filepath.empty())
remove(fifo_filepath.c_str());
unlink(fifo_filepath.c_str());
}
bool Rpc::create(const char *name) {
@@ -44,15 +44,16 @@ namespace gsr {
char fifo_filepath_tmp[PATH_MAX];
get_runtime_filepath(fifo_filepath_tmp, sizeof(fifo_filepath_tmp), name);
fifo_filepath = fifo_filepath_tmp;
remove(fifo_filepath.c_str());
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();
return false;
}
if(!open_filepath(fifo_filepath.c_str())) {
remove(fifo_filepath.c_str());
unlink(fifo_filepath.c_str());
fifo_filepath.clear();
return false;
}

View File

@@ -120,6 +120,12 @@ namespace gsr {
if(!theme->trash_texture.load_from_file((resources_path + "images/trash.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->masked_texture.load_from_file((resources_path + "images/masked.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->unmasked_texture.load_from_file((resources_path + "images/unmasked.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->ps4_home_texture.load_from_file((resources_path + "images/ps4_home.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;

View File

@@ -36,7 +36,7 @@ namespace gsr {
for(size_t i = 0; i < items.size(); ++i) {
Item &item = items[i];
item_size.y = padding_top + item.text.get_bounds().size.y + padding_bottom;
if(mgl::FloatRect(item.position, item_size).contains(mouse_pos)) {
if(mgl::FloatRect(item.position, item_size).contains(mouse_pos) && item.enabled) {
const size_t prev_selected_item = selected_item;
selected_item = i;
show_dropdown = false;
@@ -93,7 +93,7 @@ namespace gsr {
void ComboBox::set_selected_item(const std::string &id, bool trigger_event, bool trigger_event_even_if_selection_not_changed) {
for(size_t i = 0; i < items.size(); ++i) {
auto &item = items[i];
if(item.id == id) {
if(item.id == id && item.enabled) {
const size_t prev_selected_item = selected_item;
selected_item = i;
dirty = true;
@@ -106,6 +106,22 @@ namespace gsr {
}
}
void ComboBox::set_item_enabled(const std::string &id, bool enabled) {
for(size_t i = 0; i < items.size(); ++i) {
auto &item = items[i];
if(item.id == id) {
item.enabled = enabled;
item.text.set_color(item.enabled ? mgl::Color(255, 255, 255, 255) : mgl::Color(255, 255, 255, 80));
if(selected_item == i) {
selected_item = 0;
show_dropdown = false;
dirty = true;
}
return;
}
}
}
const std::string& ComboBox::get_selected_id() const {
if(items.empty()) {
static std::string dummy;
@@ -150,7 +166,7 @@ namespace gsr {
Item &item = items[i];
item_size.y = padding_top + item.text.get_bounds().size.y + padding_bottom;
if(!cursor_inside) {
if(!cursor_inside && item.enabled) {
cursor_inside = mgl::FloatRect(items_draw_pos, item_size).contains(mouse_pos);
if(cursor_inside) {
mgl::Rectangle item_background(items_draw_pos.floor(), item_size.floor());

View File

@@ -1,12 +1,12 @@
#include "../../include/gui/Entry.hpp"
#include "../../include/gui/Utils.hpp"
#include "../../include/Theme.hpp"
#include <mglpp/graphics/Rectangle.hpp>
#include <mglpp/window/Window.hpp>
#include <mglpp/window/Event.hpp>
#include <mglpp/system/FloatRect.hpp>
#include <mglpp/system/Utf8.hpp>
#include <optional>
#include <string.h>
namespace gsr {
static const float padding_top_scale = 0.004629f;
@@ -16,8 +16,20 @@ namespace gsr {
static const float border_scale = 0.0015f;
static const float caret_width_scale = 0.001f;
Entry::Entry(mgl::Font *font, const char *text, float max_width) : text("", *font), max_width(max_width) {
static void string_replace_all(std::string &str, char old_char, char new_char) {
for(char &c : str) {
if(c == old_char)
c = new_char;
}
}
Entry::Entry(mgl::Font *font, const char *text, float max_width) :
text(std::u32string(), *font),
masked_text(std::u32string(), *font),
max_width(max_width)
{
this->text.set_color(get_color_theme().text_color);
this->masked_text.set_color(get_color_theme().text_color);
set_text(text);
}
@@ -25,25 +37,133 @@ namespace gsr {
if(!visible)
return true;
mgl::Text32 &active_text = masked ? masked_text : text;
if(event.type == mgl::Event::MouseButtonPressed && event.mouse_button.button == mgl::Mouse::Left) {
selected = mgl::FloatRect(position + offset, get_size()).contains({ (float)event.mouse_button.x, (float)event.mouse_button.y });
} else if(event.type == mgl::Event::KeyPressed && selected) {
if(event.key.code == mgl::Keyboard::Backspace && !text.get_string().empty()) {
std::string str = text.get_string();
const size_t prev_index = mgl::utf8_get_start_of_codepoint((const unsigned char*)str.c_str(), str.size(), str.size());
str.erase(prev_index, std::string::npos);
set_text(std::move(str));
} else if(event.key.code == mgl::Keyboard::V && event.key.control) {
std::string clipboard_text = window.get_clipboard_string();
std::string str = text.get_string();
str += clipboard_text;
set_text(std::move(str));
const mgl::vec2f mouse_pos = { (float)event.mouse_button.x, (float)event.mouse_button.y };
selected = mgl::FloatRect(position + offset, get_size()).contains(mouse_pos);
if(selected) {
selecting_text = true;
const auto caret_index_mouse = find_closest_caret_index_by_position(mouse_pos);
caret.index = caret_index_mouse.index;
caret.offset_x = caret_index_mouse.pos.x - active_text.get_position().x;
selection_start_caret = caret;
show_selection = true;
} else {
selecting_text = false;
selecting_with_keyboard = false;
show_selection = false;
}
} else if(event.type == mgl::Event::TextEntered && selected && event.text.codepoint >= 32) {
std::string str = text.get_string();
str.append(event.text.str, event.text.size);
set_text(std::move(str));
} else if(event.type == mgl::Event::MouseButtonReleased && event.mouse_button.button == mgl::Mouse::Left) {
selecting_text = false;
if(caret.index == selection_start_caret.index)
show_selection = false;
} else if(event.type == mgl::Event::MouseMoved && selected) {
if(selecting_text) {
const auto caret_index_mouse = find_closest_caret_index_by_position(mgl::vec2f(event.mouse_move.x, event.mouse_move.y));
caret.index = caret_index_mouse.index;
caret.offset_x = caret_index_mouse.pos.x - active_text.get_position().x;
return false;
}
} else if(event.type == mgl::Event::KeyPressed && selected) {
int selection_start_byte = caret.index;
int selection_end_byte = caret.index;
if(show_selection) {
selection_start_byte = std::min(caret.index, selection_start_caret.index);
selection_end_byte = std::max(caret.index, selection_start_caret.index);
}
if(event.key.code == mgl::Keyboard::Backspace) {
if(selection_start_byte == selection_end_byte && caret.index > 0)
selection_start_byte -= 1;
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, std::u32string());
} else if(event.key.code == mgl::Keyboard::Delete) {
if(selection_start_byte == selection_end_byte && caret.index < (int)active_text.get_string().size())
selection_end_byte += 1;
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, std::u32string());
} else if(event.key.code == mgl::Keyboard::C && event.key.control) {
const size_t selection_num_bytes = selection_end_byte - selection_start_byte;
if(selection_num_bytes > 0)
window.set_clipboard(mgl::utf32_to_utf8(text.get_string().substr(selection_start_byte, selection_num_bytes)));
} else if(event.key.code == mgl::Keyboard::V && event.key.control) {
std::string clipboard_string = window.get_clipboard_string();
string_replace_all(clipboard_string, '\n', ' ');
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, mgl::utf8_to_utf32(clipboard_string));
} else if(event.key.code == mgl::Keyboard::A && event.key.control) {
selection_start_caret.index = 0;
selection_start_caret.offset_x = 0.0f;
caret.index = active_text.get_string().size();
// TODO: Optimize
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
show_selection = true;
} else if(event.key.code == mgl::Keyboard::Left) {
if(!selecting_with_keyboard && show_selection)
show_selection = false;
else
move_caret_word(Direction::LEFT, event.key.control ? 999999 : 1);
if(!selecting_with_keyboard) {
selection_start_caret = caret;
show_selection = false;
}
} else if(event.key.code == mgl::Keyboard::Right) {
if(!selecting_with_keyboard && show_selection)
show_selection = false;
else
move_caret_word(Direction::RIGHT, event.key.control ? 999999 : 1);
if(!selecting_with_keyboard) {
selection_start_caret = caret;
show_selection = false;
}
} else if(event.key.code == mgl::Keyboard::Home) {
caret.index = 0;
caret.offset_x = 0.0f;
if(!selecting_with_keyboard) {
selection_start_caret = caret;
show_selection = false;
}
} else if(event.key.code == mgl::Keyboard::End) {
caret.index = active_text.get_string().size();
// TODO: Optimize
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
if(!selecting_with_keyboard) {
selection_start_caret = caret;
show_selection = false;
}
} else if(event.key.code == mgl::Keyboard::LShift || event.key.code == mgl::Keyboard::RShift) {
if(!show_selection)
selection_start_caret = caret;
selecting_with_keyboard = true;
show_selection = true;
}
return false;
} else if(event.type == mgl::Event::KeyReleased && selected) {
if(event.key.code == mgl::Keyboard::LShift || event.key.code == mgl::Keyboard::RShift) {
selecting_with_keyboard = false;
}
return false;
} else if(event.type == mgl::Event::TextEntered && selected && event.text.codepoint >= 32 && event.text.codepoint != 127) {
int selection_start_byte = caret.index;
int selection_end_byte = caret.index;
if(show_selection) {
selection_start_byte = std::min(caret.index, selection_start_caret.index);
selection_end_byte = std::max(caret.index, selection_start_caret.index);
}
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, mgl::utf8_to_utf32((const unsigned char*)event.text.str, event.text.size));
return false;
}
return true;
}
@@ -54,26 +174,91 @@ namespace gsr {
const mgl::vec2f draw_pos = position + offset;
const int padding_top = padding_top_scale * get_theme().window_height;
const int padding_bottom = padding_bottom_scale * get_theme().window_height;
const int padding_left = padding_left_scale * get_theme().window_height;
const int padding_right = padding_right_scale * get_theme().window_height;
mgl::Rectangle background(get_size());
mgl::Text32 &active_text = masked ? masked_text : text;
background.set_size(get_size());
background.set_position(draw_pos.floor());
background.set_color(selected ? mgl::Color(0, 0, 0, 255) : mgl::Color(0, 0, 0, 120));
window.draw(background);
const int caret_width = std::max(1.0f, caret_width_scale * get_theme().window_height);
const mgl::vec2f caret_size = mgl::vec2f(caret_width, active_text.get_bounds().size.y).floor();
const float overflow_left = (caret.offset_x + padding_left) - (padding_left + text_overflow);
if(overflow_left < 0.0f)
text_overflow += overflow_left;
const float overflow_right = (caret.offset_x + padding_left) - (background.get_size().x - padding_right);
if(overflow_right - text_overflow > 0.0f)
text_overflow = overflow_right;
active_text.set_position((draw_pos + mgl::vec2f(padding_left, get_size().y * 0.5f - active_text.get_bounds().size.y * 0.5f) - mgl::vec2f(text_overflow, 0.0f)).floor());
const auto text_bounds = active_text.get_bounds();
const bool text_larger_than_background = text_bounds.size.x > (background.get_size().x - padding_left - padding_right);
const float text_overflow_right = (text_bounds.position.x + text_bounds.size.x) - (background.get_position().x + background.get_size().x - padding_right);
if(text_larger_than_background) {
if(text_overflow_right < 0.0f) {
text_overflow += text_overflow_right;
active_text.set_position(active_text.get_position() + mgl::vec2f(-text_overflow_right, 0.0f));
}
} else {
active_text.set_position(active_text.get_position() + mgl::vec2f(-text_overflow, 0.0f));
text_overflow = 0.0f;
}
if(selected) {
const int border_size = std::max(1.0f, border_scale * get_theme().window_height);
draw_rectangle_outline(window, draw_pos.floor(), get_size().floor(), get_color_theme().tint_color, border_size);
const int caret_width = std::max(1.0f, caret_width_scale * get_theme().window_height);
mgl::Rectangle caret({(float)caret_width, text.get_bounds().size.y});
caret.set_position((draw_pos + mgl::vec2f(padding_left + caret_offset_x, padding_top)).floor());
caret.set_color(mgl::Color(255, 255, 255));
window.draw(caret);
draw_caret(window, draw_pos, caret_size);
}
text.set_position((draw_pos + mgl::vec2f(padding_left, get_size().y * 0.5f - text.get_bounds().size.y * 0.5f)).floor());
window.draw(text);
const mgl::Scissor parent_scissor = window.get_scissor();
const mgl::Scissor scissor = scissor_get_sub_area(parent_scissor,
mgl::Scissor{
(background.get_position() + mgl::vec2f(padding_left, padding_top)).to_vec2i(),
(background.get_size() - mgl::vec2f(padding_left + padding_right, padding_top + padding_bottom)).to_vec2i()
});
window.set_scissor(scissor);
window.draw(active_text);
if(show_selection)
draw_caret_selection(window, draw_pos, caret_size);
window.set_scissor(parent_scissor);
}
void Entry::draw_caret(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size) {
const int padding_top = padding_top_scale * get_theme().window_height;
const int padding_left = padding_left_scale * get_theme().window_height;
mgl::Rectangle caret_rect(caret_size);
mgl::vec2f caret_draw_pos = draw_pos + mgl::vec2f(padding_left + caret.offset_x - text_overflow, padding_top);
caret_rect.set_position(caret_draw_pos.floor());
caret_rect.set_color(mgl::Color(255, 255, 255));
window.draw(caret_rect);
}
void Entry::draw_caret_selection(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size) {
if(selection_start_caret.index == caret.index)
return;
const int padding_top = padding_top_scale * get_theme().window_height;
const int padding_left = padding_left_scale * get_theme().window_height;
const int caret_width = std::max(1.0f, caret_width_scale * get_theme().window_height);
const int offset = caret.index < selection_start_caret.index ? caret_width : 0;
mgl::Rectangle caret_selection_rect(mgl::vec2f(std::abs(selection_start_caret.offset_x - caret.offset_x) - offset, caret_size.y).floor());
caret_selection_rect.set_position((draw_pos + mgl::vec2f(padding_left + std::min(caret.offset_x, selection_start_caret.offset_x) - text_overflow + offset, padding_top)).floor());
mgl::Color caret_select_color = get_color_theme().tint_color;
caret_select_color.a = 100;
caret_selection_rect.set_color(caret_select_color);
window.draw(caret_selection_rect);
}
mgl::vec2f Entry::get_size() {
@@ -85,24 +270,157 @@ namespace gsr {
return { max_width, text.get_bounds().size.y + padding_top + padding_bottom };
}
void Entry::set_text(std::string str) {
if(!validate_handler || validate_handler(str)) {
text.set_string(std::move(str));
caret_offset_x = text.find_character_pos(99999).x - this->text.get_position().x;
if(on_changed)
on_changed(text.get_string());
void Entry::move_caret_word(Direction direction, size_t max_codepoints) {
mgl::Text32 &active_text = masked ? masked_text : text;
const int dir_step = direction == Direction::LEFT ? -1 : 1;
const int num_delimiter_chars = 15;
const char delimiter_chars[num_delimiter_chars + 1] = " \t\n/.,:;\\[](){}";
const char32_t *text_str = active_text.get_string().data();
int num_non_delimiter_chars_found = 0;
for(size_t i = 0; i < max_codepoints; ++i) {
const uint32_t codepoint = text_str[caret.index];
const bool is_delimiter_char = codepoint < 127 && !!memchr(delimiter_chars, codepoint, num_delimiter_chars);
if(is_delimiter_char) {
if(num_non_delimiter_chars_found > 0)
break;
} else {
++num_non_delimiter_chars_found;
}
if(caret.index + dir_step < 0 || caret.index + dir_step > (int)active_text.get_string().size())
break;
caret.index += dir_step;
}
// TODO: Move right by some characters instead of calculating every character to caret index
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
}
const std::string& Entry::get_text() const {
return text.get_string();
EntryValidateHandlerResult Entry::set_text(const std::string &str) {
EntryValidateHandlerResult validate_result = set_text_internal(mgl::utf8_to_utf32(str));
if(validate_result == EntryValidateHandlerResult::ALLOW) {
mgl::Text32 &active_text = masked ? masked_text : text;
caret.index = active_text.get_string().size();
// TODO: Optimize
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
selection_start_caret = caret;
selecting_text = false;
selecting_with_keyboard = false;
show_selection = false;
}
return validate_result;
}
EntryValidateHandlerResult Entry::set_text_internal(std::u32string str) {
EntryValidateHandlerResult validate_result = EntryValidateHandlerResult::ALLOW;
if(validate_handler)
validate_result = validate_handler(*this, str);
if(validate_result == EntryValidateHandlerResult::ALLOW) {
text.set_string(std::move(str));
if(masked)
masked_text.set_string(std::u32string(text.get_string().size(), '*'));
// TODO: Call callback with utf32 instead?
if(on_changed)
on_changed(mgl::utf32_to_utf8(text.get_string()));
}
return validate_result;
}
std::string Entry::get_text() const {
return mgl::utf32_to_utf8(text.get_string());
}
void Entry::set_masked(bool masked) {
if(masked == this->masked)
return;
this->masked = masked;
if(masked)
masked_text.set_string(std::u32string(text.get_string().size(), '*'));
else
masked_text.set_string(std::u32string());
mgl::Text32 &active_text = masked ? masked_text : text;
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
selection_start_caret.offset_x = active_text.find_character_pos(selection_start_caret.index).x - active_text.get_position().x;
}
bool Entry::is_masked() const {
return masked;
}
void Entry::replace_text(size_t index, size_t size, const std::u32string &replacement) {
if(index + size > text.get_string().size())
return;
const auto prev_caret = caret;
if((int)index >= caret.index)
caret.index += replacement.size();
else
caret.index = caret.index - size + replacement.size();
std::u32string str = text.get_string();
str.replace(index, size, replacement);
const EntryValidateHandlerResult validate_result = set_text_internal(std::move(str));
if(validate_result == EntryValidateHandlerResult::DENY) {
caret = prev_caret;
return;
} else if(validate_result == EntryValidateHandlerResult::REPLACED) {
return;
}
mgl::Text32 &active_text = masked ? masked_text : text;
// TODO: Optimize
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
selection_start_caret = caret;
selecting_text = false;
selecting_with_keyboard = false;
show_selection = false;
}
CaretIndexPos Entry::find_closest_caret_index_by_position(mgl::vec2f position) {
mgl::Text32 &active_text = masked ? masked_text : text;
const std::u32string &str = active_text.get_string();
mgl::Font *font = active_text.get_font();
CaretIndexPos result = {0, {active_text.get_position().x, active_text.get_position().y}};
for(result.index = 0; result.index < (int)str.size(); ++result.index) {
const uint32_t codepoint = str[result.index];
float glyph_width = 0.0f;
if(codepoint == '\t') {
const auto glyph = font->get_glyph(' ');
const int tab_width = 4;
glyph_width = glyph.advance * tab_width;
} else {
const auto glyph = font->get_glyph(codepoint);
glyph_width = glyph.advance;
}
if(result.pos.x + glyph_width * 0.5f >= position.x)
break;
result.pos.x += glyph_width;
}
return result;
}
static bool is_number(uint8_t c) {
return c >= '0' && c <= '9';
}
static std::optional<int> to_integer(const std::string &str) {
static std::optional<int> to_integer(const std::u32string &str) {
if(str.empty())
return std::nullopt;
@@ -114,7 +432,7 @@ namespace gsr {
int number = 0;
for(; i < str.size(); ++i) {
if(!is_number(str[i]))
return false;
return std::nullopt;
const int new_number = number * 10 + (str[i] - '0');
if(new_number < number)
@@ -129,19 +447,23 @@ namespace gsr {
}
EntryValidateHandler create_entry_validator_integer_in_range(int min, int max) {
return [min, max](std::string &str) {
return [min, max](Entry &entry, const std::u32string &str) {
if(str.empty())
return true;
return EntryValidateHandlerResult::ALLOW;
std::optional<int> number = to_integer(str);
const std::optional<int> number = to_integer(str);
if(!number)
return false;
return EntryValidateHandlerResult::DENY;
if(number.value() < min)
str = std::to_string(min);
else if(number.value() > max)
str = std::to_string(max);
return true;
if(number.value() < min) {
entry.set_text(std::to_string(min));
return EntryValidateHandlerResult::REPLACED;
} else if(number.value() > max) {
entry.set_text(std::to_string(max));
return EntryValidateHandlerResult::REPLACED;
}
return EntryValidateHandlerResult::ALLOW;
};
}
}

View File

@@ -348,6 +348,27 @@ namespace gsr {
return list;
}
std::unique_ptr<List> GlobalSettingsPage::create_screenshot_window_hotkey_options() {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
char str[128];
if(gsr_info->system_info.display_server == DisplayServer::X11)
snprintf(str, sizeof(str), "Take a screenshot of a window:");
else
snprintf(str, sizeof(str), "Take a screenshot with desktop portal:");
list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color));
auto take_screenshot_window_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
take_screenshot_window_button_ptr = take_screenshot_window_button.get();
list->add_widget(std::move(take_screenshot_window_button));
take_screenshot_window_button_ptr->on_click = [this] {
configure_hotkey_start(ConfigureHotkeyType::TAKE_SCREENSHOT_WINDOW);
};
return list;
}
std::unique_ptr<List> GlobalSettingsPage::create_hotkey_control_buttons() {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
@@ -362,6 +383,7 @@ namespace gsr {
config.replay_config.save_10_min_hotkey = {mgl::Keyboard::Unknown, 0};
config.screenshot_config.take_screenshot_hotkey = {mgl::Keyboard::Unknown, 0};
config.screenshot_config.take_screenshot_region_hotkey = {mgl::Keyboard::Unknown, 0};
config.screenshot_config.take_screenshot_window_hotkey = {mgl::Keyboard::Unknown, 0};
config.main_config.show_hide_hotkey = {mgl::Keyboard::Unknown, 0};
load_hotkeys();
overlay->rebind_all_keyboard_hotkeys();
@@ -404,6 +426,7 @@ namespace gsr {
list_ptr->add_widget(create_stream_hotkey_options());
list_ptr->add_widget(create_screenshot_hotkey_options());
list_ptr->add_widget(create_screenshot_region_hotkey_options());
list_ptr->add_widget(create_screenshot_window_hotkey_options());
list_ptr->add_widget(create_hotkey_control_buttons());
return subsection;
}
@@ -444,13 +467,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) {
@@ -512,6 +564,8 @@ namespace gsr {
enable_keyboard_hotkeys_radio_button_ptr->set_selected_item(config.main_config.hotkeys_enable_option, false, false);
enable_joystick_hotkeys_radio_button_ptr->set_selected_item(config.main_config.joystick_hotkeys_enable_option, false, false);
notification_speed_button_ptr->set_selected_item(config.main_config.notification_speed);
load_hotkeys();
}
@@ -528,6 +582,7 @@ namespace gsr {
take_screenshot_button_ptr->set_text(config.screenshot_config.take_screenshot_hotkey.to_string());
take_screenshot_region_button_ptr->set_text(config.screenshot_config.take_screenshot_region_hotkey.to_string());
take_screenshot_window_button_ptr->set_text(config.screenshot_config.take_screenshot_window_hotkey.to_string());
show_hide_button_ptr->set_text(config.main_config.show_hide_hotkey.to_string());
}
@@ -537,6 +592,7 @@ namespace gsr {
config.main_config.tint_color = tint_color_radio_button_ptr->get_selected_id();
config.main_config.hotkeys_enable_option = enable_keyboard_hotkeys_radio_button_ptr->get_selected_id();
config.main_config.joystick_hotkeys_enable_option = enable_joystick_hotkeys_radio_button_ptr->get_selected_id();
config.main_config.notification_speed = notification_speed_button_ptr->get_selected_id();
save_config(config);
}
@@ -611,6 +667,8 @@ namespace gsr {
return take_screenshot_button_ptr;
case ConfigureHotkeyType::TAKE_SCREENSHOT_REGION:
return take_screenshot_region_button_ptr;
case ConfigureHotkeyType::TAKE_SCREENSHOT_WINDOW:
return take_screenshot_window_button_ptr;
case ConfigureHotkeyType::SHOW_HIDE:
return show_hide_button_ptr;
}
@@ -639,6 +697,8 @@ namespace gsr {
return &config.screenshot_config.take_screenshot_hotkey;
case ConfigureHotkeyType::TAKE_SCREENSHOT_REGION:
return &config.screenshot_config.take_screenshot_region_hotkey;
case ConfigureHotkeyType::TAKE_SCREENSHOT_WINDOW:
return &config.screenshot_config.take_screenshot_window_hotkey;
case ConfigureHotkeyType::SHOW_HIDE:
return &config.main_config.show_hide_hotkey;
}
@@ -654,6 +714,7 @@ namespace gsr {
&config.streaming_config.start_stop_hotkey,
&config.screenshot_config.take_screenshot_hotkey,
&config.screenshot_config.take_screenshot_region_hotkey,
&config.screenshot_config.take_screenshot_window_hotkey,
&config.main_config.show_hide_hotkey
};
for(ConfigHotkey *config_hotkey : config_hotkeys) {
@@ -702,6 +763,13 @@ namespace gsr {
case ConfigureHotkeyType::TAKE_SCREENSHOT_REGION:
hotkey_configure_action_name = "Take a screenshot of a region";
break;
case ConfigureHotkeyType::TAKE_SCREENSHOT_WINDOW: {
if(gsr_info->system_info.display_server == DisplayServer::X11)
hotkey_configure_action_name = "Take a screenshot of a window";
else
hotkey_configure_action_name = "Take a screenshot with desktop portal";
break;
}
case ConfigureHotkeyType::SHOW_HIDE:
hotkey_configure_action_name = "Show/hide UI";
break;

View File

@@ -120,7 +120,7 @@ namespace gsr {
ll->add_widget(std::move(capture_target_list));
ll->add_widget(create_change_image_resolution_section());
return std::make_unique<Subsection>("Record area", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
return std::make_unique<Subsection>("Capture", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
std::unique_ptr<List> ScreenshotSettingsPage::create_image_quality_section() {
@@ -215,8 +215,17 @@ namespace gsr {
return checkbox;
}
std::unique_ptr<CheckBox> ScreenshotSettingsPage::create_save_screenshot_to_clipboard() {
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Save screenshot to clipboard");
save_screenshot_to_clipboard_checkbox_ptr = checkbox.get();
return checkbox;
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_general_section() {
return std::make_unique<Subsection>("General", create_save_screenshot_in_game_folder(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
list->add_widget(create_save_screenshot_in_game_folder());
list->add_widget(create_save_screenshot_to_clipboard());
return std::make_unique<Subsection>("General", std::move(list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_notifications_section() {
@@ -281,6 +290,7 @@ 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);
save_screenshot_to_clipboard_checkbox_ptr->set_checked(config.screenshot_config.save_screenshot_to_clipboard);
show_screenshot_saved_notification_checkbox_ptr->set_checked(config.screenshot_config.show_screenshot_saved_notifications);
if(config.screenshot_config.image_width == 0)
@@ -309,6 +319,7 @@ 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.save_screenshot_to_clipboard = save_screenshot_to_clipboard_checkbox_ptr->is_checked();
config.screenshot_config.show_screenshot_saved_notifications = show_screenshot_saved_notification_checkbox_ptr->is_checked();
if(config.screenshot_config.image_width == 0)

View File

@@ -4,6 +4,7 @@
#include "../../include/gui/PageStack.hpp"
#include "../../include/gui/FileChooser.hpp"
#include "../../include/gui/Subsection.hpp"
#include "../../include/gui/Image.hpp"
#include "../../include/Theme.hpp"
#include "../../include/GsrInfo.hpp"
#include "../../include/Utils.hpp"
@@ -183,7 +184,7 @@ namespace gsr {
ll->add_widget(std::move(capture_target_list));
ll->add_widget(create_change_video_resolution_section());
return std::make_unique<Subsection>("Record area", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
return std::make_unique<Subsection>("Capture", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
static bool audio_device_is_output(const std::string &audio_device_id) {
@@ -567,6 +568,10 @@ namespace gsr {
framerate_mode_box->add_item("Auto (Recommended)", "auto");
framerate_mode_box->add_item("Constant", "cfr");
framerate_mode_box->add_item("Variable", "vfr");
if(gsr_info->system_info.display_server == DisplayServer::X11)
framerate_mode_box->add_item("Sync to content", "content");
else
framerate_mode_box->add_item("Sync to content (Only X11 or desktop portal capture)", "content");
framerate_mode_box_ptr = framerate_mode_box.get();
return framerate_mode_box;
}
@@ -965,36 +970,75 @@ namespace gsr {
return streaming_service_list;
}
static std::unique_ptr<Button> create_mask_toggle_button(Entry *entry_to_toggle, mgl::vec2f size) {
auto button = std::make_unique<Button>(&get_theme().body_font, "", size, mgl::Color(0, 0, 0, 0));
Button *button_ptr = button.get();
button->set_icon(&get_theme().masked_texture);
button->on_click = [entry_to_toggle, button_ptr]() {
const bool is_masked = entry_to_toggle->is_masked();
button_ptr->set_icon(is_masked ? &get_theme().unmasked_texture : &get_theme().masked_texture);
entry_to_toggle->set_masked(!is_masked);
};
return button;
}
static Entry* add_stream_key_entry_to_list(List *stream_key_list) {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
auto key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
key_entry->set_masked(true);
Entry *key_entry_ptr = key_entry.get();
const float mask_icon_size = key_entry_ptr->get_size().y * 0.9f;
list->add_widget(std::move(key_entry));
list->add_widget(create_mask_toggle_button(key_entry_ptr, mgl::vec2f(mask_icon_size, mask_icon_size)));
stream_key_list->add_widget(std::move(list));
return key_entry_ptr;
}
std::unique_ptr<List> SettingsPage::create_stream_key_section() {
auto stream_key_list = std::make_unique<List>(List::Orientation::VERTICAL);
stream_key_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream key:", get_color_theme().text_color));
auto twitch_stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
twitch_stream_key_entry_ptr = twitch_stream_key_entry.get();
stream_key_list->add_widget(std::move(twitch_stream_key_entry));
auto youtube_stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
youtube_stream_key_entry_ptr = youtube_stream_key_entry.get();
stream_key_list->add_widget(std::move(youtube_stream_key_entry));
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());
stream_key_list_ptr = stream_key_list.get();
return stream_key_list;
}
std::unique_ptr<List> SettingsPage::create_stream_url_section() {
auto stream_url_list = std::make_unique<List>(List::Orientation::VERTICAL);
stream_url_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "URL:", get_color_theme().text_color));
std::unique_ptr<List> SettingsPage::create_stream_custom_url() {
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
auto stream_url_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
stream_url_entry_ptr = stream_url_entry.get();
stream_url_list->add_widget(std::move(stream_url_entry));
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream URL:", get_color_theme().text_color));
list->add_widget(std::move(stream_url_entry));
return list;
}
stream_url_list_ptr = stream_url_list.get();
return stream_url_list;
std::unique_ptr<List> SettingsPage::create_stream_custom_key() {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
auto stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
stream_key_entry->set_masked(true);
stream_key_entry_ptr = stream_key_entry.get();
const float mask_icon_size = stream_key_entry_ptr->get_size().y * 0.9f;
list->add_widget(std::move(stream_key_entry));
list->add_widget(create_mask_toggle_button(stream_key_entry_ptr, mgl::vec2f(mask_icon_size, mask_icon_size)));
return list;
}
std::unique_ptr<List> SettingsPage::create_stream_custom_section() {
auto custom_stream_list = std::make_unique<List>(List::Orientation::VERTICAL);
auto stream_url_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
stream_url_list->add_widget(create_stream_custom_url());
stream_url_list->add_widget(create_stream_container());
custom_stream_list->add_widget(std::move(stream_url_list));
custom_stream_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream key:", get_color_theme().text_color));
custom_stream_list->add_widget(create_stream_custom_key());
custom_stream_list_ptr = custom_stream_list.get();
return custom_stream_list;
}
std::unique_ptr<ComboBox> SettingsPage::create_stream_container_box() {
@@ -1007,11 +1051,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;
}
@@ -1019,8 +1062,7 @@ namespace gsr {
auto streaming_info_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
streaming_info_list->add_widget(create_streaming_service_section());
streaming_info_list->add_widget(create_stream_key_section());
streaming_info_list->add_widget(create_stream_url_section());
streaming_info_list->add_widget(create_stream_container_section());
streaming_info_list->add_widget(create_stream_custom_section());
settings_list_ptr->add_widget(std::make_unique<Subsection>("Streaming info", std::move(streaming_info_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL);
@@ -1045,11 +1087,10 @@ namespace gsr {
const bool rumble_option = id == "rumble";
const bool custom_option = id == "custom";
stream_key_list_ptr->set_visible(!custom_option);
stream_url_list_ptr->set_visible(custom_option);
container_list_ptr->set_visible(custom_option);
twitch_stream_key_entry_ptr->set_visible(twitch_option);
youtube_stream_key_entry_ptr->set_visible(youtube_option);
rumble_stream_key_entry_ptr->set_visible(rumble_option);
custom_stream_list_ptr->set_visible(custom_option);
twitch_stream_key_entry_ptr->get_parent_widget()->set_visible(twitch_option);
youtube_stream_key_entry_ptr->get_parent_widget()->set_visible(youtube_option);
rumble_stream_key_entry_ptr->get_parent_widget()->set_visible(rumble_option);
return true;
};
streaming_service_box_ptr->on_selection_changed("Twitch", "twitch");
@@ -1253,6 +1294,7 @@ namespace gsr {
twitch_stream_key_entry_ptr->set_text(config.streaming_config.twitch.stream_key);
rumble_stream_key_entry_ptr->set_text(config.streaming_config.rumble.stream_key);
stream_url_entry_ptr->set_text(config.streaming_config.custom.url);
stream_key_entry_ptr->set_text(config.streaming_config.custom.key);
container_box_ptr->set_selected_item(config.streaming_config.custom.container);
}
@@ -1395,6 +1437,7 @@ namespace gsr {
config.streaming_config.twitch.stream_key = twitch_stream_key_entry_ptr->get_text();
config.streaming_config.rumble.stream_key = rumble_stream_key_entry_ptr->get_text();
config.streaming_config.custom.url = stream_url_entry_ptr->get_text();
config.streaming_config.custom.key = stream_key_entry_ptr->get_text();
config.streaming_config.custom.container = container_box_ptr->get_selected_id();
}
}

View File

@@ -5,6 +5,18 @@
namespace gsr {
static double frame_delta_seconds = 1.0;
static mgl::vec2i min_vec2i(mgl::vec2i a, mgl::vec2i b) {
return { std::min(a.x, b.x), std::min(a.y, b.y) };
}
static mgl::vec2i max_vec2i(mgl::vec2i a, mgl::vec2i b) {
return { std::max(a.x, b.x), std::max(a.y, b.y) };
}
static mgl::vec2i clamp_vec2i(mgl::vec2i value, mgl::vec2i min, mgl::vec2i max) {
return min_vec2i(max, max_vec2i(value, min));
}
// TODO: Use vertices to make it one draw call
void draw_rectangle_outline(mgl::Window &window, mgl::vec2f pos, mgl::vec2f size, mgl::Color color, float border_size) {
pos = pos.floor();
@@ -74,4 +86,12 @@ namespace gsr {
else
return from;
}
mgl::Scissor scissor_get_sub_area(mgl::Scissor parent, mgl::Scissor child) {
const mgl::vec2i pos = clamp_vec2i(child.position, parent.position, parent.position + parent.size);
return mgl::Scissor{
pos,
min_vec2i(child.size, parent.position + parent.size - pos)
};
}
}

View File

@@ -64,6 +64,10 @@ namespace gsr {
this->visible = visible;
}
Widget* Widget::get_parent_widget() {
return parent_widget;
}
void add_widget_to_remove(std::unique_ptr<Widget> widget) {
widgets_to_remove.push_back(std::move(widget));
}

View File

@@ -4,7 +4,6 @@
#include "../include/Process.hpp"
#include "../include/Rpc.hpp"
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <limits.h>
@@ -14,7 +13,6 @@
#include <mglpp/system/Clock.hpp>
// TODO: Make keyboard/controller controllable for steam deck (and other controllers).
// TODO: Keep track of gpu screen recorder run by other programs to not allow recording at the same time, or something.
// TODO: Add systray by using org.kde.StatusNotifierWatcher/etc dbus directly.
// TODO: Make sure the overlay always stays on top. Test with starting the overlay and then opening youtube in fullscreen.
// This is done in Overlay::force_window_on_top, but it's not called right now. It cant be used because the overlay will be on top of
@@ -97,6 +95,11 @@ static void rpc_add_commands(gsr::Rpc *rpc, gsr::Overlay *overlay) {
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
overlay->take_screenshot_region();
});
rpc->add_handler("take-screenshot-window", [overlay](const std::string &name) {
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
overlay->take_screenshot_window();
});
}
static bool is_gsr_ui_virtual_keyboard_running() {
@@ -192,7 +195,9 @@ enum class LaunchAction {
int main(int argc, char **argv) {
setlocale(LC_ALL, "C"); // Sigh... stupid C
#ifdef __GLIBC__
mallopt(M_MMAP_THRESHOLD, 65536);
#endif
if(geteuid() == 0) {
fprintf(stderr, "Error: don't run gsr-ui as the root user\n");
@@ -220,17 +225,17 @@ int main(int argc, char **argv) {
set_display_server_environment_variables();
// TODO: This is a shitty method to detect if multiple instances of gsr-ui is running but this will work properly even in flatpak
// that uses pid sandboxing. Replace this with a better method once we no longer rely on linux global hotkeys on some platform.
// TODO: This method doesn't work when disabling hotkeys and the method below with pidof gsr-ui doesn't work in flatpak.
// What do? creating a pid file doesn't work in flatpak either.
// TODO: This doesn't work in flatpak when disabling hotkeys.
if(is_gsr_ui_virtual_keyboard_running() || gsr::pidof("gsr-ui", getpid()) != -1) {
auto rpc = std::make_unique<gsr::Rpc>();
const bool rpc_created = rpc->create("gsr-ui");
if(!rpc_created)
fprintf(stderr, "Error: Failed to create rpc\n");
if(is_gsr_ui_virtual_keyboard_running() || !rpc_created) {
if(launch_action == LaunchAction::LAUNCH_DAEMON)
return 1;
gsr::Rpc rpc;
if(rpc.open("gsr-ui") && rpc.write("show_ui\n", 8)) {
rpc = std::make_unique<gsr::Rpc>();
if(rpc->open("gsr-ui") && rpc->write("show_ui\n", 8)) {
fprintf(stderr, "Error: another instance of gsr-ui is already running, opening that one instead\n");
} else {
fprintf(stderr, "Error: failed to send command to running gsr-ui instance, user will have to open the UI manually with Alt+Z\n");
@@ -240,6 +245,16 @@ int main(int argc, char **argv) {
return 1;
}
if(gsr::pidof("gpu-screen-recorder", -1) != -1) {
const char *args[] = { "gsr-notify", "--text", "GPU Screen Recorder is already running in another process.\nPlease close it before using GPU Screen Recorder UI.", "--timeout", "5.0", "--icon-color", "ff0000", "--bg-color", "ff0000", nullptr };
gsr::exec_program_daemonized(args);
}
if(mgl_init(MGL_WINDOW_SYSTEM_X11) != 0) {
fprintf(stderr, "Error: failed to initialize mgl. Failed to either connect to the X11 server or setup opengl\n");
return 1;
}
if(is_flatpak())
install_flatpak_systemd_service();
else
@@ -283,11 +298,6 @@ int main(int argc, char **argv) {
disable_prime_run();
}
if(mgl_init(MGL_WINDOW_SYSTEM_X11) != 0) {
fprintf(stderr, "Error: failed to initialize mgl. Failed to either connect to the X11 server or setup opengl\n");
exit(1);
}
gsr::SupportedCaptureOptions capture_options = gsr::get_supported_capture_options(gsr_info);
std::string resources_path;
@@ -320,10 +330,6 @@ int main(int argc, char **argv) {
if(launch_action == LaunchAction::LAUNCH_SHOW)
overlay->show();
auto rpc = std::make_unique<gsr::Rpc>();
if(!rpc->create("gsr-ui"))
fprintf(stderr, "Error: Failed to create rpc, commands won't be received\n");
rpc_add_commands(rpc.get(), overlay.get());
// TODO: Add hotkeys in Overlay when using x11 global hotkeys. The hotkeys in Overlay should duplicate each key that is used for x11 global hotkeys.

View File

@@ -64,6 +64,8 @@ static void usage(void) {
printf(" Take a screenshot.\n");
printf(" take-screenshot-region\n");
printf(" Take a screenshot of a region.\n");
printf(" take-screenshot-window\n");
printf(" Take a screenshot of a window (or desktop portal on Wayland).\n");
printf("\n");
printf("EXAMPLES:\n");
printf(" gsr-ui-cli toggle-show\n");
@@ -83,6 +85,7 @@ static bool is_valid_command(const char *command) {
"replay-save-10-min",
"take-screenshot",
"take-screenshot-region",
"take-screenshot-window",
NULL
};