Compare commits

...

16 Commits
1.7.3 ... 1.7.7

Author SHA1 Message Date
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
20 changed files with 536 additions and 37 deletions

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

7
TODO
View File

@@ -171,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.
Add option to copy screenshot to clipboard. Does it work properly on Wayland compositors? Maybe need to wait until the application becomes Wayland native instead of XWayland.
Show message that replay/streaming has to be restarted if recording settings are changed while replay/streaming is ongoing.
Support vector graphics. Maybe support svg, rendering it to a texture for better performance.
@@ -208,3 +206,8 @@ Support localization.
Add option to not capture cursor in screenshot when doing region/window capture.
Window selection doesn't work when a window is fullscreen on x11.
Make it possible to change replay duration of the "save 1 min" and "save 10 min" by adding them to the replay settings as options.
If replay duration is set below the "save 1 min" or "save 10 min" then gray them out and when hovering over those buttons
show a tooltip that says that those buttons cant be used because the replay duration in replay settings is set to a lower value than that (and display the replay duration there).

60
include/ClipboardFile.hpp Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,6 @@
project('gsr-ui', ['c', 'cpp'], version : '1.7.3', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
project('gsr-ui', ['c', 'cpp'], version : '1.7.7', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
add_project_arguments('-D_FILE_OFFSET_BITS=64', language : ['c', 'cpp'])
if get_option('buildtype') == 'debug'
add_project_arguments('-g3', language : ['c', 'cpp'])
@@ -47,6 +49,7 @@ src = [
'src/Overlay.cpp',
'src/AudioPlayer.cpp',
'src/Hotplug.cpp',
'src/ClipboardFile.cpp',
'src/Rpc.cpp',
'src/main.cpp',
]
@@ -62,7 +65,7 @@ datadir = get_option('datadir')
gsr_ui_resources_path = join_paths(prefix, datadir, 'gsr-ui')
add_project_arguments('-DGSR_UI_VERSION="' + meson.project_version() + '"', language: ['c', 'cpp'])
add_project_arguments('-DGSR_FLATPAK_VERSION="5.7.6"', language: ['c', 'cpp'])
add_project_arguments('-DGSR_FLATPAK_VERSION="5.8.2"', language: ['c', 'cpp'])
executable(
meson.project_name(),

View File

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

294
src/ClipboardFile.cpp Normal file
View File

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

View File

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

View File

@@ -50,6 +50,7 @@ namespace gsr {
static const double force_window_on_top_timeout_seconds = 1.0;
static const double replay_status_update_check_timeout_seconds = 1.5;
static const double replay_saving_notification_timeout_seconds = 0.5;
static const double short_notification_timeout_seconds = 2.0;
static const double notification_timeout_seconds = 3.0;
static const double notification_error_timeout_seconds = 5.0;
static const double cursor_tracker_update_timeout_sec = 0.1;
@@ -446,6 +447,17 @@ namespace gsr {
return global_hotkeys_js;
}
static NotificationSpeed to_notification_speed(const std::string &notification_speed_str) {
if(notification_speed_str == "normal")
return NotificationSpeed::NORMAL;
else if(notification_speed_str == "fast")
return NotificationSpeed::FAST;
else {
assert(false);
return NotificationSpeed::NORMAL;
}
}
Overlay::Overlay(std::string resources_path, GsrInfo gsr_info, SupportedCaptureOptions capture_options, egl_functions egl_funcs) :
resources_path(std::move(resources_path)),
gsr_info(std::move(gsr_info)),
@@ -474,6 +486,7 @@ namespace gsr {
power_supply_online_filepath = get_power_supply_online_filepath();
replay_startup_mode = replay_startup_string_to_type(config.replay_config.turn_on_replay_automatically_mode.c_str());
set_notification_speed(to_notification_speed(config.main_config.notification_speed));
if(config.main_config.hotkeys_enable_option == "enable_hotkeys")
global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::ALL);
@@ -499,7 +512,7 @@ namespace gsr {
hide();
if(notification_process > 0) {
kill(notification_process, SIGKILL);
kill(notification_process, SIGINT);
int status;
if(waitpid(notification_process, &status, 0) == -1) {
perror("waitpid failed");
@@ -946,6 +959,7 @@ namespace gsr {
const bool is_kwin = wm_name == "KWin";
const bool is_wlroots = wm_name.find("wlroots") != std::string::npos;
const bool is_hyprland = wm_name.find("Hyprland") != std::string::npos;
//const bool is_smithay = wm_name.find("Smithay") != std::string::npos;
const bool hyprland_waybar_is_dock = is_hyprland && is_hyprland_waybar_running_as_dock();
std::optional<CursorInfo> cursor_info;
@@ -973,8 +987,7 @@ namespace gsr {
// 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
// a fullscreen window for the ui.
// TODO: (x11_cursor_window && is_window_fullscreen_on_monitor(display, x11_cursor_window, *focused_monitor))
const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || x11_cursor_window || is_wlroots || is_hyprland;
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;
if(prevent_game_minimizing) {
window_pos = focused_monitor->position;
@@ -1645,6 +1658,8 @@ namespace gsr {
}
void Overlay::show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type, const char *capture_target) {
timeout_seconds *= notification_duration_multiplier;
char timeout_seconds_str[32];
snprintf(timeout_seconds_str, sizeof(timeout_seconds_str), "%f", timeout_seconds);
@@ -1685,7 +1700,7 @@ namespace gsr {
notification_args[arg_index++] = nullptr;
if(notification_process > 0) {
kill(notification_process, SIGKILL);
kill(notification_process, SIGINT);
int status = 0;
waitpid(notification_process, &status, 0);
}
@@ -1737,6 +1752,17 @@ namespace gsr {
bind_linux_hotkeys(static_cast<GlobalHotkeysLinux*>(global_hotkeys.get()), this);
}
void Overlay::set_notification_speed(NotificationSpeed notification_speed) {
switch(notification_speed) {
case NotificationSpeed::NORMAL:
notification_duration_multiplier = 1.0;
break;
case NotificationSpeed::FAST:
notification_duration_multiplier = 0.3;
break;
}
}
void Overlay::update_notification_process_status() {
if(notification_process <= 0)
return;
@@ -1802,8 +1828,6 @@ namespace gsr {
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;
}
@@ -1816,6 +1840,15 @@ namespace gsr {
return replay_duration_sec;
}
static ClipboardFile::FileType filename_to_clipboard_file_type(const std::string &filename) {
if(ends_with(filename, ".jpg") || ends_with(filename, ".jpeg"))
return ClipboardFile::FileType::JPG;
else if(ends_with(filename, ".png"))
return ClipboardFile::FileType::PNG;
assert(false);
return ClipboardFile::FileType::PNG;
}
void Overlay::save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type) {
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
@@ -1870,6 +1903,9 @@ namespace gsr {
snprintf(msg, sizeof(msg), "Saved a screenshot of %s\nto \"%s\"",
capture_target_get_notification_name(screenshot_capture_target.c_str(), true).c_str(), focused_window_name.c_str());
capture_target = screenshot_capture_target.c_str();
if(config.screenshot_config.save_screenshot_to_clipboard)
clipboard_file.set_current_file(new_video_filepath, filename_to_clipboard_file_type(new_video_filepath));
break;
}
case NotificationType::NONE:
@@ -1966,7 +2002,9 @@ namespace gsr {
const char *prefix = "";
switch(notification_type) {
case NotificationType::NONE:
break;
case NotificationType::SCREENSHOT:
prefix = "Failed to take a screenshot";
break;
case NotificationType::RECORD:
prefix = "Failed to start/save recording";
@@ -2009,7 +2047,7 @@ namespace gsr {
update_ui_replay_stopped();
if(exit_code == 0) {
if(config.replay_config.show_replay_stopped_notifications)
show_notification("Replay stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
show_notification("Replay stopped", short_notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
} else {
on_gsr_process_error(exit_code, NotificationType::REPLAY);
}
@@ -2024,7 +2062,7 @@ namespace gsr {
update_ui_streaming_stopped();
if(exit_code == 0) {
if(config.streaming_config.show_streaming_stopped_notifications)
show_notification("Streaming has stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
show_notification("Streaming has stopped", short_notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
} else {
on_gsr_process_error(exit_code, NotificationType::STREAM);
}
@@ -2058,6 +2096,9 @@ namespace gsr {
snprintf(msg, sizeof(msg), "Saved a screenshot of %s",
capture_target_get_notification_name(screenshot_capture_target.c_str(), true).c_str());
show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::SCREENSHOT, screenshot_capture_target.c_str());
if(config.screenshot_config.save_screenshot_to_clipboard)
clipboard_file.set_current_file(screenshot_filepath, filename_to_clipboard_file_type(screenshot_filepath));
}
} else {
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_screenshot_process, exit_code);
@@ -2517,6 +2558,14 @@ namespace gsr {
*container = change_container_if_codec_not_supported(*video_codec, *container);
}
static std::string get_framerate_mode_validate(const RecordOptions &record_options, const GsrInfo &gsr_info) {
(void)gsr_info;
std::string framerate_mode = record_options.framerate_mode;
if(framerate_mode == "auto")
framerate_mode = "vfr";
return framerate_mode;
}
bool Overlay::on_press_start_replay(bool disable_notification, bool finished_selection) {
if(region_selector.is_started() || window_selector.is_started())
return false;
@@ -2589,7 +2638,7 @@ namespace gsr {
const std::string video_bitrate = std::to_string(config.replay_config.record_options.video_bitrate);
const std::string output_directory = config.replay_config.save_directory;
const std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.replay_config.record_options.audio_tracks_list, gsr_info);
const std::string framerate_mode = config.replay_config.record_options.framerate_mode == "auto" ? "vfr" : config.replay_config.record_options.framerate_mode;
const std::string framerate_mode = get_framerate_mode_validate(config.replay_config.record_options, gsr_info);
const std::string replay_time = std::to_string(config.replay_config.replay_time);
const char *container = config.replay_config.container.c_str();
const char *video_codec = config.replay_config.record_options.video_codec.c_str();
@@ -2665,7 +2714,7 @@ namespace gsr {
if(!disable_notification && config.replay_config.show_replay_started_notifications) {
char msg[256];
snprintf(msg, sizeof(msg), "Started replaying %s", capture_target_get_notification_name(recording_capture_target.c_str(), 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")
@@ -2693,7 +2742,7 @@ namespace gsr {
if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) {
if(!replay_recording) {
if(config.record_config.show_recording_started_notifications)
show_notification("Started recording in the replay session", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD);
show_notification("Started recording in the replay session", short_notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD);
update_ui_recording_started();
// TODO: This will be incorrect if the user uses portal capture, as capture wont start until the user has
@@ -2714,7 +2763,7 @@ namespace gsr {
if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) {
if(!replay_recording) {
if(config.record_config.show_recording_started_notifications)
show_notification("Started recording in the streaming session", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD);
show_notification("Started recording in the streaming session", short_notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD);
update_ui_recording_started();
// TODO: This will be incorrect if the user uses portal capture, as capture wont start until the user has
@@ -2786,7 +2835,7 @@ namespace gsr {
const std::string video_bitrate = std::to_string(config.record_config.record_options.video_bitrate);
const std::string output_file = config.record_config.save_directory + "/Video_" + get_date_str() + "." + container_to_file_extension(config.record_config.container.c_str());
const std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.record_config.record_options.audio_tracks_list, gsr_info);
const std::string framerate_mode = config.record_config.record_options.framerate_mode == "auto" ? "vfr" : config.record_config.record_options.framerate_mode;
const std::string framerate_mode = get_framerate_mode_validate(config.record_config.record_options, gsr_info);
const char *container = config.record_config.container.c_str();
const char *video_codec = config.record_config.record_options.video_codec.c_str();
const char *encoder = "gpu";
@@ -2840,7 +2889,7 @@ namespace gsr {
if(config.record_config.show_recording_started_notifications) {
char msg[256];
snprintf(msg, sizeof(msg), "Started recording %s", capture_target_get_notification_name(recording_capture_target.c_str(), 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")
@@ -2962,7 +3011,7 @@ namespace gsr {
// But we check it anyways as streaming on some sites can fail if there is more than one audio track
if(audio_tracks.size() > 1)
audio_tracks.resize(1);
const std::string framerate_mode = config.streaming_config.record_options.framerate_mode == "auto" ? "vfr" : config.streaming_config.record_options.framerate_mode;
const std::string framerate_mode = get_framerate_mode_validate(config.streaming_config.record_options, gsr_info);
const char *container = "flv";
if(config.streaming_config.streaming_service == "custom")
container = config.streaming_config.custom.container.c_str();
@@ -3026,7 +3075,7 @@ namespace gsr {
if(config.streaming_config.show_streaming_started_notifications) {
char msg[256];
snprintf(msg, sizeof(msg), "Started streaming %s", capture_target_get_notification_name(recording_capture_target.c_str(), 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")

View File

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

View File

@@ -350,6 +350,7 @@ namespace gsr {
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 {

View File

@@ -467,13 +467,42 @@ namespace gsr {
return exit_program_button;
}
std::unique_ptr<List> GlobalSettingsPage::create_notification_speed() {
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Notification speed", get_color_theme().text_color));
auto radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL);
notification_speed_button_ptr = radio_button.get();
radio_button->add_item("Normal", "normal");
radio_button->add_item("Fast", "fast");
radio_button->on_selection_changed = [this](const std::string&, const std::string &id) {
if(id == "normal")
overlay->set_notification_speed(NotificationSpeed::NORMAL);
else if(id == "fast")
overlay->set_notification_speed(NotificationSpeed::FAST);
return true;
};
list->add_widget(std::move(radio_button));
return list;
}
std::unique_ptr<Subsection> GlobalSettingsPage::create_application_options_subsection(ScrollablePage *parent_page) {
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
List *list_ptr = list.get();
auto subsection = std::make_unique<Subsection>("Application options", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
list_ptr->add_widget(create_notification_speed());
list_ptr->add_widget(std::make_unique<LineSeparator>(LineSeparator::Orientation::HORIZONTAL, subsection->get_inner_size().x));
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL);
list->add_widget(create_exit_program_button());
auto navigate_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
navigate_list->add_widget(create_exit_program_button());
if(inside_flatpak)
list->add_widget(create_go_back_to_old_ui_button());
return std::make_unique<Subsection>("Application options", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
navigate_list->add_widget(create_go_back_to_old_ui_button());
list_ptr->add_widget(std::move(navigate_list));
return subsection;
}
std::unique_ptr<Subsection> GlobalSettingsPage::create_application_info_subsection(ScrollablePage *parent_page) {
@@ -535,6 +564,8 @@ namespace gsr {
enable_keyboard_hotkeys_radio_button_ptr->set_selected_item(config.main_config.hotkeys_enable_option, false, false);
enable_joystick_hotkeys_radio_button_ptr->set_selected_item(config.main_config.joystick_hotkeys_enable_option, false, false);
notification_speed_button_ptr->set_selected_item(config.main_config.notification_speed);
load_hotkeys();
}
@@ -561,6 +592,7 @@ namespace gsr {
config.main_config.tint_color = tint_color_radio_button_ptr->get_selected_id();
config.main_config.hotkeys_enable_option = enable_keyboard_hotkeys_radio_button_ptr->get_selected_id();
config.main_config.joystick_hotkeys_enable_option = enable_joystick_hotkeys_radio_button_ptr->get_selected_id();
config.main_config.notification_speed = notification_speed_button_ptr->get_selected_id();
save_config(config);
}

View File

@@ -215,8 +215,17 @@ namespace gsr {
return checkbox;
}
std::unique_ptr<CheckBox> ScreenshotSettingsPage::create_save_screenshot_to_clipboard() {
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Save screenshot to clipboard");
save_screenshot_to_clipboard_checkbox_ptr = checkbox.get();
return checkbox;
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_general_section() {
return std::make_unique<Subsection>("General", create_save_screenshot_in_game_folder(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
list->add_widget(create_save_screenshot_in_game_folder());
list->add_widget(create_save_screenshot_to_clipboard());
return std::make_unique<Subsection>("General", std::move(list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_notifications_section() {
@@ -281,6 +290,7 @@ namespace gsr {
restore_portal_session_checkbox_ptr->set_checked(config.screenshot_config.restore_portal_session);
save_directory_button_ptr->set_text(config.screenshot_config.save_directory);
save_screenshot_in_game_folder_checkbox_ptr->set_checked(config.screenshot_config.save_screenshot_in_game_folder);
save_screenshot_to_clipboard_checkbox_ptr->set_checked(config.screenshot_config.save_screenshot_to_clipboard);
show_screenshot_saved_notification_checkbox_ptr->set_checked(config.screenshot_config.show_screenshot_saved_notifications);
if(config.screenshot_config.image_width == 0)
@@ -309,6 +319,7 @@ namespace gsr {
config.screenshot_config.restore_portal_session = restore_portal_session_checkbox_ptr->is_checked();
config.screenshot_config.save_directory = save_directory_button_ptr->get_text();
config.screenshot_config.save_screenshot_in_game_folder = save_screenshot_in_game_folder_checkbox_ptr->is_checked();
config.screenshot_config.save_screenshot_to_clipboard = save_screenshot_to_clipboard_checkbox_ptr->is_checked();
config.screenshot_config.show_screenshot_saved_notifications = show_screenshot_saved_notification_checkbox_ptr->is_checked();
if(config.screenshot_config.image_width == 0)

View File

@@ -568,6 +568,10 @@ namespace gsr {
framerate_mode_box->add_item("Auto (Recommended)", "auto");
framerate_mode_box->add_item("Constant", "cfr");
framerate_mode_box->add_item("Variable", "vfr");
if(gsr_info->system_info.display_server == DisplayServer::X11)
framerate_mode_box->add_item("Sync to content", "content");
else
framerate_mode_box->add_item("Sync to content (Only X11 or desktop portal capture)", "content");
framerate_mode_box_ptr = framerate_mode_box.get();
return framerate_mode_box;
}

View File

@@ -195,7 +195,9 @@ enum class LaunchAction {
int main(int argc, char **argv) {
setlocale(LC_ALL, "C"); // Sigh... stupid C
#ifdef __GLIBC__
mallopt(M_MMAP_THRESHOLD, 65536);
#endif
if(geteuid() == 0) {
fprintf(stderr, "Error: don't run gsr-ui as the root user\n");