Compare commits

..

34 Commits
1.1.4 ... 1.2.2

Author SHA1 Message Date
dec05eba
3c8dd9c4db 1.2.2 2025-03-07 20:22:13 +01:00
dec05eba
c7fcf251e3 Fix shortcut keys changed 2025-03-07 20:21:25 +01:00
dec05eba
6d58b2495d Update screenshot image 2025-03-07 20:14:40 +01:00
dec05eba
347eced060 Change notification timeout 2025-03-07 17:38:50 +01:00
dec05eba
6449133c57 Update mglpp 2025-03-05 22:10:30 +01:00
dec05eba
1168e68278 1.2.1 2025-02-27 15:57:25 +01:00
dec05eba
4836c661ce 1.2.0 2025-02-27 15:56:35 +01:00
dec05eba
f0bbbbe4a9 Replay on startup: wait until audio devices are available before turning replay on 2025-02-25 17:37:25 +01:00
dec05eba
d9a1e5c2eb Add option to press backspace to remove hotkey 2025-02-25 01:21:23 +01:00
dec05eba
b6c59e1049 Update README 2025-02-22 23:01:06 +01:00
dec05eba
af6984cd7e 1.2.0 2025-02-22 14:39:40 +01:00
dec05eba
51a47193d7 Show correct process on screenshot failure 2025-02-22 13:39:06 +01:00
dec05eba
189736c1a9 Add option to take a screenshot (default hotkey: alt+f1) 2025-02-22 13:31:51 +01:00
dec05eba
8003c209fe m 2025-02-11 22:40:11 +01:00
dec05eba
1734d48af6 window get title: cleanup data 2025-02-10 19:41:56 +01:00
dec05eba
fc2f6f4c50 Better detect focused x11 window on wayland, properly get focused game name on wayland 2025-02-10 19:31:27 +01:00
dec05eba
f4e44cbef5 Prepare for sound. Fix game name being gsr-ui on wayland in some cases when saving video when the ui is open 2025-02-10 18:22:21 +01:00
dec05eba
3d6354c642 m 2025-02-08 03:18:16 +01:00
dec05eba
efb5fc53c1 Show notification when saving a large replay that is taking some time 2025-02-07 19:41:39 +01:00
dec05eba
51367ac078 Change replay duration max limit to 3 hours 2025-02-06 02:17:56 +01:00
dec05eba
b0ab2099fd 1.1.7 2025-02-05 22:36:19 +01:00
dec05eba
4a0612ae8f Update flatpak version 2025-02-05 22:16:22 +01:00
dec05eba
c650974a11 Launch gsr-global-hotkeys in flatpak through kms-server-proxy 2025-02-05 22:12:10 +01:00
dec05eba
6fe9f1a8d5 Fix global hotkeys when using virtual mapper that pretends to be a joystick as well (kanata) 2025-02-05 21:03:42 +01:00
dec05eba
8c148aceda Limit combobox item width, use multiple rows 2025-02-05 20:41:11 +01:00
dec05eba
b4c85910ce 1.1.6 2025-02-03 20:43:08 +01:00
dec05eba
fd63ac3626 Fix for steamdeck 2025-02-03 20:27:35 +01:00
dec05eba
2a0782eb02 Attempt to fix global hotkeys not working on steam deck (grabs keys, cant press buttons) 2025-02-03 19:58:42 +01:00
dec05eba
f505323d56 1.1.5 2025-02-03 01:25:32 +01:00
dec05eba
309cc3425b Use bundled cursor if cursor fails to load 2025-01-27 17:35:35 +01:00
dec05eba
81cb8f539f banana 2025-01-27 16:47:55 +01:00
dec05eba
5214fb1d7f Try fixing missing cursor texture on some broken systems 2025-01-27 16:46:54 +01:00
dec05eba
9aebe81ec4 amend 2025-01-27 11:56:37 +01:00
dec05eba
d73bd68a70 Default to default cursor if cursor not found 2025-01-27 11:53:49 +01:00
49 changed files with 1543 additions and 397 deletions

View File

@@ -2,9 +2,7 @@
# GPU Screen Recorder UI
A fullscreen overlay UI for [GPU Screen Recorder](https://git.dec05eba.com/gpu-screen-recorder/about/) in the style of ShadowPlay.\
The application is currently primarly designed for X11 but it can run on Wayland as well through XWayland, with some caveats because of Wayland limitations.\
Note: This software is still in early alpha. Expect bugs, and please report any if you experience them. Some are already known, but it doesn't hurt to report them anyways.\
You can report an issue by emailing the issue to dec05eba@protonmail.com.
The application is currently primarly designed for X11 but it can run on Wayland as well through XWayland, with some caveats because of Wayland limitations.
# Usage
Run `gsr-ui` and press `Left Alt+Z` to show/hide the UI. You can start the overlay UI at system startup by running `systemctl enable --now --user gpu-screen-recorder-ui`.
@@ -27,6 +25,7 @@ These are the dependencies needed to build GPU Screen Recorder UI:
* libxcursor
* libglvnd (which provides libgl, libglx and libegl)
* linux-api-headers
* libpulse (libpulse-simple)
## Runtime dependencies
There are also additional dependencies needed at runtime:
@@ -39,7 +38,10 @@ This program has to grab all keyboards and create a virtual keyboard (`gsr-ui vi
This might cause issues for you if you use input remapping software. To workaround this you can go into settings and select "Only grab virtual devices"
# License
This software is licensed under GPL3.0-only. Files under `fonts/` directory belong to the Noto Sans Google fonts project and they are licensed under `SIL Open Font License`.
This software is licensed under GPL3.0-only. Files under `fonts/` directory belong to the Noto Sans Google fonts project and they are licensed under `SIL Open Font License`. `images/default.cur` it part of the [Adwaita icon theme](https://gitlab.gnome.org/GNOME/adwaita-icon-theme/-/tree/master) which is licensed under `Creative Commons Attribution-Share Alike 3.0`.
# Reporting bugs, contributing patches, questions or donation
See [https://git.dec05eba.com/?p=about](https://git.dec05eba.com/?p=about).
# Demo
[![Click here to watch a demo video on youtube](https://img.youtube.com/vi/SOqXusCTXXA/0.jpg)](https://www.youtube.com/watch?v=SOqXusCTXXA)
@@ -48,11 +50,6 @@ This software is licensed under GPL3.0-only. Files under `fonts/` directory belo
![](https://dec05eba.com/images/front_page.jpg)
![](https://dec05eba.com/images/settings_page.jpg)
# Donations
If you want to donate you can donate via bitcoin or monero.
* Bitcoin: bc1qqvuqnwrdyppf707ge27fqz2n9y9gu7lf5ypyuf
* Monero: 4An9kp2qW1C9Gah7ewv4JzcNFQ5TAX7ineGCqXWK6vQnhsGGcRpNgcn8r9EC3tMcgY7vqCKs3nSRXhejMHBaGvFdN2egYet
# 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 fullscreened can mess up the game window a bit on Hyprland. I believe this is an issue with Hyprland.

36
TODO
View File

@@ -78,8 +78,6 @@ Dont allow autostart of replay if capture option is window recording (when windo
Use global shortcuts desktop portal protocol on wayland when available.
When support for window capture is enabled on x11 then make sure to not save the window except temporary while the program is open.
Support CJK.
Move ui hover code from ::draw to ::on_event, to properly handle widget event stack.
@@ -107,4 +105,36 @@ When adding window capture only add it to recording and streaming and do the win
Show an error that prime run will be disabled when using desktop portal capture option. This can cause issues as the user may have selected a video codec option that isn't available on their iGPU but is available on the prime-run dGPU.
Is it possible to configure hotkey and the new hotkey to get triggered immediately?
Is it possible to configure hotkey and the new hotkey to get triggered immediately?
For keyboards that report supporting mice the keyboard grab will be delayed until any key has been pressed (and then released), see: https://github.com/dec05eba/gpu-screen-recorder-issues/issues/97
See if there is any way around this.
Instead of installing gsr-global-hotkeys in flatpak use kms-server-proxy to launch gsr-global-hotkeys inside the flatpak with root, just like gsr-kms-server. This removes the need to update gsr-global-hotkeys everytime there is an update.
Check if "modprobe uinput" is needed on some systems (old fedora?).
Add recording timer to see duration of recording/streaming.
Make folder with window name work when using gamescope. Gamescope runs x11 itself so to get the window name inside that we have to connect to the gamescope X11 server (DISPLAY=:1 on x11 and DISPLAY=:2 on wayland, but not always).
When clicking on current directory in file manager show a dropdown menu where you can select common directories (HOME, Videos, Downloads and mounted drives) for quick navigation. Maybe even button to search.
Maybe change gsr-ui startup retry time in the systemd service, from 5 seconds to 2 seconds.
Add support for window capture. This should not prompt for window selection directly but instead prompt for window selection when recording starts and hide the ui first.
For screenshots window capture should exist but "follow focused" option should not exist.
Improve audio design. It should have a button to add/remove audio tracks and button to add audio into each audio track separately and "record audio from all applications except the selected ones" for each audio track. Then also remove the "merge audio tracks" option.
Make it possible to take a screenshot through a button in the ui instead of having to use hotkey.
Handle failing to save a replay. gsr should output "failed to save replay, or something like that" to make it possible to detect that.
Dont allow saving replay while a replay save is in progress.
Make input work with cjk input systems (such as fcitx).
System startup option should also support runit and some other init systems, not only soystemd.
Allow using a hotkey such as printscreen or any other non-alphanumeric key without a modifier. Allow that in gsr-ui and gsr-global-hotkeys. Update the ui to match that.

BIN
images/default.cur Normal file

Binary file not shown.

BIN
images/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

22
include/AudioPlayer.hpp Normal file
View File

@@ -0,0 +1,22 @@
#pragma once
#include <thread>
namespace gsr {
// Only plays raw stereo PCM audio in 48000hz in s16le format.
// Use this command to convert an audio file (input.wav) to a format playable by this class (output.pcm):
// ffmpeg -i input.wav -f s16le -acodec pcm_s16le -ar 48000 output.pcm
class AudioPlayer {
public:
AudioPlayer() = default;
~AudioPlayer();
AudioPlayer(const AudioPlayer&) = delete;
AudioPlayer& operator=(const AudioPlayer&) = delete;
bool play(const char *filepath);
private:
std::thread thread;
bool stop_playing_audio = false;
int audio_file_fd = -1;
};
}

View File

@@ -11,12 +11,23 @@
namespace gsr {
struct SupportedCaptureOptions;
enum class ReplayStartupMode {
DONT_TURN_ON_AUTOMATICALLY,
TURN_ON_AT_SYSTEM_STARTUP,
TURN_ON_AT_FULLSCREEN,
TURN_ON_AT_POWER_SUPPLY_CONNECTED
};
ReplayStartupMode replay_startup_string_to_type(const char *startup_mode_str);
struct ConfigHotkey {
int64_t key = 0; // Mgl key
uint32_t modifiers = 0; // HotkeyModifier
bool operator==(const ConfigHotkey &other) const;
bool operator!=(const ConfigHotkey &other) const;
std::string to_string(bool spaces = true, bool modifier_side = true) const;
};
struct RecordOptions {
@@ -101,15 +112,34 @@ namespace gsr {
ConfigHotkey save_hotkey;
};
struct ScreenshotConfig {
std::string record_area_option = "screen";
int32_t image_width = 0;
int32_t image_height = 0;
bool change_image_resolution = false;
std::string image_quality = "very_high";
std::string image_format = "jpg";
bool record_cursor = true;
bool restore_portal_session = true;
bool save_screenshot_in_game_folder = false;
bool show_screenshot_saved_notifications = true;
std::string save_directory;
ConfigHotkey take_screenshot_hotkey;
};
struct Config {
Config(const SupportedCaptureOptions &capture_options);
bool operator==(const Config &other);
bool operator!=(const Config &other);
void set_hotkeys_to_default();
MainConfig main_config;
StreamingConfig streaming_config;
RecordConfig record_config;
ReplayConfig replay_config;
ScreenshotConfig screenshot_config;
};
std::optional<Config> read_config(const SupportedCaptureOptions &capture_options);

View File

@@ -20,6 +20,11 @@ namespace gsr {
bool vp9 = false;
};
struct SupportedImageFormats {
bool jpeg = false;
bool png = false;
};
struct GsrMonitor {
std::string name;
mgl::vec2i size;
@@ -75,6 +80,7 @@ namespace gsr {
SystemInfo system_info;
GpuInfo gpu_info;
SupportedVideoCodecs supported_video_codecs;
SupportedImageFormats supported_image_formats;
};
enum class GsrInfoExitStatus {

View File

@@ -8,6 +8,7 @@
#include "WindowUtils.hpp"
#include "GlobalHotkeysLinux.hpp"
#include "GlobalHotkeysJoystick.hpp"
#include "AudioPlayer.hpp"
#include <mglpp/window/Window.hpp>
#include <mglpp/window/Event.hpp>
@@ -34,7 +35,8 @@ namespace gsr {
NONE,
RECORD,
REPLAY,
STREAM
STREAM,
SCREENSHOT
};
class Overlay {
@@ -56,6 +58,7 @@ namespace gsr {
void toggle_stream();
void toggle_replay();
void save_replay();
void take_screenshot();
void show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type);
bool is_open() const;
bool should_exit(std::string &reason) const;
@@ -69,6 +72,7 @@ namespace gsr {
void handle_keyboard_mapping_event();
void on_event(mgl::Event &event);
void create_frontpage_ui_components();
void xi_setup();
void handle_xi_events();
void process_key_bindings(mgl::Event &event);
@@ -80,12 +84,15 @@ namespace gsr {
void update_notification_process_status();
void save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type);
void on_replay_saved(const char *replay_saved_filepath);
void update_gsr_replay_save();
void update_gsr_process_status();
void update_gsr_screenshot_process_status();
void replay_status_update_status();
void update_focused_fullscreen_status();
void update_power_supply_status();
void update_system_startup_status();
void on_stop_recording(int exit_code);
@@ -102,9 +109,10 @@ namespace gsr {
void update_ui_replay_stopped();
void on_press_save_replay();
void on_press_start_replay(bool disable_notification);
bool on_press_start_replay(bool disable_notification);
void on_press_start_record();
void on_press_start_stream();
void on_press_take_screenshot();
bool update_compositor_texture(const Monitor &monitor);
void force_window_on_top();
@@ -147,6 +155,7 @@ namespace gsr {
pid_t notification_process = -1;
int gpu_screen_recorder_process_output_fd = -1;
FILE *gpu_screen_recorder_process_output_file = nullptr;
pid_t gpu_screen_recorder_screenshot_process = -1;
DropdownButton *replay_dropdown_button_ptr = nullptr;
DropdownButton *record_dropdown_button_ptr = nullptr;
@@ -163,6 +172,7 @@ namespace gsr {
bool focused_window_is_fullscreen = false;
std::string record_filepath;
std::string screenshot_filepath;
Display *xi_display = nullptr;
int xi_opcode = 0;
@@ -185,5 +195,12 @@ namespace gsr {
std::unique_ptr<GlobalHotkeysJoystick> global_hotkeys_js = nullptr;
Display *x11_mapping_display = nullptr;
XEvent x11_mapping_xev;
mgl::Clock replay_save_clock;
bool replay_save_show_notification = false;
ReplayStartupMode replay_startup_mode = ReplayStartupMode::TURN_ON_AT_SYSTEM_STARTUP;
bool try_replay_startup = true;
AudioPlayer audio_player;
};
}

View File

@@ -12,14 +12,14 @@ namespace gsr {
};
// Arguments ending with NULL
bool exec_program_daemonized(const char **args);
bool exec_program_daemonized(const char **args, bool debug = true);
// Arguments ending with NULL. |read_fd| can be NULL
pid_t exec_program(const char **args, int *read_fd);
pid_t exec_program(const char **args, int *read_fd, bool debug = true);
// Arguments ending with NULL. Returns the exit status of the program or -1 on error
int exec_program_get_stdout(const char **args, std::string &result);
int exec_program_get_stdout(const char **args, std::string &result, bool debug = true);
// Arguments ending with NULL. Returns the exit status of the program or -1 on error.
// This works the same as |exec_program_get_stdout|, except on flatpak where this runs the program on the
// host machine with flatpak-spawn --host
int exec_program_on_host_get_stdout(const char **args, std::string &result);
int exec_program_on_host_get_stdout(const char **args, std::string &result, bool debug = true);
pid_t pidof(const char *process_name, pid_t ignore_pid);
}

View File

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

View File

@@ -24,6 +24,8 @@ namespace gsr {
std::map<std::string, std::string> get_xdg_variables();
std::string get_videos_dir();
std::string get_pictures_dir();
// Returns 0 on success
int create_directory_recursive(char *path);
bool file_get_content(const char *filepath, std::string &file_content);

View File

@@ -3,6 +3,7 @@
#include <mglpp/system/vec.hpp>
#include <string>
#include <vector>
#include <optional>
#include <X11/Xlib.h>
namespace gsr {
@@ -16,8 +17,11 @@ namespace gsr {
mgl::vec2i size;
};
std::optional<std::string> get_window_title(Display *dpy, Window window);
Window get_focused_window(Display *dpy, WindowCaptureType cap_type);
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type);
std::string get_window_name_at_position(Display *dpy, mgl::vec2i position, Window ignore_window);
std::string get_window_name_at_cursor_position(Display *dpy, Window ignore_window);
mgl::vec2i get_cursor_position(Display *dpy, Window *window);
mgl::vec2i create_window_get_center_position(Display *display);
std::string get_window_manager_name(Display *display);

View File

@@ -30,6 +30,7 @@ namespace gsr {
std::function<void()> on_click;
private:
void scale_sprite_to_button_size();
float get_button_height();
private:
mgl::vec2f size;
mgl::Color bg_color;

View File

@@ -20,6 +20,7 @@ namespace gsr {
void add_item(const std::string &text, const std::string &id, const std::string &description = "");
void set_item_label(const std::string &id, const std::string &new_label);
void set_item_icon(const std::string &id, mgl::Texture *texture);
void set_item_description(const std::string &id, const std::string &new_description);
void set_description(std::string description_text);
void set_activated(bool activated);

View File

@@ -25,6 +25,7 @@ namespace gsr {
RECORD_START_STOP,
RECORD_PAUSE_UNPAUSE,
STREAM_START_STOP,
TAKE_SCREENSHOT,
SHOW_HIDE
};
@@ -44,6 +45,7 @@ namespace gsr {
std::function<void(const char *reason)> on_click_exit_program_button;
std::function<void(const char *hotkey_option)> on_keyboard_hotkey_changed;
std::function<void(const char *hotkey_option)> on_joystick_hotkey_changed;
std::function<void()> on_page_closed;
private:
void load_hotkeys();
@@ -55,6 +57,7 @@ namespace gsr {
std::unique_ptr<List> create_replay_hotkey_options();
std::unique_ptr<List> create_record_hotkey_options();
std::unique_ptr<List> create_stream_hotkey_options();
std::unique_ptr<List> create_screenshot_hotkey_options();
std::unique_ptr<List> create_hotkey_control_buttons();
std::unique_ptr<Subsection> create_hotkey_subsection(ScrollablePage *parent_page);
std::unique_ptr<Button> create_exit_program_button();
@@ -86,6 +89,7 @@ namespace gsr {
Button *start_stop_recording_button_ptr = nullptr;
Button *pause_unpause_recording_button_ptr = nullptr;
Button *start_stop_streaming_button_ptr = nullptr;
Button *take_screenshot_button_ptr = nullptr;
Button *show_hide_button_ptr = nullptr;
ConfigHotkey configure_config_hotkey;

View File

@@ -9,7 +9,7 @@
namespace gsr {
class GsrPage : public Page {
public:
GsrPage();
GsrPage(const char *top_text, const char *bottom_text);
GsrPage(const GsrPage&) = delete;
GsrPage& operator=(const GsrPage&) = delete;
@@ -42,7 +42,8 @@ namespace gsr {
float margin_bottom_scale = 0.0f;
float margin_left_scale = 0.0f;
float margin_right_scale = 0.0f;
mgl::Text label_text;
mgl::Text top_text;
mgl::Text bottom_text;
std::vector<ButtonItem> buttons;
};
}

View File

@@ -0,0 +1,78 @@
#pragma once
#include "StaticPage.hpp"
#include "List.hpp"
#include "ComboBox.hpp"
#include "Entry.hpp"
#include "CheckBox.hpp"
#include "../GsrInfo.hpp"
#include "../Config.hpp"
namespace gsr {
class PageStack;
class GsrPage;
class ScrollablePage;
class Button;
class ScreenshotSettingsPage : public StaticPage {
public:
ScreenshotSettingsPage(const GsrInfo *gsr_info, Config &config, PageStack *page_stack);
ScreenshotSettingsPage(const ScreenshotSettingsPage&) = delete;
ScreenshotSettingsPage& operator=(const ScreenshotSettingsPage&) = delete;
void load();
void save();
void on_navigate_away_from_page() override;
private:
std::unique_ptr<ComboBox> create_record_area_box();
std::unique_ptr<Widget> create_record_area();
std::unique_ptr<List> create_select_window();
std::unique_ptr<Entry> create_image_width_entry();
std::unique_ptr<Entry> create_image_height_entry();
std::unique_ptr<List> create_image_resolution();
std::unique_ptr<List> create_image_resolution_section();
std::unique_ptr<CheckBox> create_restore_portal_session_checkbox();
std::unique_ptr<List> create_restore_portal_session_section();
std::unique_ptr<Widget> create_change_image_resolution_section();
std::unique_ptr<Widget> create_capture_target_section();
std::unique_ptr<List> create_image_quality_section();
std::unique_ptr<Widget> create_record_cursor_section();
std::unique_ptr<Widget> create_image_section();
std::unique_ptr<List> create_save_directory(const char *label);
std::unique_ptr<ComboBox> create_image_format_box();
std::unique_ptr<List> create_image_format_section();
std::unique_ptr<Widget> create_file_info_section();
std::unique_ptr<CheckBox> create_save_screenshot_in_game_folder();
std::unique_ptr<Widget> create_general_section();
std::unique_ptr<Widget> create_notifications_section();
std::unique_ptr<Widget> create_settings();
void add_widgets();
void save(RecordOptions &record_options);
private:
Config &config;
const GsrInfo *gsr_info = nullptr;
SupportedCaptureOptions capture_options;
GsrPage *content_page_ptr = nullptr;
ScrollablePage *settings_scrollable_page_ptr = nullptr;
List *select_window_list_ptr = nullptr;
List *image_resolution_list_ptr = nullptr;
List *restore_portal_session_list_ptr = nullptr;
List *color_range_list_ptr = nullptr;
Widget *image_format_ptr = nullptr;
ComboBox *record_area_box_ptr = nullptr;
Entry *image_width_entry_ptr = nullptr;
Entry *image_height_entry_ptr = nullptr;
CheckBox *record_cursor_checkbox_ptr = nullptr;
CheckBox *restore_portal_session_checkbox_ptr = nullptr;
CheckBox *change_image_resolution_checkbox_ptr = nullptr;
ComboBox *image_quality_box_ptr = nullptr;
ComboBox *image_format_box_ptr = nullptr;
Button *save_directory_button_ptr = nullptr;
CheckBox *save_screenshot_in_game_folder_checkbox_ptr = nullptr;
CheckBox *show_screenshot_saved_notification_checkbox_ptr = nullptr;
PageStack *page_stack = nullptr;
};
}

View File

@@ -52,7 +52,7 @@ namespace gsr {
std::unique_ptr<CheckBox> create_restore_portal_session_checkbox();
std::unique_ptr<List> create_restore_portal_session_section();
std::unique_ptr<Widget> create_change_video_resolution_section();
std::unique_ptr<Widget> create_capture_target();
std::unique_ptr<Widget> create_capture_target_section();
std::unique_ptr<ComboBox> create_audio_device_selection_combobox();
std::unique_ptr<Button> create_remove_audio_device_button(List *audio_device_list_ptr);
std::unique_ptr<List> create_audio_device();
@@ -93,13 +93,14 @@ namespace gsr {
std::unique_ptr<List> create_save_directory(const char *label);
std::unique_ptr<ComboBox> create_container_box();
std::unique_ptr<List> create_container_section();
std::unique_ptr<Entry> create_replay_time_entry();
std::unique_ptr<List> create_replay_time_entry();
std::unique_ptr<List> create_replay_time();
std::unique_ptr<RadioButton> create_start_replay_automatically();
std::unique_ptr<CheckBox> create_save_replay_in_game_folder();
std::unique_ptr<CheckBox> create_restart_replay_on_save();
std::unique_ptr<Label> create_estimated_replay_file_size();
void update_estimated_replay_file_size();
void update_replay_time_text();
std::unique_ptr<CheckBox> create_save_recording_in_game_folder();
std::unique_ptr<Label> create_estimated_record_file_size();
void update_estimated_record_file_size();
@@ -186,6 +187,7 @@ namespace gsr {
Entry *youtube_stream_key_entry_ptr = nullptr;
Entry *stream_url_entry_ptr = nullptr;
Entry *replay_time_entry_ptr = nullptr;
Label *replay_time_label_ptr = nullptr;
RadioButton *turn_on_replay_automatically_mode_ptr = nullptr;
PageStack *page_stack = nullptr;

View File

@@ -1,4 +1,4 @@
project('gsr-ui', ['c', 'cpp'], version : '1.1.4', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
project('gsr-ui', ['c', 'cpp'], version : '1.2.2', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
if get_option('buildtype') == 'debug'
add_project_arguments('-g3', language : ['c', 'cpp'])
@@ -27,6 +27,7 @@ src = [
'src/gui/CustomRendererWidget.cpp',
'src/gui/FileChooser.cpp',
'src/gui/SettingsPage.cpp',
'src/gui/ScreenshotSettingsPage.cpp',
'src/gui/GlobalSettingsPage.cpp',
'src/gui/GsrPage.cpp',
'src/gui/Subsection.cpp',
@@ -39,6 +40,7 @@ src = [
'src/GlobalHotkeysX11.cpp',
'src/GlobalHotkeysLinux.cpp',
'src/GlobalHotkeysJoystick.cpp',
'src/AudioPlayer.cpp',
'src/Hotplug.cpp',
'src/Rpc.cpp',
'src/main.cpp',
@@ -52,7 +54,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.1.2"', language: ['c', 'cpp'])
add_project_arguments('-DGSR_FLATPAK_VERSION="5.1.7"', language: ['c', 'cpp'])
executable(
meson.project_name(),
@@ -65,6 +67,7 @@ executable(
dependency('xfixes'),
dependency('xi'),
dependency('xcursor'),
dependency('libpulse-simple'),
],
cpp_args : '-DGSR_UI_RESOURCES_PATH="' + gsr_ui_resources_path + '"',
)
@@ -74,6 +77,7 @@ executable(
[
'tools/gsr-global-hotkeys/hotplug.c',
'tools/gsr-global-hotkeys/keyboard_event.c',
'tools/gsr-global-hotkeys/keys.c',
'tools/gsr-global-hotkeys/main.c'
],
c_args : '-fstack-protector-all',

View File

@@ -1,7 +1,7 @@
[package]
name = "gsr-ui"
type = "executable"
version = "1.1.4"
version = "1.2.2"
platforms = ["posix"]
[lang.cpp]
@@ -15,3 +15,4 @@ xcomposite = ">=0"
xfixes = ">=0"
xi = ">=0"
xcursor = ">=1"
libpulse-simple = ">=0"

86
src/AudioPlayer.cpp Normal file
View File

@@ -0,0 +1,86 @@
#include "../include/AudioPlayer.hpp"
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <pulse/simple.h>
#include <pulse/error.h>
#define BUFSIZE 4096
namespace gsr {
AudioPlayer::~AudioPlayer() {
if(thread.joinable()) {
stop_playing_audio = true;
thread.join();
}
if(audio_file_fd > 0)
close(audio_file_fd);
}
bool AudioPlayer::play(const char *filepath) {
if(thread.joinable()) {
stop_playing_audio = true;
thread.join();
}
stop_playing_audio = false;
audio_file_fd = open(filepath, O_RDONLY);
if(audio_file_fd == -1)
return false;
thread = std::thread([this]() {
pa_sample_spec ss;
ss.format = PA_SAMPLE_S16LE;
ss.rate = 48000;
ss.channels = 2;
pa_simple *s = NULL;
int error;
/* Create a new playback stream */
if(!(s = pa_simple_new(NULL, "gsr-ui-audio-playback", PA_STREAM_PLAYBACK, NULL, "playback", &ss, NULL, NULL, &error))) {
fprintf(stderr, __FILE__": pa_simple_new() failed: %s\n", pa_strerror(error));
goto finish;
}
uint8_t buf[BUFSIZE];
for(;;) {
ssize_t r;
if(stop_playing_audio)
goto finish;
if((r = read(audio_file_fd, buf, sizeof(buf))) <= 0) {
if(r == 0) /* EOF */
break;
fprintf(stderr, __FILE__": read() failed: %s\n", strerror(errno));
goto finish;
}
if(pa_simple_write(s, buf, (size_t) r, &error) < 0) {
fprintf(stderr, __FILE__": pa_simple_write() failed: %s\n", pa_strerror(error));
goto finish;
}
}
if(pa_simple_drain(s, &error) < 0) {
fprintf(stderr, __FILE__": pa_simple_drain() failed: %s\n", pa_strerror(error));
goto finish;
}
finish:
if(s)
pa_simple_free(s);
close(audio_file_fd);
audio_file_fd = -1;
});
return true;
}
}

View File

@@ -6,6 +6,8 @@
#include <limits.h>
#include <inttypes.h>
#include <libgen.h>
#include <string.h>
#include <assert.h>
#include <mglpp/window/Keyboard.hpp>
#define FORMAT_I32 "%" PRIi32
@@ -13,6 +15,50 @@
#define FORMAT_U32 "%" PRIu32
namespace gsr {
static std::vector<mgl::Keyboard::Key> hotkey_modifiers_to_mgl_keys(uint32_t modifiers) {
std::vector<mgl::Keyboard::Key> result;
if(modifiers & HOTKEY_MOD_LCTRL)
result.push_back(mgl::Keyboard::LControl);
if(modifiers & HOTKEY_MOD_LSHIFT)
result.push_back(mgl::Keyboard::LShift);
if(modifiers & HOTKEY_MOD_LALT)
result.push_back(mgl::Keyboard::LAlt);
if(modifiers & HOTKEY_MOD_LSUPER)
result.push_back(mgl::Keyboard::LSystem);
if(modifiers & HOTKEY_MOD_RCTRL)
result.push_back(mgl::Keyboard::RControl);
if(modifiers & HOTKEY_MOD_RSHIFT)
result.push_back(mgl::Keyboard::RShift);
if(modifiers & HOTKEY_MOD_RALT)
result.push_back(mgl::Keyboard::RAlt);
if(modifiers & HOTKEY_MOD_RSUPER)
result.push_back(mgl::Keyboard::RSystem);
return result;
}
static void string_remove_all(std::string &str, const std::string &substr) {
size_t index = 0;
while(true) {
index = str.find(substr, index);
if(index == std::string::npos)
break;
str.erase(index, substr.size());
}
}
ReplayStartupMode replay_startup_string_to_type(const char *startup_mode_str) {
if(strcmp(startup_mode_str, "dont_turn_on_automatically") == 0)
return ReplayStartupMode::DONT_TURN_ON_AUTOMATICALLY;
else if(strcmp(startup_mode_str, "turn_on_at_system_startup") == 0)
return ReplayStartupMode::TURN_ON_AT_SYSTEM_STARTUP;
else if(strcmp(startup_mode_str, "turn_on_at_fullscreen") == 0)
return ReplayStartupMode::TURN_ON_AT_FULLSCREEN;
else if(strcmp(startup_mode_str, "turn_on_at_power_supply_connected") == 0)
return ReplayStartupMode::TURN_ON_AT_POWER_SUPPLY_CONNECTED;
else
return ReplayStartupMode::DONT_TURN_ON_AUTOMATICALLY;
}
bool ConfigHotkey::operator==(const ConfigHotkey &other) const {
return key == other.key && modifiers == other.modifiers;
}
@@ -21,36 +67,83 @@ namespace gsr {
return !operator==(other);
}
Config::Config(const SupportedCaptureOptions &capture_options) {
const std::string default_save_directory = get_videos_dir();
std::string ConfigHotkey::to_string(bool spaces, bool modifier_side) const {
std::string result;
const std::vector<mgl::Keyboard::Key> modifier_keys = hotkey_modifiers_to_mgl_keys(modifiers);
std::string modifier_str;
for(const mgl::Keyboard::Key modifier_key : modifier_keys) {
if(!result.empty()) {
if(spaces)
result += " + ";
else
result += "+";
}
modifier_str = mgl::Keyboard::key_to_string(modifier_key);
if(!modifier_side) {
string_remove_all(modifier_str, "Left");
string_remove_all(modifier_str, "Right");
}
result += modifier_str;
}
if(key != 0) {
if(!result.empty()) {
if(spaces)
result += " + ";
else
result += "+";
}
result += mgl::Keyboard::key_to_string((mgl::Keyboard::Key)key);
}
return result;
}
Config::Config(const SupportedCaptureOptions &capture_options) {
const std::string default_videos_save_directory = get_videos_dir();
const std::string default_pictures_save_directory = get_pictures_dir();
set_hotkeys_to_default();
streaming_config.start_stop_hotkey = {mgl::Keyboard::F8, HOTKEY_MOD_LALT};
streaming_config.record_options.video_quality = "custom";
streaming_config.record_options.audio_tracks.push_back("default_output");
streaming_config.record_options.video_bitrate = 15000;
record_config.start_stop_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LALT};
record_config.pause_unpause_hotkey = {mgl::Keyboard::F7, HOTKEY_MOD_LALT};
record_config.save_directory = default_save_directory;
record_config.save_directory = default_videos_save_directory;
record_config.record_options.audio_tracks.push_back("default_output");
record_config.record_options.video_bitrate = 45000;
replay_config.start_stop_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT | HOTKEY_MOD_LSHIFT};
replay_config.save_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT};
replay_config.record_options.video_quality = "custom";
replay_config.save_directory = default_save_directory;
replay_config.save_directory = default_videos_save_directory;
replay_config.record_options.audio_tracks.push_back("default_output");
replay_config.record_options.video_bitrate = 45000;
main_config.show_hide_hotkey = {mgl::Keyboard::Z, HOTKEY_MOD_LALT};
screenshot_config.save_directory = default_pictures_save_directory;
if(!capture_options.monitors.empty()) {
streaming_config.record_options.record_area_option = capture_options.monitors.front().name;
record_config.record_options.record_area_option = capture_options.monitors.front().name;
replay_config.record_options.record_area_option = capture_options.monitors.front().name;
screenshot_config.record_area_option = capture_options.monitors.front().name;
}
}
void Config::set_hotkeys_to_default() {
streaming_config.start_stop_hotkey = {mgl::Keyboard::F8, HOTKEY_MOD_LALT};
record_config.start_stop_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LALT};
record_config.pause_unpause_hotkey = {mgl::Keyboard::F7, HOTKEY_MOD_LALT};
replay_config.start_stop_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT | HOTKEY_MOD_LSHIFT};
replay_config.save_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT};
screenshot_config.take_screenshot_hotkey = {mgl::Keyboard::F1, HOTKEY_MOD_LALT};
main_config.show_hide_hotkey = {mgl::Keyboard::Z, HOTKEY_MOD_LALT};
}
static std::optional<KeyValue> parse_key_value(std::string_view line) {
const size_t space_index = line.find(' ');
if(space_index == std::string_view::npos)
@@ -156,7 +249,20 @@ namespace gsr {
{"replay.container", &config.replay_config.container},
{"replay.time", &config.replay_config.replay_time},
{"replay.start_stop_hotkey", &config.replay_config.start_stop_hotkey},
{"replay.save_hotkey", &config.replay_config.save_hotkey}
{"replay.save_hotkey", &config.replay_config.save_hotkey},
{"screenshot.record_area_option", &config.screenshot_config.record_area_option},
{"screenshot.image_width", &config.screenshot_config.image_width},
{"screenshot.image_height", &config.screenshot_config.image_height},
{"screenshot.change_image_resolution", &config.screenshot_config.change_image_resolution},
{"screenshot.image_quality", &config.screenshot_config.image_quality},
{"screenshot.image_format", &config.screenshot_config.image_format},
{"screenshot.record_cursor", &config.screenshot_config.record_cursor},
{"screenshot.restore_portal_session", &config.screenshot_config.restore_portal_session},
{"screenshot.save_screenshot_in_game_folder", &config.screenshot_config.save_screenshot_in_game_folder},
{"screenshot.show_screenshot_saved_notifications", &config.screenshot_config.show_screenshot_saved_notifications},
{"screenshot.save_directory", &config.screenshot_config.save_directory},
{"screenshot.take_screenshot_hotkey", &config.screenshot_config.take_screenshot_hotkey}
};
}
@@ -183,6 +289,8 @@ namespace gsr {
} else if(std::holds_alternative<std::vector<std::string>*>(it.second)) {
if(*std::get<std::vector<std::string>*>(it.second) != *std::get<std::vector<std::string>*>(it_other->second))
return false;
} else {
assert(false);
}
}
return true;
@@ -245,6 +353,8 @@ namespace gsr {
} else if(std::holds_alternative<std::vector<std::string>*>(it->second)) {
std::string array_value(key_value->value);
std::get<std::vector<std::string>*>(it->second)->push_back(std::move(array_value));
} else {
assert(false);
}
return true;
@@ -294,6 +404,8 @@ namespace gsr {
for(const std::string &value : *array) {
fprintf(file, "%.*s %s\n", (int)it.first.size(), it.first.data(), value.c_str());
}
} else {
assert(false);
}
}

View File

@@ -91,15 +91,6 @@ namespace gsr {
if(!user_homepath)
user_homepath = "/tmp";
char gsr_global_hotkeys_flatpak[PATH_MAX];
snprintf(gsr_global_hotkeys_flatpak, sizeof(gsr_global_hotkeys_flatpak), "%s/.local/share/gpu-screen-recorder/gsr-global-hotkeys", user_homepath);
const char *display = getenv("DISPLAY");
if(!display)
display = ":0";
char env_arg[256];
snprintf(env_arg, sizeof(env_arg), "--env=DISPLAY=%s", display);
if(process_id > 0)
return false;
@@ -136,7 +127,7 @@ namespace gsr {
}
if(inside_flatpak) {
const char *args[] = { "flatpak-spawn", "--host", env_arg, "--", gsr_global_hotkeys_flatpak, grab_type_arg, nullptr };
const char *args[] = { "flatpak-spawn", "--host", "/var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/bin/kms-server-proxy", "launch-gsr-global-hotkeys", user_homepath, grab_type_arg, nullptr };
execvp(args[0], (char* const*)args);
} else {
const char *args[] = { "gsr-global-hotkeys", grab_type_arg, nullptr };

View File

@@ -157,11 +157,19 @@ namespace gsr {
gsr_info->supported_video_codecs.vp9 = true;
}
static void parse_image_formats_line(GsrInfo *gsr_info, std::string_view line) {
if(line == "jpeg")
gsr_info->supported_image_formats.jpeg = true;
else if(line == "png")
gsr_info->supported_image_formats.png = true;
}
enum class GsrInfoSection {
UNKNOWN,
SYSTEM_INFO,
GPU_INFO,
VIDEO_CODECS,
IMAGE_FORMATS,
CAPTURE_OPTIONS
};
@@ -194,6 +202,8 @@ namespace gsr {
section = GsrInfoSection::GPU_INFO;
else if(section_name == "video_codecs")
section = GsrInfoSection::VIDEO_CODECS;
else if(section_name == "image_formats")
section = GsrInfoSection::IMAGE_FORMATS;
else if(section_name == "capture_options")
section = GsrInfoSection::CAPTURE_OPTIONS;
else
@@ -217,6 +227,10 @@ namespace gsr {
parse_video_codecs_line(gsr_info, line);
break;
}
case GsrInfoSection::IMAGE_FORMATS: {
parse_image_formats_line(gsr_info, line);
break;
}
case GsrInfoSection::CAPTURE_OPTIONS: {
// Intentionally ignore, get capture options with get_supported_capture_options instead
break;
@@ -244,7 +258,7 @@ namespace gsr {
std::string stdout_str;
const char *args[] = { "gpu-screen-recorder", "--list-audio-devices", nullptr };
if(exec_program_get_stdout(args, stdout_str) != 0) {
if(exec_program_get_stdout(args, stdout_str, false) != 0) {
fprintf(stderr, "error: 'gpu-screen-recorder --list-audio-devices' failed\n");
return audio_devices;
}

View File

@@ -44,9 +44,10 @@ namespace gsr {
}
void Hotplug::process_event_data(int fd, const HotplugEventCallback &callback) {
const int bytes_read = read(fd, event_data, sizeof(event_data));
const int bytes_read = read(fd, event_data, sizeof(event_data) - 1);
if(bytes_read <= 0)
return;
event_data[bytes_read] = '\0';
/* Hotplug data ends with a newline and a null terminator */
int data_index = 0;

View File

@@ -7,10 +7,10 @@
#include "../include/gui/DropdownButton.hpp"
#include "../include/gui/CustomRendererWidget.hpp"
#include "../include/gui/SettingsPage.hpp"
#include "../include/gui/ScreenshotSettingsPage.hpp"
#include "../include/gui/GlobalSettingsPage.hpp"
#include "../include/gui/Utils.hpp"
#include "../include/gui/PageStack.hpp"
#include "../include/gui/GsrPage.hpp"
#include "../include/WindowUtils.hpp"
#include "../include/GlobalHotkeys.hpp"
@@ -20,7 +20,9 @@
#include <limits.h>
#include <fcntl.h>
#include <poll.h>
#include <malloc.h>
#include <stdexcept>
#include <algorithm>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
@@ -40,7 +42,10 @@ extern "C" {
namespace gsr {
static const mgl::Color bg_color(0, 0, 0, 100);
static const double force_window_on_top_timeout_seconds = 1.0;
static const double replay_status_update_check_timeout_seconds = 1.0;
static 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.0;
static const double notification_error_timeout_seconds = 5.0;
static mgl::Texture texture_from_ximage(XImage *img) {
uint8_t *texture_data = (uint8_t*)malloc(img->width * img->height * 3);
@@ -228,15 +233,6 @@ namespace gsr {
return is_fullscreen;
}
static void set_focused_window(Display *dpy, Window window) {
XSetInputFocus(dpy, window, RevertToPointerRoot, CurrentTime);
const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False);
XChangeProperty(dpy, DefaultRootWindow(dpy), net_active_window_atom, XA_WINDOW, 32, PropModeReplace, (const unsigned char*)&window, 1);
XFlush(dpy);
}
#define _NET_WM_STATE_REMOVE 0
#define _NET_WM_STATE_ADD 1
#define _NET_WM_STATE_TOGGLE 2
@@ -385,6 +381,13 @@ namespace gsr {
fprintf(stderr, "pressed %s\n", id.c_str());
overlay->save_replay();
});
global_hotkeys->bind_key_press(
config_hotkey_to_hotkey(overlay->get_config().screenshot_config.take_screenshot_hotkey),
"take_screenshot", [overlay](const std::string &id) {
fprintf(stderr, "pressed %s\n", id.c_str());
overlay->take_screenshot();
});
}
static std::unique_ptr<GlobalHotkeysLinux> register_linux_hotkeys(Overlay *overlay, GlobalHotkeysLinux::GrabType grab_type) {
@@ -436,9 +439,7 @@ namespace gsr {
init_color_theme(config, this->gsr_info);
power_supply_online_filepath = get_power_supply_online_filepath();
if(config.replay_config.turn_on_replay_automatically_mode == "turn_on_at_system_startup")
on_press_start_replay(true);
replay_startup_mode = replay_startup_string_to_type(config.replay_config.turn_on_replay_automatically_mode.c_str());
if(config.main_config.hotkeys_enable_option == "enable_hotkeys")
global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::ALL);
@@ -478,6 +479,16 @@ namespace gsr {
gpu_screen_recorder_process = -1;
}
if(gpu_screen_recorder_screenshot_process > 0) {
kill(gpu_screen_recorder_screenshot_process, SIGINT);
int status;
if(waitpid(gpu_screen_recorder_screenshot_process, &status, 0) == -1) {
perror("waitpid failed");
/* Ignore... */
}
gpu_screen_recorder_screenshot_process = -1;
}
close_gpu_screen_recorder_output();
deinit_color_theme();
@@ -682,6 +693,7 @@ namespace gsr {
update_notification_process_status();
update_gsr_replay_save();
update_gsr_process_status();
update_gsr_screenshot_process_status();
replay_status_update_status();
if(!visible)
@@ -772,9 +784,32 @@ namespace gsr {
if(cursor_size <= 1)
cursor_size = 24;
XcursorImage *cursor_image = XcursorShapeLoadImage(XC_left_ptr, cursor_theme, cursor_size);
XcursorImage *cursor_image = nullptr;
for(int cursor_size_test : {cursor_size, 24}) {
for(const char *cursor_theme_test : {cursor_theme, "default", "Adwaita"}) {
for(unsigned int shape : {XC_left_ptr, XC_arrow}) {
cursor_image = XcursorShapeLoadImage(shape, cursor_theme_test, cursor_size_test);
if(cursor_image)
goto done;
}
}
}
done:
if(!cursor_image) {
fprintf(stderr, "Error: failed to get cursor, loading bundled default cursor instead\n");
const std::string default_cursor_path = resources_path + "images/default.cur";
for(int cursor_size_test : {cursor_size, 24}) {
cursor_image = XcursorFilenameLoadImage(default_cursor_path.c_str(), cursor_size_test);
if(cursor_image)
break;
}
}
if(!cursor_image) {
fprintf(stderr, "Error: failed to get cursor\n");
XFixesShowCursor(xi_display, DefaultRootWindow(xi_display));
XFlush(xi_display);
return;
}
@@ -924,6 +959,71 @@ namespace gsr {
update_compositor_texture(*focused_monitor);
create_frontpage_ui_components();
// The focused application can be an xwayland application but the cursor can hover over a wayland application.
// This is even the case when hovering over the titlebar of the xwayland application.
const bool fake_cursor = is_wlroots ? x11_cursor_window != None : prevent_game_minimizing;
if(fake_cursor)
xi_setup();
//window->set_fullscreen(true);
if(gsr_info.system_info.display_server == DisplayServer::X11)
make_window_click_through(display, window->get_system_handle());
window->set_visible(true);
make_window_sticky(display, window->get_system_handle());
hide_window_from_taskbar(display, window->get_system_handle());
if(default_cursor) {
XFreeCursor(display, default_cursor);
default_cursor = 0;
}
default_cursor = XCreateFontCursor(display, XC_left_ptr);
XFlush(display);
grab_mouse_and_keyboard();
// The real cursor doesn't move when all devices are grabbed, so we create our own cursor and diplay that while grabbed
xi_setup_fake_cursor();
// We want to grab all devices to prevent any other application below the UI from receiving events.
// Owlboy seems to use xi events and XGrabPointer doesn't prevent owlboy from receiving events.
xi_grab_all_mouse_devices();
if(!is_wlroots)
window->set_fullscreen(true);
visible = true;
if(gpu_screen_recorder_process > 0) {
switch(recording_status) {
case RecordingStatus::NONE:
break;
case RecordingStatus::REPLAY:
update_ui_replay_started();
break;
case RecordingStatus::RECORD:
update_ui_recording_started();
break;
case RecordingStatus::STREAM:
update_ui_streaming_started();
break;
}
}
if(paused)
update_ui_recording_paused();
// Wayland compositors have retarded fullscreen animations that we cant disable in a proper way
// without messing up window position.
show_overlay_timeout_seconds = prevent_game_minimizing ? 0.0 : 0.15;
show_overlay_clock.restart();
draw();
}
void Overlay::create_frontpage_ui_components() {
bg_screenshot_overlay = mgl::Rectangle(mgl::vec2f(get_theme().window_width, get_theme().window_height));
top_bar_background = mgl::Rectangle(mgl::vec2f(get_theme().window_width, get_theme().window_height*0.06f).floor());
top_bar_text = mgl::Text("GPU Screen Recorder", get_theme().top_bar_font);
@@ -961,8 +1061,8 @@ namespace gsr {
auto button = std::make_unique<DropdownButton>(&get_theme().title_font, &get_theme().body_font, "Instant Replay", "Off", &get_theme().replay_button_texture,
mgl::vec2f(button_width, button_height));
replay_dropdown_button_ptr = button.get();
button->add_item("Turn on", "start", "Alt+Shift+F10");
button->add_item("Save", "save", "Alt+F10");
button->add_item("Turn on", "start", config.replay_config.start_stop_hotkey.to_string(false, false));
button->add_item("Save", "save", config.replay_config.save_hotkey.to_string(false, false));
button->add_item("Settings", "settings");
button->set_item_icon("start", &get_theme().play_texture);
button->set_item_icon("save", &get_theme().save_texture);
@@ -971,8 +1071,9 @@ namespace gsr {
if(id == "settings") {
auto replay_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::REPLAY, &gsr_info, config, &page_stack);
replay_settings_page->on_config_changed = [this]() {
replay_startup_mode = replay_startup_string_to_type(config.replay_config.turn_on_replay_automatically_mode.c_str());
if(recording_status == RecordingStatus::REPLAY)
show_notification("Replay settings have been modified.\nYou may need to restart replay to apply the changes.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
show_notification("Replay settings have been modified.\nYou may need to restart replay to apply the changes.", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
};
page_stack.push(std::move(replay_settings_page));
} else if(id == "save") {
@@ -987,8 +1088,8 @@ namespace gsr {
auto button = std::make_unique<DropdownButton>(&get_theme().title_font, &get_theme().body_font, "Record", "Not recording", &get_theme().record_button_texture,
mgl::vec2f(button_width, button_height));
record_dropdown_button_ptr = button.get();
button->add_item("Start", "start", "Alt+F9");
button->add_item("Pause", "pause", "Alt+F7");
button->add_item("Start", "start", config.record_config.start_stop_hotkey.to_string(false, false));
button->add_item("Pause", "pause", config.record_config.pause_unpause_hotkey.to_string(false, false));
button->add_item("Settings", "settings");
button->set_item_icon("start", &get_theme().play_texture);
button->set_item_icon("pause", &get_theme().pause_texture);
@@ -998,7 +1099,7 @@ namespace gsr {
auto record_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::RECORD, &gsr_info, config, &page_stack);
record_settings_page->on_config_changed = [this]() {
if(recording_status == RecordingStatus::RECORD)
show_notification("Recording settings have been modified.\nYou may need to restart recording to apply the changes.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
show_notification("Recording settings have been modified.\nYou may need to restart recording to apply the changes.", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
};
page_stack.push(std::move(record_settings_page));
} else if(id == "pause") {
@@ -1013,7 +1114,7 @@ namespace gsr {
auto button = std::make_unique<DropdownButton>(&get_theme().title_font, &get_theme().body_font, "Livestream", "Not streaming", &get_theme().stream_button_texture,
mgl::vec2f(button_width, button_height));
stream_dropdown_button_ptr = button.get();
button->add_item("Start", "start", "Alt+F8");
button->add_item("Start", "start", config.streaming_config.start_stop_hotkey.to_string(false, false));
button->add_item("Settings", "settings");
button->set_item_icon("start", &get_theme().play_texture);
button->set_item_icon("settings", &get_theme().settings_small_texture);
@@ -1022,7 +1123,7 @@ namespace gsr {
auto stream_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::STREAM, &gsr_info, config, &page_stack);
stream_settings_page->on_config_changed = [this]() {
if(recording_status == RecordingStatus::STREAM)
show_notification("Streaming settings have been modified.\nYou may need to restart streaming to apply the changes.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
show_notification("Streaming settings have been modified.\nYou may need to restart streaming to apply the changes.", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
};
page_stack.push(std::move(stream_settings_page));
} else if(id == "start") {
@@ -1038,7 +1139,7 @@ namespace gsr {
{
const mgl::vec2f main_buttons_size = main_buttons_list_ptr->get_size();
const int settings_button_size = main_buttons_size.y * 0.2f;
const int settings_button_size = main_buttons_size.y * 0.33f;
auto button = std::make_unique<Button>(&get_theme().title_font, "", mgl::vec2f(settings_button_size, settings_button_size), mgl::Color(0, 0, 0, 180));
button->set_position((main_buttons_list_ptr->get_position() + main_buttons_size - mgl::vec2f(0.0f, settings_button_size) + mgl::vec2f(settings_button_size * 0.333f, 0.0f)).floor());
button->set_bg_hover_color(mgl::Color(0, 0, 0, 255));
@@ -1052,12 +1153,12 @@ namespace gsr {
if(exit_status == 127) {
if(enable)
show_notification("Failed to add GPU Screen Recorder to system startup.\nThis option only works on systems that use systemd.\nYou have to manually add \"gsr-ui\" to system startup on systems that uses another init system.", 10.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE);
show_notification("Failed to add GPU Screen Recorder to system startup.\nThis option only works on systems that use systemd.\nYou have to manually add \"gsr-ui\" to system startup on systems that uses another init system.", 7.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE);
} else {
if(enable)
show_notification("Failed to add GPU Screen Recorder to system startup", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE);
show_notification("Failed to add GPU Screen Recorder to system startup", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE);
else
show_notification("Failed to remove GPU Screen Recorder from system startup", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE);
show_notification("Failed to remove GPU Screen Recorder from system startup", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE);
}
};
@@ -1084,11 +1185,45 @@ namespace gsr {
global_hotkeys_js.reset();
};
settings_page->on_page_closed = [this]() {
if(global_hotkeys) {
replay_dropdown_button_ptr->set_item_description("start", config.replay_config.start_stop_hotkey.to_string(false, false));
replay_dropdown_button_ptr->set_item_description("save", config.replay_config.save_hotkey.to_string(false, false));
record_dropdown_button_ptr->set_item_description("start", config.record_config.start_stop_hotkey.to_string(false, false));
record_dropdown_button_ptr->set_item_description("pause", config.record_config.pause_unpause_hotkey.to_string(false, false));
stream_dropdown_button_ptr->set_item_description("start", config.streaming_config.start_stop_hotkey.to_string(false, false));
} else {
replay_dropdown_button_ptr->set_item_description("start", "");
replay_dropdown_button_ptr->set_item_description("save", "");
record_dropdown_button_ptr->set_item_description("start", "");
record_dropdown_button_ptr->set_item_description("pause", "");
stream_dropdown_button_ptr->set_item_description("start", "");
}
};
page_stack.push(std::move(settings_page));
};
front_page_ptr->add_widget(std::move(button));
}
{
const mgl::vec2f main_buttons_size = main_buttons_list_ptr->get_size();
const int settings_button_size = main_buttons_size.y * 0.33f;
auto button = std::make_unique<Button>(&get_theme().title_font, "", mgl::vec2f(settings_button_size, settings_button_size), mgl::Color(0, 0, 0, 180));
button->set_position((main_buttons_list_ptr->get_position() + main_buttons_size - mgl::vec2f(0.0f, settings_button_size*2) + mgl::vec2f(settings_button_size * 0.333f, 0.0f)).floor());
button->set_bg_hover_color(mgl::Color(0, 0, 0, 255));
button->set_icon(&get_theme().screenshot_texture);
button->on_click = [&]() {
auto screenshot_settings_page = std::make_unique<ScreenshotSettingsPage>(&gsr_info, config, &page_stack);
page_stack.push(std::move(screenshot_settings_page));
};
front_page_ptr->add_widget(std::move(button));
}
close_button_widget.draw_handler = [&](mgl::Window &window, mgl::vec2f pos, mgl::vec2f size) {
const int border_size = std::max(1.0f, 0.0015f * get_theme().window_height);
const float padding_size = std::max(1.0f, 0.003f * get_theme().window_height);
@@ -1115,72 +1250,6 @@ namespace gsr {
}
return true;
};
// The focused application can be an xwayland application but the cursor can hover over a wayland application.
// This is even the case when hovering over the titlebar of the xwayland application.
const bool fake_cursor = is_wlroots ? x11_cursor_window != None : prevent_game_minimizing;
if(fake_cursor)
xi_setup();
//window->set_fullscreen(true);
if(gsr_info.system_info.display_server == DisplayServer::X11)
make_window_click_through(display, window->get_system_handle());
window->set_visible(true);
make_window_sticky(display, window->get_system_handle());
hide_window_from_taskbar(display, window->get_system_handle());
if(default_cursor) {
XFreeCursor(display, default_cursor);
default_cursor = 0;
}
default_cursor = XCreateFontCursor(display, XC_left_ptr);
XFlush(display);
grab_mouse_and_keyboard();
// The real cursor doesn't move when all devices are grabbed, so we create our own cursor and diplay that while grabbed
xi_setup_fake_cursor();
// We want to grab all devices to prevent any other application below the UI from receiving events.
// Owlboy seems to use xi events and XGrabPointer doesn't prevent owlboy from receiving events.
xi_grab_all_mouse_devices();
// if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) {
// set_focused_window(display, window->get_system_handle());
// XFlush(display);
// }
if(!is_wlroots)
window->set_fullscreen(true);
visible = true;
if(gpu_screen_recorder_process > 0) {
switch(recording_status) {
case RecordingStatus::NONE:
break;
case RecordingStatus::REPLAY:
update_ui_replay_started();
break;
case RecordingStatus::RECORD:
update_ui_recording_started();
break;
case RecordingStatus::STREAM:
update_ui_streaming_started();
break;
}
}
if(paused)
update_ui_recording_paused();
// Wayland compositors have retarded fullscreen animations that we cant disable in a proper way
// without messing up window position.
show_overlay_timeout_seconds = prevent_game_minimizing ? 0.0 : 0.15;
show_overlay_clock.restart();
draw();
}
void Overlay::hide() {
@@ -1259,6 +1328,7 @@ namespace gsr {
}
deinit_theme();
malloc_trim(0);
}
void Overlay::toggle_show() {
@@ -1284,10 +1354,10 @@ namespace gsr {
if(paused) {
update_ui_recording_unpaused();
show_notification("Recording has been unpaused", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
show_notification("Recording has been unpaused", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
} else {
update_ui_recording_paused();
show_notification("Recording has been paused", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
show_notification("Recording has been paused", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
}
kill(gpu_screen_recorder_process, SIGUSR2);
@@ -1306,12 +1376,17 @@ namespace gsr {
on_press_save_replay();
}
void Overlay::take_screenshot() {
on_press_take_screenshot();
}
static const char* notification_type_to_string(NotificationType notification_type) {
switch(notification_type) {
case NotificationType::NONE: return nullptr;
case NotificationType::RECORD: return "record";
case NotificationType::REPLAY: return "replay";
case NotificationType::STREAM: return "stream";
case NotificationType::NONE: return nullptr;
case NotificationType::RECORD: return "record";
case NotificationType::REPLAY: return "replay";
case NotificationType::STREAM: return "stream";
case NotificationType::SCREENSHOT: return "screenshot";
}
return nullptr;
}
@@ -1426,7 +1501,10 @@ namespace gsr {
Display *display = (Display*)context->connection;
const std::string video_filename = filepath_get_filename(video_filepath);
std::string focused_window_name = get_focused_window_name(display, WindowCaptureType::FOCUSED);
const Window gsr_ui_window = window ? window->get_system_handle() : None;
std::string focused_window_name = get_window_name_at_cursor_position(display, gsr_ui_window);
if(focused_window_name.empty())
focused_window_name = get_focused_window_name(display, WindowCaptureType::FOCUSED);
if(focused_window_name.empty())
focused_window_name = "Game";
@@ -1453,14 +1531,35 @@ namespace gsr {
text = "Saved replay to '" + focused_window_name + "/" + video_filename + "'";
break;
}
case NotificationType::SCREENSHOT: {
if(!config.screenshot_config.show_screenshot_saved_notifications)
return;
text = "Saved screenshot to '" + focused_window_name + "/" + video_filename + "'";
break;
}
case NotificationType::NONE:
case NotificationType::STREAM:
break;
}
show_notification(text.c_str(), 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, notification_type);
show_notification(text.c_str(), notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, notification_type);
}
void Overlay::on_replay_saved(const char *replay_saved_filepath) {
replay_save_show_notification = false;
if(config.replay_config.save_video_in_game_folder) {
save_video_in_current_game_directory(replay_saved_filepath, NotificationType::REPLAY);
} else {
const std::string text = "Saved replay to '" + filepath_get_filename(replay_saved_filepath) + "'";
show_notification(text.c_str(), notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
}
}
void Overlay::update_gsr_replay_save() {
if(replay_save_show_notification && replay_save_clock.get_elapsed_time_seconds() >= replay_saving_notification_timeout_seconds) {
replay_save_show_notification = false;
show_notification("Saving replay, this might take some time", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
}
if(gpu_screen_recorder_process_output_file) {
char buffer[1024];
char *replay_saved_filepath = fgets(buffer, sizeof(buffer), gpu_screen_recorder_process_output_file);
@@ -1471,12 +1570,7 @@ namespace gsr {
if(replay_saved_filepath[line_len - 1] == '\n')
replay_saved_filepath[line_len - 1] = '\0';
if(config.replay_config.save_video_in_game_folder) {
save_video_in_current_game_directory(replay_saved_filepath, NotificationType::REPLAY);
} else {
const std::string text = "Saved replay to '" + filepath_get_filename(replay_saved_filepath) + "'";
show_notification(text.c_str(), 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
}
on_replay_saved(replay_saved_filepath);
} else if(gpu_screen_recorder_process_output_fd > 0) {
char buffer[1024];
read(gpu_screen_recorder_process_output_fd, buffer, sizeof(buffer));
@@ -1506,10 +1600,10 @@ namespace gsr {
update_ui_replay_stopped();
if(exit_code == 0) {
if(config.replay_config.show_replay_stopped_notifications)
show_notification("Replay stopped", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
show_notification("Replay stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
} else {
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
show_notification("Replay stopped because of an error. Verify if settings are correct", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY);
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);
}
break;
}
@@ -1522,10 +1616,10 @@ namespace gsr {
update_ui_streaming_stopped();
if(exit_code == 0) {
if(config.streaming_config.show_streaming_stopped_notifications)
show_notification("Streaming has stopped", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
show_notification("Streaming has stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
} else {
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
show_notification("Streaming stopped because of an error. Verify if settings are correct", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM);
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);
}
break;
}
@@ -1535,6 +1629,62 @@ namespace gsr {
recording_status = RecordingStatus::NONE;
}
void Overlay::update_gsr_screenshot_process_status() {
if(gpu_screen_recorder_screenshot_process <= 0)
return;
int status;
if(waitpid(gpu_screen_recorder_screenshot_process, &status, WNOHANG) == 0) {
// Still running
return;
}
int exit_code = -1;
if(WIFEXITED(status))
exit_code = WEXITSTATUS(status);
if(exit_code == 0) {
if(config.screenshot_config.save_screenshot_in_game_folder) {
save_video_in_current_game_directory(screenshot_filepath.c_str(), NotificationType::SCREENSHOT);
} else {
const std::string text = "Saved screenshot to '" + filepath_get_filename(screenshot_filepath.c_str()) + "'";
show_notification(text.c_str(), notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::SCREENSHOT);
}
} else {
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_screenshot_process, exit_code);
show_notification("Failed to take a screenshot. Verify if settings are correct", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::SCREENSHOT);
}
gpu_screen_recorder_screenshot_process = -1;
}
static bool starts_with(std::string_view str, const char *substr) {
size_t len = strlen(substr);
return str.size() >= len && memcmp(str.data(), substr, len) == 0;
}
static bool are_all_audio_tracks_available_to_capture(const std::vector<std::string> &audio_tracks) {
const auto audio_devices = get_audio_devices();
for(const std::string &audio_track : audio_tracks) {
std::string_view audio_track_name(audio_track.c_str());
const bool is_app_audio = starts_with(audio_track_name, "app:");
if(is_app_audio)
continue;
if(starts_with(audio_track_name, "device:"))
audio_track_name.remove_prefix(7);
auto it = std::find_if(audio_devices.begin(), audio_devices.end(), [&](const auto &audio_device) {
return audio_device.name == audio_track_name;
});
if(it == audio_devices.end()) {
//fprintf(stderr, "Audio not ready\n");
return false;
}
}
return true;
}
void Overlay::replay_status_update_status() {
if(replay_status_update_clock.get_elapsed_time_seconds() < replay_status_update_check_timeout_seconds)
return;
@@ -1542,10 +1692,11 @@ namespace gsr {
replay_status_update_clock.restart();
update_focused_fullscreen_status();
update_power_supply_status();
update_system_startup_status();
}
void Overlay::update_focused_fullscreen_status() {
if(config.replay_config.turn_on_replay_automatically_mode != "turn_on_at_fullscreen")
if(replay_startup_mode != ReplayStartupMode::TURN_ON_AT_FULLSCREEN)
return;
mgl_context *context = mgl_get_context();
@@ -1558,39 +1709,51 @@ namespace gsr {
const bool prev_focused_window_is_fullscreen = focused_window_is_fullscreen;
focused_window_is_fullscreen = focused_window != 0 && window_is_fullscreen(display, focused_window);
if(focused_window_is_fullscreen != prev_focused_window_is_fullscreen) {
if(recording_status == RecordingStatus::NONE && focused_window_is_fullscreen)
on_press_start_replay(false);
else if(recording_status == RecordingStatus::REPLAY && !focused_window_is_fullscreen)
if(recording_status == RecordingStatus::NONE && focused_window_is_fullscreen) {
if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks))
on_press_start_replay(false);
} else if(recording_status == RecordingStatus::REPLAY && !focused_window_is_fullscreen) {
on_press_start_replay(true);
}
}
}
// TODO: Instead of checking power supply status periodically listen to power supply event
void Overlay::update_power_supply_status() {
if(config.replay_config.turn_on_replay_automatically_mode != "turn_on_at_power_supply_connected")
if(replay_startup_mode != ReplayStartupMode::TURN_ON_AT_POWER_SUPPLY_CONNECTED)
return;
const bool prev_power_supply_status = power_supply_connected;
power_supply_connected = power_supply_online_filepath.empty() || power_supply_is_connected(power_supply_online_filepath.c_str());
if(power_supply_connected != prev_power_supply_status) {
if(recording_status == RecordingStatus::NONE && power_supply_connected)
on_press_start_replay(false);
else if(recording_status == RecordingStatus::REPLAY && !power_supply_connected)
if(recording_status == RecordingStatus::NONE && power_supply_connected) {
if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks))
on_press_start_replay(false);
} else if(recording_status == RecordingStatus::REPLAY && !power_supply_connected) {
on_press_start_replay(false);
}
}
}
void Overlay::update_system_startup_status() {
if(replay_startup_mode != ReplayStartupMode::TURN_ON_AT_SYSTEM_STARTUP || recording_status != RecordingStatus::NONE || !try_replay_startup)
return;
if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks))
on_press_start_replay(true);
}
void Overlay::on_stop_recording(int exit_code) {
if(exit_code == 0) {
if(config.record_config.save_video_in_game_folder) {
save_video_in_current_game_directory(record_filepath.c_str(), NotificationType::RECORD);
} else {
const std::string text = "Saved recording to '" + filepath_get_filename(record_filepath.c_str()) + "'";
show_notification(text.c_str(), 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
show_notification(text.c_str(), notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
}
} 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", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD);
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);
}
}
@@ -1695,11 +1858,6 @@ namespace gsr {
return container;
}
static bool starts_with(std::string_view str, const char *substr) {
size_t len = strlen(substr);
return str.size() >= len && memcmp(str.data(), substr, len) == 0;
}
static std::vector<std::string> create_audio_tracks_real_names(const std::vector<std::string> &audio_tracks, bool application_audio_invert, const GsrInfo &gsr_info) {
std::vector<std::string> result;
for(const std::string &audio_track : audio_tracks) {
@@ -1780,23 +1938,27 @@ namespace gsr {
if(recording_status != RecordingStatus::REPLAY || gpu_screen_recorder_process <= 0)
return;
replay_save_show_notification = true;
replay_save_clock.restart();
kill(gpu_screen_recorder_process, SIGUSR1);
}
void Overlay::on_press_start_replay(bool disable_notification) {
bool Overlay::on_press_start_replay(bool disable_notification) {
switch(recording_status) {
case RecordingStatus::NONE:
case RecordingStatus::REPLAY:
break;
case RecordingStatus::RECORD:
show_notification("Unable to start replay when recording.\nStop recording before starting replay.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
return;
show_notification("Unable to start replay when recording.\nStop recording before starting replay.", notification_error_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
return false;
case RecordingStatus::STREAM:
show_notification("Unable to start replay when streaming.\nStop streaming before starting replay.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
return;
show_notification("Unable to start replay when streaming.\nStop streaming before starting replay.", notification_error_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
return false;
}
paused = false;
replay_save_show_notification = false;
try_replay_startup = false;
// window->close();
// usleep(1000 * 50); // 50 milliseconds
@@ -1817,15 +1979,16 @@ namespace gsr {
// TODO: Show this with a slight delay to make sure it doesn't show up in the video
if(!disable_notification && config.replay_config.show_replay_stopped_notifications)
show_notification("Replay stopped", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
return;
show_notification("Replay stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
return true;
}
if(!validate_capture_target(gsr_info, config.replay_config.record_options.record_area_option)) {
char err_msg[256];
snprintf(err_msg, sizeof(err_msg), "Failed to start replay, capture target \"%s\" is invalid. Please change capture target in settings", config.replay_config.record_options.record_area_option.c_str());
show_notification(err_msg, 3.0, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::REPLAY);
return;
show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::REPLAY);
return false;
}
// TODO: Validate input, fallback to valid values
@@ -1899,7 +2062,9 @@ namespace gsr {
// program and start another one. This can also be used to check when the notification has finished by checking with waitpid NOWAIT
// to see when the program has exit.
if(!disable_notification && config.replay_config.show_replay_started_notifications)
show_notification("Replay has started", 3.0, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::REPLAY);
show_notification("Replay has started", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::REPLAY);
return true;
}
void Overlay::on_press_start_record() {
@@ -1908,10 +2073,10 @@ namespace gsr {
case RecordingStatus::RECORD:
break;
case RecordingStatus::REPLAY:
show_notification("Unable to start recording when replay is turned on.\nTurn off replay before starting recording.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
show_notification("Unable to start recording when replay is turned on.\nTurn off replay before starting recording.", notification_error_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
return;
case RecordingStatus::STREAM:
show_notification("Unable to start recording when streaming.\nStop streaming before starting recording.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
show_notification("Unable to start recording when streaming.\nStop streaming before starting recording.", notification_error_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
return;
}
@@ -1943,7 +2108,7 @@ namespace gsr {
if(!validate_capture_target(gsr_info, config.record_config.record_options.record_area_option)) {
char err_msg[256];
snprintf(err_msg, sizeof(err_msg), "Failed to start recording, capture target \"%s\" is invalid. Please change capture target in settings", config.record_config.record_options.record_area_option.c_str());
show_notification(err_msg, 3.0, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::RECORD);
show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::RECORD);
return;
}
@@ -2005,7 +2170,7 @@ namespace gsr {
// 2...
// 1...
if(config.record_config.show_recording_started_notifications)
show_notification("Recording has started", 3.0, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD);
show_notification("Recording has started", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD);
}
static std::string streaming_get_url(const Config &config) {
@@ -2046,10 +2211,10 @@ namespace gsr {
case RecordingStatus::STREAM:
break;
case RecordingStatus::REPLAY:
show_notification("Unable to start streaming when replay is turned on.\nTurn off replay before starting streaming.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
show_notification("Unable to start streaming when replay is turned on.\nTurn off replay before starting streaming.", notification_error_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
return;
case RecordingStatus::RECORD:
show_notification("Unable to start streaming when recording.\nStop recording before starting streaming.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
show_notification("Unable to start streaming when recording.\nStop recording before starting streaming.", notification_error_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
return;
}
@@ -2072,14 +2237,14 @@ namespace gsr {
// TODO: Show this with a slight delay to make sure it doesn't show up in the video
if(config.streaming_config.show_streaming_stopped_notifications)
show_notification("Streaming has stopped", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
show_notification("Streaming has stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
return;
}
if(!validate_capture_target(gsr_info, config.streaming_config.record_options.record_area_option)) {
char err_msg[256];
snprintf(err_msg, sizeof(err_msg), "Failed to start streaming, capture target \"%s\" is invalid. Please change capture target in settings", config.streaming_config.record_options.record_area_option.c_str());
show_notification(err_msg, 3.0, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::STREAM);
show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::STREAM);
return;
}
@@ -2146,7 +2311,53 @@ namespace gsr {
// program and start another one. This can also be used to check when the notification has finished by checking with waitpid NOWAIT
// to see when the program has exit.
if(config.streaming_config.show_streaming_started_notifications)
show_notification("Streaming has started", 3.0, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM);
show_notification("Streaming has started", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM);
}
void Overlay::on_press_take_screenshot() {
if(gpu_screen_recorder_screenshot_process > 0) {
fprintf(stderr, "Error: failed to take screenshot, another screenshot is currently being saved\n");
return;
}
if(!validate_capture_target(gsr_info, config.screenshot_config.record_area_option)) {
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", config.screenshot_config.record_area_option.c_str());
show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::SCREENSHOT);
return;
}
// TODO: Validate input, fallback to valid values
const std::string output_file = config.screenshot_config.save_directory + "/Screenshot_" + get_date_str() + "." + config.screenshot_config.image_format; // TODO: Validate image format
std::vector<const char*> args = {
"gpu-screen-recorder", "-w", config.screenshot_config.record_area_option.c_str(),
"-cursor", config.screenshot_config.record_cursor ? "yes" : "no",
"-v", "no",
"-q", config.screenshot_config.image_quality.c_str(),
"-o", output_file.c_str()
};
char region[64];
region[0] = '\0';
if(config.screenshot_config.change_image_resolution) {
snprintf(region, sizeof(region), "%dx%d", (int)config.screenshot_config.image_width, (int)config.screenshot_config.image_height);
args.push_back("-s");
args.push_back(region);
}
if(config.screenshot_config.restore_portal_session) {
args.push_back("-restore-portal-session");
args.push_back("yes");
}
args.push_back(nullptr);
screenshot_filepath = output_file;
gpu_screen_recorder_screenshot_process = exec_program(args.data(), nullptr);
if(gpu_screen_recorder_screenshot_process == -1) {
// TODO: Show notification failed to start
}
}
bool Overlay::update_compositor_texture(const Monitor &monitor) {

View File

@@ -40,12 +40,13 @@ namespace gsr {
return num_args;
}
bool exec_program_daemonized(const char **args) {
bool exec_program_daemonized(const char **args, bool debug) {
/* 1 argument */
if(args[0] == nullptr)
return false;
debug_print_args(args);
if(debug)
debug_print_args(args);
const pid_t pid = vfork();
if(pid == -1) {
@@ -72,7 +73,7 @@ namespace gsr {
return true;
}
pid_t exec_program(const char **args, int *read_fd) {
pid_t exec_program(const char **args, int *read_fd, bool debug) {
if(read_fd)
*read_fd = -1;
@@ -84,7 +85,8 @@ namespace gsr {
if(pipe(fds) == -1)
return -1;
debug_print_args(args);
if(debug)
debug_print_args(args);
const pid_t pid = vfork();
if(pid == -1) {
@@ -110,10 +112,10 @@ namespace gsr {
}
}
int exec_program_get_stdout(const char **args, std::string &result) {
int exec_program_get_stdout(const char **args, std::string &result, bool debug) {
result.clear();
int read_fd = -1;
const pid_t process_id = exec_program(args, &read_fd);
const pid_t process_id = exec_program(args, &read_fd, debug);
if(process_id == -1)
return -1;
@@ -152,7 +154,7 @@ namespace gsr {
return exit_status;
}
int exec_program_on_host_get_stdout(const char **args, std::string &result) {
int exec_program_on_host_get_stdout(const char **args, std::string &result, bool debug) {
if(count_num_args(args) > 64 - 3) {
fprintf(stderr, "Error: too many arguments when trying to launch \"%s\"\n", args[0]);
return -1;
@@ -170,9 +172,9 @@ namespace gsr {
}
modified_args[i] = arg;
}
return exec_program_get_stdout(modified_args, result);
return exec_program_get_stdout(modified_args, result, debug);
} else {
return exec_program_get_stdout(args, result);
return exec_program_get_stdout(args, result, debug);
}
}

View File

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

View File

@@ -114,6 +114,14 @@ namespace gsr {
return xdg_videos_dir;
}
std::string get_pictures_dir() {
auto xdg_vars = get_xdg_variables();
std::string xdg_videos_dir = xdg_vars["XDG_PICTURES_DIR"];
if(xdg_videos_dir.empty())
xdg_videos_dir = get_home_dir() + "/Pictures";
return xdg_videos_dir;
}
int create_directory_recursive(char *path) {
int path_len = strlen(path);
char *p = path;

View File

@@ -16,8 +16,6 @@ extern "C" {
#include <string.h>
#include <poll.h>
#include <optional>
#define MAX_PROPERTY_VALUE_LEN 4096
namespace gsr {
@@ -105,8 +103,17 @@ namespace gsr {
unsigned int dummy_u;
mgl::vec2i root_pos;
XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, window, &root_pos.x, &root_pos.y, &dummy_i, &dummy_i, &dummy_u);
if(window)
*window = window_get_target_window_child(dpy, *window);
const Window direct_window = *window;
*window = window_get_target_window_child(dpy, *window);
// HACK: Count some other x11 windows as having an x11 window focused. Some games seem to create an Input window and that gets focused.
if(!*window) {
XWindowAttributes attr;
memset(&attr, 0, sizeof(attr));
XGetWindowAttributes(dpy, direct_window, &attr);
if(attr.c_class == InputOnly && !get_window_title(dpy, direct_window))
*window = direct_window;
}
return root_pos;
}
@@ -149,7 +156,7 @@ namespace gsr {
std::string result;
for(int i = 0; i < size;) {
// Some games such as the finals has utf8-bom between each character, wtf?
if(i + 3 < size && memcmp(str + i, "\xEF\xBB\xBF", 3) == 0) {
if(i + 3 <= size && memcmp(str + i, "\xEF\xBB\xBF", 3) == 0) {
i += 3;
continue;
}
@@ -163,7 +170,8 @@ namespace gsr {
return result;
}
static std::optional<std::string> get_window_title(Display *dpy, Window window) {
std::optional<std::string> get_window_title(Display *dpy, Window window) {
std::optional<std::string> result;
const Atom net_wm_name_atom = XInternAtom(dpy, "_NET_WM_NAME", False);
const Atom wm_name_atom = XInternAtom(dpy, "WM_NAME", False);
const Atom utf8_string_atom = XInternAtom(dpy, "UTF8_STRING", False);
@@ -175,8 +183,13 @@ namespace gsr {
unsigned char *data = NULL;
XGetWindowProperty(dpy, window, net_wm_name_atom, 0, 1024, False, utf8_string_atom, &type, &format, &num_items, &bytes_left, &data);
if(type == utf8_string_atom && format == 8 && data)
return utf8_sanitize(data, num_items);
if(type == utf8_string_atom && format == 8 && data) {
result = utf8_sanitize(data, num_items);
goto done;
}
if(data)
XFree(data);
type = None;
format = 0;
@@ -185,10 +198,15 @@ namespace gsr {
data = NULL;
XGetWindowProperty(dpy, window, wm_name_atom, 0, 1024, False, 0, &type, &format, &num_items, &bytes_left, &data);
if((type == XA_STRING || type == utf8_string_atom) && data)
return utf8_sanitize(data, num_items);
if((type == XA_STRING || type == utf8_string_atom) && data) {
result = utf8_sanitize(data, num_items);
goto done;
}
return std::nullopt;
done:
if(data)
XFree(data);
return result;
}
static std::string strip(const std::string &str) {
@@ -228,14 +246,61 @@ namespace gsr {
XClassHint class_hint = {nullptr, nullptr};
XGetClassHint(dpy, focused_window, &class_hint);
if(class_hint.res_class) {
if(class_hint.res_class)
result = strip(class_hint.res_class);
return result;
}
if(class_hint.res_name)
XFree(class_hint.res_name);
if(class_hint.res_class)
XFree(class_hint.res_class);
return result;
}
std::string get_window_name_at_position(Display *dpy, mgl::vec2i position, Window ignore_window) {
std::string result;
Window root;
Window parent;
Window *children = nullptr;
unsigned int num_children = 0;
if(!XQueryTree(dpy, DefaultRootWindow(dpy), &root, &parent, &children, &num_children) || !children)
return result;
for(int i = (int)num_children - 1; i >= 0; --i) {
if(children[i] == ignore_window)
continue;
XWindowAttributes attr;
memset(&attr, 0, sizeof(attr));
XGetWindowAttributes(dpy, children[i], &attr);
if(attr.override_redirect || attr.c_class != InputOutput || attr.map_state != IsViewable)
continue;
if(position.x >= attr.x && position.x <= attr.x + attr.width && position.y >= attr.y && position.y <= attr.y + attr.height) {
const Window real_window = window_get_target_window_child(dpy, children[i]);
if(!real_window || real_window == ignore_window)
continue;
const std::optional<std::string> window_title = get_window_title(dpy, real_window);
if(window_title)
result = strip(window_title.value());
break;
}
}
XFree(children);
return result;
}
std::string get_window_name_at_cursor_position(Display *dpy, Window ignore_window) {
Window cursor_window;
const mgl::vec2i cursor_position = get_cursor_position(dpy, &cursor_window);
return get_window_name_at_position(dpy, cursor_position, ignore_window);
}
typedef struct {
unsigned long flags;
unsigned long functions;

View File

@@ -15,8 +15,8 @@ namespace gsr {
// These are relative to the button size
static const float padding_top_icon_scale = 0.25f;
static const float padding_bottom_icon_scale = 0.25f;
static const float padding_left_icon_scale = 0.25f;
static const float padding_right_icon_scale = 0.25f;
//static const float padding_left_icon_scale = 0.25f;
static const float padding_right_icon_scale = 0.15f;
Button::Button(mgl::Font *font, const char *text, mgl::vec2f size, mgl::Color bg_color) :
size(size), bg_color(bg_color), bg_hover_color(bg_color), text(text, *font)
@@ -53,13 +53,21 @@ namespace gsr {
background.set_color(mouse_inside ? bg_hover_color : bg_color);
window.draw(background);
text.set_position((draw_pos + item_size * 0.5f - text.get_bounds().size * 0.5f).floor());
window.draw(text);
if(sprite.get_texture() && sprite.get_texture()->is_valid()) {
scale_sprite_to_button_size();
sprite.set_position((background.get_position() + background.get_size() * 0.5f - sprite.get_size() * 0.5f).floor());
const int padding_left = padding_left_scale * get_theme().window_height;
if(text.get_string().empty()) // Center
sprite.set_position((background.get_position() + background.get_size() * 0.5f - sprite.get_size() * 0.5f).floor());
else // Left
sprite.set_position((draw_pos + mgl::vec2f(padding_left, background.get_size().y * 0.5f - sprite.get_size().y * 0.5f)).floor());
window.draw(sprite);
const int padding_icon_right = padding_right_icon_scale * get_button_height();
text.set_position((sprite.get_position() + mgl::vec2f(sprite.get_size().x + padding_icon_right, sprite.get_size().y * 0.5f - text.get_bounds().size.y * 0.5f)).floor());
window.draw(text);
} else {
text.set_position((draw_pos + item_size * 0.5f - text.get_bounds().size * 0.5f).floor());
window.draw(text);
}
if(mouse_inside) {
@@ -72,18 +80,25 @@ namespace gsr {
if(!visible)
return {0.0f, 0.0f};
const int padding_top = padding_top_scale * get_theme().window_height;
const int padding_bottom = padding_bottom_scale * get_theme().window_height;
const int padding_left = padding_left_scale * get_theme().window_height;
const int padding_right = padding_right_scale * get_theme().window_height;
const mgl::vec2f text_bounds = text.get_bounds().size;
mgl::vec2f s = size;
if(s.x < 0.0001f)
s.x = padding_left + text_bounds.x + padding_right;
if(s.y < 0.0001f)
s.y = padding_top + text_bounds.y + padding_bottom;
return s;
mgl::vec2f widget_size = size;
if(widget_size.y < 0.0001f)
widget_size.y = get_button_height();
if(widget_size.x < 0.0001f) {
widget_size.x = padding_left + text_bounds.x + padding_right;
if(sprite.get_texture() && sprite.get_texture()->is_valid()) {
scale_sprite_to_button_size();
const int padding_icon_right = text_bounds.x > 0.001f ? padding_right_icon_scale * widget_size.y : 0.0f;
widget_size.x += sprite.get_size().x + padding_icon_right;
}
}
return widget_size;
}
void Button::set_border_scale(float scale) {
@@ -110,13 +125,23 @@ namespace gsr {
if(!sprite.get_texture() || !sprite.get_texture()->is_valid())
return;
const mgl::vec2f button_size = get_size();
const int padding_icon_top = padding_top_icon_scale * button_size.y;
const int padding_icon_bottom = padding_bottom_icon_scale * button_size.y;
const int padding_icon_left = padding_left_icon_scale * button_size.y;
const int padding_icon_right = padding_right_icon_scale * button_size.y;
const float widget_height = get_button_height();
const mgl::vec2f desired_size = button_size - mgl::vec2f(padding_icon_left + padding_icon_right, padding_icon_top + padding_icon_bottom);
sprite.set_size(scale_keep_aspect_ratio(sprite.get_texture()->get_size().to_vec2f(), desired_size).floor());
const int padding_icon_top = padding_top_icon_scale * widget_height;
const int padding_icon_bottom = padding_bottom_icon_scale * widget_height;
const float desired_height = widget_height - (padding_icon_top + padding_icon_bottom);
sprite.set_height((int)desired_height);
}
float Button::get_button_height() {
const int padding_top = padding_top_scale * get_theme().window_height;
const int padding_bottom = padding_bottom_scale * get_theme().window_height;
float widget_height = size.y;
if(widget_height < 0.0001f)
widget_height = padding_top + text.get_bounds().size.y + padding_bottom;
return widget_height;
}
}

View File

@@ -26,16 +26,21 @@ namespace gsr {
return true;
if(event.type == mgl::Event::MouseButtonPressed && event.mouse_button.button == mgl::Mouse::Left) {
const int padding_top = padding_top_scale * get_theme().window_height;
const int padding_bottom = padding_bottom_scale * get_theme().window_height;
const mgl::vec2f mouse_pos = { (float)event.mouse_button.x, (float)event.mouse_button.y };
const mgl::vec2f item_size = get_size();
mgl::vec2f item_size = get_size();
if(show_dropdown) {
for(size_t i = 0; i < items.size(); ++i) {
Item &item = items[i];
item_size.y = padding_top + item.text.get_bounds().size.y + padding_bottom;
if(mgl::FloatRect(item.position, item_size).contains(mouse_pos)) {
const size_t prev_selected_item = selected_item;
selected_item = i;
show_dropdown = false;
dirty = true;
remove_widget_as_selected_in_parent();
if(selected_item != prev_selected_item && on_selection_changed)
@@ -47,6 +52,7 @@ namespace gsr {
}
const mgl::vec2f draw_pos = position + offset;
item_size = get_size();
if(mgl::FloatRect(draw_pos, item_size).contains(mouse_pos)) {
show_dropdown = !show_dropdown;
if(show_dropdown)
@@ -66,9 +72,10 @@ namespace gsr {
if(!visible)
return;
//const mgl::Scissor scissor = window.get_scissor();
update_if_dirty();
const mgl::vec2f draw_pos = (position + offset).floor();
//max_size.x = std::min((scissor.position.x + scissor.size.x) - draw_pos.x, max_size.x);
if(show_dropdown)
draw_selected(window, draw_pos);
@@ -78,6 +85,8 @@ namespace gsr {
void ComboBox::add_item(const std::string &text, const std::string &id) {
items.push_back({mgl::Text(text, *font), id, {0.0f, 0.0f}});
items.back().text.set_max_width(font->get_character_size() * 22); // TODO: Make a proper solution
//items.back().text.set_max_rows(1);
dirty = true;
}
@@ -87,6 +96,7 @@ namespace gsr {
if(item.id == id) {
const size_t prev_selected_item = selected_item;
selected_item = i;
dirty = true;
if(trigger_event && (trigger_event_even_if_selection_not_changed || selected_item != prev_selected_item) && on_selection_changed)
on_selection_changed(item.text.get_string(), item.id);
@@ -107,13 +117,13 @@ namespace gsr {
void ComboBox::draw_selected(mgl::Window &window, mgl::vec2f draw_pos) {
const int padding_top = padding_top_scale * get_theme().window_height;
const int padding_bottom = padding_bottom_scale * get_theme().window_height;
const int padding_left = padding_left_scale * get_theme().window_height;
mgl_scissor scissor;
mgl_window_get_scissor(window.internal_window(), &scissor);
const mgl::Scissor scissor = window.get_scissor();
const bool bottom_is_outside_scissor = draw_pos.y + max_size.y > scissor.position.y + scissor.size.y;
const mgl::vec2f item_size = get_size();
mgl::vec2f item_size = get_size();
mgl::vec2f items_draw_pos = draw_pos + mgl::vec2f(0.0f, item_size.y);
mgl::Rectangle background(draw_pos, item_size.floor());
@@ -137,6 +147,9 @@ namespace gsr {
const mgl::vec2f mouse_pos = window.get_mouse_position().to_vec2f();
for(size_t i = 0; i < items.size(); ++i) {
Item &item = items[i];
item_size.y = padding_top + item.text.get_bounds().size.y + padding_bottom;
if(!cursor_inside) {
cursor_inside = mgl::FloatRect(items_draw_pos, item_size).contains(mouse_pos);
if(cursor_inside) {
@@ -146,7 +159,6 @@ namespace gsr {
}
}
Item &item = items[i];
item.text.set_position((items_draw_pos + mgl::vec2f(padding_left, padding_top)).floor());
window.draw(item.text);
@@ -160,7 +172,7 @@ namespace gsr {
const int padding_left = padding_left_scale * get_theme().window_height;
const int padding_right = padding_right_scale * get_theme().window_height;
const mgl::vec2f item_size = get_size();
mgl::vec2f item_size = get_size();
mgl::Rectangle background(draw_pos.floor(), item_size.floor());
background.set_color(mgl::Color(0, 0, 0, 120));
window.draw(background);
@@ -197,11 +209,12 @@ namespace gsr {
const int padding_left = padding_left_scale * get_theme().window_height;
const int padding_right = padding_right_scale * get_theme().window_height;
max_size = { 0.0f, font->get_character_size() + (float)padding_top + (float)padding_bottom };
Item *selected_item_ptr = (selected_item < items.size()) ? &items[selected_item] : nullptr;
max_size = { 0.0f, padding_top + padding_bottom + (selected_item_ptr ? selected_item_ptr->text.get_bounds().size.y : 0.0f) };
for(Item &item : items) {
const mgl::vec2f bounds = item.text.get_bounds().size;
max_size.x = std::max(max_size.x, bounds.x + padding_left + padding_right);
max_size.y += bounds.y + padding_top + padding_bottom;
max_size.y += padding_top + bounds.y + padding_bottom;
}
if(max_size.x <= 0.001f)
@@ -219,7 +232,8 @@ namespace gsr {
const int padding_top = padding_top_scale * get_theme().window_height;
const int padding_bottom = padding_bottom_scale * get_theme().window_height;
return { max_size.x, font->get_character_size() + (float)padding_top + (float)padding_bottom };
Item *selected_item_ptr = (selected_item < items.size()) ? &items[selected_item] : nullptr;
return { max_size.x, padding_top + padding_bottom + (selected_item_ptr ? selected_item_ptr->text.get_bounds().size.y : 0.0f) };
}
float ComboBox::get_dropdown_arrow_height() const {

View File

@@ -17,19 +17,11 @@ namespace gsr {
const mgl::vec2f draw_pos = position + offset;
mgl_scissor prev_scissor;
mgl_window_get_scissor(window.internal_window(), &prev_scissor);
const mgl_scissor new_scissor = {
mgl_vec2i{(int)draw_pos.x, (int)draw_pos.y},
mgl_vec2i{(int)size.x, (int)size.y}
};
mgl_window_set_scissor(window.internal_window(), &new_scissor);
const mgl::Scissor prev_scissor = window.get_scissor();
window.set_scissor({draw_pos.to_vec2i(), size.to_vec2i()});
if(draw_handler)
draw_handler(window, draw_pos, size);
mgl_window_set_scissor(window.internal_window(), &prev_scissor);
window.set_scissor(prev_scissor);
}
mgl::vec2f CustomRendererWidget::get_size() {

View File

@@ -201,6 +201,15 @@ namespace gsr {
}
}
void DropdownButton::set_item_description(const std::string &id, const std::string &new_description) {
for(auto &item : items) {
if(item.id == id) {
item.description_text.set_string(new_description);
return;
}
}
}
void DropdownButton::set_description(std::string description_text) {
description.set_string(std::move(description_text));
}

View File

@@ -65,8 +65,7 @@ namespace gsr {
if(!visible)
return;
mgl_scissor scissor;
mgl_window_get_scissor(window.internal_window(), &scissor);
const mgl::Scissor scissor = window.get_scissor();
const mgl::vec2f draw_pos = position + offset;
const mgl::vec2f mouse_pos = window.get_mouse_position().to_vec2f();

View File

@@ -67,46 +67,6 @@ namespace gsr {
return 0;
}
static std::vector<mgl::Keyboard::Key> hotkey_modifiers_to_mgl_keys(uint32_t modifiers) {
std::vector<mgl::Keyboard::Key> result;
if(modifiers & HOTKEY_MOD_LCTRL)
result.push_back(mgl::Keyboard::LControl);
if(modifiers & HOTKEY_MOD_LSHIFT)
result.push_back(mgl::Keyboard::LShift);
if(modifiers & HOTKEY_MOD_LALT)
result.push_back(mgl::Keyboard::LAlt);
if(modifiers & HOTKEY_MOD_LSUPER)
result.push_back(mgl::Keyboard::LSystem);
if(modifiers & HOTKEY_MOD_RCTRL)
result.push_back(mgl::Keyboard::RControl);
if(modifiers & HOTKEY_MOD_RSHIFT)
result.push_back(mgl::Keyboard::RShift);
if(modifiers & HOTKEY_MOD_RALT)
result.push_back(mgl::Keyboard::RAlt);
if(modifiers & HOTKEY_MOD_RSUPER)
result.push_back(mgl::Keyboard::RSystem);
return result;
}
static std::string config_hotkey_to_string(ConfigHotkey config_hotkey) {
std::string result;
const std::vector<mgl::Keyboard::Key> modifier_keys = hotkey_modifiers_to_mgl_keys(config_hotkey.modifiers);
for(const mgl::Keyboard::Key modifier_key : modifier_keys) {
if(!result.empty())
result += " + ";
result += mgl::Keyboard::key_to_string(modifier_key);
}
if(config_hotkey.key != 0) {
if(!result.empty())
result += " + ";
result += mgl::Keyboard::key_to_string((mgl::Keyboard::Key)config_hotkey.key);
}
return result;
}
GlobalSettingsPage::GlobalSettingsPage(Overlay *overlay, const GsrInfo *gsr_info, Config &config, PageStack *page_stack) :
StaticPage(mgl::vec2f(get_theme().window_width, get_theme().window_height).floor()),
overlay(overlay),
@@ -114,7 +74,7 @@ namespace gsr {
gsr_info(gsr_info),
page_stack(page_stack)
{
auto content_page = std::make_unique<GsrPage>();
auto content_page = std::make_unique<GsrPage>("Global", "Settings");
content_page->add_button("Back", "back", get_color_theme().page_bg_color);
content_page->on_click = [page_stack](const std::string &id) {
if(id == "back")
@@ -134,7 +94,7 @@ namespace gsr {
mgl::Text title_text("Press a key combination to use for the hotkey \"" + hotkey_configure_action_name + "\":", get_theme().title_font);
mgl::Text hotkey_text(configure_hotkey_button->get_text(), get_theme().top_bar_font);
mgl::Text description_text("The hotkey has to contain one or more of these keys: Alt, Ctrl, Shift and Super. Press Esc to cancel.", get_theme().body_font);
mgl::Text description_text("The hotkey has to contain one or more of these keys: Alt, Ctrl, Shift and Super. Press Esc to cancel or Backspace to remove the hotkey.", get_theme().body_font);
const float text_max_width = std::max(title_text.get_bounds().size.x, std::max(hotkey_text.get_bounds().size.x, description_text.get_bounds().size.x));
const float padding_horizontal = int(get_theme().window_height * 0.01f);
@@ -171,7 +131,7 @@ namespace gsr {
std::unique_ptr<Subsection> GlobalSettingsPage::create_appearance_subsection(ScrollablePage *parent_page) {
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Tint color", get_color_theme().text_color));
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Accent color", get_color_theme().text_color));
auto tint_color_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL);
tint_color_radio_button_ptr = tint_color_radio_button.get();
tint_color_radio_button->add_item("Red", "amd");
@@ -322,30 +282,41 @@ namespace gsr {
return list;
}
std::unique_ptr<List> GlobalSettingsPage::create_screenshot_hotkey_options() {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Take a screenshot:", get_color_theme().text_color));
auto take_screenshot_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
take_screenshot_button_ptr = take_screenshot_button.get();
list->add_widget(std::move(take_screenshot_button));
take_screenshot_button_ptr->on_click = [this] {
configure_hotkey_start(ConfigureHotkeyType::TAKE_SCREENSHOT);
};
return list;
}
std::unique_ptr<List> GlobalSettingsPage::create_hotkey_control_buttons() {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
// auto clear_hotkeys_button = std::make_unique<Button>(&get_theme().body_font, "Clear hotkeys", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
// clear_hotkeys_button->on_click = [this] {
// config.streaming_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
// config.record_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
// config.record_config.pause_unpause_hotkey = {mgl::Keyboard::Unknown, 0};
// config.replay_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
// config.replay_config.save_hotkey = {mgl::Keyboard::Unknown, 0};
// config.main_config.show_hide_hotkey = {mgl::Keyboard::Unknown, 0};
// load_hotkeys();
// overlay->rebind_all_keyboard_hotkeys();
// };
// list->add_widget(std::move(clear_hotkeys_button));
auto clear_hotkeys_button = std::make_unique<Button>(&get_theme().body_font, "Clear hotkeys", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
clear_hotkeys_button->on_click = [this] {
config.streaming_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
config.record_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
config.record_config.pause_unpause_hotkey = {mgl::Keyboard::Unknown, 0};
config.replay_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
config.replay_config.save_hotkey = {mgl::Keyboard::Unknown, 0};
config.screenshot_config.take_screenshot_hotkey = {mgl::Keyboard::Unknown, 0};
config.main_config.show_hide_hotkey = {mgl::Keyboard::Unknown, 0};
load_hotkeys();
overlay->rebind_all_keyboard_hotkeys();
};
list->add_widget(std::move(clear_hotkeys_button));
auto reset_hotkeys_button = std::make_unique<Button>(&get_theme().body_font, "Reset hotkeys to default", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
reset_hotkeys_button->on_click = [this] {
config.streaming_config.start_stop_hotkey = {mgl::Keyboard::F8, HOTKEY_MOD_LALT};
config.record_config.start_stop_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LALT};
config.record_config.pause_unpause_hotkey = {mgl::Keyboard::F7, HOTKEY_MOD_LALT};
config.replay_config.start_stop_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT | HOTKEY_MOD_LSHIFT};
config.replay_config.save_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT};
config.main_config.show_hide_hotkey = {mgl::Keyboard::Z, HOTKEY_MOD_LALT};
config.set_hotkeys_to_default();
load_hotkeys();
overlay->rebind_all_keyboard_hotkeys();
};
@@ -368,6 +339,7 @@ namespace gsr {
list_ptr->add_widget(create_replay_hotkey_options());
list_ptr->add_widget(create_record_hotkey_options());
list_ptr->add_widget(create_stream_hotkey_options());
list_ptr->add_widget(create_screenshot_hotkey_options());
list_ptr->add_widget(std::make_unique<Label>(&get_theme().body_font, "Double-click the controller share button to save a replay", get_color_theme().text_color));
list_ptr->add_widget(create_hotkey_control_buttons());
return subsection;
@@ -440,6 +412,8 @@ namespace gsr {
void GlobalSettingsPage::on_navigate_away_from_page() {
save();
if(on_page_closed)
on_page_closed();
}
void GlobalSettingsPage::load() {
@@ -460,15 +434,17 @@ namespace gsr {
}
void GlobalSettingsPage::load_hotkeys() {
turn_replay_on_off_button_ptr->set_text(config_hotkey_to_string(config.replay_config.start_stop_hotkey));
save_replay_button_ptr->set_text(config_hotkey_to_string(config.replay_config.save_hotkey));
turn_replay_on_off_button_ptr->set_text(config.replay_config.start_stop_hotkey.to_string());
save_replay_button_ptr->set_text(config.replay_config.save_hotkey.to_string());
start_stop_recording_button_ptr->set_text(config_hotkey_to_string(config.record_config.start_stop_hotkey));
pause_unpause_recording_button_ptr->set_text(config_hotkey_to_string(config.record_config.pause_unpause_hotkey));
start_stop_recording_button_ptr->set_text(config.record_config.start_stop_hotkey.to_string());
pause_unpause_recording_button_ptr->set_text(config.record_config.pause_unpause_hotkey.to_string());
start_stop_streaming_button_ptr->set_text(config_hotkey_to_string(config.streaming_config.start_stop_hotkey));
start_stop_streaming_button_ptr->set_text(config.streaming_config.start_stop_hotkey.to_string());
show_hide_button_ptr->set_text(config_hotkey_to_string(config.main_config.show_hide_hotkey));
take_screenshot_button_ptr->set_text(config.screenshot_config.take_screenshot_hotkey.to_string());
show_hide_button_ptr->set_text(config.main_config.show_hide_hotkey.to_string());
}
void GlobalSettingsPage::save() {
@@ -494,12 +470,19 @@ namespace gsr {
if(event.key.code == mgl::Keyboard::Escape)
return false;
if(event.key.code == mgl::Keyboard::Backspace) {
configure_config_hotkey = {mgl::Keyboard::Unknown, 0};
configure_hotkey_button->set_text("");
configure_hotkey_stop_and_save();
return false;
}
if(mgl::Keyboard::key_is_modifier(event.key.code)) {
configure_config_hotkey.modifiers |= mgl_modifier_to_hotkey_modifier(event.key.code);
configure_hotkey_button->set_text(config_hotkey_to_string(configure_config_hotkey));
configure_hotkey_button->set_text(configure_config_hotkey.to_string());
} else if(configure_config_hotkey.modifiers != 0) {
configure_config_hotkey.key = event.key.code;
configure_hotkey_button->set_text(config_hotkey_to_string(configure_config_hotkey));
configure_hotkey_button->set_text(configure_config_hotkey.to_string());
configure_hotkey_stop_and_save();
}
@@ -512,7 +495,7 @@ namespace gsr {
if(mgl::Keyboard::key_is_modifier(event.key.code)) {
configure_config_hotkey.modifiers &= ~mgl_modifier_to_hotkey_modifier(event.key.code);
configure_hotkey_button->set_text(config_hotkey_to_string(configure_config_hotkey));
configure_hotkey_button->set_text(configure_config_hotkey.to_string());
}
return false;
@@ -535,6 +518,8 @@ namespace gsr {
return pause_unpause_recording_button_ptr;
case ConfigureHotkeyType::STREAM_START_STOP:
return start_stop_streaming_button_ptr;
case ConfigureHotkeyType::TAKE_SCREENSHOT:
return take_screenshot_button_ptr;
case ConfigureHotkeyType::SHOW_HIDE:
return show_hide_button_ptr;
}
@@ -555,6 +540,8 @@ namespace gsr {
return &config.record_config.pause_unpause_hotkey;
case ConfigureHotkeyType::STREAM_START_STOP:
return &config.streaming_config.start_stop_hotkey;
case ConfigureHotkeyType::TAKE_SCREENSHOT:
return &config.screenshot_config.take_screenshot_hotkey;
case ConfigureHotkeyType::SHOW_HIDE:
return &config.main_config.show_hide_hotkey;
}
@@ -568,6 +555,7 @@ namespace gsr {
&config.record_config.start_stop_hotkey,
&config.record_config.pause_unpause_hotkey,
&config.streaming_config.start_stop_hotkey,
&config.screenshot_config.take_screenshot_hotkey,
&config.main_config.show_hide_hotkey
};
for(ConfigHotkey *config_hotkey : config_hotkeys) {
@@ -604,6 +592,9 @@ namespace gsr {
case ConfigureHotkeyType::STREAM_START_STOP:
hotkey_configure_action_name = "Start/stop streaming";
break;
case ConfigureHotkeyType::TAKE_SCREENSHOT:
hotkey_configure_action_name = "Take a screenshot";
break;
case ConfigureHotkeyType::SHOW_HIDE:
hotkey_configure_action_name = "Show/hide UI";
break;
@@ -614,7 +605,7 @@ namespace gsr {
Button *config_hotkey_button = configure_hotkey_get_button_by_active_type();
ConfigHotkey *config_hotkey = configure_hotkey_get_config_by_active_type();
if(config_hotkey_button && config_hotkey)
config_hotkey_button->set_text(config_hotkey_to_string(*config_hotkey));
config_hotkey_button->set_text(config_hotkey->to_string());
configure_config_hotkey = {0, 0};
configure_hotkey_type = ConfigureHotkeyType::NONE;
@@ -628,15 +619,17 @@ namespace gsr {
ConfigHotkey *config_hotkey = configure_hotkey_get_config_by_active_type();
if(config_hotkey_button && config_hotkey) {
bool hotkey_used_by_another_action = false;
for_each_config_hotkey([&](ConfigHotkey *config_hotkey_item) {
if(config_hotkey_item != config_hotkey && *config_hotkey_item == configure_config_hotkey)
hotkey_used_by_another_action = true;
});
if(configure_config_hotkey.key != mgl::Keyboard::Unknown) {
for_each_config_hotkey([&](ConfigHotkey *config_hotkey_item) {
if(config_hotkey_item != config_hotkey && *config_hotkey_item == configure_config_hotkey)
hotkey_used_by_another_action = true;
});
}
if(hotkey_used_by_another_action) {
const std::string error_msg = "The hotkey \"" + config_hotkey_to_string(configure_config_hotkey) + " is already used for something else";
const std::string error_msg = "The hotkey \"" + configure_config_hotkey.to_string() + " is already used for something else";
overlay->show_notification(error_msg.c_str(), 3.0, mgl::Color(255, 0, 0, 255), mgl::Color(255, 0, 0, 255), NotificationType::NONE);
config_hotkey_button->set_text(config_hotkey_to_string(*config_hotkey));
config_hotkey_button->set_text(config_hotkey->to_string());
configure_config_hotkey = {0, 0};
return;
}

View File

@@ -8,8 +8,9 @@
namespace gsr {
static const float button_spacing_scale = 0.015f;
GsrPage::GsrPage() :
label_text("Settings", get_theme().title_font)
GsrPage::GsrPage(const char *top_text, const char *bottom_text) :
top_text(top_text, get_theme().title_font),
bottom_text(bottom_text, get_theme().title_font)
{
const float margin = 0.02f;
set_margins(margin, margin, margin, margin);
@@ -80,13 +81,17 @@ namespace gsr {
window.draw(background);
const int text_margin = background.get_size().y * 0.085;
label_text.set_position((background.get_position() + mgl::vec2f(background.get_size().x * 0.5f - label_text.get_bounds().size.x * 0.5f, text_margin)).floor());
window.draw(label_text);
top_text.set_position((background.get_position() + mgl::vec2f(background.get_size().x * 0.5f - top_text.get_bounds().size.x * 0.5f, text_margin)).floor());
window.draw(top_text);
mgl::Sprite icon(&get_theme().settings_texture);
icon.set_height((int)(background.get_size().y * 0.5f));
icon.set_position((background.get_position() + background.get_size() * 0.5f - icon.get_size() * 0.5f).floor());
window.draw(icon);
bottom_text.set_position((background.get_position() + mgl::vec2f(background.get_size().x * 0.5f - bottom_text.get_bounds().size.x * 0.5f, background.get_size().y - bottom_text.get_bounds().size.y - text_margin)).floor());
window.draw(bottom_text);
}
void GsrPage::draw_buttons(mgl::Window &window, mgl::vec2f body_pos, mgl::vec2f body_size) {
@@ -102,15 +107,8 @@ namespace gsr {
void GsrPage::draw_children(mgl::Window &window, mgl::vec2f position) {
Widget *selected_widget = selected_child_widget;
mgl_scissor prev_scissor;
mgl_window_get_scissor(window.internal_window(), &prev_scissor);
const mgl::vec2f inner_size = get_inner_size();
const mgl_scissor new_scissor = {
mgl_vec2i{(int)position.x, (int)position.y},
mgl_vec2i{(int)inner_size.x, (int)inner_size.y}
};
mgl_window_set_scissor(window.internal_window(), &new_scissor);
const mgl::Scissor prev_scissor = window.get_scissor();
window.set_scissor({position.to_vec2i(), get_inner_size().to_vec2i()});
for(size_t i = 0; i < widgets.size(); ++i) {
auto &widget = widgets[i];
@@ -121,7 +119,7 @@ namespace gsr {
if(selected_widget)
selected_widget->draw(window, position);
mgl_window_set_scissor(window.internal_window(), &prev_scissor);
window.set_scissor(prev_scissor);
}
mgl::vec2f GsrPage::get_size() {

View File

@@ -0,0 +1,341 @@
#include "../../include/gui/ScreenshotSettingsPage.hpp"
#include "../../include/gui/GsrPage.hpp"
#include "../../include/gui/PageStack.hpp"
#include "../../include/Theme.hpp"
#include "../../include/GsrInfo.hpp"
#include "../../include/Utils.hpp"
#include "../../include/gui/List.hpp"
#include "../../include/gui/ScrollablePage.hpp"
#include "../../include/gui/Label.hpp"
#include "../../include/gui/Subsection.hpp"
#include "../../include/gui/FileChooser.hpp"
namespace gsr {
ScreenshotSettingsPage::ScreenshotSettingsPage(const GsrInfo *gsr_info, Config &config, PageStack *page_stack) :
StaticPage(mgl::vec2f(get_theme().window_width, get_theme().window_height).floor()),
config(config),
gsr_info(gsr_info),
page_stack(page_stack)
{
capture_options = get_supported_capture_options(*gsr_info);
auto content_page = std::make_unique<GsrPage>("Screenshot", "Settings");
content_page->add_button("Back", "back", get_color_theme().page_bg_color);
content_page->on_click = [page_stack](const std::string &id) {
if(id == "back")
page_stack->pop();
};
content_page_ptr = content_page.get();
add_widget(std::move(content_page));
add_widgets();
load();
}
std::unique_ptr<ComboBox> ScreenshotSettingsPage::create_record_area_box() {
auto record_area_box = std::make_unique<ComboBox>(&get_theme().body_font);
// TODO: Show options not supported but disable them
// TODO: Enable this
//if(capture_options.window)
// record_area_box->add_item("Window", "window");
for(const auto &monitor : capture_options.monitors) {
char name[256];
snprintf(name, sizeof(name), "Monitor %s (%dx%d)", monitor.name.c_str(), monitor.size.x, monitor.size.y);
record_area_box->add_item(name, monitor.name);
}
if(capture_options.portal)
record_area_box->add_item("Desktop portal", "portal");
record_area_box_ptr = record_area_box.get();
return record_area_box;
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_record_area() {
auto record_area_list = std::make_unique<List>(List::Orientation::VERTICAL);
record_area_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Capture target:", get_color_theme().text_color));
record_area_list->add_widget(create_record_area_box());
return record_area_list;
}
std::unique_ptr<List> ScreenshotSettingsPage::create_select_window() {
auto select_window_list = std::make_unique<List>(List::Orientation::VERTICAL);
select_window_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Select window:", get_color_theme().text_color));
select_window_list->add_widget(std::make_unique<Button>(&get_theme().body_font, "Click here to select a window...", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)));
select_window_list_ptr = select_window_list.get();
return select_window_list;
}
std::unique_ptr<Entry> ScreenshotSettingsPage::create_image_width_entry() {
auto image_width_entry = std::make_unique<Entry>(&get_theme().body_font, "1920", get_theme().body_font.get_character_size() * 3);
image_width_entry->validate_handler = create_entry_validator_integer_in_range(1, 1 << 15);
image_width_entry_ptr = image_width_entry.get();
return image_width_entry;
}
std::unique_ptr<Entry> ScreenshotSettingsPage::create_image_height_entry() {
auto image_height_entry = std::make_unique<Entry>(&get_theme().body_font, "1080", get_theme().body_font.get_character_size() * 3);
image_height_entry->validate_handler = create_entry_validator_integer_in_range(1, 1 << 15);
image_height_entry_ptr = image_height_entry.get();
return image_height_entry;
}
std::unique_ptr<List> ScreenshotSettingsPage::create_image_resolution() {
auto area_size_params_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
area_size_params_list->add_widget(create_image_width_entry());
area_size_params_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "x", get_color_theme().text_color));
area_size_params_list->add_widget(create_image_height_entry());
return area_size_params_list;
}
std::unique_ptr<List> ScreenshotSettingsPage::create_image_resolution_section() {
auto image_resolution_list = std::make_unique<List>(List::Orientation::VERTICAL);
image_resolution_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Image resolution limit:", get_color_theme().text_color));
image_resolution_list->add_widget(create_image_resolution());
image_resolution_list_ptr = image_resolution_list.get();
return image_resolution_list;
}
std::unique_ptr<CheckBox> ScreenshotSettingsPage::create_restore_portal_session_checkbox() {
auto restore_portal_session_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Restore portal session");
restore_portal_session_checkbox->set_checked(true);
restore_portal_session_checkbox_ptr = restore_portal_session_checkbox.get();
return restore_portal_session_checkbox;
}
std::unique_ptr<List> ScreenshotSettingsPage::create_restore_portal_session_section() {
auto restore_portal_session_list = std::make_unique<List>(List::Orientation::VERTICAL);
restore_portal_session_list->add_widget(std::make_unique<Label>(&get_theme().body_font, " ", get_color_theme().text_color));
restore_portal_session_list->add_widget(create_restore_portal_session_checkbox());
restore_portal_session_list_ptr = restore_portal_session_list.get();
return restore_portal_session_list;
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_change_image_resolution_section() {
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Change image resolution");
change_image_resolution_checkbox_ptr = checkbox.get();
return checkbox;
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_capture_target_section() {
auto ll = std::make_unique<List>(List::Orientation::VERTICAL);
auto capture_target_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
capture_target_list->add_widget(create_record_area());
capture_target_list->add_widget(create_select_window());
capture_target_list->add_widget(create_image_resolution_section());
capture_target_list->add_widget(create_restore_portal_session_section());
ll->add_widget(std::move(capture_target_list));
ll->add_widget(create_change_image_resolution_section());
return std::make_unique<Subsection>("Record area", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
std::unique_ptr<List> ScreenshotSettingsPage::create_image_quality_section() {
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Image quality:", get_color_theme().text_color));
auto image_quality_box = std::make_unique<ComboBox>(&get_theme().body_font);
image_quality_box->add_item("Medium", "medium");
image_quality_box->add_item("High", "high");
image_quality_box->add_item("Very high (Recommended)", "very_high");
image_quality_box->add_item("Ultra", "ultra");
image_quality_box->set_selected_item("very_high");
image_quality_box_ptr = image_quality_box.get();
list->add_widget(std::move(image_quality_box));
return list;
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_record_cursor_section() {
auto record_cursor_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Record cursor");
record_cursor_checkbox->set_checked(true);
record_cursor_checkbox_ptr = record_cursor_checkbox.get();
return record_cursor_checkbox;
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_image_section() {
auto image_section_list = std::make_unique<List>(List::Orientation::VERTICAL);
image_section_list->add_widget(create_image_quality_section());
image_section_list->add_widget(create_record_cursor_section());
return std::make_unique<Subsection>("Image", std::move(image_section_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
std::unique_ptr<List> ScreenshotSettingsPage::create_save_directory(const char *label) {
auto save_directory_list = std::make_unique<List>(List::Orientation::VERTICAL);
save_directory_list->add_widget(std::make_unique<Label>(&get_theme().body_font, label, get_color_theme().text_color));
auto save_directory_button = std::make_unique<Button>(&get_theme().body_font, get_pictures_dir().c_str(), mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
save_directory_button_ptr = save_directory_button.get();
save_directory_button->on_click = [this]() {
auto select_directory_page = std::make_unique<GsrPage>("File", "Settings");
select_directory_page->add_button("Save", "save", get_color_theme().tint_color);
select_directory_page->add_button("Cancel", "cancel", get_color_theme().page_bg_color);
auto file_chooser = std::make_unique<FileChooser>(save_directory_button_ptr->get_text().c_str(), select_directory_page->get_inner_size());
FileChooser *file_chooser_ptr = file_chooser.get();
select_directory_page->add_widget(std::move(file_chooser));
select_directory_page->on_click = [this, file_chooser_ptr](const std::string &id) {
if(id == "save") {
save_directory_button_ptr->set_text(file_chooser_ptr->get_current_directory());
page_stack->pop();
} else if(id == "cancel") {
page_stack->pop();
}
};
page_stack->push(std::move(select_directory_page));
};
save_directory_list->add_widget(std::move(save_directory_button));
return save_directory_list;
}
std::unique_ptr<ComboBox> ScreenshotSettingsPage::create_image_format_box() {
auto box = std::make_unique<ComboBox>(&get_theme().body_font);
if(gsr_info->supported_image_formats.jpeg)
box->add_item("jpg", "jpg");
if(gsr_info->supported_image_formats.png)
box->add_item("png", "png");
image_format_box_ptr = box.get();
return box;
}
std::unique_ptr<List> ScreenshotSettingsPage::create_image_format_section() {
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Image format:", get_color_theme().text_color));
list->add_widget(create_image_format_box());
return list;
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_file_info_section() {
auto file_info_data_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
file_info_data_list->add_widget(create_save_directory("Directory to save the screenshot:"));
file_info_data_list->add_widget(create_image_format_section());
return std::make_unique<Subsection>("File info", std::move(file_info_data_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
std::unique_ptr<CheckBox> ScreenshotSettingsPage::create_save_screenshot_in_game_folder() {
char text[256];
snprintf(text, sizeof(text), "Save screenshot in a folder with the name of the game%s", gsr_info->system_info.display_server == DisplayServer::X11 ? "" : " (X11 applications only)");
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, text);
save_screenshot_in_game_folder_checkbox_ptr = checkbox.get();
return checkbox;
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_general_section() {
return std::make_unique<Subsection>("General", create_save_screenshot_in_game_folder(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_notifications_section() {
auto show_screenshot_saved_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show screenshot saved notification");
show_screenshot_saved_notification_checkbox->set_checked(true);
show_screenshot_saved_notification_checkbox_ptr = show_screenshot_saved_notification_checkbox.get();
return std::make_unique<Subsection>("Notifications", std::move(show_screenshot_saved_notification_checkbox), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_settings() {
auto page_list = std::make_unique<List>(List::Orientation::VERTICAL);
page_list->set_spacing(0.018f);
auto scrollable_page = std::make_unique<ScrollablePage>(content_page_ptr->get_inner_size() - mgl::vec2f(0.0f, page_list->get_size().y + 0.018f * get_theme().window_height));
settings_scrollable_page_ptr = scrollable_page.get();
page_list->add_widget(std::move(scrollable_page));
auto settings_list = std::make_unique<List>(List::Orientation::VERTICAL);
settings_list->set_spacing(0.018f);
settings_list->add_widget(create_capture_target_section());
settings_list->add_widget(create_image_section());
settings_list->add_widget(create_file_info_section());
settings_list->add_widget(create_general_section());
settings_list->add_widget(create_notifications_section());
settings_scrollable_page_ptr->add_widget(std::move(settings_list));
return page_list;
}
void ScreenshotSettingsPage::add_widgets() {
content_page_ptr->add_widget(create_settings());
record_area_box_ptr->on_selection_changed = [this](const std::string &text, const std::string &id) {
(void)text;
const bool window_selected = id == "window";
const bool portal_selected = id == "portal";
select_window_list_ptr->set_visible(window_selected);
image_resolution_list_ptr->set_visible(change_image_resolution_checkbox_ptr->is_checked());
restore_portal_session_list_ptr->set_visible(portal_selected);
return true;
};
change_image_resolution_checkbox_ptr->on_changed = [this](bool checked) {
image_resolution_list_ptr->set_visible(checked);
};
if(!capture_options.monitors.empty())
record_area_box_ptr->set_selected_item(capture_options.monitors.front().name);
else if(capture_options.portal)
record_area_box_ptr->set_selected_item("portal");
else if(capture_options.window)
record_area_box_ptr->set_selected_item("window");
else
record_area_box_ptr->on_selection_changed("", "");
}
void ScreenshotSettingsPage::on_navigate_away_from_page() {
save();
}
void ScreenshotSettingsPage::load() {
record_area_box_ptr->set_selected_item(config.screenshot_config.record_area_option);
change_image_resolution_checkbox_ptr->set_checked(config.screenshot_config.change_image_resolution);
image_quality_box_ptr->set_selected_item(config.screenshot_config.image_quality);
image_format_box_ptr->set_selected_item(config.screenshot_config.image_format);
record_cursor_checkbox_ptr->set_checked(config.screenshot_config.record_cursor);
restore_portal_session_checkbox_ptr->set_checked(config.screenshot_config.restore_portal_session);
save_directory_button_ptr->set_text(config.screenshot_config.save_directory);
save_screenshot_in_game_folder_checkbox_ptr->set_checked(config.screenshot_config.save_screenshot_in_game_folder);
show_screenshot_saved_notification_checkbox_ptr->set_checked(config.screenshot_config.show_screenshot_saved_notifications);
if(config.screenshot_config.image_width == 0)
config.screenshot_config.image_width = 1920;
if(config.screenshot_config.image_height == 0)
config.screenshot_config.image_height = 1080;
if(config.screenshot_config.image_width < 32)
config.screenshot_config.image_width = 32;
image_width_entry_ptr->set_text(std::to_string(config.screenshot_config.image_width));
if(config.screenshot_config.image_height < 32)
config.screenshot_config.image_height = 32;
image_height_entry_ptr->set_text(std::to_string(config.screenshot_config.image_height));
}
void ScreenshotSettingsPage::save() {
config.screenshot_config.record_area_option = record_area_box_ptr->get_selected_id();
config.screenshot_config.image_width = atoi(image_width_entry_ptr->get_text().c_str());
config.screenshot_config.image_height = atoi(image_height_entry_ptr->get_text().c_str());
config.screenshot_config.change_image_resolution = change_image_resolution_checkbox_ptr->is_checked();
config.screenshot_config.image_quality = image_quality_box_ptr->get_selected_id();
config.screenshot_config.image_format = image_format_box_ptr->get_selected_id();
config.screenshot_config.record_cursor = record_cursor_checkbox_ptr->is_checked();
config.screenshot_config.restore_portal_session = restore_portal_session_checkbox_ptr->is_checked();
config.screenshot_config.save_directory = save_directory_button_ptr->get_text();
config.screenshot_config.save_screenshot_in_game_folder = save_screenshot_in_game_folder_checkbox_ptr->is_checked();
config.screenshot_config.show_screenshot_saved_notifications = show_screenshot_saved_notification_checkbox_ptr->is_checked();
if(config.screenshot_config.image_width == 0)
config.screenshot_config.image_width = 1920;
if(config.screenshot_config.image_height == 0)
config.screenshot_config.image_height = 1080;
if(config.screenshot_config.image_width < 32) {
config.screenshot_config.image_width = 32;
image_width_entry_ptr->set_text("32");
}
if(config.screenshot_config.image_height < 32) {
config.screenshot_config.image_height = 32;
image_height_entry_ptr->set_text("32");
}
save_config(config);
}
}

View File

@@ -89,8 +89,7 @@ namespace gsr {
offset = position + offset;
mgl_scissor prev_scissor;
mgl_window_get_scissor(window.internal_window(), &prev_scissor);
const mgl::Scissor prev_scissor = window.get_scissor();
const mgl::vec2f content_size = get_inner_size();
const mgl_scissor new_scissor = {
@@ -150,7 +149,7 @@ namespace gsr {
apply_animation();
limit_scroll(child_height);
mgl_window_set_scissor(window.internal_window(), &prev_scissor);
window.set_scissor(prev_scissor);
double scrollbar_height = 1.0;
if(child_height > 0.001)

View File

@@ -8,11 +8,6 @@
#include "../../include/GsrInfo.hpp"
#include "../../include/Utils.hpp"
#include <mglpp/graphics/Rectangle.hpp>
#include <mglpp/graphics/Sprite.hpp>
#include <mglpp/graphics/Text.hpp>
#include <mglpp/window/Window.hpp>
#include <string.h>
namespace gsr {
@@ -22,6 +17,15 @@ namespace gsr {
APPLICATION_CUSTOM
};
static const char* settings_page_type_to_title_text(SettingsPage::Type type) {
switch(type) {
case SettingsPage::Type::REPLAY: return "Instant Replay";
case SettingsPage::Type::RECORD: return "Record";
case SettingsPage::Type::STREAM: return "Livestream";
}
return "";
}
SettingsPage::SettingsPage(Type type, const GsrInfo *gsr_info, Config &config, PageStack *page_stack) :
StaticPage(mgl::vec2f(get_theme().window_width, get_theme().window_height).floor()),
type(type),
@@ -33,7 +37,7 @@ namespace gsr {
application_audio = get_application_audio();
capture_options = get_supported_capture_options(*gsr_info);
auto content_page = std::make_unique<GsrPage>();
auto content_page = std::make_unique<GsrPage>(settings_page_type_to_title_text(type), "Settings");
content_page->add_button("Back", "back", get_color_theme().page_bg_color);
content_page->on_click = [page_stack](const std::string &id) {
if(id == "back")
@@ -171,7 +175,7 @@ namespace gsr {
return checkbox;
}
std::unique_ptr<Widget> SettingsPage::create_capture_target() {
std::unique_ptr<Widget> SettingsPage::create_capture_target_section() {
auto ll = std::make_unique<List>(List::Orientation::VERTICAL);
auto capture_target_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
@@ -512,7 +516,7 @@ namespace gsr {
auto settings_list = std::make_unique<List>(List::Orientation::VERTICAL);
settings_list->set_spacing(0.018f);
settings_list->add_widget(create_capture_target());
settings_list->add_widget(create_capture_target_section());
settings_list->add_widget(create_audio_section());
settings_list->add_widget(create_video_section());
settings_list_ptr = settings_list.get();
@@ -589,7 +593,7 @@ namespace gsr {
auto save_directory_button = std::make_unique<Button>(&get_theme().body_font, get_videos_dir().c_str(), mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
save_directory_button_ptr = save_directory_button.get();
save_directory_button->on_click = [this]() {
auto select_directory_page = std::make_unique<GsrPage>();
auto select_directory_page = std::make_unique<GsrPage>("File", "Settings");
select_directory_page->add_button("Save", "save", get_color_theme().tint_color);
select_directory_page->add_button("Cancel", "cancel", get_color_theme().page_bg_color);
@@ -629,16 +633,24 @@ namespace gsr {
return container_list;
}
std::unique_ptr<Entry> SettingsPage::create_replay_time_entry() {
std::unique_ptr<List> SettingsPage::create_replay_time_entry() {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
auto replay_time_entry = std::make_unique<Entry>(&get_theme().body_font, "60", get_theme().body_font.get_character_size() * 3);
replay_time_entry->validate_handler = create_entry_validator_integer_in_range(1, 1200);
replay_time_entry->validate_handler = create_entry_validator_integer_in_range(1, 10800);
replay_time_entry_ptr = replay_time_entry.get();
return replay_time_entry;
list->add_widget(std::move(replay_time_entry));
auto replay_time_label = std::make_unique<Label>(&get_theme().body_font, "00h:00m:00s", get_color_theme().text_color);
replay_time_label_ptr = replay_time_label.get();
list->add_widget(std::move(replay_time_label));
return list;
}
std::unique_ptr<List> SettingsPage::create_replay_time() {
auto replay_time_list = std::make_unique<List>(List::Orientation::VERTICAL);
replay_time_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Replay time in seconds:", get_color_theme().text_color));
replay_time_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Replay duration in seconds:", get_color_theme().text_color));
replay_time_list->add_widget(create_replay_time_entry());
return replay_time_list;
}
@@ -681,11 +693,25 @@ namespace gsr {
const int64_t video_bitrate_bps = atoi(video_bitrate_entry_ptr->get_text().c_str()) * 1000LL / 8LL;
const double video_filesize_mb = ((double)replay_time_seconds * (double)video_bitrate_bps) / 1000.0 / 1000.0 * 1.024;
char buffer[512];
snprintf(buffer, sizeof(buffer), "Estimated video max file size in RAM: %.2fMB", video_filesize_mb);
char buffer[256];
snprintf(buffer, sizeof(buffer), "Estimated video max file size in RAM: %.2fMB.\nChange video bitrate or replay duration to change file size.", video_filesize_mb);
estimated_file_size_ptr->set_text(buffer);
}
void SettingsPage::update_replay_time_text() {
int seconds = atoi(replay_time_entry_ptr->get_text().c_str());
const int hours = seconds / 60 / 60;
seconds -= (hours * 60 * 60);
const int minutes = seconds / 60;
seconds -= (minutes * 60);
char buffer[256];
snprintf(buffer, sizeof(buffer), "%02dh:%02dm:%02ds", hours, minutes, seconds);
replay_time_label_ptr->set_text(buffer);
}
void SettingsPage::add_replay_widgets() {
auto file_info_list = std::make_unique<List>(List::Orientation::VERTICAL);
auto file_info_data_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
@@ -739,6 +765,7 @@ namespace gsr {
replay_time_entry_ptr->on_changed = [this](const std::string&) {
update_estimated_replay_file_size();
update_replay_time_text();
};
video_bitrate_entry_ptr->on_changed = [this](const std::string&) {
@@ -778,9 +805,7 @@ namespace gsr {
file_info_list->add_widget(create_estimated_record_file_size());
settings_list_ptr->add_widget(std::make_unique<Subsection>("File info", std::move(file_info_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
auto general_list = std::make_unique<List>(List::Orientation::VERTICAL);
general_list->add_widget(create_save_recording_in_game_folder());
settings_list_ptr->add_widget(std::make_unique<Subsection>("General", std::move(general_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
settings_list_ptr->add_widget(std::make_unique<Subsection>("General", create_save_recording_in_game_folder(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL);
@@ -1081,8 +1106,10 @@ namespace gsr {
save_directory_button_ptr->set_text(config.replay_config.save_directory);
container_box_ptr->set_selected_item(config.replay_config.container);
if(config.replay_config.replay_time < 5)
config.replay_config.replay_time = 5;
if(config.replay_config.replay_time < 2)
config.replay_config.replay_time = 2;
if(config.replay_config.replay_time > 10800)
config.replay_config.replay_time = 10800;
replay_time_entry_ptr->set_text(std::to_string(config.replay_config.replay_time));
}

View File

@@ -36,14 +36,8 @@ namespace gsr {
offset = draw_pos;
Widget *selected_widget = selected_child_widget;
mgl_scissor prev_scissor;
mgl_window_get_scissor(window.internal_window(), &prev_scissor);
const mgl_scissor new_scissor = {
mgl_vec2i{(int)draw_pos.x, (int)draw_pos.y},
mgl_vec2i{(int)size.x, (int)size.y}
};
mgl_window_set_scissor(window.internal_window(), &new_scissor);
const mgl::Scissor prev_scissor = window.get_scissor();
window.set_scissor({draw_pos.to_vec2i(), size.to_vec2i()});
for(size_t i = 0; i < widgets.size(); ++i) {
auto &widget = widgets[i];
@@ -54,7 +48,7 @@ namespace gsr {
if(selected_widget)
selected_widget->draw(window, offset);
mgl_window_set_scissor(window.internal_window(), &prev_scissor);
window.set_scissor(prev_scissor);
}
mgl::vec2f StaticPage::get_size() {

View File

@@ -8,6 +8,7 @@
#include <signal.h>
#include <string.h>
#include <limits.h>
#include <malloc.h>
#include <mglpp/mglpp.hpp>
#include <mglpp/system/Clock.hpp>
@@ -72,6 +73,11 @@ static void rpc_add_commands(gsr::Rpc *rpc, gsr::Overlay *overlay) {
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
overlay->save_replay();
});
rpc->add_handler("take-screenshot", [overlay](const std::string &name) {
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
overlay->take_screenshot();
});
}
static bool is_gsr_ui_virtual_keyboard_running() {
@@ -150,6 +156,7 @@ enum class LaunchAction {
int main(int argc, char **argv) {
setlocale(LC_ALL, "C"); // Sigh... stupid C
mallopt(M_MMAP_THRESHOLD, 65536);
if(geteuid() == 0) {
fprintf(stderr, "Error: don't run gsr-ui as the root user\n");
@@ -182,6 +189,7 @@ int main(int argc, char **argv) {
// 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) {
gsr::Rpc rpc;
if(rpc.open("gsr-ui") && rpc.write("show_ui\n", 8)) {

View File

@@ -0,0 +1,21 @@
# About
Global hotkeys for X11 and all Wayland compositors by using linux device api. Keyboards are grabbed and only the non-hotkey keys are passed through to the system.
The program accepts text commands as input. Run the program with the option `--virtual` to only grab virtual devices. This is useful when using keyboard input mapping software such as
kanata, otherwise kanata may fail to launch or this program may fail to launch.
# Commands
## Bind
To add a key send `bind <action> <keycode+keycode+...><newline>` to the programs stdin, for example:
```
bind show_hide 56+44
```
which will bind alt+z. When alt+z is pressed the program will output `show_hide` (and a newline) to stdout.
The program only accepts one key for each keybind command but accepts a multiple modifier keys.
The keybinding requires at least one modifier key (ctrl, alt, super or shift) and a key to be used.
The keycodes are values from `<linux/input-event-codes.h>` linux api header (which is the same as X11 keycode value minus 8).
## Unbind
To unbind all keys send `unbind_all<newline>` to the programs stdin, for example:
```
unbind_all
```

View File

@@ -65,9 +65,10 @@ static void hotplug_event_parse_netlink_data(hotplug_event *self, const char *li
/* Netlink uevent structure is documented here: https://web.archive.org/web/20160127215232/https://www.kernel.org/doc/pending/hotplug.txt */
void hotplug_event_process_event_data(hotplug_event *self, int fd, hotplug_device_added_callback callback, void *userdata) {
const int bytes_read = read(fd, self->event_data, sizeof(self->event_data));
const int bytes_read = read(fd, self->event_data, sizeof(self->event_data) - 1);
if(bytes_read <= 0)
return;
self->event_data[bytes_read] = '\0';
/* Hotplug data ends with a newline and a null terminator */
int data_index = 0;

View File

@@ -1,4 +1,5 @@
#include "keyboard_event.h"
#include "keys.h"
/* C stdlib */
#include <stdio.h>
@@ -154,12 +155,6 @@ static uint32_t keycode_to_modifier_bit(uint32_t keycode) {
return 0;
}
static bool key_is_mouse_button(uint32_t keycode) {
return (keycode >= BTN_MISC && keycode <= BTN_GEAR_UP)
|| (keycode >= BTN_TRIGGER_HAPPY && keycode <= BTN_TRIGGER_HAPPY40)
|| (keycode >= BTN_DPAD_UP && keycode <= BTN_DPAD_RIGHT);
}
static void keyboard_event_process_input_event_data(keyboard_event *self, event_extra_data *extra_data, int fd) {
struct input_event event;
if(read(fd, &event, sizeof(event)) != sizeof(event)) {
@@ -177,7 +172,7 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
//fprintf(stderr, "fd: %d, type: %d, pressed %d, value: %d\n", fd, event.type, event.code, event.value);
//}
if(event.type == EV_KEY && !key_is_mouse_button(event.code)) {
if(event.type == EV_KEY && is_keyboard_key(event.code)) {
keyboard_event_process_key_state_change(self, &event, extra_data, fd);
const uint32_t modifier_bit = keycode_to_modifier_bit(event.code);
if(modifier_bit == 0) {
@@ -276,7 +271,8 @@ static bool keyboard_event_try_add_device_if_keyboard(keyboard_event *self, cons
if(dev_input_id == -1)
return false;
if(self->grab_type == KEYBOARD_GRAB_TYPE_VIRTUAL && !dev_input_is_virtual(dev_input_id))
const bool is_virtual_device = dev_input_is_virtual(dev_input_id);
if(self->grab_type == KEYBOARD_GRAB_TYPE_VIRTUAL && !is_virtual_device)
return false;
if(keyboard_event_has_event_with_dev_input_fd(self, dev_input_id))
@@ -292,7 +288,7 @@ static bool keyboard_event_try_add_device_if_keyboard(keyboard_event *self, cons
unsigned long evbit = 0;
ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);
const bool is_keyboard = (evbit & (1 << EV_SYN)) && (evbit & (1 << EV_KEY)) && (evbit & (1 << EV_MSC)) && (evbit & (1 << EV_REP));
const bool is_keyboard = (evbit & (1 << EV_SYN)) && (evbit & (1 << EV_KEY));
if(is_keyboard && strcmp(device_name, GSR_UI_VIRTUAL_KEYBOARD_NAME) != 0) {
unsigned char key_bits[KEY_MAX/8 + 1] = {0};
@@ -303,7 +299,7 @@ static bool keyboard_event_try_add_device_if_keyboard(keyboard_event *self, cons
//const bool supports_touch_events = key_bits[BTN_TOUCH/8] & (1 << (BTN_TOUCH % 8));
const bool supports_joystick_events = key_bits[BTN_JOYSTICK/8] & (1 << (BTN_JOYSTICK % 8));
const bool supports_wheel_events = key_bits[BTN_WHEEL/8] & (1 << (BTN_WHEEL % 8));
if(supports_key_events && !supports_joystick_events && !supports_wheel_events) {
if(supports_key_events && (is_virtual_device || (!supports_joystick_events && !supports_wheel_events))) {
unsigned char *key_states = calloc(1, KEY_STATES_SIZE);
if(key_states && self->num_event_polls < MAX_EVENT_POLLS) {
//fprintf(stderr, "%s (%s) supports key inputs\n", dev_input_filepath, device_name);
@@ -320,7 +316,7 @@ static bool keyboard_event_try_add_device_if_keyboard(keyboard_event *self, cons
.num_keys_pressed = 0
};
if(supports_mouse_events) {
if(supports_mouse_events || supports_joystick_events || supports_wheel_events) {
fprintf(stderr, "Info: device not grabbed yet because it might be a mouse: /dev/input/event%d\n", dev_input_id);
fsync(fd);
if(ioctl(fd, EVIOCGKEY(KEY_STATES_SIZE), self->event_extra_data[self->num_event_polls].key_states) == -1)
@@ -408,7 +404,8 @@ static int setup_virtual_keyboard_input(const char *name) {
success &= (ioctl(fd, UI_SET_MSCBIT, MSC_SCAN) != -1);
for(int i = 1; i < KEY_MAX; ++i) {
success &= (ioctl(fd, UI_SET_KEYBIT, i) != -1);
if(is_keyboard_key(i) || is_mouse_button(i))
success &= (ioctl(fd, UI_SET_KEYBIT, i) != -1);
}
for(int i = 0; i < REL_MAX; ++i) {
success &= (ioctl(fd, UI_SET_RELBIT, i) != -1);
@@ -526,6 +523,7 @@ void keyboard_event_deinit(keyboard_event *self) {
self->num_global_hotkeys = 0;
if(self->uinput_fd > 0) {
ioctl(self->uinput_fd, UI_DEV_DESTROY);
close(self->uinput_fd);
self->uinput_fd = -1;
}

View File

@@ -0,0 +1,21 @@
#include "keys.h"
#include <linux/input-event-codes.h>
bool is_keyboard_key(uint32_t keycode) {
return (keycode >= KEY_ESC && keycode <= KEY_KPDOT)
|| (keycode >= KEY_ZENKAKUHANKAKU && keycode <= KEY_F24)
|| (keycode >= KEY_PLAYCD && keycode <= KEY_MICMUTE)
|| (keycode >= KEY_OK && keycode <= KEY_IMAGES)
|| (keycode >= KEY_DEL_EOL && keycode <= KEY_DEL_LINE)
|| (keycode >= KEY_FN && keycode <= KEY_FN_B)
|| (keycode >= KEY_BRL_DOT1 && keycode <= KEY_BRL_DOT10)
|| (keycode >= KEY_NUMERIC_0 && keycode <= KEY_LIGHTS_TOGGLE)
|| (keycode == KEY_ALS_TOGGLE)
|| (keycode >= KEY_BUTTONCONFIG && keycode <= KEY_VOICECOMMAND)
|| (keycode >= KEY_BRIGHTNESS_MIN && keycode <= KEY_BRIGHTNESS_MAX)
|| (keycode >= KEY_KBDINPUTASSIST_PREV && keycode <= KEY_ONSCREEN_KEYBOARD);
}
bool is_mouse_button(uint32_t keycode) {
return (keycode >= BTN_MOUSE && keycode <= BTN_TASK);
}

View File

@@ -0,0 +1,10 @@
#ifndef KEYS_H
#define KEYS_H
#include <stdbool.h>
#include <stdint.h>
bool is_keyboard_key(uint32_t keycode);
bool is_mouse_button(uint32_t keycode);
#endif /* KEYS_H */

View File

@@ -50,6 +50,7 @@ static void usage(void) {
printf(" toggle-stream Start/stop streaming.\n");
printf(" toggle-replay Start/stop replay.\n");
printf(" replay-save Save replay.\n");
printf(" take-screenshot Take a screenshot.\n");
printf("\n");
printf("EXAMPLES:\n");
printf(" gsr-ui-cli toggle-show\n");
@@ -65,6 +66,7 @@ static bool is_valid_command(const char *command) {
"toggle-stream",
"toggle-replay",
"replay-save",
"take-screenshot",
NULL
};