Compare commits

...

36 Commits
1.7.2 ... 1.7.9

Author SHA1 Message Date
dec05eba
5bd600fad6 1.7.9 2025-10-29 18:16:35 +01:00
dec05eba
5144994575 Fix screenshot clipboard paste not working in browsers: ignore image/png request, send as jpg anyways 2025-10-29 18:15:56 +01:00
dec05eba
1c24616388 Support more controllers than real ps4 controllers 2025-10-26 14:26:46 +01:00
dec05eba
ecd9a1f13f Replace / and \ with space in application name 2025-10-26 01:04:44 +02:00
dec05eba
4181d80405 Fix for strict aliasing build 2025-10-18 15:52:13 +02:00
dec05eba
085f4d8bad Add donation link in settings 2025-10-16 19:30:02 +02:00
dec05eba
bb320e97ed Update flatpak version reference 2025-10-05 13:09:37 +02:00
dec05eba
ccf96030da Force no cursor in capture when using region/window screenshot hotkey 2025-10-03 18:00:16 +02:00
dec05eba
ca4061f171 1.7.8 2025-10-03 13:09:16 +02:00
dec05eba
0b4af1e6bb Fix rpc file getting deleted when launching gsr-ui twice. Use unix domain socket instead 2025-10-03 13:08:57 +02:00
dec05eba
9e03cd0354 Test fix for alpha egl 2025-10-03 01:56:50 +02:00
dec05eba
3d4badf5cd Fix build for old meson version 2025-09-30 11:14:10 +02:00
dec05eba
071ecf46de Update TODO 2025-09-30 11:07:09 +02:00
dec05eba
5ee2b95384 Workaround amd driver bug: kill notifications with SIGINT instead of SIGKILL 2025-09-24 18:38:22 +02:00
dec05eba
d610a980f8 Update flatpak version reference 2025-09-23 19:43:43 +02:00
dec05eba
70780ae14e 1.7.6 2025-09-21 03:32:19 +02:00
dec05eba
5f7cb94f4e Only do override-redirect on wayland if the focused x11 application is fullscreen (fixes input focus issue on cosmic when clicking on a window behind the overlay 2025-09-19 14:14:46 +02:00
dec05eba
748c51e2b6 Reorder README.md 2025-09-17 18:02:30 +02:00
dec05eba
3ba9ce771b Update flatpak version reference 2025-09-10 21:36:19 +02:00
dec05eba
c18b062180 README 2025-09-09 16:45:59 +02:00
dec05eba
705da21363 README 2025-09-09 16:43:25 +02:00
dec05eba
609a3e54fd 1.7.5 2025-09-06 19:16:09 +02:00
dec05eba
4e62d12e8c Allow 'sync to content' framerate mode option on wayland (only desktop portal) 2025-09-06 01:58:28 +02:00
dec05eba
b4e003c8f7 Only use mallopt M_MMAP_THRESHOLD if glibc 2025-09-03 18:21:58 +02:00
dec05eba
9efe9d3c91 Add content framerate mode (for x11) 2025-09-01 18:11:16 +02:00
dec05eba
ef4a0fe7cb Update flatpak version reference 2025-08-25 22:30:10 +02:00
dec05eba
dacf6126bf Screenshot: add option to save screenshot to clipboard 2025-08-25 22:26:54 +02:00
dec05eba
9bbec944de GlobalSettings: Add notification speed setting, change recording start notification speed 2025-08-24 22:12:34 +02:00
dec05eba
6a55338b12 Entry: update selection caret when changing masked state 2025-08-07 21:20:05 +02:00
dec05eba
2d3abace0e Fix build 2025-08-07 20:47:14 +02:00
dec05eba
47c02fc6c8 1.7.3 2025-08-07 20:22:20 +02:00
dec05eba
5f8c366b43 Show video codec error messages from gsr
let gsr choose video codec automatically for us when using auto (prefer h264, then hevc and then av1),
fallback to software encoding in gsr ui if none of them are available.
2025-08-07 20:21:22 +02:00
dec05eba
f4ed622510 Entry: add more delimiter for moving 2025-08-07 02:51:55 +02:00
dec05eba
f1ee19d014 Mask stream keys, add button to unmask it 2025-08-07 02:00:35 +02:00
dec05eba
67a8040e57 Entry: use text32 (utf32) instead of text (utf8). This simplifies text editing and other features such as text masking (password) 2025-08-07 00:13:59 +02:00
dec05eba
ff00be30df Entry: implement moving care by word with ctrl+arrow keys 2025-08-06 14:54:25 +02:00
35 changed files with 1203 additions and 418 deletions

2
.gitignore vendored
View File

@@ -4,3 +4,5 @@ compile_commands.json
**/xdg-output-unstable-v1-client-protocol.h **/xdg-output-unstable-v1-client-protocol.h
**/xdg-output-unstable-v1-protocol.c **/xdg-output-unstable-v1-protocol.c
depends/.wraplock

View File

@@ -4,18 +4,18 @@
A fullscreen overlay UI for [GPU Screen Recorder](https://git.dec05eba.com/gpu-screen-recorder/about/) in the style of ShadowPlay.\ 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. The application is currently primarly designed for X11 but it can run on Wayland as well through XWayland, with some caveats because of Wayland limitations.
# Usage
You can start the overlay UI and make it start automatically on system startup by running `systemctl enable --now --user gpu-screen-recorder-ui`.
Alternatively you can run `gsr-ui` and go into settings and enable start on system startup setting.\
Press `Left Alt+Z` to show/hide the UI. Go into settings to view all of the different hotkeys configured.\
If you use a non-systemd distro and want to start the UI on system startup then you have to manually add `gsr-ui` to your system startup script.\
A program called `gsr-ui-cli` is also installed when installing this software. This can be used to remotely control the UI. Run `gsr-ui-cli --help` to list the available commands.
# Installation # Installation
If you are using an Arch Linux based distro then you can find gpu screen recorder ui on aur under the name gpu-screen-recorder-ui (`yay -S gpu-screen-recorder-ui`).\ If you are using an Arch Linux based distro then you can find gpu screen recorder ui on aur under the name gpu-screen-recorder-ui (`yay -S gpu-screen-recorder-ui`).\
If you are running another distro then you can run `sudo ./install.sh`, but you need to manually install the dependencies, as described below.\ If you are running another distro then you can run `sudo ./install.sh`, but you need to manually install the dependencies, as described below.\
You can also install gpu screen recorder from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder) which includes this UI. You can also install gpu screen recorder from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder) which includes this UI.
# Usage
Press `Left Alt+Z` to show/hide the UI. Go into settings (the icon on the right) to view all of the different hotkeys configured.\
You can start the overlay UI and make it start automatically on system startup by running `systemctl enable --now --user gpu-screen-recorder-ui`.
Alternatively you can run `gsr-ui` and go into settings and enable start on system startup setting.\
If you use a non-systemd distro and want to start the UI on system startup then you have to manually add `gsr-ui launch-daemon` to your system startup script.\
A program called `gsr-ui-cli` is also installed when installing this software. This can be used to remotely control the UI. Run `gsr-ui-cli --help` to list the available commands.
# Dependencies # Dependencies
GPU Screen Recorder UI uses meson build system so you need to install `meson` to build GPU Screen Recorder UI. GPU Screen Recorder UI uses meson build system so you need to install `meson` to build GPU Screen Recorder UI.

30
TODO
View File

@@ -25,8 +25,6 @@ Have different modes. Overlay, window and side menu. Overlay can be used on x11,
Show navigation breadcrumbs for settings and deeper navigation (such as selecting a directory to save videos). Show navigation breadcrumbs for settings and deeper navigation (such as selecting a directory to save videos).
Add option to hide stream key like a password input.
Add global setting. In that setting there should be an option to enable/disable gsr-ui from system startup (the systemd service). Add global setting. In that setting there should be an option to enable/disable gsr-ui from system startup (the systemd service).
Add profiles and hotkey to switch between profiles (show notification when switching profile). Add profiles and hotkey to switch between profiles (show notification when switching profile).
@@ -173,8 +171,6 @@ Add a bug report page that automatically includes system info (make this clear t
Make it possible to change controller hotkeys. Also read from /dev/input/eventN instead of /dev/input/jsN. This is readable for controllers. Make it possible to change controller hotkeys. Also read from /dev/input/eventN instead of /dev/input/jsN. This is readable for controllers.
Add option to copy screenshot to clipboard. Does it work properly on Wayland compositors? Maybe need to wait until the application becomes Wayland native instead of XWayland.
Show message that replay/streaming has to be restarted if recording settings are changed while replay/streaming is ongoing. Show message that replay/streaming has to be restarted if recording settings are changed while replay/streaming is ongoing.
Support vector graphics. Maybe support svg, rendering it to a texture for better performance. Support vector graphics. Maybe support svg, rendering it to a texture for better performance.
@@ -210,3 +206,29 @@ Support localization.
Add option to not capture cursor in screenshot when doing region/window capture. Add option to not capture cursor in screenshot when doing region/window capture.
Window selection doesn't work when a window is fullscreen on x11. Window selection doesn't work when a window is fullscreen on x11.
Make it possible to change replay duration of the "save 1 min" and "save 10 min" by adding them to the replay settings as options.
If replay duration is set below the "save 1 min" or "save 10 min" then gray them out and when hovering over those buttons
show a tooltip that says that those buttons cant be used because the replay duration in replay settings is set to a lower value than that (and display the replay duration there).
The UI is unusable on a vertical monitor.
Steam overlay interfers with controller input in gsr ui. Maybe move controller handling the gsr-global-hotkeys to do a grab on the controller, to only give the key input to gsr ui.
Add option to show recording status with scroll lock led (use x11 xkb). Blink when starting/stopping recording and set led on when recording is running and set led off when not recording.
For joysticks (gamepads) create a virtual device for each one (/dev/uinput) that has the same vendor, product and name. This is to make sure that it behaves the same way in applications since applications
access joysticks directly through /dev/input/eventN or /dev/input/jsN. It needs the same number of buttons and pretend to be a controller of the same time, for example a ps4 controller
so that games automatically display ps4 buttons if supported.
This also allows us to copy event bits and other data from the device instead of saying we support everything.
This should fix the issue of not being able to write to /dev/uinput with ABS_X event bit set.
Maybe do this for regular keyboard inputs as well?
Use generic icons for controller input in settings and allow configuring them.
Add option to include game name in file name (video and screenshot). Replace / with space.
Check if the focused window is on top on x11 when choosing to take screenshot or show the window as the background of the overlay.
Convert clipboard image to requested type (from jpg to png for example).

BIN
images/masked.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

BIN
images/unmasked.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

60
include/ClipboardFile.hpp Normal file
View File

@@ -0,0 +1,60 @@
#pragma once
#include <string>
#include <thread>
#include <mutex>
#include <vector>
#include <X11/Xlib.h>
namespace gsr {
struct ClipboardCopy {
Window requestor = None;
uint64_t file_offset = 0;
Atom property = None;
Atom requestor_target = None;
};
class ClipboardFile {
public:
enum class FileType {
JPG,
PNG
};
ClipboardFile();
~ClipboardFile();
ClipboardFile(const ClipboardFile&) = delete;
ClipboardFile& operator=(const ClipboardFile&) = delete;
// Set this to an empty string to unset clipboard
void set_current_file(const std::string &filepath, FileType file_type);
private:
bool file_type_matches_request_atom(FileType file_type, Atom request_atom);
const char* file_type_clipboard_get_name(Atom request_atom);
const char* file_type_get_name(FileType file_type);
void send_clipboard_start(XSelectionRequestEvent *xselectionrequest);
void transfer_clipboard_data(XSelectionRequestEvent *xselectionrequest, ClipboardCopy *clipboard_copy);
ClipboardCopy* get_clipboard_copy_by_requestor(Window requestor);
void remove_clipboard_copy(Window requestor);
private:
Display *dpy = nullptr;
Window clipboard_window = None;
int file_fd = -1;
uint64_t file_size = 0;
FileType file_type = FileType::JPG;
Atom incr_atom = None;
Atom targets_atom = None;
Atom clipboard_atom = None;
Atom image_jpg_atom = None;
Atom image_jpeg_atom = None;
Atom image_png_atom = None;
std::thread event_thread;
std::mutex mutex;
bool running = true;
std::vector<ClipboardCopy> clipboard_copies;
bool should_clear_selection = false;
};
}

View File

@@ -68,6 +68,7 @@ namespace gsr {
std::string hotkeys_enable_option = "enable_hotkeys"; std::string hotkeys_enable_option = "enable_hotkeys";
std::string joystick_hotkeys_enable_option = "disable_hotkeys"; std::string joystick_hotkeys_enable_option = "disable_hotkeys";
std::string tint_color; std::string tint_color;
std::string notification_speed = "normal";
ConfigHotkey show_hide_hotkey; ConfigHotkey show_hide_hotkey;
}; };
@@ -142,6 +143,7 @@ namespace gsr {
bool restore_portal_session = true; bool restore_portal_session = true;
bool save_screenshot_in_game_folder = false; bool save_screenshot_in_game_folder = false;
bool save_screenshot_to_clipboard = false;
bool show_screenshot_saved_notifications = true; bool show_screenshot_saved_notifications = true;
std::string save_directory; std::string save_directory;
ConfigHotkey take_screenshot_hotkey; ConfigHotkey take_screenshot_hotkey;

View File

@@ -4,8 +4,10 @@
#include "../Hotplug.hpp" #include "../Hotplug.hpp"
#include <unordered_map> #include <unordered_map>
#include <thread> #include <thread>
#include <mutex>
#include <condition_variable>
#include <poll.h> #include <poll.h>
#include <linux/joystick.h> #include <linux/input.h>
namespace gsr { namespace gsr {
static constexpr int max_js_poll_fd = 16; static constexpr int max_js_poll_fd = 16;
@@ -30,8 +32,10 @@ namespace gsr {
bool bind_action(const std::string &id, GlobalHotkeyCallback callback) override; bool bind_action(const std::string &id, GlobalHotkeyCallback callback) override;
void poll_events() override; void poll_events() override;
private: private:
void close_fds();
void read_events(); void read_events();
void process_js_event(int fd, js_event &event); void process_input_event(int fd, input_event &event);
void add_all_joystick_devices();
bool add_device(const char *dev_input_filepath, bool print_error = true); bool add_device(const char *dev_input_filepath, bool print_error = true);
bool remove_device(const char *dev_input_filepath); bool remove_device(const char *dev_input_filepath);
bool remove_poll_fd(int index); bool remove_poll_fd(int index);
@@ -45,6 +49,11 @@ namespace gsr {
std::unordered_map<std::string, GlobalHotkeyCallback> bound_actions_by_id; std::unordered_map<std::string, GlobalHotkeyCallback> bound_actions_by_id;
std::thread read_thread; std::thread read_thread;
std::thread close_fd_thread;
std::vector<int> fds_to_close;
std::mutex close_fd_mutex;
std::condition_variable close_fd_cv;
pollfd poll_fd[max_js_poll_fd]; pollfd poll_fd[max_js_poll_fd];
ExtraData extra_data[max_js_poll_fd]; ExtraData extra_data[max_js_poll_fd];
int num_poll_fd = 0; int num_poll_fd = 0;
@@ -56,8 +65,6 @@ namespace gsr {
bool down_pressed = false; bool down_pressed = false;
bool left_pressed = false; bool left_pressed = false;
bool right_pressed = false; bool right_pressed = false;
bool l3_button_pressed = false;
bool r3_button_pressed = false;
bool save_replay = false; bool save_replay = false;
bool save_1_min_replay = false; bool save_1_min_replay = false;

View File

@@ -10,6 +10,7 @@
#include "AudioPlayer.hpp" #include "AudioPlayer.hpp"
#include "RegionSelector.hpp" #include "RegionSelector.hpp"
#include "WindowSelector.hpp" #include "WindowSelector.hpp"
#include "ClipboardFile.hpp"
#include "CursorTracker/CursorTracker.hpp" #include "CursorTracker/CursorTracker.hpp"
#include <mglpp/window/Window.hpp> #include <mglpp/window/Window.hpp>
@@ -47,6 +48,11 @@ namespace gsr {
WINDOW WINDOW
}; };
enum class NotificationSpeed {
NORMAL,
FAST
};
class Overlay { class Overlay {
public: public:
Overlay(std::string resources_path, GsrInfo gsr_info, SupportedCaptureOptions capture_options, egl_functions egl_funcs); Overlay(std::string resources_path, GsrInfo gsr_info, SupportedCaptureOptions capture_options, egl_functions egl_funcs);
@@ -59,7 +65,7 @@ namespace gsr {
bool draw(); bool draw();
void show(); void show();
void hide(); void hide_next_frame();
void toggle_show(); void toggle_show();
void toggle_record(); void toggle_record();
void toggle_pause(); void toggle_pause();
@@ -81,7 +87,11 @@ namespace gsr {
void unbind_all_keyboard_hotkeys(); void unbind_all_keyboard_hotkeys();
void rebind_all_keyboard_hotkeys(); void rebind_all_keyboard_hotkeys();
void set_notification_speed(NotificationSpeed notification_speed);
private: private:
void hide();
void handle_keyboard_mapping_event(); void handle_keyboard_mapping_event();
void on_event(mgl::Event &event); void on_event(mgl::Event &event);
@@ -245,5 +255,7 @@ namespace gsr {
mgl::Clock cursor_tracker_update_clock; mgl::Clock cursor_tracker_update_clock;
bool hide_ui = false; bool hide_ui = false;
double notification_duration_multiplier = 1.0;
ClipboardFile clipboard_file;
}; };
} }

View File

@@ -4,31 +4,47 @@
#include <functional> #include <functional>
#include <unordered_map> #include <unordered_map>
#include <string> #include <string>
#include <poll.h>
typedef struct _IO_FILE FILE; #define GSR_RPC_MAX_CONNECTIONS 8
#define GSR_RPC_MAX_POLLS (1 + GSR_RPC_MAX_CONNECTIONS) /* +1 to include the socket_fd itself for accept */
#define GSR_RPC_MAX_MESSAGE_SIZE 128
namespace gsr { namespace gsr {
using RpcCallback = std::function<void(const std::string &name)>; using RpcCallback = std::function<void(const std::string &name)>;
enum class RpcOpenResult {
OK,
CONNECTION_REFUSED,
ERROR
};
class Rpc { class Rpc {
public: public:
Rpc() = default; struct PollData {
char buffer[GSR_RPC_MAX_MESSAGE_SIZE];
int buffer_size = 0;
};
Rpc();
Rpc(const Rpc&) = delete; Rpc(const Rpc&) = delete;
Rpc& operator=(const Rpc&) = delete; Rpc& operator=(const Rpc&) = delete;
~Rpc(); ~Rpc();
bool create(const char *name); bool create(const char *name);
bool open(const char *name); RpcOpenResult open(const char *name);
bool write(const char *str, size_t size); bool write(const char *str, size_t size);
void poll(); void poll();
bool add_handler(const std::string &name, RpcCallback callback); bool add_handler(const std::string &name, RpcCallback callback);
private: private:
bool open_filepath(const char *filepath); void handle_client_data(int client_fd, PollData &poll_data);
private: private:
int fd = 0; int socket_fd = 0;
FILE *file = nullptr; std::string socket_filepath;
std::string fifo_filepath; struct pollfd polls[GSR_RPC_MAX_POLLS];
PollData polls_data[GSR_RPC_MAX_POLLS];
int num_polls = 0;
std::unordered_map<std::string, RpcCallback> handlers_by_name; std::unordered_map<std::string, RpcCallback> handlers_by_name;
}; };
} }

View File

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

View File

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

View File

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

View File

@@ -70,8 +70,10 @@ namespace gsr {
std::unique_ptr<Subsection> create_controller_hotkey_subsection(ScrollablePage *parent_page); std::unique_ptr<Subsection> create_controller_hotkey_subsection(ScrollablePage *parent_page);
std::unique_ptr<Button> create_exit_program_button(); std::unique_ptr<Button> create_exit_program_button();
std::unique_ptr<Button> create_go_back_to_old_ui_button(); std::unique_ptr<Button> create_go_back_to_old_ui_button();
std::unique_ptr<List> create_notification_speed();
std::unique_ptr<Subsection> create_application_options_subsection(ScrollablePage *parent_page); std::unique_ptr<Subsection> create_application_options_subsection(ScrollablePage *parent_page);
std::unique_ptr<Subsection> create_application_info_subsection(ScrollablePage *parent_page); std::unique_ptr<Subsection> create_application_info_subsection(ScrollablePage *parent_page);
std::unique_ptr<Subsection> create_donate_subsection(ScrollablePage *parent_page);
void add_widgets(); void add_widgets();
Button* configure_hotkey_get_button_by_active_type(); Button* configure_hotkey_get_button_by_active_type();
@@ -103,6 +105,7 @@ namespace gsr {
Button *take_screenshot_region_button_ptr = nullptr; Button *take_screenshot_region_button_ptr = nullptr;
Button *take_screenshot_window_button_ptr = nullptr; Button *take_screenshot_window_button_ptr = nullptr;
Button *show_hide_button_ptr = nullptr; Button *show_hide_button_ptr = nullptr;
RadioButton *notification_speed_button_ptr = nullptr;
ConfigHotkey configure_config_hotkey; ConfigHotkey configure_config_hotkey;
ConfigureHotkeyType configure_hotkey_type = ConfigureHotkeyType::NONE; ConfigureHotkeyType configure_hotkey_type = ConfigureHotkeyType::NONE;

View File

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

View File

@@ -118,9 +118,11 @@ namespace gsr {
std::unique_ptr<ComboBox> create_streaming_service_box(); std::unique_ptr<ComboBox> create_streaming_service_box();
std::unique_ptr<List> create_streaming_service_section(); std::unique_ptr<List> create_streaming_service_section();
std::unique_ptr<List> create_stream_key_section(); std::unique_ptr<List> create_stream_key_section();
std::unique_ptr<List> create_stream_custom_url();
std::unique_ptr<List> create_stream_custom_key();
std::unique_ptr<List> create_stream_custom_section(); std::unique_ptr<List> create_stream_custom_section();
std::unique_ptr<ComboBox> create_stream_container_box(); std::unique_ptr<ComboBox> create_stream_container_box();
std::unique_ptr<List> create_stream_container_section(); std::unique_ptr<List> create_stream_container();
void add_stream_widgets(); void add_stream_widgets();
void load_audio_tracks(const RecordOptions &record_options); void load_audio_tracks(const RecordOptions &record_options);
@@ -173,8 +175,7 @@ namespace gsr {
ComboBox *container_box_ptr = nullptr; ComboBox *container_box_ptr = nullptr;
ComboBox *streaming_service_box_ptr = nullptr; ComboBox *streaming_service_box_ptr = nullptr;
List *stream_key_list_ptr = nullptr; List *stream_key_list_ptr = nullptr;
List *stream_url_list_ptr = nullptr; List *custom_stream_list_ptr = nullptr;
List *container_list_ptr = nullptr;
CheckBox *save_replay_in_game_folder_ptr = nullptr; CheckBox *save_replay_in_game_folder_ptr = nullptr;
CheckBox *restart_replay_on_save = nullptr; CheckBox *restart_replay_on_save = nullptr;
Label *estimated_file_size_ptr = nullptr; Label *estimated_file_size_ptr = nullptr;

View File

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

View File

@@ -1,4 +1,6 @@
project('gsr-ui', ['c', 'cpp'], version : '1.7.2', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends') project('gsr-ui', ['c', 'cpp'], version : '1.7.9', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
add_project_arguments('-D_FILE_OFFSET_BITS=64', language : ['c', 'cpp'])
if get_option('buildtype') == 'debug' if get_option('buildtype') == 'debug'
add_project_arguments('-g3', language : ['c', 'cpp']) add_project_arguments('-g3', language : ['c', 'cpp'])
@@ -47,6 +49,7 @@ src = [
'src/Overlay.cpp', 'src/Overlay.cpp',
'src/AudioPlayer.cpp', 'src/AudioPlayer.cpp',
'src/Hotplug.cpp', 'src/Hotplug.cpp',
'src/ClipboardFile.cpp',
'src/Rpc.cpp', 'src/Rpc.cpp',
'src/main.cpp', 'src/main.cpp',
] ]
@@ -62,7 +65,7 @@ datadir = get_option('datadir')
gsr_ui_resources_path = join_paths(prefix, datadir, 'gsr-ui') 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_UI_VERSION="' + meson.project_version() + '"', language: ['c', 'cpp'])
add_project_arguments('-DGSR_FLATPAK_VERSION="5.7.5"', language: ['c', 'cpp']) add_project_arguments('-DGSR_FLATPAK_VERSION="5.8.3"', language: ['c', 'cpp'])
executable( executable(
meson.project_name(), meson.project_name(),

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "gsr-ui" name = "gsr-ui"
type = "executable" type = "executable"
version = "1.7.2" version = "1.7.9"
platforms = ["posix"] platforms = ["posix"]
[lang.cpp] [lang.cpp]
@@ -10,6 +10,9 @@ version = "c++17"
[config] [config]
ignore_dirs = ["build", "tools"] ignore_dirs = ["build", "tools"]
[define]
_FILE_OFFSET_BITS = "64"
[dependencies] [dependencies]
xcomposite = ">=0" xcomposite = ">=0"
xfixes = ">=0" xfixes = ">=0"

299
src/ClipboardFile.cpp Normal file
View File

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

View File

@@ -174,6 +174,7 @@ namespace gsr {
{"main.hotkeys_enable_option", &config.main_config.hotkeys_enable_option}, {"main.hotkeys_enable_option", &config.main_config.hotkeys_enable_option},
{"main.joystick_hotkeys_enable_option", &config.main_config.joystick_hotkeys_enable_option}, {"main.joystick_hotkeys_enable_option", &config.main_config.joystick_hotkeys_enable_option},
{"main.tint_color", &config.main_config.tint_color}, {"main.tint_color", &config.main_config.tint_color},
{"main.notification_speed", &config.main_config.notification_speed},
{"main.show_hide_hotkey", &config.main_config.show_hide_hotkey}, {"main.show_hide_hotkey", &config.main_config.show_hide_hotkey},
{"streaming.record_options.record_area_option", &config.streaming_config.record_options.record_area_option}, {"streaming.record_options.record_area_option", &config.streaming_config.record_options.record_area_option},
@@ -283,6 +284,7 @@ namespace gsr {
{"screenshot.record_cursor", &config.screenshot_config.record_cursor}, {"screenshot.record_cursor", &config.screenshot_config.record_cursor},
{"screenshot.restore_portal_session", &config.screenshot_config.restore_portal_session}, {"screenshot.restore_portal_session", &config.screenshot_config.restore_portal_session},
{"screenshot.save_screenshot_in_game_folder", &config.screenshot_config.save_screenshot_in_game_folder}, {"screenshot.save_screenshot_in_game_folder", &config.screenshot_config.save_screenshot_in_game_folder},
{"screenshot.save_screenshot_to_clipboard", &config.screenshot_config.save_screenshot_to_clipboard},
{"screenshot.show_screenshot_saved_notifications", &config.screenshot_config.show_screenshot_saved_notifications}, {"screenshot.show_screenshot_saved_notifications", &config.screenshot_config.show_screenshot_saved_notifications},
{"screenshot.save_directory", &config.screenshot_config.save_directory}, {"screenshot.save_directory", &config.screenshot_config.save_directory},
{"screenshot.take_screenshot_hotkey", &config.screenshot_config.take_screenshot_hotkey}, {"screenshot.take_screenshot_hotkey", &config.screenshot_config.take_screenshot_hotkey},

View File

@@ -3,92 +3,48 @@
#include <errno.h> #include <errno.h>
#include <fcntl.h> #include <fcntl.h>
#include <unistd.h> #include <unistd.h>
#include <dirent.h>
#include <sys/eventfd.h> #include <sys/eventfd.h>
namespace gsr { namespace gsr {
static constexpr int button_pressed = 1; static constexpr int button_pressed = 1;
static constexpr int cross_button = 0;
static constexpr int triangle_button = 2;
static constexpr int options_button = 9;
static constexpr int playstation_button = 10;
static constexpr int l3_button = 11;
static constexpr int r3_button = 12;
static constexpr int axis_up_down = 7;
static constexpr int axis_left_right = 6;
struct DeviceId {
uint16_t vendor;
uint16_t product;
};
static bool read_file_hex_number(const char *path, unsigned int *value) {
*value = 0;
FILE *f = fopen(path, "rb");
if(!f)
return false;
fscanf(f, "%x", value);
fclose(f);
return true;
}
static DeviceId joystick_get_device_id(const char *path) {
DeviceId device_id;
device_id.vendor = 0;
device_id.product = 0;
const char *js_path_id = nullptr;
const int len = strlen(path);
for(int i = len - 1; i >= 0; --i) {
if(path[i] == '/') {
js_path_id = path + i + 1;
break;
}
}
if(!js_path_id)
return device_id;
unsigned int vendor = 0;
unsigned int product = 0;
char path_buf[1024];
snprintf(path_buf, sizeof(path_buf), "/sys/class/input/%s/device/id/vendor", js_path_id);
if(!read_file_hex_number(path_buf, &vendor))
return device_id;
snprintf(path_buf, sizeof(path_buf), "/sys/class/input/%s/device/id/product", js_path_id);
if(!read_file_hex_number(path_buf, &product))
return device_id;
device_id.vendor = vendor;
device_id.product = product;
return device_id;
}
static bool is_ps4_controller(DeviceId device_id) {
return device_id.vendor == 0x054C && (device_id.product == 0x09CC || device_id.product == 0x0BA0 || device_id.product == 0x05C4);
}
static bool is_ps5_controller(DeviceId device_id) {
return device_id.vendor == 0x054C && (device_id.product == 0x0DF2 || device_id.product == 0x0CE6);
}
static bool is_stadia_controller(DeviceId device_id) {
return device_id.vendor == 0x18D1 && (device_id.product == 0x9400);
}
// Returns -1 on error // Returns -1 on error
static int get_js_dev_input_id_from_filepath(const char *dev_input_filepath) { static int get_dev_input_event_id_from_filepath(const char *dev_input_filepath) {
if(strncmp(dev_input_filepath, "/dev/input/js", 13) != 0) if(strncmp(dev_input_filepath, "/dev/input/event", 16) != 0)
return -1; return -1;
int dev_input_id = -1; int dev_input_id = -1;
if(sscanf(dev_input_filepath + 13, "%d", &dev_input_id) == 1) if(sscanf(dev_input_filepath + 16, "%d", &dev_input_id) == 1)
return dev_input_id; return dev_input_id;
return -1; return -1;
} }
static inline bool supports_key(unsigned char *key_bits, unsigned int key) {
return key_bits[key/8] & (1 << (key % 8));
}
static bool supports_joystick_keys(unsigned char *key_bits) {
const int keys[7] = { BTN_A, BTN_B, BTN_X, BTN_Y, BTN_SELECT, BTN_START, BTN_SELECT };
for(int i = 0; i < 7; ++i) {
if(supports_key(key_bits, keys[i]))
return true;
}
return false;
}
static bool is_input_device_joystick(int input_fd) {
unsigned long evbit = 0;
ioctl(input_fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);
if((evbit & (1 << EV_SYN)) && (evbit & (1 << EV_KEY))) {
unsigned char key_bits[KEY_MAX/8 + 1];
memset(key_bits, 0, sizeof(key_bits));
ioctl(input_fd, EVIOCGBIT(EV_KEY, sizeof(key_bits)), &key_bits);
return supports_joystick_keys(key_bits);
}
return false;
}
GlobalHotkeysJoystick::~GlobalHotkeysJoystick() { GlobalHotkeysJoystick::~GlobalHotkeysJoystick() {
if(event_fd > 0) { if(event_fd > 0) {
const uint64_t exit = 1; const uint64_t exit = 1;
@@ -98,8 +54,18 @@ namespace gsr {
if(read_thread.joinable()) if(read_thread.joinable())
read_thread.join(); read_thread.join();
if(event_fd > 0) if(event_fd > 0) {
close(event_fd); close(event_fd);
event_fd = 0;
}
close_fd_cv.notify_one();
if(close_fd_thread.joinable())
close_fd_thread.join();
for(int fd : fds_to_close) {
close(fd);
}
for(int i = 0; i < num_poll_fd; ++i) { for(int i = 0; i < num_poll_fd; ++i) {
if(poll_fd[i].fd > 0) if(poll_fd[i].fd > 0)
@@ -141,16 +107,10 @@ namespace gsr {
++num_poll_fd; ++num_poll_fd;
} }
char dev_input_path[128]; add_all_joystick_devices();
for(int i = 0; i < 8; ++i) {
snprintf(dev_input_path, sizeof(dev_input_path), "/dev/input/js%d", i);
add_device(dev_input_path, false);
}
if(num_poll_fd == 0)
fprintf(stderr, "Info: no joysticks found, assuming they might be connected later\n");
read_thread = std::thread(&GlobalHotkeysJoystick::read_events, this); read_thread = std::thread(&GlobalHotkeysJoystick::read_events, this);
close_fd_thread = std::thread(&GlobalHotkeysJoystick::close_fds, this);
return true; return true;
} }
@@ -214,8 +174,30 @@ namespace gsr {
} }
} }
// Retarded linux takes very long time to close /dev/input/eventN files, even though they are virtual and opened read-only
void GlobalHotkeysJoystick::close_fds() {
std::vector<int> current_fds_to_close;
while(event_fd > 0) {
{
std::unique_lock<std::mutex> lock(close_fd_mutex);
close_fd_cv.wait(lock, [this]{ return !fds_to_close.empty() || event_fd <= 0; });
}
{
std::lock_guard<std::mutex> lock(close_fd_mutex);
current_fds_to_close = std::move(fds_to_close);
fds_to_close.clear();
}
for(int fd : current_fds_to_close) {
close(fd);
}
current_fds_to_close.clear();
}
}
void GlobalHotkeysJoystick::read_events() { void GlobalHotkeysJoystick::read_events() {
js_event event; input_event event;
while(poll(poll_fd, num_poll_fd, -1) > 0) { while(poll(poll_fd, num_poll_fd, -1) > 0) {
for(int i = 0; i < num_poll_fd; ++i) { for(int i = 0; i < num_poll_fd; ++i) {
if(poll_fd[i].revents & (POLLHUP|POLLERR|POLLNVAL)) { if(poll_fd[i].revents & (POLLHUP|POLLERR|POLLNVAL)) {
@@ -223,7 +205,7 @@ namespace gsr {
goto done; goto done;
char dev_input_filepath[256]; char dev_input_filepath[256];
snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/input/js%d", extra_data[i].dev_input_id); snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/input/event%d", extra_data[i].dev_input_id);
fprintf(stderr, "Info: removed joystick: %s\n", dev_input_filepath); fprintf(stderr, "Info: removed joystick: %s\n", dev_input_filepath);
if(remove_poll_fd(i)) if(remove_poll_fd(i))
--i; // This item was removed so we want to repeat the same index to continue to the next item --i; // This item was removed so we want to repeat the same index to continue to the next item
@@ -240,7 +222,7 @@ namespace gsr {
hotplug.process_event_data(poll_fd[i].fd, [&](HotplugAction hotplug_action, const char *devname) { hotplug.process_event_data(poll_fd[i].fd, [&](HotplugAction hotplug_action, const char *devname) {
switch(hotplug_action) { switch(hotplug_action) {
case HotplugAction::ADD: { case HotplugAction::ADD: {
add_device(devname); add_device(devname, false);
break; break;
} }
case HotplugAction::REMOVE: { case HotplugAction::REMOVE: {
@@ -251,7 +233,7 @@ namespace gsr {
} }
}); });
} else { } else {
process_js_event(poll_fd[i].fd, event); process_input_event(poll_fd[i].fd, event);
} }
} }
} }
@@ -260,54 +242,44 @@ namespace gsr {
; ;
} }
void GlobalHotkeysJoystick::process_js_event(int fd, js_event &event) { void GlobalHotkeysJoystick::process_input_event(int fd, input_event &event) {
if(read(fd, &event, sizeof(event)) != sizeof(event)) if(read(fd, &event, sizeof(event)) != sizeof(event))
return; return;
if((event.type & JS_EVENT_BUTTON) == JS_EVENT_BUTTON) { if(event.type == EV_KEY) {
switch(event.number) { switch(event.code) {
case playstation_button: { case BTN_MODE: {
// Workaround weird steam input (in-game) behavior where steam triggers playstation button + options when pressing both l3 and r3 at the same time playstation_button_pressed = (event.value == button_pressed);
playstation_button_pressed = (event.value == button_pressed) && !l3_button_pressed && !r3_button_pressed;
break; break;
} }
case options_button: { case BTN_START: {
if(playstation_button_pressed && event.value == button_pressed) if(playstation_button_pressed && event.value == button_pressed)
toggle_show = true; toggle_show = true;
break; break;
} }
case cross_button: { case BTN_SOUTH: {
if(playstation_button_pressed && event.value == button_pressed) if(playstation_button_pressed && event.value == button_pressed)
save_1_min_replay = true; save_1_min_replay = true;
break; break;
} }
case triangle_button: { case BTN_NORTH: {
if(playstation_button_pressed && event.value == button_pressed) if(playstation_button_pressed && event.value == button_pressed)
save_10_min_replay = true; save_10_min_replay = true;
break; break;
} }
case l3_button: {
l3_button_pressed = event.value == button_pressed;
break;
}
case r3_button: {
r3_button_pressed = event.value == button_pressed;
break;
}
} }
} else if((event.type & JS_EVENT_AXIS) == JS_EVENT_AXIS && playstation_button_pressed) { } else if(event.type == EV_ABS && playstation_button_pressed) {
const int trigger_threshold = 16383;
const bool prev_up_pressed = up_pressed; const bool prev_up_pressed = up_pressed;
const bool prev_down_pressed = down_pressed; const bool prev_down_pressed = down_pressed;
const bool prev_left_pressed = left_pressed; const bool prev_left_pressed = left_pressed;
const bool prev_right_pressed = right_pressed; const bool prev_right_pressed = right_pressed;
if(event.number == axis_up_down) { if(event.code == ABS_HAT0Y) {
up_pressed = event.value <= -trigger_threshold; up_pressed = event.value == -1;
down_pressed = event.value >= trigger_threshold; down_pressed = event.value == 1;
} else if(event.number == axis_left_right) { } else if(event.code == ABS_HAT0X) {
left_pressed = event.value <= -trigger_threshold; left_pressed = event.value == -1;
right_pressed = event.value >= trigger_threshold; right_pressed = event.value == 1;
} }
if(up_pressed && !prev_up_pressed) if(up_pressed && !prev_up_pressed)
@@ -321,13 +293,36 @@ namespace gsr {
} }
} }
void GlobalHotkeysJoystick::add_all_joystick_devices() {
DIR *dir = opendir("/dev/input");
if(!dir) {
fprintf(stderr, "Error: failed to open /dev/input, error: %s\n", strerror(errno));
return;
}
char dev_input_filepath[1024];
for(;;) {
struct dirent *entry = readdir(dir);
if(!entry)
break;
if(strncmp(entry->d_name, "event", 5) != 0)
continue;
snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/input/%s", entry->d_name);
add_device(dev_input_filepath, false);
}
closedir(dir);
}
bool GlobalHotkeysJoystick::add_device(const char *dev_input_filepath, bool print_error) { bool GlobalHotkeysJoystick::add_device(const char *dev_input_filepath, bool print_error) {
if(num_poll_fd >= max_js_poll_fd) { if(num_poll_fd >= max_js_poll_fd) {
fprintf(stderr, "Warning: failed to add joystick device %s, too many joysticks have been added\n", dev_input_filepath); fprintf(stderr, "Warning: failed to add joystick device %s, too many joysticks have been added\n", dev_input_filepath);
return false; return false;
} }
const int dev_input_id = get_js_dev_input_id_from_filepath(dev_input_filepath); const int dev_input_id = get_dev_input_event_id_from_filepath(dev_input_filepath);
if(dev_input_id == -1) if(dev_input_id == -1)
return false; return false;
@@ -338,6 +333,15 @@ namespace gsr {
return false; return false;
} }
if(!is_input_device_joystick(fd)) {
{
std::lock_guard<std::mutex> lock(close_fd_mutex);
fds_to_close.push_back(fd);
}
close_fd_cv.notify_one();
return false;
}
poll_fd[num_poll_fd] = { poll_fd[num_poll_fd] = {
fd, fd,
POLLIN, POLLIN,
@@ -356,7 +360,7 @@ namespace gsr {
} }
bool GlobalHotkeysJoystick::remove_device(const char *dev_input_filepath) { bool GlobalHotkeysJoystick::remove_device(const char *dev_input_filepath) {
const int dev_input_id = get_js_dev_input_id_from_filepath(dev_input_filepath); const int dev_input_id = get_dev_input_event_id_from_filepath(dev_input_filepath);
if(dev_input_id == -1) if(dev_input_id == -1)
return false; return false;
@@ -372,8 +376,13 @@ namespace gsr {
if(index < 0 || index >= num_poll_fd) if(index < 0 || index >= num_poll_fd)
return false; return false;
if(poll_fd[index].fd > 0) if(poll_fd[index].fd > 0) {
close(poll_fd[index].fd); {
std::lock_guard<std::mutex> lock(close_fd_mutex);
fds_to_close.push_back(poll_fd[index].fd);
}
close_fd_cv.notify_one();
}
for(int i = index + 1; i < num_poll_fd; ++i) { for(int i = index + 1; i < num_poll_fd; ++i) {
poll_fd[i - 1] = poll_fd[i]; poll_fd[i - 1] = poll_fd[i];

View File

@@ -50,6 +50,7 @@ namespace gsr {
static const double force_window_on_top_timeout_seconds = 1.0; static const double force_window_on_top_timeout_seconds = 1.0;
static const double replay_status_update_check_timeout_seconds = 1.5; static const double replay_status_update_check_timeout_seconds = 1.5;
static const double replay_saving_notification_timeout_seconds = 0.5; static const double replay_saving_notification_timeout_seconds = 0.5;
static const double short_notification_timeout_seconds = 2.0;
static const double notification_timeout_seconds = 3.0; static const double notification_timeout_seconds = 3.0;
static const double notification_error_timeout_seconds = 5.0; static const double notification_error_timeout_seconds = 5.0;
static const double cursor_tracker_update_timeout_sec = 0.1; static const double cursor_tracker_update_timeout_sec = 0.1;
@@ -446,6 +447,17 @@ namespace gsr {
return global_hotkeys_js; return global_hotkeys_js;
} }
static NotificationSpeed to_notification_speed(const std::string &notification_speed_str) {
if(notification_speed_str == "normal")
return NotificationSpeed::NORMAL;
else if(notification_speed_str == "fast")
return NotificationSpeed::FAST;
else {
assert(false);
return NotificationSpeed::NORMAL;
}
}
Overlay::Overlay(std::string resources_path, GsrInfo gsr_info, SupportedCaptureOptions capture_options, egl_functions egl_funcs) : Overlay::Overlay(std::string resources_path, GsrInfo gsr_info, SupportedCaptureOptions capture_options, egl_functions egl_funcs) :
resources_path(std::move(resources_path)), resources_path(std::move(resources_path)),
gsr_info(std::move(gsr_info)), gsr_info(std::move(gsr_info)),
@@ -474,6 +486,7 @@ namespace gsr {
power_supply_online_filepath = get_power_supply_online_filepath(); power_supply_online_filepath = get_power_supply_online_filepath();
replay_startup_mode = replay_startup_string_to_type(config.replay_config.turn_on_replay_automatically_mode.c_str()); replay_startup_mode = replay_startup_string_to_type(config.replay_config.turn_on_replay_automatically_mode.c_str());
set_notification_speed(to_notification_speed(config.main_config.notification_speed));
if(config.main_config.hotkeys_enable_option == "enable_hotkeys") if(config.main_config.hotkeys_enable_option == "enable_hotkeys")
global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::ALL); global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::ALL);
@@ -499,7 +512,7 @@ namespace gsr {
hide(); hide();
if(notification_process > 0) { if(notification_process > 0) {
kill(notification_process, SIGKILL); kill(notification_process, SIGINT);
int status; int status;
if(waitpid(notification_process, &status, 0) == -1) { if(waitpid(notification_process, &status, 0) == -1) {
perror("waitpid failed"); perror("waitpid failed");
@@ -946,6 +959,7 @@ namespace gsr {
const bool is_kwin = wm_name == "KWin"; const bool is_kwin = wm_name == "KWin";
const bool is_wlroots = wm_name.find("wlroots") != std::string::npos; const bool is_wlroots = wm_name.find("wlroots") != std::string::npos;
const bool is_hyprland = wm_name.find("Hyprland") != std::string::npos; const bool is_hyprland = wm_name.find("Hyprland") != std::string::npos;
//const bool is_smithay = wm_name.find("Smithay") != std::string::npos;
const bool hyprland_waybar_is_dock = is_hyprland && is_hyprland_waybar_running_as_dock(); const bool hyprland_waybar_is_dock = is_hyprland && is_hyprland_waybar_running_as_dock();
std::optional<CursorInfo> cursor_info; std::optional<CursorInfo> cursor_info;
@@ -973,8 +987,7 @@ namespace gsr {
// Wayland doesn't allow XGrabPointer/XGrabKeyboard when a wayland application is focused. // Wayland doesn't allow XGrabPointer/XGrabKeyboard when a wayland application is focused.
// If the focused window is a wayland application then don't use override redirect and instead create // If the focused window is a wayland application then don't use override redirect and instead create
// a fullscreen window for the ui. // a fullscreen window for the ui.
// TODO: (x11_cursor_window && is_window_fullscreen_on_monitor(display, x11_cursor_window, *focused_monitor)) const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || (x11_cursor_window && is_window_fullscreen_on_monitor(display, x11_cursor_window, *focused_monitor)) || is_wlroots || is_hyprland;
const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || x11_cursor_window || is_wlroots || is_hyprland;
if(prevent_game_minimizing) { if(prevent_game_minimizing) {
window_pos = focused_monitor->position; window_pos = focused_monitor->position;
@@ -1433,6 +1446,10 @@ namespace gsr {
malloc_trim(0); malloc_trim(0);
} }
void Overlay::hide_next_frame() {
hide_ui = true;
}
void Overlay::toggle_show() { void Overlay::toggle_show() {
if(visible) { if(visible) {
//hide(); //hide();
@@ -1645,6 +1662,8 @@ namespace gsr {
} }
void Overlay::show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type, const char *capture_target) { void Overlay::show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type, const char *capture_target) {
timeout_seconds *= notification_duration_multiplier;
char timeout_seconds_str[32]; char timeout_seconds_str[32];
snprintf(timeout_seconds_str, sizeof(timeout_seconds_str), "%f", timeout_seconds); snprintf(timeout_seconds_str, sizeof(timeout_seconds_str), "%f", timeout_seconds);
@@ -1685,7 +1704,7 @@ namespace gsr {
notification_args[arg_index++] = nullptr; notification_args[arg_index++] = nullptr;
if(notification_process > 0) { if(notification_process > 0) {
kill(notification_process, SIGKILL); kill(notification_process, SIGINT);
int status = 0; int status = 0;
waitpid(notification_process, &status, 0); waitpid(notification_process, &status, 0);
} }
@@ -1737,6 +1756,17 @@ namespace gsr {
bind_linux_hotkeys(static_cast<GlobalHotkeysLinux*>(global_hotkeys.get()), this); bind_linux_hotkeys(static_cast<GlobalHotkeysLinux*>(global_hotkeys.get()), this);
} }
void Overlay::set_notification_speed(NotificationSpeed notification_speed) {
switch(notification_speed) {
case NotificationSpeed::NORMAL:
notification_duration_multiplier = 1.0;
break;
case NotificationSpeed::FAST:
notification_duration_multiplier = 0.3;
break;
}
}
void Overlay::update_notification_process_status() { void Overlay::update_notification_process_status() {
if(notification_process <= 0) if(notification_process <= 0)
return; return;
@@ -1802,8 +1832,6 @@ namespace gsr {
result += std::to_string(seconds) + " second" + (seconds == 1 ? "" : "s"); result += std::to_string(seconds) + " second" + (seconds == 1 ? "" : "s");
} }
fprintf(stderr, "to duration string: %f, %d, %d, %d\n", duration_sec, seconds, minutes, hours);
return result; return result;
} }
@@ -1816,6 +1844,15 @@ namespace gsr {
return replay_duration_sec; return replay_duration_sec;
} }
static ClipboardFile::FileType filename_to_clipboard_file_type(const std::string &filename) {
if(ends_with(filename, ".jpg") || ends_with(filename, ".jpeg"))
return ClipboardFile::FileType::JPG;
else if(ends_with(filename, ".png"))
return ClipboardFile::FileType::PNG;
assert(false);
return ClipboardFile::FileType::PNG;
}
void Overlay::save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type) { void Overlay::save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type) {
mgl_context *context = mgl_get_context(); mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection; Display *display = (Display*)context->connection;
@@ -1828,7 +1865,7 @@ namespace gsr {
if(focused_window_name.empty()) if(focused_window_name.empty())
focused_window_name = "Game"; focused_window_name = "Game";
string_replace_characters(focused_window_name.data(), "/\\", '_'); string_replace_characters(focused_window_name.data(), "/\\", ' ');
std::string video_directory = filepath_get_directory(video_filepath) + "/" + focused_window_name; std::string video_directory = filepath_get_directory(video_filepath) + "/" + focused_window_name;
create_directory_recursive(video_directory.data()); create_directory_recursive(video_directory.data());
@@ -1870,6 +1907,9 @@ namespace gsr {
snprintf(msg, sizeof(msg), "Saved a screenshot of %s\nto \"%s\"", snprintf(msg, sizeof(msg), "Saved a screenshot of %s\nto \"%s\"",
capture_target_get_notification_name(screenshot_capture_target.c_str(), true).c_str(), focused_window_name.c_str()); capture_target_get_notification_name(screenshot_capture_target.c_str(), true).c_str(), focused_window_name.c_str());
capture_target = screenshot_capture_target.c_str(); capture_target = screenshot_capture_target.c_str();
if(config.screenshot_config.save_screenshot_to_clipboard)
clipboard_file.set_current_file(new_video_filepath, filename_to_clipboard_file_type(new_video_filepath));
break; break;
} }
case NotificationType::NONE: case NotificationType::NONE:
@@ -1951,14 +1991,24 @@ namespace gsr {
void Overlay::on_gsr_process_error(int exit_code, NotificationType notification_type) { void Overlay::on_gsr_process_error(int exit_code, NotificationType notification_type) {
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code); fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
if(exit_code == 50) { if(exit_code == 50) {
show_notification("Desktop portal capture failed.\nEither you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture\nor it's incorrectly setup on your system", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type); show_notification("Desktop portal capture failed.\nEither you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture\nor it's incorrectly setup on your system.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else if(exit_code == 51) {
show_notification("Monitor capture failed.\nThe monitor you are trying to capture is invalid.\nPlease validate your capture settings.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else if(exit_code == 52) {
show_notification("Capture failed. Neither H264, HEVC nor AV1 video codecs are supported\non your system or you are trying to capture at a resolution higher than your\nsystem supports for each video codec.", 10.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else if(exit_code == 53) {
show_notification("Capture failed. Your system doesn't support the resolution you are trying to\nrecord at with the video codec you have chosen.\nChange capture resolution or video codec and try again.\nNote: AV1 supports the highest resolution, then HEVC and then H264.", 10.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else if(exit_code == 54) {
show_notification("Capture failed. Your system doesn't support the video codec you have chosen.\nChange video codec and try again.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else if(exit_code == 60) { } else if(exit_code == 60) {
show_notification("Stopped capture because the user canceled the desktop portal", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type); show_notification("Stopped capture because the user canceled the desktop portal", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else { } else {
const char *prefix = ""; const char *prefix = "";
switch(notification_type) { switch(notification_type) {
case NotificationType::NONE: case NotificationType::NONE:
break;
case NotificationType::SCREENSHOT: case NotificationType::SCREENSHOT:
prefix = "Failed to take a screenshot";
break; break;
case NotificationType::RECORD: case NotificationType::RECORD:
prefix = "Failed to start/save recording"; prefix = "Failed to start/save recording";
@@ -2001,7 +2051,7 @@ namespace gsr {
update_ui_replay_stopped(); update_ui_replay_stopped();
if(exit_code == 0) { if(exit_code == 0) {
if(config.replay_config.show_replay_stopped_notifications) if(config.replay_config.show_replay_stopped_notifications)
show_notification("Replay stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); show_notification("Replay stopped", short_notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
} else { } else {
on_gsr_process_error(exit_code, NotificationType::REPLAY); on_gsr_process_error(exit_code, NotificationType::REPLAY);
} }
@@ -2016,7 +2066,7 @@ namespace gsr {
update_ui_streaming_stopped(); update_ui_streaming_stopped();
if(exit_code == 0) { if(exit_code == 0) {
if(config.streaming_config.show_streaming_stopped_notifications) if(config.streaming_config.show_streaming_stopped_notifications)
show_notification("Streaming has stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM); show_notification("Streaming has stopped", short_notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
} else { } else {
on_gsr_process_error(exit_code, NotificationType::STREAM); on_gsr_process_error(exit_code, NotificationType::STREAM);
} }
@@ -2050,6 +2100,9 @@ namespace gsr {
snprintf(msg, sizeof(msg), "Saved a screenshot of %s", snprintf(msg, sizeof(msg), "Saved a screenshot of %s",
capture_target_get_notification_name(screenshot_capture_target.c_str(), true).c_str()); capture_target_get_notification_name(screenshot_capture_target.c_str(), true).c_str());
show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::SCREENSHOT, screenshot_capture_target.c_str()); show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::SCREENSHOT, screenshot_capture_target.c_str());
if(config.screenshot_config.save_screenshot_to_clipboard)
clipboard_file.set_current_file(screenshot_filepath, filename_to_clipboard_file_type(screenshot_filepath));
} }
} else { } else {
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_screenshot_process, exit_code); fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_screenshot_process, exit_code);
@@ -2468,7 +2521,7 @@ namespace gsr {
kill(gpu_screen_recorder_process, SIGRTMIN+5); kill(gpu_screen_recorder_process, SIGRTMIN+5);
} }
static const char* switch_video_codec_to_usable_hardware_encoder(const GsrInfo &gsr_info) { static const char* get_first_usable_hardware_video_codec_name(const GsrInfo &gsr_info) {
if(gsr_info.supported_video_codecs.h264) if(gsr_info.supported_video_codecs.h264)
return "h264"; return "h264";
else if(gsr_info.supported_video_codecs.hevc) else if(gsr_info.supported_video_codecs.hevc)
@@ -2501,8 +2554,7 @@ namespace gsr {
*video_codec = "h264"; *video_codec = "h264";
*encoder = "cpu"; *encoder = "cpu";
} else if(strcmp(*video_codec, "auto") == 0) { } else if(strcmp(*video_codec, "auto") == 0) {
*video_codec = switch_video_codec_to_usable_hardware_encoder(gsr_info); if(!get_first_usable_hardware_video_codec_name(gsr_info)) {
if(!*video_codec) {
*video_codec = "h264"; *video_codec = "h264";
*encoder = "cpu"; *encoder = "cpu";
} }
@@ -2510,6 +2562,14 @@ namespace gsr {
*container = change_container_if_codec_not_supported(*video_codec, *container); *container = change_container_if_codec_not_supported(*video_codec, *container);
} }
static std::string get_framerate_mode_validate(const RecordOptions &record_options, const GsrInfo &gsr_info) {
(void)gsr_info;
std::string framerate_mode = record_options.framerate_mode;
if(framerate_mode == "auto")
framerate_mode = "vfr";
return framerate_mode;
}
bool Overlay::on_press_start_replay(bool disable_notification, bool finished_selection) { bool Overlay::on_press_start_replay(bool disable_notification, bool finished_selection) {
if(region_selector.is_started() || window_selector.is_started()) if(region_selector.is_started() || window_selector.is_started())
return false; return false;
@@ -2582,7 +2642,7 @@ namespace gsr {
const std::string video_bitrate = std::to_string(config.replay_config.record_options.video_bitrate); const std::string video_bitrate = std::to_string(config.replay_config.record_options.video_bitrate);
const std::string output_directory = config.replay_config.save_directory; const std::string output_directory = config.replay_config.save_directory;
const std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.replay_config.record_options.audio_tracks_list, gsr_info); const std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.replay_config.record_options.audio_tracks_list, gsr_info);
const std::string framerate_mode = config.replay_config.record_options.framerate_mode == "auto" ? "vfr" : config.replay_config.record_options.framerate_mode; const std::string framerate_mode = get_framerate_mode_validate(config.replay_config.record_options, gsr_info);
const std::string replay_time = std::to_string(config.replay_config.replay_time); const std::string replay_time = std::to_string(config.replay_config.replay_time);
const char *container = config.replay_config.container.c_str(); const char *container = config.replay_config.container.c_str();
const char *video_codec = config.replay_config.record_options.video_codec.c_str(); const char *video_codec = config.replay_config.record_options.video_codec.c_str();
@@ -2658,7 +2718,7 @@ namespace gsr {
if(!disable_notification && config.replay_config.show_replay_started_notifications) { if(!disable_notification && config.replay_config.show_replay_started_notifications) {
char msg[256]; char msg[256];
snprintf(msg, sizeof(msg), "Started replaying %s", capture_target_get_notification_name(recording_capture_target.c_str(), false).c_str()); snprintf(msg, sizeof(msg), "Started replaying %s", capture_target_get_notification_name(recording_capture_target.c_str(), false).c_str());
show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::REPLAY, recording_capture_target.c_str()); show_notification(msg, short_notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::REPLAY, recording_capture_target.c_str());
} }
if(config.replay_config.record_options.record_area_option == "portal") if(config.replay_config.record_options.record_area_option == "portal")
@@ -2686,7 +2746,7 @@ namespace gsr {
if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) { if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) {
if(!replay_recording) { if(!replay_recording) {
if(config.record_config.show_recording_started_notifications) if(config.record_config.show_recording_started_notifications)
show_notification("Started recording in the replay session", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD); show_notification("Started recording in the replay session", short_notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD);
update_ui_recording_started(); update_ui_recording_started();
// TODO: This will be incorrect if the user uses portal capture, as capture wont start until the user has // TODO: This will be incorrect if the user uses portal capture, as capture wont start until the user has
@@ -2707,7 +2767,7 @@ namespace gsr {
if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) { if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) {
if(!replay_recording) { if(!replay_recording) {
if(config.record_config.show_recording_started_notifications) if(config.record_config.show_recording_started_notifications)
show_notification("Started recording in the streaming session", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD); show_notification("Started recording in the streaming session", short_notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD);
update_ui_recording_started(); update_ui_recording_started();
// TODO: This will be incorrect if the user uses portal capture, as capture wont start until the user has // TODO: This will be incorrect if the user uses portal capture, as capture wont start until the user has
@@ -2779,7 +2839,7 @@ namespace gsr {
const std::string video_bitrate = std::to_string(config.record_config.record_options.video_bitrate); const std::string video_bitrate = std::to_string(config.record_config.record_options.video_bitrate);
const std::string output_file = config.record_config.save_directory + "/Video_" + get_date_str() + "." + container_to_file_extension(config.record_config.container.c_str()); const std::string output_file = config.record_config.save_directory + "/Video_" + get_date_str() + "." + container_to_file_extension(config.record_config.container.c_str());
const std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.record_config.record_options.audio_tracks_list, gsr_info); const std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.record_config.record_options.audio_tracks_list, gsr_info);
const std::string framerate_mode = config.record_config.record_options.framerate_mode == "auto" ? "vfr" : config.record_config.record_options.framerate_mode; const std::string framerate_mode = get_framerate_mode_validate(config.record_config.record_options, gsr_info);
const char *container = config.record_config.container.c_str(); const char *container = config.record_config.container.c_str();
const char *video_codec = config.record_config.record_options.video_codec.c_str(); const char *video_codec = config.record_config.record_options.video_codec.c_str();
const char *encoder = "gpu"; const char *encoder = "gpu";
@@ -2833,7 +2893,7 @@ namespace gsr {
if(config.record_config.show_recording_started_notifications) { if(config.record_config.show_recording_started_notifications) {
char msg[256]; char msg[256];
snprintf(msg, sizeof(msg), "Started recording %s", capture_target_get_notification_name(recording_capture_target.c_str(), false).c_str()); snprintf(msg, sizeof(msg), "Started recording %s", capture_target_get_notification_name(recording_capture_target.c_str(), false).c_str());
show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str()); show_notification(msg, short_notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str());
} }
if(config.record_config.record_options.record_area_option == "portal") if(config.record_config.record_options.record_area_option == "portal")
@@ -2955,7 +3015,7 @@ namespace gsr {
// But we check it anyways as streaming on some sites can fail if there is more than one audio track // But we check it anyways as streaming on some sites can fail if there is more than one audio track
if(audio_tracks.size() > 1) if(audio_tracks.size() > 1)
audio_tracks.resize(1); audio_tracks.resize(1);
const std::string framerate_mode = config.streaming_config.record_options.framerate_mode == "auto" ? "vfr" : config.streaming_config.record_options.framerate_mode; const std::string framerate_mode = get_framerate_mode_validate(config.streaming_config.record_options, gsr_info);
const char *container = "flv"; const char *container = "flv";
if(config.streaming_config.streaming_service == "custom") if(config.streaming_config.streaming_service == "custom")
container = config.streaming_config.custom.container.c_str(); container = config.streaming_config.custom.container.c_str();
@@ -3019,7 +3079,7 @@ namespace gsr {
if(config.streaming_config.show_streaming_started_notifications) { if(config.streaming_config.show_streaming_started_notifications) {
char msg[256]; char msg[256];
snprintf(msg, sizeof(msg), "Started streaming %s", capture_target_get_notification_name(recording_capture_target.c_str(), false).c_str()); snprintf(msg, sizeof(msg), "Started streaming %s", capture_target_get_notification_name(recording_capture_target.c_str(), false).c_str());
show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM, recording_capture_target.c_str()); show_notification(msg, short_notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM, recording_capture_target.c_str());
} }
if(config.streaming_config.record_options.record_area_option == "portal") if(config.streaming_config.record_options.record_area_option == "portal")
@@ -3035,7 +3095,6 @@ namespace gsr {
return; return;
} }
bool hotkey_window_capture = false;
std::string record_area_option; std::string record_area_option;
switch(force_type) { switch(force_type) {
case ScreenshotForceType::NONE: case ScreenshotForceType::NONE:
@@ -3046,7 +3105,6 @@ namespace gsr {
break; break;
case ScreenshotForceType::WINDOW: case ScreenshotForceType::WINDOW:
record_area_option = gsr_info.system_info.display_server == DisplayServer::X11 ? "window" : "portal"; record_area_option = gsr_info.system_info.display_server == DisplayServer::X11 ? "window" : "portal";
hotkey_window_capture = true;
break; break;
} }
@@ -3077,10 +3135,11 @@ namespace gsr {
// TODO: Validate input, fallback to valid values // 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 const std::string output_file = config.screenshot_config.save_directory + "/Screenshot_" + get_date_str() + "." + config.screenshot_config.image_format; // TODO: Validate image format
const bool capture_cursor = force_type == ScreenshotForceType::NONE && config.screenshot_config.record_cursor;
std::vector<const char*> args = { std::vector<const char*> args = {
"gpu-screen-recorder", "-w", screenshot_capture_target.c_str(), "gpu-screen-recorder", "-w", screenshot_capture_target.c_str(),
"-cursor", config.screenshot_config.record_cursor ? "yes" : "no", "-cursor", capture_cursor ? "yes" : "no",
"-v", "no", "-v", "no",
"-q", config.screenshot_config.image_quality.c_str(), "-q", config.screenshot_config.image_quality.c_str(),
"-o", output_file.c_str() "-o", output_file.c_str()
@@ -3094,7 +3153,7 @@ namespace gsr {
args.push_back(size); args.push_back(size);
} }
if(config.screenshot_config.restore_portal_session && !hotkey_window_capture) { if(config.screenshot_config.restore_portal_session && force_type != ScreenshotForceType::WINDOW) {
args.push_back("-restore-portal-session"); args.push_back("-restore-portal-session");
args.push_back("yes"); args.push_back("yes");
} }
@@ -3102,7 +3161,7 @@ namespace gsr {
const std::string hotkey_window_capture_portal_session_token_filepath = get_config_dir() + "/gpu-screen-recorder/gsr-ui-window-capture-token"; const std::string hotkey_window_capture_portal_session_token_filepath = get_config_dir() + "/gpu-screen-recorder/gsr-ui-window-capture-token";
if(record_area_option == "portal") { if(record_area_option == "portal") {
hide_ui = true; hide_ui = true;
if(hotkey_window_capture) { if(force_type == ScreenshotForceType::WINDOW) {
args.push_back("-portal-session-token-filepath"); args.push_back("-portal-session-token-filepath");
args.push_back(hotkey_window_capture_portal_session_token_filepath.c_str()); args.push_back(hotkey_window_capture_portal_session_token_filepath.c_str());
} }

View File

@@ -5,11 +5,12 @@
#include <limits.h> #include <limits.h>
#include <string.h> #include <string.h>
#include <errno.h> #include <errno.h>
#include <sys/stat.h> #include <poll.h>
#include <fcntl.h> #include <sys/socket.h>
#include <sys/un.h>
namespace gsr { namespace gsr {
static void get_runtime_filepath(char *buffer, size_t buffer_size, const char *filename) { static void get_socket_filepath(char *buffer, size_t buffer_size, const char *filename) {
char dir[PATH_MAX]; char dir[PATH_MAX];
const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); const char *runtime_dir = getenv("XDG_RUNTIME_DIR");
@@ -24,79 +25,117 @@ namespace gsr {
snprintf(buffer, buffer_size, "%s/%s", dir, filename); snprintf(buffer, buffer_size, "%s/%s", dir, filename);
} }
static int create_socket(const char *name, struct sockaddr_un *addr, std::string &socket_filepath) {
char socket_filepath_tmp[PATH_MAX];
get_socket_filepath(socket_filepath_tmp, sizeof(socket_filepath_tmp), name);
socket_filepath = socket_filepath_tmp;
memset(addr, 0, sizeof(*addr));
if(strlen(name) > sizeof(addr->sun_path))
return false;
addr->sun_family = AF_UNIX;
snprintf(addr->sun_path, sizeof(addr->sun_path), "%s", socket_filepath.c_str());
return socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0);
}
Rpc::Rpc() {
num_polls = 0;
}
Rpc::~Rpc() { Rpc::~Rpc() {
if(fd > 0) if(socket_fd > 0)
close(fd); close(socket_fd);
if(file) if(!socket_filepath.empty())
fclose(file); unlink(socket_filepath.c_str());
if(!fifo_filepath.empty())
unlink(fifo_filepath.c_str());
} }
bool Rpc::create(const char *name) { bool Rpc::create(const char *name) {
if(file) { if(socket_fd > 0) {
fprintf(stderr, "Error: Rpc::create: already created/opened\n"); fprintf(stderr, "Error: Rpc::create: already created/opened\n");
return false; return false;
} }
char fifo_filepath_tmp[PATH_MAX]; struct sockaddr_un addr;
get_runtime_filepath(fifo_filepath_tmp, sizeof(fifo_filepath_tmp), name); socket_fd = create_socket(name, &addr, socket_filepath);
fifo_filepath = fifo_filepath_tmp; if(socket_fd <= 0) {
unlink(fifo_filepath.c_str()); fprintf(stderr, "Error: Rpc::create: failed to create socket, error: %s\n", strerror(errno));
if(mkfifo(fifo_filepath.c_str(), 0600) != 0) {
fprintf(stderr, "Error: mkfifo failed, error: %s, %s\n", strerror(errno), fifo_filepath.c_str());
fifo_filepath.clear();
return false; return false;
} }
if(!open_filepath(fifo_filepath.c_str())) { unlink(socket_filepath.c_str());
unlink(fifo_filepath.c_str());
fifo_filepath.clear(); if(bind(socket_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
const int err = errno;
close(socket_fd);
socket_fd = 0;
fprintf(stderr, "Error: Rpc::create: failed to bind, error: %s\n", strerror(err));
return false; return false;
} }
if(listen(socket_fd, GSR_RPC_MAX_CONNECTIONS) == -1) {
const int err = errno;
close(socket_fd);
socket_fd = 0;
fprintf(stderr, "Error: Rpc::create: failed to listen, error: %s\n", strerror(err));
return false;
}
polls[0].fd = socket_fd;
polls[0].events = POLLIN;
polls[0].revents = 0;
++num_polls;
return true; return true;
} }
bool Rpc::open(const char *name) { RpcOpenResult Rpc::open(const char *name) {
if(file) { if(socket_fd > 0) {
fprintf(stderr, "Error: Rpc::open: already created/opened\n"); fprintf(stderr, "Error: Rpc::open: already created/opened\n");
return false; return RpcOpenResult::ERROR;
} }
char fifo_filepath_tmp[PATH_MAX]; struct sockaddr_un addr;
get_runtime_filepath(fifo_filepath_tmp, sizeof(fifo_filepath_tmp), name); socket_fd = create_socket(name, &addr, socket_filepath);
return open_filepath(fifo_filepath_tmp); socket_filepath.clear(); /* We dont want to delete the socket on exit as the client */
} if(socket_fd <= 0) {
fprintf(stderr, "Error: Rpc::open: failed to create socket, error: %s\n", strerror(errno));
bool Rpc::open_filepath(const char *filepath) { return RpcOpenResult::ERROR;
fd = ::open(filepath, O_RDWR | O_NONBLOCK);
if(fd <= 0)
return false;
file = fdopen(fd, "r+");
if(!file) {
close(fd);
fd = 0;
return false;
} }
fd = 0;
return true; while(true) {
if(connect(socket_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
const int err = errno;
if(err == EWOULDBLOCK) {
usleep(10 * 1000);
} else {
close(socket_fd);
socket_fd = 0;
if(err != ENOENT && err != ECONNREFUSED)
fprintf(stderr, "Error: Rpc::create: failed to connect, error: %s\n", strerror(err));
return RpcOpenResult::ERROR;
}
} else {
break;
}
}
return RpcOpenResult::OK;
} }
bool Rpc::write(const char *str, size_t size) { bool Rpc::write(const char *str, size_t size) {
if(!file) { if(socket_fd <= 0) {
fprintf(stderr, "Error: Rpc::write: fifo not created/opened yet\n"); fprintf(stderr, "Error: Rpc::write: unix domain socket not created/opened yet\n");
return false; return false;
} }
ssize_t offset = 0; ssize_t offset = 0;
while(offset < (ssize_t)size) { while(offset < (ssize_t)size) {
const ssize_t bytes_written = fwrite(str + offset, 1, size - offset, file); const ssize_t bytes_written = ::write(socket_fd, str + offset, size - offset);
fflush(file);
if(bytes_written > 0) if(bytes_written > 0)
offset += bytes_written; offset += bytes_written;
} }
@@ -104,30 +143,73 @@ namespace gsr {
} }
void Rpc::poll() { void Rpc::poll() {
if(!file) { if(socket_fd <= 0) {
//fprintf(stderr, "Error: Rpc::poll: fifo not created/opened yet\n"); //fprintf(stderr, "Error: Rpc::poll: unix domain socket not created/opened yet\n");
return; return;
} }
std::string name; std::string name;
char line[1024]; while(::poll(polls, num_polls, 0) > 0) {
while(fgets(line, sizeof(line), file)) { for(int i = 0; i < num_polls; ++i) {
int line_len = strlen(line); if(polls[i].fd == socket_fd) {
if(line_len == 0) if(polls[i].revents & (POLLERR|POLLHUP)) {
continue; close(socket_fd);
socket_fd = 0;
return;
}
if(line[line_len - 1] == '\n') { const int client_fd = accept(socket_fd, NULL, NULL);
line[line_len - 1] = '\0'; if(num_polls >= GSR_RPC_MAX_POLLS) {
--line_len; if(errno != EWOULDBLOCK)
fprintf(stderr, "Error: Rpc::poll: unable to accept more clients, error: %s\n", strerror(errno));
} else {
polls[num_polls].fd = client_fd;
polls[num_polls].events = POLLIN;
polls[num_polls].revents = 0;
++num_polls;
}
continue;
}
if(polls[i].revents & POLLIN)
handle_client_data(polls[i].fd, polls_data[i]);
if(polls[i].revents & (POLLERR|POLLHUP)) {
close(polls[i].fd);
polls[i] = polls[num_polls - 1];
memcpy(polls_data[i].buffer, polls_data[num_polls - 1].buffer, polls_data[num_polls - 1].buffer_size);
polls_data[i].buffer_size = polls_data[num_polls - 1].buffer_size;
--num_polls;
--i;
}
} }
name = line;
auto it = handlers_by_name.find(name);
if(it != handlers_by_name.end())
it->second(name);
} }
} }
void Rpc::handle_client_data(int client_fd, PollData &poll_data) {
char *write_buffer = poll_data.buffer + poll_data.buffer_size;
const ssize_t num_bytes_read = read(client_fd, write_buffer, sizeof(poll_data.buffer) - poll_data.buffer_size);
if(num_bytes_read <= 0)
return;
poll_data.buffer_size += num_bytes_read;
const char *newline_p = (const char*)memchr(write_buffer, '\n', num_bytes_read);
if(!newline_p)
return;
const size_t command_size = newline_p - poll_data.buffer;
std::string name;
name.assign(poll_data.buffer, command_size);
memmove(poll_data.buffer, newline_p + 1, poll_data.buffer_size - (command_size + 1));
poll_data.buffer_size -= (command_size + 1);
auto it = handlers_by_name.find(name);
if(it != handlers_by_name.end())
it->second(name);
}
bool Rpc::add_handler(const std::string &name, RpcCallback callback) { bool Rpc::add_handler(const std::string &name, RpcCallback callback) {
return handlers_by_name.insert(std::make_pair(name, std::move(callback))).second; return handlers_by_name.insert(std::make_pair(name, std::move(callback))).second;
} }

View File

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

View File

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

View File

@@ -6,6 +6,7 @@
#include <mglpp/system/FloatRect.hpp> #include <mglpp/system/FloatRect.hpp>
#include <mglpp/system/Utf8.hpp> #include <mglpp/system/Utf8.hpp>
#include <optional> #include <optional>
#include <string.h>
namespace gsr { namespace gsr {
static const float padding_top_scale = 0.004629f; static const float padding_top_scale = 0.004629f;
@@ -22,8 +23,13 @@ namespace gsr {
} }
} }
Entry::Entry(mgl::Font *font, const char *text, float max_width) : text("", *font), max_width(max_width) { Entry::Entry(mgl::Font *font, const char *text, float max_width) :
text(std::u32string(), *font),
masked_text(std::u32string(), *font),
max_width(max_width)
{
this->text.set_color(get_color_theme().text_color); this->text.set_color(get_color_theme().text_color);
this->masked_text.set_color(get_color_theme().text_color);
set_text(text); set_text(text);
} }
@@ -31,6 +37,8 @@ namespace gsr {
if(!visible) if(!visible)
return true; return true;
mgl::Text32 &active_text = masked ? masked_text : text;
if(event.type == mgl::Event::MouseButtonPressed && event.mouse_button.button == mgl::Mouse::Left) { if(event.type == mgl::Event::MouseButtonPressed && event.mouse_button.button == mgl::Mouse::Left) {
const mgl::vec2f mouse_pos = { (float)event.mouse_button.x, (float)event.mouse_button.y }; const mgl::vec2f mouse_pos = { (float)event.mouse_button.x, (float)event.mouse_button.y };
selected = mgl::FloatRect(position + offset, get_size()).contains(mouse_pos); selected = mgl::FloatRect(position + offset, get_size()).contains(mouse_pos);
@@ -38,9 +46,8 @@ namespace gsr {
selecting_text = true; selecting_text = true;
const auto caret_index_mouse = find_closest_caret_index_by_position(mouse_pos); const auto caret_index_mouse = find_closest_caret_index_by_position(mouse_pos);
caret.byte_index = caret_index_mouse.byte_index; caret.index = caret_index_mouse.index;
caret.utf8_index = caret_index_mouse.utf8_index; caret.offset_x = caret_index_mouse.pos.x - active_text.get_position().x;
caret.offset_x = caret_index_mouse.pos.x - this->text.get_position().x;
selection_start_caret = caret; selection_start_caret = caret;
show_selection = true; show_selection = true;
} else { } else {
@@ -50,89 +57,72 @@ namespace gsr {
} }
} else if(event.type == mgl::Event::MouseButtonReleased && event.mouse_button.button == mgl::Mouse::Left) { } else if(event.type == mgl::Event::MouseButtonReleased && event.mouse_button.button == mgl::Mouse::Left) {
selecting_text = false; selecting_text = false;
if(caret.byte_index == selection_start_caret.byte_index) if(caret.index == selection_start_caret.index)
show_selection = false; show_selection = false;
} else if(event.type == mgl::Event::MouseMoved && selected) { } else if(event.type == mgl::Event::MouseMoved && selected) {
if(selecting_text) { if(selecting_text) {
const auto caret_index_mouse = find_closest_caret_index_by_position(mgl::vec2f(event.mouse_move.x, event.mouse_move.y)); const auto caret_index_mouse = find_closest_caret_index_by_position(mgl::vec2f(event.mouse_move.x, event.mouse_move.y));
caret.byte_index = caret_index_mouse.byte_index; caret.index = caret_index_mouse.index;
caret.utf8_index = caret_index_mouse.utf8_index; caret.offset_x = caret_index_mouse.pos.x - active_text.get_position().x;
caret.offset_x = caret_index_mouse.pos.x - this->text.get_position().x;
return false; return false;
} }
} else if(event.type == mgl::Event::KeyPressed && selected) { } else if(event.type == mgl::Event::KeyPressed && selected) {
int selection_start_byte = caret.byte_index; int selection_start_byte = caret.index;
int selection_end_byte = caret.byte_index; int selection_end_byte = caret.index;
if(show_selection) { if(show_selection) {
selection_start_byte = std::min(caret.byte_index, selection_start_caret.byte_index); selection_start_byte = std::min(caret.index, selection_start_caret.index);
selection_end_byte = std::max(caret.byte_index, selection_start_caret.byte_index); selection_end_byte = std::max(caret.index, selection_start_caret.index);
} }
if(event.key.code == mgl::Keyboard::Backspace) { if(event.key.code == mgl::Keyboard::Backspace) {
if(selection_start_byte == selection_end_byte && caret.byte_index > 0) if(selection_start_byte == selection_end_byte && caret.index > 0)
selection_start_byte = mgl::utf8_get_start_of_codepoint((const unsigned char*)text.get_string().c_str(), text.get_string().size(), caret.byte_index - 1); selection_start_byte -= 1;
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, ""); replace_text(selection_start_byte, selection_end_byte - selection_start_byte, std::u32string());
} else if(event.key.code == mgl::Keyboard::Delete) { } else if(event.key.code == mgl::Keyboard::Delete) {
if(selection_start_byte == selection_end_byte && caret.byte_index < (int)text.get_string().size()) { if(selection_start_byte == selection_end_byte && caret.index < (int)active_text.get_string().size())
size_t codepoint_length = 1; selection_end_byte += 1;
mgl::utf8_get_codepoint_length(((const unsigned char*)text.get_string().c_str())[caret.byte_index], &codepoint_length);
selection_end_byte = selection_start_byte + codepoint_length;
}
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, ""); replace_text(selection_start_byte, selection_end_byte - selection_start_byte, std::u32string());
} else if(event.key.code == mgl::Keyboard::C && event.key.control) { } else if(event.key.code == mgl::Keyboard::C && event.key.control) {
const size_t selection_num_bytes = selection_end_byte - selection_start_byte; const size_t selection_num_bytes = selection_end_byte - selection_start_byte;
if(selection_num_bytes > 0) if(selection_num_bytes > 0)
window.set_clipboard(text.get_string().substr(selection_start_byte, selection_num_bytes)); window.set_clipboard(mgl::utf32_to_utf8(text.get_string().substr(selection_start_byte, selection_num_bytes)));
} else if(event.key.code == mgl::Keyboard::V && event.key.control) { } else if(event.key.code == mgl::Keyboard::V && event.key.control) {
std::string clipboard_string = window.get_clipboard_string(); std::string clipboard_string = window.get_clipboard_string();
string_replace_all(clipboard_string, '\n', ' '); string_replace_all(clipboard_string, '\n', ' ');
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, std::move(clipboard_string)); replace_text(selection_start_byte, selection_end_byte - selection_start_byte, mgl::utf8_to_utf32(clipboard_string));
} else if(event.key.code == mgl::Keyboard::A && event.key.control) { } else if(event.key.code == mgl::Keyboard::A && event.key.control) {
selection_start_caret.byte_index = 0; selection_start_caret.index = 0;
selection_start_caret.utf8_index = 0;
selection_start_caret.offset_x = 0.0f; selection_start_caret.offset_x = 0.0f;
caret.byte_index = text.get_string().size(); caret.index = active_text.get_string().size();
caret.utf8_index = mgl::utf8_get_character_count((const unsigned char*)text.get_string().data(), text.get_string().size());
// TODO: Optimize // TODO: Optimize
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x; caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
show_selection = true; show_selection = true;
} else if(event.key.code == mgl::Keyboard::Left && caret.byte_index > 0) { } else if(event.key.code == mgl::Keyboard::Left) {
if(!selecting_with_keyboard && show_selection) { if(!selecting_with_keyboard && show_selection)
show_selection = false; show_selection = false;
} else { else
caret.byte_index = mgl::utf8_get_start_of_codepoint((const unsigned char*)text.get_string().data(), text.get_string().size(), caret.byte_index - 1); move_caret_word(Direction::LEFT, event.key.control ? 999999 : 1);
caret.utf8_index -= 1;
// TODO: Move left by one character instead of calculating every character to caret index
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
}
if(!selecting_with_keyboard) { if(!selecting_with_keyboard) {
selection_start_caret = caret; selection_start_caret = caret;
show_selection = false; show_selection = false;
} }
} else if(event.key.code == mgl::Keyboard::Right) { } else if(event.key.code == mgl::Keyboard::Right) {
if(!selecting_with_keyboard && show_selection) { if(!selecting_with_keyboard && show_selection)
show_selection = false; show_selection = false;
} else { else
const int caret_byte_index_before = caret.byte_index; move_caret_word(Direction::RIGHT, event.key.control ? 999999 : 1);
caret.byte_index = mgl::utf8_index_to_byte_index((const unsigned char*)text.get_string().data(), text.get_string().size(), caret.utf8_index + 1);
if(caret.byte_index != caret_byte_index_before)
caret.utf8_index += 1;
// TODO: Move right by one character instead of calculating every character to caret index
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
}
if(!selecting_with_keyboard) { if(!selecting_with_keyboard) {
selection_start_caret = caret; selection_start_caret = caret;
show_selection = false; show_selection = false;
} }
} else if(event.key.code == mgl::Keyboard::Home) { } else if(event.key.code == mgl::Keyboard::Home) {
caret.byte_index = 0; caret.index = 0;
caret.utf8_index = 0;
caret.offset_x = 0.0f; caret.offset_x = 0.0f;
if(!selecting_with_keyboard) { if(!selecting_with_keyboard) {
@@ -140,10 +130,9 @@ namespace gsr {
show_selection = false; show_selection = false;
} }
} else if(event.key.code == mgl::Keyboard::End) { } else if(event.key.code == mgl::Keyboard::End) {
caret.byte_index = text.get_string().size(); caret.index = active_text.get_string().size();
caret.utf8_index = mgl::utf8_get_character_count((const unsigned char*)text.get_string().data(), text.get_string().size());
// TODO: Optimize // TODO: Optimize
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x; caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
if(!selecting_with_keyboard) { if(!selecting_with_keyboard) {
selection_start_caret = caret; selection_start_caret = caret;
@@ -164,14 +153,14 @@ namespace gsr {
return false; return false;
} else if(event.type == mgl::Event::TextEntered && selected && event.text.codepoint >= 32 && event.text.codepoint != 127) { } else if(event.type == mgl::Event::TextEntered && selected && event.text.codepoint >= 32 && event.text.codepoint != 127) {
int selection_start_byte = caret.byte_index; int selection_start_byte = caret.index;
int selection_end_byte = caret.byte_index; int selection_end_byte = caret.index;
if(show_selection) { if(show_selection) {
selection_start_byte = std::min(caret.byte_index, selection_start_caret.byte_index); selection_start_byte = std::min(caret.index, selection_start_caret.index);
selection_end_byte = std::max(caret.byte_index, selection_start_caret.byte_index); selection_end_byte = std::max(caret.index, selection_start_caret.index);
} }
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, std::string(event.text.str, event.text.size)); replace_text(selection_start_byte, selection_end_byte - selection_start_byte, mgl::utf8_to_utf32((const unsigned char*)event.text.str, event.text.size));
return false; return false;
} }
@@ -189,13 +178,15 @@ namespace gsr {
const int padding_left = padding_left_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 int padding_right = padding_right_scale * get_theme().window_height;
mgl::Text32 &active_text = masked ? masked_text : text;
background.set_size(get_size()); background.set_size(get_size());
background.set_position(draw_pos.floor()); background.set_position(draw_pos.floor());
background.set_color(selected ? mgl::Color(0, 0, 0, 255) : mgl::Color(0, 0, 0, 120)); background.set_color(selected ? mgl::Color(0, 0, 0, 255) : mgl::Color(0, 0, 0, 120));
window.draw(background); window.draw(background);
const int caret_width = std::max(1.0f, caret_width_scale * get_theme().window_height); const int caret_width = std::max(1.0f, caret_width_scale * get_theme().window_height);
const mgl::vec2f caret_size = mgl::vec2f(caret_width, text.get_bounds().size.y).floor(); const mgl::vec2f caret_size = mgl::vec2f(caret_width, active_text.get_bounds().size.y).floor();
const float overflow_left = (caret.offset_x + padding_left) - (padding_left + text_overflow); const float overflow_left = (caret.offset_x + padding_left) - (padding_left + text_overflow);
if(overflow_left < 0.0f) if(overflow_left < 0.0f)
@@ -205,18 +196,18 @@ namespace gsr {
if(overflow_right - text_overflow > 0.0f) if(overflow_right - text_overflow > 0.0f)
text_overflow = overflow_right; text_overflow = overflow_right;
text.set_position((draw_pos + mgl::vec2f(padding_left, get_size().y * 0.5f - text.get_bounds().size.y * 0.5f) - mgl::vec2f(text_overflow, 0.0f)).floor()); active_text.set_position((draw_pos + mgl::vec2f(padding_left, get_size().y * 0.5f - active_text.get_bounds().size.y * 0.5f) - mgl::vec2f(text_overflow, 0.0f)).floor());
const auto text_bounds = text.get_bounds(); const auto text_bounds = active_text.get_bounds();
const bool text_larger_than_background = text_bounds.size.x > (background.get_size().x - padding_left - padding_right); const bool text_larger_than_background = text_bounds.size.x > (background.get_size().x - padding_left - padding_right);
const float text_overflow_right = (text_bounds.position.x + text_bounds.size.x) - (background.get_position().x + background.get_size().x - padding_right); const float text_overflow_right = (text_bounds.position.x + text_bounds.size.x) - (background.get_position().x + background.get_size().x - padding_right);
if(text_larger_than_background) { if(text_larger_than_background) {
if(text_overflow_right < 0.0f) { if(text_overflow_right < 0.0f) {
text_overflow += text_overflow_right; text_overflow += text_overflow_right;
text.set_position(text.get_position() + mgl::vec2f(-text_overflow_right, 0.0f)); active_text.set_position(active_text.get_position() + mgl::vec2f(-text_overflow_right, 0.0f));
} }
} else { } else {
text.set_position(text.get_position() + mgl::vec2f(-text_overflow, 0.0f)); active_text.set_position(active_text.get_position() + mgl::vec2f(-text_overflow, 0.0f));
text_overflow = 0.0f; text_overflow = 0.0f;
} }
@@ -234,7 +225,7 @@ namespace gsr {
}); });
window.set_scissor(scissor); window.set_scissor(scissor);
window.draw(text); window.draw(active_text);
if(show_selection) if(show_selection)
draw_caret_selection(window, draw_pos, caret_size); draw_caret_selection(window, draw_pos, caret_size);
@@ -254,11 +245,16 @@ namespace gsr {
} }
void Entry::draw_caret_selection(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size) { void Entry::draw_caret_selection(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size) {
if(selection_start_caret.index == caret.index)
return;
const int padding_top = padding_top_scale * get_theme().window_height; const int padding_top = padding_top_scale * get_theme().window_height;
const int padding_left = padding_left_scale * get_theme().window_height; const int padding_left = padding_left_scale * get_theme().window_height;
const int caret_width = std::max(1.0f, caret_width_scale * get_theme().window_height);
const int offset = caret.index < selection_start_caret.index ? caret_width : 0;
mgl::Rectangle caret_selection_rect(mgl::vec2f(std::abs(selection_start_caret.offset_x - caret.offset_x), caret_size.y).floor()); mgl::Rectangle caret_selection_rect(mgl::vec2f(std::abs(selection_start_caret.offset_x - caret.offset_x) - offset, caret_size.y).floor());
caret_selection_rect.set_position((draw_pos + mgl::vec2f(padding_left + std::min(caret.offset_x, selection_start_caret.offset_x) - text_overflow, padding_top)).floor()); caret_selection_rect.set_position((draw_pos + mgl::vec2f(padding_left + std::min(caret.offset_x, selection_start_caret.offset_x) - text_overflow + offset, padding_top)).floor());
mgl::Color caret_select_color = get_color_theme().tint_color; mgl::Color caret_select_color = get_color_theme().tint_color;
caret_select_color.a = 100; caret_select_color.a = 100;
caret_selection_rect.set_color(caret_select_color); caret_selection_rect.set_color(caret_select_color);
@@ -274,13 +270,43 @@ namespace gsr {
return { max_width, text.get_bounds().size.y + padding_top + padding_bottom }; return { max_width, text.get_bounds().size.y + padding_top + padding_bottom };
} }
EntryValidateHandlerResult Entry::set_text(std::string str) { void Entry::move_caret_word(Direction direction, size_t max_codepoints) {
EntryValidateHandlerResult validate_result = set_text_internal(std::move(str)); mgl::Text32 &active_text = masked ? masked_text : text;
const int dir_step = direction == Direction::LEFT ? -1 : 1;
const int num_delimiter_chars = 15;
const char delimiter_chars[num_delimiter_chars + 1] = " \t\n/.,:;\\[](){}";
const char32_t *text_str = active_text.get_string().data();
int num_non_delimiter_chars_found = 0;
for(size_t i = 0; i < max_codepoints; ++i) {
const uint32_t codepoint = text_str[caret.index];
const bool is_delimiter_char = codepoint < 127 && !!memchr(delimiter_chars, codepoint, num_delimiter_chars);
if(is_delimiter_char) {
if(num_non_delimiter_chars_found > 0)
break;
} else {
++num_non_delimiter_chars_found;
}
if(caret.index + dir_step < 0 || caret.index + dir_step > (int)active_text.get_string().size())
break;
caret.index += dir_step;
}
// TODO: Move right by some characters instead of calculating every character to caret index
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
}
EntryValidateHandlerResult Entry::set_text(const std::string &str) {
EntryValidateHandlerResult validate_result = set_text_internal(mgl::utf8_to_utf32(str));
if(validate_result == EntryValidateHandlerResult::ALLOW) { if(validate_result == EntryValidateHandlerResult::ALLOW) {
caret.byte_index = text.get_string().size(); mgl::Text32 &active_text = masked ? masked_text : text;
caret.utf8_index = mgl::utf8_get_character_count((const unsigned char*)text.get_string().data(), text.get_string().size()); caret.index = active_text.get_string().size();
// TODO: Optimize // TODO: Optimize
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x; caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
selection_start_caret = caret; selection_start_caret = caret;
selecting_text = false; selecting_text = false;
@@ -290,40 +316,59 @@ namespace gsr {
return validate_result; return validate_result;
} }
EntryValidateHandlerResult Entry::set_text_internal(std::string str) { EntryValidateHandlerResult Entry::set_text_internal(std::u32string str) {
EntryValidateHandlerResult validate_result = EntryValidateHandlerResult::ALLOW; EntryValidateHandlerResult validate_result = EntryValidateHandlerResult::ALLOW;
if(validate_handler) if(validate_handler)
validate_result = validate_handler(*this, str); validate_result = validate_handler(*this, str);
if(validate_result == EntryValidateHandlerResult::ALLOW) { if(validate_result == EntryValidateHandlerResult::ALLOW) {
text.set_string(std::move(str)); text.set_string(std::move(str));
if(masked)
masked_text.set_string(std::u32string(text.get_string().size(), '*'));
// TODO: Call callback with utf32 instead?
if(on_changed) if(on_changed)
on_changed(text.get_string()); on_changed(mgl::utf32_to_utf8(text.get_string()));
} }
return validate_result; return validate_result;
} }
const std::string& Entry::get_text() const { std::string Entry::get_text() const {
return text.get_string(); return mgl::utf32_to_utf8(text.get_string());
} }
void Entry::replace_text(size_t index, size_t size, const std::string &replacement) { void Entry::set_masked(bool masked) {
if(masked == this->masked)
return;
this->masked = masked;
if(masked)
masked_text.set_string(std::u32string(text.get_string().size(), '*'));
else
masked_text.set_string(std::u32string());
mgl::Text32 &active_text = masked ? masked_text : text;
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
selection_start_caret.offset_x = active_text.find_character_pos(selection_start_caret.index).x - active_text.get_position().x;
}
bool Entry::is_masked() const {
return masked;
}
void Entry::replace_text(size_t index, size_t size, const std::u32string &replacement) {
if(index + size > text.get_string().size()) if(index + size > text.get_string().size())
return; return;
const auto prev_caret = caret; const auto prev_caret = caret;
if((int)index >= caret.byte_index) { if((int)index >= caret.index)
caret.utf8_index += mgl::utf8_get_character_count((const unsigned char*)replacement.c_str(), replacement.size()); caret.index += replacement.size();
caret.byte_index += replacement.size(); else
} else { caret.index = caret.index - size + replacement.size();
caret.utf8_index -= mgl::utf8_get_character_count((const unsigned char*)(text.get_string().c_str() + caret.byte_index - size), size);
caret.utf8_index += mgl::utf8_get_character_count((const unsigned char*)replacement.c_str(), replacement.size());
caret.byte_index = caret.byte_index - size + replacement.size();
}
std::string str = text.get_string(); std::u32string str = text.get_string();
str.replace(index, size, replacement); str.replace(index, size, replacement);
const EntryValidateHandlerResult validate_result = set_text_internal(std::move(str)); const EntryValidateHandlerResult validate_result = set_text_internal(std::move(str));
if(validate_result == EntryValidateHandlerResult::DENY) { if(validate_result == EntryValidateHandlerResult::DENY) {
@@ -333,8 +378,9 @@ namespace gsr {
return; return;
} }
mgl::Text32 &active_text = masked ? masked_text : text;
// TODO: Optimize // TODO: Optimize
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x; caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
selection_start_caret = caret; selection_start_caret = caret;
selecting_text = false; selecting_text = false;
@@ -342,17 +388,14 @@ namespace gsr {
show_selection = false; show_selection = false;
} }
mgl_index_codepoint_pair Entry::find_closest_caret_index_by_position(mgl::vec2f position) { CaretIndexPos Entry::find_closest_caret_index_by_position(mgl::vec2f position) {
const std::string &str = text.get_string(); mgl::Text32 &active_text = masked ? masked_text : text;
mgl::Font *font = text.get_font(); const std::u32string &str = active_text.get_string();
mgl::Font *font = active_text.get_font();
mgl_index_codepoint_pair result = {0, 0, {text.get_position().x, text.get_position().y}}; CaretIndexPos result = {0, {active_text.get_position().x, active_text.get_position().y}};
for(result.index = 0; result.index < (int)str.size(); ++result.index) {
for(; result.byte_index < str.size();) { const uint32_t codepoint = str[result.index];
uint32_t codepoint = ' ';
size_t clen = 1;
if(!mgl::utf8_decode((const unsigned char*)&str[result.byte_index], str.size() - result.byte_index, &codepoint, &clen))
clen = 1;
float glyph_width = 0.0f; float glyph_width = 0.0f;
if(codepoint == '\t') { if(codepoint == '\t') {
@@ -368,8 +411,6 @@ namespace gsr {
break; break;
result.pos.x += glyph_width; result.pos.x += glyph_width;
result.byte_index += clen;
result.utf8_index += 1;
} }
return result; return result;
@@ -379,7 +420,7 @@ namespace gsr {
return c >= '0' && c <= '9'; return c >= '0' && c <= '9';
} }
static std::optional<int> to_integer(const std::string &str) { static std::optional<int> to_integer(const std::u32string &str) {
if(str.empty()) if(str.empty())
return std::nullopt; return std::nullopt;
@@ -406,7 +447,7 @@ namespace gsr {
} }
EntryValidateHandler create_entry_validator_integer_in_range(int min, int max) { EntryValidateHandler create_entry_validator_integer_in_range(int min, int max) {
return [min, max](Entry &entry, const std::string &str) { return [min, max](Entry &entry, const std::u32string &str) {
if(str.empty()) if(str.empty())
return EntryValidateHandlerResult::ALLOW; return EntryValidateHandlerResult::ALLOW;

View File

@@ -467,13 +467,42 @@ namespace gsr {
return exit_program_button; return exit_program_button;
} }
std::unique_ptr<List> GlobalSettingsPage::create_notification_speed() {
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Notification speed", get_color_theme().text_color));
auto radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL);
notification_speed_button_ptr = radio_button.get();
radio_button->add_item("Normal", "normal");
radio_button->add_item("Fast", "fast");
radio_button->on_selection_changed = [this](const std::string&, const std::string &id) {
if(id == "normal")
overlay->set_notification_speed(NotificationSpeed::NORMAL);
else if(id == "fast")
overlay->set_notification_speed(NotificationSpeed::FAST);
return true;
};
list->add_widget(std::move(radio_button));
return list;
}
std::unique_ptr<Subsection> GlobalSettingsPage::create_application_options_subsection(ScrollablePage *parent_page) { std::unique_ptr<Subsection> GlobalSettingsPage::create_application_options_subsection(ScrollablePage *parent_page) {
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
List *list_ptr = list.get();
auto subsection = std::make_unique<Subsection>("Application options", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
list_ptr->add_widget(create_notification_speed());
list_ptr->add_widget(std::make_unique<LineSeparator>(LineSeparator::Orientation::HORIZONTAL, subsection->get_inner_size().x));
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL; const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL); auto navigate_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
list->add_widget(create_exit_program_button()); navigate_list->add_widget(create_exit_program_button());
if(inside_flatpak) if(inside_flatpak)
list->add_widget(create_go_back_to_old_ui_button()); navigate_list->add_widget(create_go_back_to_old_ui_button());
return std::make_unique<Subsection>("Application options", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f)); list_ptr->add_widget(std::move(navigate_list));
return subsection;
} }
std::unique_ptr<Subsection> GlobalSettingsPage::create_application_info_subsection(ScrollablePage *parent_page) { std::unique_ptr<Subsection> GlobalSettingsPage::create_application_info_subsection(ScrollablePage *parent_page) {
@@ -499,6 +528,22 @@ namespace gsr {
return std::make_unique<Subsection>("Application info", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f)); return std::make_unique<Subsection>("Application info", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
} }
std::unique_ptr<Subsection> GlobalSettingsPage::create_donate_subsection(ScrollablePage *parent_page) {
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "If you would like to donate you can do so by donating at https://buymeacoffee.com/dec05eba:", get_color_theme().text_color));
auto donate_button = std::make_unique<Button>(&get_theme().body_font, "Donate", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
donate_button->on_click = [this] {
const char *args[] = { "xdg-open", "https://buymeacoffee.com/dec05eba", nullptr };
exec_program_daemonized(args);
overlay->hide_next_frame();
};
list->add_widget(std::move(donate_button));
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "All donations go toward developing software (including GPU Screen Recorder)\nand buying hardware to test the software.", get_color_theme().text_color));
return std::make_unique<Subsection>("Donate", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
}
void GlobalSettingsPage::add_widgets() { void GlobalSettingsPage::add_widgets() {
auto scrollable_page = std::make_unique<ScrollablePage>(content_page_ptr->get_inner_size()); auto scrollable_page = std::make_unique<ScrollablePage>(content_page_ptr->get_inner_size());
@@ -510,6 +555,7 @@ namespace gsr {
settings_list->add_widget(create_controller_hotkey_subsection(scrollable_page.get())); settings_list->add_widget(create_controller_hotkey_subsection(scrollable_page.get()));
settings_list->add_widget(create_application_options_subsection(scrollable_page.get())); settings_list->add_widget(create_application_options_subsection(scrollable_page.get()));
settings_list->add_widget(create_application_info_subsection(scrollable_page.get())); settings_list->add_widget(create_application_info_subsection(scrollable_page.get()));
settings_list->add_widget(create_donate_subsection(scrollable_page.get()));
scrollable_page->add_widget(std::move(settings_list)); scrollable_page->add_widget(std::move(settings_list));
content_page_ptr->add_widget(std::move(scrollable_page)); content_page_ptr->add_widget(std::move(scrollable_page));
@@ -535,6 +581,8 @@ namespace gsr {
enable_keyboard_hotkeys_radio_button_ptr->set_selected_item(config.main_config.hotkeys_enable_option, false, false); enable_keyboard_hotkeys_radio_button_ptr->set_selected_item(config.main_config.hotkeys_enable_option, false, false);
enable_joystick_hotkeys_radio_button_ptr->set_selected_item(config.main_config.joystick_hotkeys_enable_option, false, false); enable_joystick_hotkeys_radio_button_ptr->set_selected_item(config.main_config.joystick_hotkeys_enable_option, false, false);
notification_speed_button_ptr->set_selected_item(config.main_config.notification_speed);
load_hotkeys(); load_hotkeys();
} }
@@ -561,6 +609,7 @@ namespace gsr {
config.main_config.tint_color = tint_color_radio_button_ptr->get_selected_id(); config.main_config.tint_color = tint_color_radio_button_ptr->get_selected_id();
config.main_config.hotkeys_enable_option = enable_keyboard_hotkeys_radio_button_ptr->get_selected_id(); config.main_config.hotkeys_enable_option = enable_keyboard_hotkeys_radio_button_ptr->get_selected_id();
config.main_config.joystick_hotkeys_enable_option = enable_joystick_hotkeys_radio_button_ptr->get_selected_id(); config.main_config.joystick_hotkeys_enable_option = enable_joystick_hotkeys_radio_button_ptr->get_selected_id();
config.main_config.notification_speed = notification_speed_button_ptr->get_selected_id();
save_config(config); save_config(config);
} }

View File

@@ -40,7 +40,7 @@ namespace gsr {
if(capture_options.region) if(capture_options.region)
record_area_box->add_item("Region", "region"); record_area_box->add_item("Region", "region");
if(!capture_options.monitors.empty()) if(!capture_options.monitors.empty())
record_area_box->add_item(gsr_info->system_info.display_server == DisplayServer::WAYLAND ? "Focused monitor (Experimental on Wayland)" : "Focused monitor", "focused_monitor"); record_area_box->add_item("Focused monitor", "focused_monitor");
for(const auto &monitor : capture_options.monitors) { for(const auto &monitor : capture_options.monitors) {
char name[256]; char name[256];
snprintf(name, sizeof(name), "Monitor %s (%dx%d)", monitor.name.c_str(), monitor.size.x, monitor.size.y); snprintf(name, sizeof(name), "Monitor %s (%dx%d)", monitor.name.c_str(), monitor.size.x, monitor.size.y);
@@ -215,8 +215,17 @@ namespace gsr {
return checkbox; return checkbox;
} }
std::unique_ptr<CheckBox> ScreenshotSettingsPage::create_save_screenshot_to_clipboard() {
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Save screenshot to clipboard");
save_screenshot_to_clipboard_checkbox_ptr = checkbox.get();
return checkbox;
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_general_section() { std::unique_ptr<Widget> ScreenshotSettingsPage::create_general_section() {
return std::make_unique<Subsection>("General", create_save_screenshot_in_game_folder(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); auto list = std::make_unique<List>(List::Orientation::VERTICAL);
list->add_widget(create_save_screenshot_in_game_folder());
list->add_widget(create_save_screenshot_to_clipboard());
return std::make_unique<Subsection>("General", std::move(list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
} }
std::unique_ptr<Widget> ScreenshotSettingsPage::create_notifications_section() { std::unique_ptr<Widget> ScreenshotSettingsPage::create_notifications_section() {
@@ -281,6 +290,7 @@ namespace gsr {
restore_portal_session_checkbox_ptr->set_checked(config.screenshot_config.restore_portal_session); 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_directory_button_ptr->set_text(config.screenshot_config.save_directory);
save_screenshot_in_game_folder_checkbox_ptr->set_checked(config.screenshot_config.save_screenshot_in_game_folder); save_screenshot_in_game_folder_checkbox_ptr->set_checked(config.screenshot_config.save_screenshot_in_game_folder);
save_screenshot_to_clipboard_checkbox_ptr->set_checked(config.screenshot_config.save_screenshot_to_clipboard);
show_screenshot_saved_notification_checkbox_ptr->set_checked(config.screenshot_config.show_screenshot_saved_notifications); show_screenshot_saved_notification_checkbox_ptr->set_checked(config.screenshot_config.show_screenshot_saved_notifications);
if(config.screenshot_config.image_width == 0) if(config.screenshot_config.image_width == 0)
@@ -309,6 +319,7 @@ namespace gsr {
config.screenshot_config.restore_portal_session = restore_portal_session_checkbox_ptr->is_checked(); config.screenshot_config.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_directory = save_directory_button_ptr->get_text();
config.screenshot_config.save_screenshot_in_game_folder = save_screenshot_in_game_folder_checkbox_ptr->is_checked(); config.screenshot_config.save_screenshot_in_game_folder = save_screenshot_in_game_folder_checkbox_ptr->is_checked();
config.screenshot_config.save_screenshot_to_clipboard = save_screenshot_to_clipboard_checkbox_ptr->is_checked();
config.screenshot_config.show_screenshot_saved_notifications = show_screenshot_saved_notification_checkbox_ptr->is_checked(); config.screenshot_config.show_screenshot_saved_notifications = show_screenshot_saved_notification_checkbox_ptr->is_checked();
if(config.screenshot_config.image_width == 0) if(config.screenshot_config.image_width == 0)

View File

@@ -4,6 +4,7 @@
#include "../../include/gui/PageStack.hpp" #include "../../include/gui/PageStack.hpp"
#include "../../include/gui/FileChooser.hpp" #include "../../include/gui/FileChooser.hpp"
#include "../../include/gui/Subsection.hpp" #include "../../include/gui/Subsection.hpp"
#include "../../include/gui/Image.hpp"
#include "../../include/Theme.hpp" #include "../../include/Theme.hpp"
#include "../../include/GsrInfo.hpp" #include "../../include/GsrInfo.hpp"
#include "../../include/Utils.hpp" #include "../../include/Utils.hpp"
@@ -72,7 +73,7 @@ namespace gsr {
if(capture_options.region) if(capture_options.region)
record_area_box->add_item("Region", "region"); record_area_box->add_item("Region", "region");
if(!capture_options.monitors.empty()) if(!capture_options.monitors.empty())
record_area_box->add_item(gsr_info->system_info.display_server == DisplayServer::WAYLAND ? "Focused monitor (Experimental on Wayland)" : "Focused monitor", "focused_monitor"); record_area_box->add_item("Focused monitor", "focused_monitor");
for(const auto &monitor : capture_options.monitors) { for(const auto &monitor : capture_options.monitors) {
char name[256]; char name[256];
snprintf(name, sizeof(name), "Monitor %s (%dx%d)", monitor.name.c_str(), monitor.size.x, monitor.size.y); snprintf(name, sizeof(name), "Monitor %s (%dx%d)", monitor.name.c_str(), monitor.size.x, monitor.size.y);
@@ -567,6 +568,10 @@ namespace gsr {
framerate_mode_box->add_item("Auto (Recommended)", "auto"); framerate_mode_box->add_item("Auto (Recommended)", "auto");
framerate_mode_box->add_item("Constant", "cfr"); framerate_mode_box->add_item("Constant", "cfr");
framerate_mode_box->add_item("Variable", "vfr"); framerate_mode_box->add_item("Variable", "vfr");
if(gsr_info->system_info.display_server == DisplayServer::X11)
framerate_mode_box->add_item("Sync to content", "content");
else
framerate_mode_box->add_item("Sync to content (Only X11 or desktop portal capture)", "content");
framerate_mode_box_ptr = framerate_mode_box.get(); framerate_mode_box_ptr = framerate_mode_box.get();
return framerate_mode_box; return framerate_mode_box;
} }
@@ -965,42 +970,75 @@ namespace gsr {
return streaming_service_list; return streaming_service_list;
} }
static std::unique_ptr<Button> create_mask_toggle_button(Entry *entry_to_toggle, mgl::vec2f size) {
auto button = std::make_unique<Button>(&get_theme().body_font, "", size, mgl::Color(0, 0, 0, 0));
Button *button_ptr = button.get();
button->set_icon(&get_theme().masked_texture);
button->on_click = [entry_to_toggle, button_ptr]() {
const bool is_masked = entry_to_toggle->is_masked();
button_ptr->set_icon(is_masked ? &get_theme().unmasked_texture : &get_theme().masked_texture);
entry_to_toggle->set_masked(!is_masked);
};
return button;
}
static Entry* add_stream_key_entry_to_list(List *stream_key_list) {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
auto key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
key_entry->set_masked(true);
Entry *key_entry_ptr = key_entry.get();
const float mask_icon_size = key_entry_ptr->get_size().y * 0.9f;
list->add_widget(std::move(key_entry));
list->add_widget(create_mask_toggle_button(key_entry_ptr, mgl::vec2f(mask_icon_size, mask_icon_size)));
stream_key_list->add_widget(std::move(list));
return key_entry_ptr;
}
std::unique_ptr<List> SettingsPage::create_stream_key_section() { std::unique_ptr<List> SettingsPage::create_stream_key_section() {
auto stream_key_list = std::make_unique<List>(List::Orientation::VERTICAL); auto stream_key_list = std::make_unique<List>(List::Orientation::VERTICAL);
stream_key_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream key:", get_color_theme().text_color)); stream_key_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream key:", get_color_theme().text_color));
auto twitch_stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20); twitch_stream_key_entry_ptr = add_stream_key_entry_to_list(stream_key_list.get());
twitch_stream_key_entry_ptr = twitch_stream_key_entry.get(); youtube_stream_key_entry_ptr = add_stream_key_entry_to_list(stream_key_list.get());
stream_key_list->add_widget(std::move(twitch_stream_key_entry)); rumble_stream_key_entry_ptr = add_stream_key_entry_to_list(stream_key_list.get());
auto youtube_stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
youtube_stream_key_entry_ptr = youtube_stream_key_entry.get();
stream_key_list->add_widget(std::move(youtube_stream_key_entry));
auto rumble_stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
rumble_stream_key_entry_ptr = rumble_stream_key_entry.get();
stream_key_list->add_widget(std::move(rumble_stream_key_entry));
stream_key_list_ptr = stream_key_list.get(); stream_key_list_ptr = stream_key_list.get();
return stream_key_list; return stream_key_list;
} }
std::unique_ptr<List> SettingsPage::create_stream_custom_section() { std::unique_ptr<List> SettingsPage::create_stream_custom_url() {
auto stream_url_list = std::make_unique<List>(List::Orientation::VERTICAL); auto list = std::make_unique<List>(List::Orientation::VERTICAL);
stream_url_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream URL:", get_color_theme().text_color));
auto stream_url_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20); auto stream_url_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
stream_url_entry_ptr = stream_url_entry.get(); stream_url_entry_ptr = stream_url_entry.get();
stream_url_list->add_widget(std::move(stream_url_entry)); list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream URL:", get_color_theme().text_color));
list->add_widget(std::move(stream_url_entry));
stream_url_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream key:", get_color_theme().text_color)); return list;
}
std::unique_ptr<List> SettingsPage::create_stream_custom_key() {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
auto stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20); auto stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
stream_key_entry->set_masked(true);
stream_key_entry_ptr = stream_key_entry.get(); stream_key_entry_ptr = stream_key_entry.get();
stream_url_list->add_widget(std::move(stream_key_entry)); const float mask_icon_size = stream_key_entry_ptr->get_size().y * 0.9f;
list->add_widget(std::move(stream_key_entry));
list->add_widget(create_mask_toggle_button(stream_key_entry_ptr, mgl::vec2f(mask_icon_size, mask_icon_size)));
return list;
}
stream_url_list_ptr = stream_url_list.get(); std::unique_ptr<List> SettingsPage::create_stream_custom_section() {
return stream_url_list; auto custom_stream_list = std::make_unique<List>(List::Orientation::VERTICAL);
auto stream_url_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
stream_url_list->add_widget(create_stream_custom_url());
stream_url_list->add_widget(create_stream_container());
custom_stream_list->add_widget(std::move(stream_url_list));
custom_stream_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream key:", get_color_theme().text_color));
custom_stream_list->add_widget(create_stream_custom_key());
custom_stream_list_ptr = custom_stream_list.get();
return custom_stream_list;
} }
std::unique_ptr<ComboBox> SettingsPage::create_stream_container_box() { std::unique_ptr<ComboBox> SettingsPage::create_stream_container_box() {
@@ -1013,11 +1051,10 @@ namespace gsr {
return container_box; return container_box;
} }
std::unique_ptr<List> SettingsPage::create_stream_container_section() { std::unique_ptr<List> SettingsPage::create_stream_container() {
auto container_list = std::make_unique<List>(List::Orientation::VERTICAL); auto container_list = std::make_unique<List>(List::Orientation::VERTICAL);
container_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Container:", get_color_theme().text_color)); container_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Container:", get_color_theme().text_color));
container_list->add_widget(create_stream_container_box()); container_list->add_widget(create_stream_container_box());
container_list_ptr = container_list.get();
return container_list; return container_list;
} }
@@ -1026,7 +1063,6 @@ namespace gsr {
streaming_info_list->add_widget(create_streaming_service_section()); streaming_info_list->add_widget(create_streaming_service_section());
streaming_info_list->add_widget(create_stream_key_section()); streaming_info_list->add_widget(create_stream_key_section());
streaming_info_list->add_widget(create_stream_custom_section()); streaming_info_list->add_widget(create_stream_custom_section());
streaming_info_list->add_widget(create_stream_container_section());
settings_list_ptr->add_widget(std::make_unique<Subsection>("Streaming info", std::move(streaming_info_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f))); settings_list_ptr->add_widget(std::make_unique<Subsection>("Streaming info", std::move(streaming_info_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL); auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL);
@@ -1051,11 +1087,10 @@ namespace gsr {
const bool rumble_option = id == "rumble"; const bool rumble_option = id == "rumble";
const bool custom_option = id == "custom"; const bool custom_option = id == "custom";
stream_key_list_ptr->set_visible(!custom_option); stream_key_list_ptr->set_visible(!custom_option);
stream_url_list_ptr->set_visible(custom_option); custom_stream_list_ptr->set_visible(custom_option);
container_list_ptr->set_visible(custom_option); twitch_stream_key_entry_ptr->get_parent_widget()->set_visible(twitch_option);
twitch_stream_key_entry_ptr->set_visible(twitch_option); youtube_stream_key_entry_ptr->get_parent_widget()->set_visible(youtube_option);
youtube_stream_key_entry_ptr->set_visible(youtube_option); rumble_stream_key_entry_ptr->get_parent_widget()->set_visible(rumble_option);
rumble_stream_key_entry_ptr->set_visible(rumble_option);
return true; return true;
}; };
streaming_service_box_ptr->on_selection_changed("Twitch", "twitch"); streaming_service_box_ptr->on_selection_changed("Twitch", "twitch");

View File

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

View File

@@ -102,24 +102,6 @@ static void rpc_add_commands(gsr::Rpc *rpc, gsr::Overlay *overlay) {
}); });
} }
static bool is_gsr_ui_virtual_keyboard_running() {
FILE *f = fopen("/proc/bus/input/devices", "rb");
if(!f)
return false;
bool virtual_keyboard_running = false;
char line[1024];
while(fgets(line, sizeof(line), f)) {
if(strstr(line, "gsr-ui virtual keyboard")) {
virtual_keyboard_running = true;
break;
}
}
fclose(f);
return virtual_keyboard_running;
}
static void install_flatpak_systemd_service() { static void install_flatpak_systemd_service() {
const bool systemd_service_exists = system( const bool systemd_service_exists = system(
"data_home=$(flatpak-spawn --host -- /bin/sh -c 'echo \"${XDG_DATA_HOME:-$HOME/.local/share}\"') && " "data_home=$(flatpak-spawn --host -- /bin/sh -c 'echo \"${XDG_DATA_HOME:-$HOME/.local/share}\"') && "
@@ -195,7 +177,9 @@ enum class LaunchAction {
int main(int argc, char **argv) { int main(int argc, char **argv) {
setlocale(LC_ALL, "C"); // Sigh... stupid C setlocale(LC_ALL, "C"); // Sigh... stupid C
#ifdef __GLIBC__
mallopt(M_MMAP_THRESHOLD, 65536); mallopt(M_MMAP_THRESHOLD, 65536);
#endif
if(geteuid() == 0) { if(geteuid() == 0) {
fprintf(stderr, "Error: don't run gsr-ui as the root user\n"); fprintf(stderr, "Error: don't run gsr-ui as the root user\n");
@@ -224,16 +208,13 @@ int main(int argc, char **argv) {
set_display_server_environment_variables(); set_display_server_environment_variables();
auto rpc = std::make_unique<gsr::Rpc>(); auto rpc = std::make_unique<gsr::Rpc>();
const bool rpc_created = rpc->create("gsr-ui"); const gsr::RpcOpenResult rpc_open_result = rpc->open("gsr-ui");
if(!rpc_created)
fprintf(stderr, "Error: Failed to create rpc\n");
if(is_gsr_ui_virtual_keyboard_running() || !rpc_created) { if(rpc_open_result == gsr::RpcOpenResult::OK) {
if(launch_action == LaunchAction::LAUNCH_DAEMON) if(launch_action == LaunchAction::LAUNCH_DAEMON)
return 1; return 1;
rpc = std::make_unique<gsr::Rpc>(); if(rpc->write("show_ui\n", 8)) {
if(rpc->open("gsr-ui") && rpc->write("show_ui\n", 8)) {
fprintf(stderr, "Error: another instance of gsr-ui is already running, opening that one instead\n"); fprintf(stderr, "Error: another instance of gsr-ui is already running, opening that one instead\n");
} else { } else {
fprintf(stderr, "Error: failed to send command to running gsr-ui instance, user will have to open the UI manually with Alt+Z\n"); fprintf(stderr, "Error: failed to send command to running gsr-ui instance, user will have to open the UI manually with Alt+Z\n");
@@ -243,6 +224,9 @@ int main(int argc, char **argv) {
return 1; return 1;
} }
if(!rpc->create("gsr-ui"))
fprintf(stderr, "Error: Failed to create rpc\n");
if(gsr::pidof("gpu-screen-recorder", -1) != -1) { if(gsr::pidof("gpu-screen-recorder", -1) != -1) {
const char *args[] = { "gsr-notify", "--text", "GPU Screen Recorder is already running in another process.\nPlease close it before using GPU Screen Recorder UI.", "--timeout", "5.0", "--icon-color", "ff0000", "--bg-color", "ff0000", nullptr }; const char *args[] = { "gsr-notify", "--text", "GPU Screen Recorder is already running in another process.\nPlease close it before using GPU Screen Recorder UI.", "--timeout", "5.0", "--icon-color", "ff0000", "--bg-color", "ff0000", nullptr };
gsr::exec_program_daemonized(args); gsr::exec_program_daemonized(args);

View File

@@ -234,14 +234,23 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
/* Retarded linux takes very long time to close /dev/input/eventN files, even though they are virtual and opened read-only */ /* Retarded linux takes very long time to close /dev/input/eventN files, even though they are virtual and opened read-only */
static void* keyboard_event_close_fds_callback(void *userdata) { static void* keyboard_event_close_fds_callback(void *userdata) {
keyboard_event *self = userdata; keyboard_event *self = userdata;
int fds_to_close_now[MAX_CLOSE_FDS];
int num_fds_to_close_now = 0;
while(self->running) { while(self->running) {
pthread_mutex_lock(&self->close_dev_input_mutex); pthread_mutex_lock(&self->close_dev_input_mutex);
for(int i = 0; i < self->num_close_fds; ++i) { for(int i = 0; i < self->num_close_fds; ++i) {
close(self->close_fds[i]); fds_to_close_now[i] = self->close_fds[i];
} }
num_fds_to_close_now = self->num_close_fds;
self->num_close_fds = 0; self->num_close_fds = 0;
pthread_mutex_unlock(&self->close_dev_input_mutex); pthread_mutex_unlock(&self->close_dev_input_mutex);
for(int i = 0; i < num_fds_to_close_now; ++i) {
close(fds_to_close_now[i]);
}
num_fds_to_close_now = 0;
usleep(100 * 1000); /* 100 milliseconds */ usleep(100 * 1000); /* 100 milliseconds */
} }
return NULL; return NULL;
@@ -456,9 +465,13 @@ static void keyboard_event_remove_event(keyboard_event *self, int index) {
if(index < 0 || index >= self->num_event_polls) if(index < 0 || index >= self->num_event_polls)
return; return;
if(self->event_polls[index].fd > 0) { const int poll_fd = self->event_polls[index].fd;
ioctl(self->event_polls[index].fd, EVIOCGRAB, 0); if(poll_fd > 0) {
close(self->event_polls[index].fd); ioctl(poll_fd, EVIOCGRAB, 0);
if(!keyboard_event_try_add_close_fd(self, poll_fd)) {
fprintf(stderr, "Error: failed to add immediately, closing now\n");
close(poll_fd);
}
} }
free(self->event_extra_data[index].key_states); free(self->event_extra_data[index].key_states);
free(self->event_extra_data[index].key_presses_grabbed); free(self->event_extra_data[index].key_presses_grabbed);

View File

@@ -5,9 +5,11 @@
#include <stdbool.h> #include <stdbool.h>
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#include <fcntl.h> #include <errno.h>
#include <sys/socket.h>
#include <sys/un.h>
static void get_runtime_filepath(char *buffer, size_t buffer_size, const char *filename) { static void get_socket_filepath(char *buffer, size_t buffer_size, const char *filename) {
char dir[PATH_MAX]; char dir[PATH_MAX];
const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); const char *runtime_dir = getenv("XDG_RUNTIME_DIR");
@@ -23,7 +25,7 @@ static void get_runtime_filepath(char *buffer, size_t buffer_size, const char *f
} }
/* Assumes |str| size is less than 256 */ /* Assumes |str| size is less than 256 */
static void fifo_write_all(int file_fd, const char *str) { static void file_write_all(int file_fd, const char *str) {
char command[256]; char command[256];
const ssize_t command_size = snprintf(command, sizeof(command), "%s\n", str); const ssize_t command_size = snprintf(command, sizeof(command), "%s\n", str);
if(command_size >= (ssize_t)sizeof(command)) { if(command_size >= (ssize_t)sizeof(command)) {
@@ -33,7 +35,7 @@ static void fifo_write_all(int file_fd, const char *str) {
ssize_t offset = 0; ssize_t offset = 0;
while(offset < (ssize_t)command_size) { while(offset < (ssize_t)command_size) {
const ssize_t bytes_written = write(file_fd, str + offset, command_size - offset); const ssize_t bytes_written = write(file_fd, command + offset, command_size - offset);
if(bytes_written > 0) if(bytes_written > 0)
offset += bytes_written; offset += bytes_written;
} }
@@ -112,15 +114,34 @@ int main(int argc, char **argv) {
usage(); usage();
} }
char fifo_filepath[PATH_MAX]; char socket_filepath[PATH_MAX];
get_runtime_filepath(fifo_filepath, sizeof(fifo_filepath), "gsr-ui"); get_socket_filepath(socket_filepath, sizeof(socket_filepath), "gsr-ui");
const int fifo_fd = open(fifo_filepath, O_RDWR | O_NONBLOCK);
if(fifo_fd <= 0) { const int socket_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0);
fprintf(stderr, "Error: failed to open fifo file %s. Maybe gsr-ui is not running?\n", fifo_filepath); if(socket_fd <= 0) {
fprintf(stderr, "Error: failed to create socket\n");
exit(2); exit(2);
} }
fifo_write_all(fifo_fd, command); struct sockaddr_un addr = {0};
close(fifo_fd); addr.sun_family = AF_UNIX;
snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", socket_filepath);
for(;;) {
if(connect(socket_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
const int err = errno;
if(err == EWOULDBLOCK) {
usleep(10 * 1000);
} else {
fprintf(stderr, "Error: failed to connect, error: %s. Maybe gsr-ui is not running?\n", strerror(err));
exit(2);
}
} else {
break;
}
}
file_write_all(socket_fd, command);
close(socket_fd);
return 0; return 0;
} }