aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Config.cpp37
-rw-r--r--src/GlobalHotkeysJoystick.cpp243
-rw-r--r--src/GlobalHotkeysLinux.cpp158
-rw-r--r--src/GlobalHotkeysX11.cpp42
-rw-r--r--src/GsrInfo.cpp89
-rw-r--r--src/Hotplug.cpp81
-rw-r--r--src/Overlay.cpp326
-rw-r--r--src/Rpc.cpp2
-rw-r--r--src/WindowUtils.cpp181
-rw-r--r--src/gui/DropdownButton.cpp4
-rw-r--r--src/gui/GlobalSettingsPage.cpp517
-rw-r--r--src/gui/SettingsPage.cpp12
-rw-r--r--src/main.cpp183
13 files changed, 1537 insertions, 338 deletions
diff --git a/src/Config.cpp b/src/Config.cpp
index 4ad1107..b9e4cb7 100644
--- a/src/Config.cpp
+++ b/src/Config.cpp
@@ -1,21 +1,20 @@
#include "../include/Config.hpp"
#include "../include/Utils.hpp"
#include "../include/GsrInfo.hpp"
+#include "../include/GlobalHotkeys.hpp"
#include <variant>
#include <limits.h>
#include <inttypes.h>
#include <libgen.h>
-#include <iostream>
+#include <mglpp/window/Keyboard.hpp>
#define FORMAT_I32 "%" PRIi32
#define FORMAT_I64 "%" PRIi64
#define FORMAT_U32 "%" PRIu32
-#define CONFIG_FILE_VERSION 1
-
namespace gsr {
bool ConfigHotkey::operator==(const ConfigHotkey &other) const {
- return keysym == other.keysym && modifiers == other.modifiers;
+ return key == other.key && modifiers == other.modifiers;
}
bool ConfigHotkey::operator!=(const ConfigHotkey &other) const {
@@ -25,19 +24,26 @@ namespace gsr {
Config::Config(const SupportedCaptureOptions &capture_options) {
const std::string default_save_directory = get_videos_dir();
+ streaming_config.start_stop_hotkey = {mgl::Keyboard::F8, HOTKEY_MOD_LALT};
streaming_config.record_options.video_quality = "custom";
streaming_config.record_options.audio_tracks.push_back("default_output");
streaming_config.record_options.video_bitrate = 15000;
+ record_config.start_stop_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LALT};
+ record_config.pause_unpause_hotkey = {mgl::Keyboard::F7, HOTKEY_MOD_LALT};
record_config.save_directory = default_save_directory;
record_config.record_options.audio_tracks.push_back("default_output");
record_config.record_options.video_bitrate = 45000;
+ replay_config.start_stop_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT | HOTKEY_MOD_LSHIFT};
+ replay_config.save_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT};
replay_config.record_options.video_quality = "custom";
replay_config.save_directory = default_save_directory;
replay_config.record_options.audio_tracks.push_back("default_output");
replay_config.record_options.video_bitrate = 45000;
+ main_config.show_hide_hotkey = {mgl::Keyboard::Z, HOTKEY_MOD_LALT};
+
if(!capture_options.monitors.empty()) {
streaming_config.record_options.record_area_option = capture_options.monitors.front().name;
record_config.record_options.record_area_option = capture_options.monitors.front().name;
@@ -59,7 +65,9 @@ namespace gsr {
{"main.config_file_version", &config.main_config.config_file_version},
{"main.software_encoding_warning_shown", &config.main_config.software_encoding_warning_shown},
{"main.hotkeys_enable_option", &config.main_config.hotkeys_enable_option},
+ {"main.joystick_hotkeys_enable_option", &config.main_config.joystick_hotkeys_enable_option},
{"main.tint_color", &config.main_config.tint_color},
+ {"main.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_width", &config.streaming_config.record_options.record_area_width},
@@ -88,7 +96,7 @@ namespace gsr {
{"streaming.twitch.key", &config.streaming_config.twitch.stream_key},
{"streaming.custom.url", &config.streaming_config.custom.url},
{"streaming.custom.container", &config.streaming_config.custom.container},
- {"streaming.start_stop_recording_hotkey", &config.streaming_config.start_stop_recording_hotkey},
+ {"streaming.start_stop_hotkey", &config.streaming_config.start_stop_hotkey},
{"record.record_options.record_area_option", &config.record_config.record_options.record_area_option},
{"record.record_options.record_area_width", &config.record_config.record_options.record_area_width},
@@ -115,8 +123,8 @@ namespace gsr {
{"record.show_video_saved_notifications", &config.record_config.show_video_saved_notifications},
{"record.save_directory", &config.record_config.save_directory},
{"record.container", &config.record_config.container},
- {"record.start_stop_recording_hotkey", &config.record_config.start_stop_recording_hotkey},
- {"record.pause_unpause_recording_hotkey", &config.record_config.pause_unpause_recording_hotkey},
+ {"record.start_stop_hotkey", &config.record_config.start_stop_hotkey},
+ {"record.pause_unpause_hotkey", &config.record_config.pause_unpause_hotkey},
{"replay.record_options.record_area_option", &config.replay_config.record_options.record_area_option},
{"replay.record_options.record_area_width", &config.replay_config.record_options.record_area_width},
@@ -140,14 +148,15 @@ namespace gsr {
{"replay.record_options.restore_portal_session", &config.replay_config.record_options.restore_portal_session},
{"replay.turn_on_replay_automatically_mode", &config.replay_config.turn_on_replay_automatically_mode},
{"replay.save_video_in_game_folder", &config.replay_config.save_video_in_game_folder},
+ {"replay.restart_replay_on_save", &config.replay_config.restart_replay_on_save},
{"replay.show_replay_started_notifications", &config.replay_config.show_replay_started_notifications},
{"replay.show_replay_stopped_notifications", &config.replay_config.show_replay_stopped_notifications},
{"replay.show_replay_saved_notifications", &config.replay_config.show_replay_saved_notifications},
{"replay.save_directory", &config.replay_config.save_directory},
{"replay.container", &config.replay_config.container},
{"replay.time", &config.replay_config.replay_time},
- {"replay.start_stop_recording_hotkey", &config.replay_config.start_stop_recording_hotkey},
- {"replay.save_recording_hotkey", &config.replay_config.save_recording_hotkey}
+ {"replay.start_stop_hotkey", &config.replay_config.start_stop_hotkey},
+ {"replay.save_hotkey", &config.replay_config.save_hotkey}
};
}
@@ -228,9 +237,9 @@ namespace gsr {
} else if(std::holds_alternative<ConfigHotkey*>(it->second)) {
std::string value_str(key_value->value);
ConfigHotkey *config_hotkey = std::get<ConfigHotkey*>(it->second);
- if(sscanf(value_str.c_str(), FORMAT_I64 " " FORMAT_U32, &config_hotkey->keysym, &config_hotkey->modifiers) != 2) {
+ if(sscanf(value_str.c_str(), FORMAT_I64 " " FORMAT_U32, &config_hotkey->key, &config_hotkey->modifiers) != 2) {
fprintf(stderr, "Warning: Invalid config option value for %.*s\n", (int)key_value->key.size(), key_value->key.data());
- config_hotkey->keysym = 0;
+ config_hotkey->key = 0;
config_hotkey->modifiers = 0;
}
} else if(std::holds_alternative<std::vector<std::string>*>(it->second)) {
@@ -241,7 +250,7 @@ namespace gsr {
return true;
});
- if(config->main_config.config_file_version != CONFIG_FILE_VERSION) {
+ if(config->main_config.config_file_version != GSR_CONFIG_FILE_VERSION) {
fprintf(stderr, "Info: the config file is outdated, resetting it\n");
config = std::nullopt;
}
@@ -250,7 +259,7 @@ namespace gsr {
}
void save_config(Config &config) {
- config.main_config.config_file_version = CONFIG_FILE_VERSION;
+ config.main_config.config_file_version = GSR_CONFIG_FILE_VERSION;
const std::string config_path = get_config_dir() + "/config_ui";
@@ -279,7 +288,7 @@ namespace gsr {
fprintf(file, "%.*s " FORMAT_I32 "\n", (int)it.first.size(), it.first.data(), *std::get<int32_t*>(it.second));
} else if(std::holds_alternative<ConfigHotkey*>(it.second)) {
const ConfigHotkey *config_hotkey = std::get<ConfigHotkey*>(it.second);
- fprintf(file, "%.*s " FORMAT_I64 " " FORMAT_U32 "\n", (int)it.first.size(), it.first.data(), config_hotkey->keysym, config_hotkey->modifiers);
+ fprintf(file, "%.*s " FORMAT_I64 " " FORMAT_U32 "\n", (int)it.first.size(), it.first.data(), config_hotkey->key, config_hotkey->modifiers);
} else if(std::holds_alternative<std::vector<std::string>*>(it.second)) {
std::vector<std::string> *array = std::get<std::vector<std::string>*>(it.second);
for(const std::string &value : *array) {
diff --git a/src/GlobalHotkeysJoystick.cpp b/src/GlobalHotkeysJoystick.cpp
new file mode 100644
index 0000000..dfe1e6f
--- /dev/null
+++ b/src/GlobalHotkeysJoystick.cpp
@@ -0,0 +1,243 @@
+#include "../include/GlobalHotkeysJoystick.hpp"
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/eventfd.h>
+
+namespace gsr {
+ static constexpr double double_click_timeout_seconds = 0.33;
+
+ // Returns -1 on error
+ static int get_js_dev_input_id_from_filepath(const char *dev_input_filepath) {
+ if(strncmp(dev_input_filepath, "/dev/input/js", 13) != 0)
+ return -1;
+
+ int dev_input_id = -1;
+ if(sscanf(dev_input_filepath + 13, "%d", &dev_input_id) == 1)
+ return dev_input_id;
+ return -1;
+ }
+
+ GlobalHotkeysJoystick::~GlobalHotkeysJoystick() {
+ if(event_fd > 0) {
+ const uint64_t exit = 1;
+ write(event_fd, &exit, sizeof(exit));
+ }
+
+ if(read_thread.joinable())
+ read_thread.join();
+
+ if(event_fd > 0)
+ close(event_fd);
+
+ for(int i = 0; i < num_poll_fd; ++i) {
+ close(poll_fd[i].fd);
+ }
+ }
+
+ bool GlobalHotkeysJoystick::start() {
+ if(num_poll_fd > 0)
+ return false;
+
+ event_fd = eventfd(0, 0);
+ if(event_fd <= 0)
+ return false;
+
+ event_index = num_poll_fd;
+ poll_fd[num_poll_fd] = {
+ event_fd,
+ POLLIN,
+ 0
+ };
+ extra_data[num_poll_fd] = {
+ -1
+ };
+ ++num_poll_fd;
+
+ if(!hotplug.start()) {
+ fprintf(stderr, "Warning: failed to setup hotplugging\n");
+ } else {
+ hotplug_poll_index = num_poll_fd;
+ poll_fd[num_poll_fd] = {
+ hotplug.steal_fd(),
+ POLLIN,
+ 0
+ };
+ extra_data[num_poll_fd] = {
+ -1
+ };
+ ++num_poll_fd;
+ }
+
+ char dev_input_path[128];
+ for(int i = 0; i < 8; ++i) {
+ snprintf(dev_input_path, sizeof(dev_input_path), "/dev/input/js%d", i);
+ add_device(dev_input_path, false);
+ }
+
+ if(num_poll_fd == 0)
+ fprintf(stderr, "Info: no joysticks found, assuming they might be connected later\n");
+
+ read_thread = std::thread(&GlobalHotkeysJoystick::read_events, this);
+ return true;
+ }
+
+ bool GlobalHotkeysJoystick::bind_action(const std::string &id, GlobalHotkeyCallback callback) {
+ if(num_poll_fd == 0)
+ return false;
+ return bound_actions_by_id.insert(std::make_pair(id, std::move(callback))).second;
+ }
+
+ void GlobalHotkeysJoystick::poll_events() {
+ if(num_poll_fd == 0)
+ return;
+
+ if(save_replay) {
+ save_replay = false;
+ auto it = bound_actions_by_id.find("save_replay");
+ if(it != bound_actions_by_id.end())
+ it->second("save_replay");
+ }
+ }
+
+ void GlobalHotkeysJoystick::read_events() {
+ js_event event;
+ while(poll(poll_fd, num_poll_fd, -1) > 0) {
+ for(int i = 0; i < num_poll_fd; ++i) {
+ if(poll_fd[i].revents & (POLLHUP|POLLERR|POLLNVAL)) {
+ if(i == event_index)
+ goto done;
+
+ if(remove_poll_fd(i))
+ --i; // This item was removed so we want to repeat the same index to continue to the next item
+
+ continue;
+ }
+
+ if(!(poll_fd[i].revents & POLLIN))
+ continue;
+
+ if(i == event_index) {
+ goto done;
+ } else if(i == hotplug_poll_index) {
+ hotplug.process_event_data(poll_fd[i].fd, [&](HotplugAction hotplug_action, const char *devname) {
+ char dev_input_filepath[1024];
+ snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/%s", devname);
+ switch(hotplug_action) {
+ case HotplugAction::ADD: {
+ // Cant open the /dev/input device immediately or it fails.
+ // TODO: Remove this hack when a better solution is found.
+ usleep(50 * 1000);
+ add_device(dev_input_filepath);
+ break;
+ }
+ case HotplugAction::REMOVE: {
+ if(remove_device(dev_input_filepath))
+ --i; // This item was removed so we want to repeat the same index to continue to the next item
+ break;
+ }
+ }
+ });
+ } else {
+ process_js_event(poll_fd[i].fd, event);
+ }
+ }
+ }
+
+ done:
+ ;
+ }
+
+ void GlobalHotkeysJoystick::process_js_event(int fd, js_event &event) {
+ if(read(fd, &event, sizeof(event)) != sizeof(event))
+ return;
+
+ if((event.type & JS_EVENT_BUTTON) == 0)
+ return;
+
+ if(event.number == 8 && event.value == 1) {
+ const double now = double_click_clock.get_elapsed_time_seconds();
+ if(!prev_time_clicked.has_value()) {
+ prev_time_clicked = now;
+ return;
+ }
+
+ if(prev_time_clicked.has_value()) {
+ const bool double_clicked = (now - prev_time_clicked.value()) < double_click_timeout_seconds;
+ if(double_clicked) {
+ save_replay = true;
+ prev_time_clicked.reset();
+ } else {
+ prev_time_clicked = now;
+ }
+ }
+ }
+ }
+
+ bool GlobalHotkeysJoystick::add_device(const char *dev_input_filepath, bool print_error) {
+ if(num_poll_fd >= max_js_poll_fd) {
+ fprintf(stderr, "Warning: failed to add joystick device %s, too many joysticks have been added\n", dev_input_filepath);
+ return false;
+ }
+
+ const int dev_input_id = get_js_dev_input_id_from_filepath(dev_input_filepath);
+ if(dev_input_id == -1)
+ return false;
+
+ const int fd = open(dev_input_filepath, O_RDONLY);
+ if(fd <= 0) {
+ if(print_error)
+ fprintf(stderr, "Error: failed to add joystick %s, error: %s\n", dev_input_filepath, strerror(errno));
+ return false;
+ }
+
+ poll_fd[num_poll_fd] = {
+ fd,
+ POLLIN,
+ 0
+ };
+
+ extra_data[num_poll_fd] = {
+ dev_input_id
+ };
+
+ ++num_poll_fd;
+ fprintf(stderr, "Info: added joystick: %s\n", dev_input_filepath);
+ return true;
+ }
+
+ bool GlobalHotkeysJoystick::remove_device(const char *dev_input_filepath) {
+ const int dev_input_id = get_js_dev_input_id_from_filepath(dev_input_filepath);
+ if(dev_input_id == -1)
+ return false;
+
+ const int poll_fd_index = get_poll_fd_index_by_dev_input_id(dev_input_id);
+ if(poll_fd_index == -1)
+ return false;
+
+ fprintf(stderr, "Info: removed joystick: %s\n", dev_input_filepath);
+ return remove_poll_fd(poll_fd_index);
+ }
+
+ bool GlobalHotkeysJoystick::remove_poll_fd(int index) {
+ if(index < 0 || index >= num_poll_fd)
+ return false;
+
+ close(poll_fd[index].fd);
+ for(int i = index + 1; i < num_poll_fd; ++i) {
+ poll_fd[i - 1] = poll_fd[i];
+ extra_data[i - 1] = extra_data[i];
+ }
+ --num_poll_fd;
+ return true;
+ }
+
+ int GlobalHotkeysJoystick::get_poll_fd_index_by_dev_input_id(int dev_input_id) const {
+ for(int i = 0; i < num_poll_fd; ++i) {
+ if(dev_input_id == extra_data[i].dev_input_id)
+ return i;
+ }
+ return -1;
+ }
+}
diff --git a/src/GlobalHotkeysLinux.cpp b/src/GlobalHotkeysLinux.cpp
index 3d1813d..357fb16 100644
--- a/src/GlobalHotkeysLinux.cpp
+++ b/src/GlobalHotkeysLinux.cpp
@@ -5,6 +5,12 @@
#include <limits.h>
#include <string.h>
+extern "C" {
+#include <mgl/mgl.h>
+}
+#include <X11/Xlib.h>
+#include <linux/input-event-codes.h>
+
#define PIPE_READ 0
#define PIPE_WRITE 1
@@ -17,16 +23,55 @@ namespace gsr {
return "--all";
}
+ static inline uint8_t x11_keycode_to_linux_keycode(uint8_t code) {
+ return code - 8;
+ }
+
+ static std::vector<uint8_t> modifiers_to_linux_keys(uint32_t modifiers) {
+ std::vector<uint8_t> result;
+ if(modifiers & HOTKEY_MOD_LSHIFT)
+ result.push_back(KEY_LEFTSHIFT);
+ if(modifiers & HOTKEY_MOD_RSHIFT)
+ result.push_back(KEY_RIGHTSHIFT);
+ if(modifiers & HOTKEY_MOD_LCTRL)
+ result.push_back(KEY_LEFTCTRL);
+ if(modifiers & HOTKEY_MOD_RCTRL)
+ result.push_back(KEY_RIGHTCTRL);
+ if(modifiers & HOTKEY_MOD_LALT)
+ result.push_back(KEY_LEFTALT);
+ if(modifiers & HOTKEY_MOD_RALT)
+ result.push_back(KEY_RIGHTALT);
+ if(modifiers & HOTKEY_MOD_LSUPER)
+ result.push_back(KEY_LEFTMETA);
+ if(modifiers & HOTKEY_MOD_RSUPER)
+ result.push_back(KEY_RIGHTMETA);
+ return result;
+ }
+
+ static std::string linux_keys_to_command_string(const uint8_t *keys, size_t size) {
+ std::string result;
+ for(size_t i = 0; i < size; ++i) {
+ if(!result.empty())
+ result += "+";
+ result += std::to_string(keys[i]);
+ }
+ return result;
+ }
+
GlobalHotkeysLinux::GlobalHotkeysLinux(GrabType grab_type) : grab_type(grab_type) {
for(int i = 0; i < 2; ++i) {
- pipes[i] = -1;
+ read_pipes[i] = -1;
+ write_pipes[i] = -1;
}
}
GlobalHotkeysLinux::~GlobalHotkeysLinux() {
for(int i = 0; i < 2; ++i) {
- if(pipes[i] > 0)
- close(pipes[i]);
+ if(read_pipes[i] > 0)
+ close(read_pipes[i]);
+
+ if(write_pipes[i] > 0)
+ close(write_pipes[i]);
}
if(read_file)
@@ -49,28 +94,49 @@ namespace gsr {
char gsr_global_hotkeys_flatpak[PATH_MAX];
snprintf(gsr_global_hotkeys_flatpak, sizeof(gsr_global_hotkeys_flatpak), "%s/.local/share/gpu-screen-recorder/gsr-global-hotkeys", user_homepath);
+ const char *display = getenv("DISPLAY");
+ if(!display)
+ display = ":0";
+ char env_arg[256];
+ snprintf(env_arg, sizeof(env_arg), "--env=DISPLAY=%s", display);
+
if(process_id > 0)
return false;
- if(pipe(pipes) == -1)
+ if(pipe(read_pipes) == -1)
return false;
+ if(pipe(write_pipes) == -1) {
+ for(int i = 0; i < 2; ++i) {
+ close(read_pipes[i]);
+ read_pipes[i] = -1;
+ }
+ return false;
+ }
+
const pid_t pid = vfork();
if(pid == -1) {
perror("Failed to vfork");
for(int i = 0; i < 2; ++i) {
- close(pipes[i]);
- pipes[i] = -1;
+ close(read_pipes[i]);
+ close(write_pipes[i]);
+ read_pipes[i] = -1;
+ write_pipes[i] = -1;
}
return false;
} else if(pid == 0) { /* child */
- dup2(pipes[PIPE_WRITE], STDOUT_FILENO);
+ dup2(read_pipes[PIPE_WRITE], STDOUT_FILENO);
for(int i = 0; i < 2; ++i) {
- close(pipes[i]);
+ close(read_pipes[i]);
+ }
+
+ dup2(write_pipes[PIPE_READ], STDIN_FILENO);
+ for(int i = 0; i < 2; ++i) {
+ close(write_pipes[i]);
}
if(inside_flatpak) {
- const char *args[] = { "flatpak-spawn", "--host", "--", gsr_global_hotkeys_flatpak, grab_type_arg, nullptr };
+ const char *args[] = { "flatpak-spawn", "--host", env_arg, "--", gsr_global_hotkeys_flatpak, grab_type_arg, nullptr };
execvp(args[0], (char* const*)args);
} else {
const char *args[] = { "gsr-global-hotkeys", grab_type_arg, nullptr };
@@ -81,34 +147,86 @@ namespace gsr {
_exit(127);
} else { /* parent */
process_id = pid;
- close(pipes[PIPE_WRITE]);
- pipes[PIPE_WRITE] = -1;
- const int fdl = fcntl(pipes[PIPE_READ], F_GETFL);
- fcntl(pipes[PIPE_READ], F_SETFL, fdl | O_NONBLOCK);
+ close(read_pipes[PIPE_WRITE]);
+ read_pipes[PIPE_WRITE] = -1;
+
+ close(write_pipes[PIPE_READ]);
+ write_pipes[PIPE_READ] = -1;
- read_file = fdopen(pipes[PIPE_READ], "r");
+ fcntl(read_pipes[PIPE_READ], F_SETFL, fcntl(read_pipes[PIPE_READ], F_GETFL) | O_NONBLOCK);
+ read_file = fdopen(read_pipes[PIPE_READ], "r");
if(read_file)
- pipes[PIPE_READ] = -1;
+ read_pipes[PIPE_READ] = -1;
else
- fprintf(stderr, "fdopen failed, error: %s\n", strerror(errno));
+ fprintf(stderr, "fdopen failed for read, error: %s\n", strerror(errno));
}
return true;
}
- bool GlobalHotkeysLinux::bind_action(const std::string &id, GlobalHotkeyCallback callback) {
- return bound_actions_by_id.insert(std::make_pair(id, std::move(callback))).second;
+ bool GlobalHotkeysLinux::bind_key_press(Hotkey hotkey, const std::string &id, GlobalHotkeyCallback callback) {
+ if(process_id <= 0)
+ return false;
+
+ if(bound_actions_by_id.find(id) != bound_actions_by_id.end())
+ return false;
+
+ if(id.find(' ') != std::string::npos || id.find('\n') != std::string::npos) {
+ fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: id \"%s\" contains either space or newline\n", id.c_str());
+ return false;
+ }
+
+ if(hotkey.key == 0) {
+ //fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: hotkey requires a key\n");
+ return false;
+ }
+
+ if(hotkey.modifiers == 0) {
+ //fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: hotkey requires a modifier\n");
+ return false;
+ }
+
+ mgl_context *context = mgl_get_context();
+ Display *display = (Display*)context->connection;
+ const uint8_t keycode = x11_keycode_to_linux_keycode(XKeysymToKeycode(display, hotkey.key));
+ const std::vector<uint8_t> modifiers = modifiers_to_linux_keys(hotkey.modifiers);
+ const std::string modifiers_command = linux_keys_to_command_string(modifiers.data(), modifiers.size());
+
+ char command[256];
+ const int command_size = snprintf(command, sizeof(command), "bind %s %d+%s\n", id.c_str(), (int)keycode, modifiers_command.c_str());
+ if(write(write_pipes[PIPE_WRITE], command, command_size) != command_size) {
+ fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: failed to write command to gsr-global-hotkeys, error: %s\n", strerror(errno));
+ return false;
+ }
+
+ bound_actions_by_id[id] = std::move(callback);
+ return true;
+ }
+
+ void GlobalHotkeysLinux::unbind_all_keys() {
+ if(process_id <= 0)
+ return;
+
+ if(bound_actions_by_id.empty())
+ return;
+
+ char command[32];
+ const int command_size = snprintf(command, sizeof(command), "unbind_all\n");
+ if(write(write_pipes[PIPE_WRITE], command, command_size) != command_size) {
+ fprintf(stderr, "Error: GlobalHotkeysLinux::unbind_all_keys: failed to write command to gsr-global-hotkeys, error: %s\n", strerror(errno));
+ }
+ bound_actions_by_id.clear();
}
void GlobalHotkeysLinux::poll_events() {
if(process_id <= 0) {
- fprintf(stderr, "error: GlobalHotkeysLinux::poll_events failed, process has not been started yet. Use GlobalHotkeysLinux::start to start the process first\n");
+ //fprintf(stderr, "error: GlobalHotkeysLinux::poll_events failed, process has not been started yet. Use GlobalHotkeysLinux::start to start the process first\n");
return;
}
if(!read_file) {
- fprintf(stderr, "error: GlobalHotkeysLinux::poll_events failed, read file hasn't opened\n");
+ //fprintf(stderr, "error: GlobalHotkeysLinux::poll_events failed, read file hasn't opened\n");
return;
}
diff --git a/src/GlobalHotkeysX11.cpp b/src/GlobalHotkeysX11.cpp
index 2943397..9af2607 100644
--- a/src/GlobalHotkeysX11.cpp
+++ b/src/GlobalHotkeysX11.cpp
@@ -50,6 +50,27 @@ namespace gsr {
return mask;
}
+ static uint32_t modifiers_to_x11_modifiers(uint32_t modifiers) {
+ uint32_t result = 0;
+ if(modifiers & HOTKEY_MOD_LSHIFT)
+ result |= ShiftMask;
+ if(modifiers & HOTKEY_MOD_RSHIFT)
+ result |= ShiftMask;
+ if(modifiers & HOTKEY_MOD_LCTRL)
+ result |= ControlMask;
+ if(modifiers & HOTKEY_MOD_RCTRL)
+ result |= ControlMask;
+ if(modifiers & HOTKEY_MOD_LALT)
+ result |= Mod1Mask;
+ if(modifiers & HOTKEY_MOD_RALT)
+ result |= Mod5Mask;
+ if(modifiers & HOTKEY_MOD_LSUPER)
+ result |= Mod4Mask;
+ if(modifiers & HOTKEY_MOD_RSUPER)
+ result |= Mod4Mask;
+ return result;
+ }
+
GlobalHotkeysX11::GlobalHotkeysX11() {
dpy = XOpenDisplay(NULL);
if(!dpy)
@@ -74,16 +95,17 @@ namespace gsr {
x_failed = false;
XErrorHandler prev_xerror = XSetErrorHandler(xerror_grab_error);
+ const uint32_t modifiers_x11 = modifiers_to_x11_modifiers(hotkey.modifiers);
unsigned int numlock_mask = x11_get_numlock_mask(dpy);
unsigned int modifiers[] = { 0, LockMask, numlock_mask, numlock_mask|LockMask };
for(int i = 0; i < 4; ++i) {
- XGrabKey(dpy, XKeysymToKeycode(dpy, hotkey.key), hotkey.modifiers | modifiers[i], DefaultRootWindow(dpy), False, GrabModeAsync, GrabModeAsync);
+ XGrabKey(dpy, XKeysymToKeycode(dpy, hotkey.key), modifiers_x11 | modifiers[i], DefaultRootWindow(dpy), False, GrabModeAsync, GrabModeAsync);
}
XSync(dpy, False);
if(x_failed) {
for(int i = 0; i < 4; ++i) {
- XUngrabKey(dpy, XKeysymToKeycode(dpy, hotkey.key), hotkey.modifiers | modifiers[i], DefaultRootWindow(dpy));
+ XUngrabKey(dpy, XKeysymToKeycode(dpy, hotkey.key), modifiers_x11 | modifiers[i], DefaultRootWindow(dpy));
}
XSync(dpy, False);
XSetErrorHandler(prev_xerror);
@@ -106,10 +128,11 @@ namespace gsr {
x_failed = false;
XErrorHandler prev_xerror = XSetErrorHandler(xerror_grab_error);
+ const uint32_t modifiers_x11 = modifiers_to_x11_modifiers(it->second.hotkey.modifiers);
unsigned int numlock_mask = x11_get_numlock_mask(dpy);
unsigned int modifiers[] = { 0, LockMask, numlock_mask, numlock_mask|LockMask };
for(int i = 0; i < 4; ++i) {
- XUngrabKey(dpy, XKeysymToKeycode(dpy, it->second.hotkey.key), it->second.hotkey.modifiers | modifiers[i], DefaultRootWindow(dpy));
+ XUngrabKey(dpy, XKeysymToKeycode(dpy, it->second.hotkey.key), modifiers_x11 | modifiers[i], DefaultRootWindow(dpy));
}
XSync(dpy, False);
@@ -127,8 +150,9 @@ namespace gsr {
unsigned int numlock_mask = x11_get_numlock_mask(dpy);
unsigned int modifiers[] = { 0, LockMask, numlock_mask, numlock_mask|LockMask };
for(auto it = bound_keys_by_id.begin(); it != bound_keys_by_id.end();) {
+ const uint32_t modifiers_x11 = modifiers_to_x11_modifiers(it->second.hotkey.modifiers);
for(int i = 0; i < 4; ++i) {
- XUngrabKey(dpy, XKeysymToKeycode(dpy, it->second.hotkey.key), it->second.hotkey.modifiers | modifiers[i], DefaultRootWindow(dpy));
+ XUngrabKey(dpy, XKeysymToKeycode(dpy, it->second.hotkey.key), modifiers_x11 | modifiers[i], DefaultRootWindow(dpy));
}
}
bound_keys_by_id.clear();
@@ -138,11 +162,14 @@ namespace gsr {
}
void GlobalHotkeysX11::poll_events() {
+ if(!dpy)
+ return;
+
while(XPending(dpy)) {
XNextEvent(dpy, &xev);
if(xev.type == KeyPress) {
const KeySym key_sym = XLookupKeysym(&xev.xkey, 0);
- call_hotkey_callback({ key_sym, xev.xkey.state });
+ call_hotkey_callback({ (uint32_t)key_sym, xev.xkey.state });
}
}
}
@@ -154,7 +181,7 @@ namespace gsr {
// Note: not all keys are mapped in mgl_key_to_key_sym. If more hotkeys are added or changed then add the key mapping there
const KeySym key_sym = mgl_key_to_key_sym(event.key.code);
const uint32_t modifiers = mgl_key_modifiers_to_x11_modifier_mask(event.key);
- return !call_hotkey_callback(Hotkey{key_sym, modifiers});
+ return !call_hotkey_callback(Hotkey{(uint32_t)key_sym, modifiers});
}
static unsigned int key_state_without_locks(unsigned int key_state) {
@@ -162,8 +189,9 @@ namespace gsr {
}
bool GlobalHotkeysX11::call_hotkey_callback(Hotkey hotkey) const {
+ const uint32_t modifiers_x11 = modifiers_to_x11_modifiers(hotkey.modifiers);
for(const auto &[key, val] : bound_keys_by_id) {
- if(val.hotkey.key == hotkey.key && key_state_without_locks(val.hotkey.modifiers) == key_state_without_locks(hotkey.modifiers)) {
+ if(val.hotkey.key == hotkey.key && key_state_without_locks(modifiers_to_x11_modifiers(val.hotkey.modifiers)) == key_state_without_locks(modifiers_x11)) {
val.callback(key);
return true;
}
diff --git a/src/GsrInfo.cpp b/src/GsrInfo.cpp
index 6665dc9..033757c 100644
--- a/src/GsrInfo.cpp
+++ b/src/GsrInfo.cpp
@@ -6,6 +6,93 @@
#include <string.h>
namespace gsr {
+ bool GsrVersion::operator>(const GsrVersion &other) const {
+ return major > other.major || (major == other.major && minor > other.minor) || (major == other.major && minor == other.minor && patch > other.patch);
+ }
+
+ bool GsrVersion::operator>=(const GsrVersion &other) const {
+ return major >= other.major || (major == other.major && minor >= other.minor) || (major == other.major && minor == other.minor && patch >= other.patch);
+ }
+
+ bool GsrVersion::operator<(const GsrVersion &other) const {
+ return !operator>=(other);
+ }
+
+ bool GsrVersion::operator<=(const GsrVersion &other) const {
+ return !operator>(other);
+ }
+
+ bool GsrVersion::operator==(const GsrVersion &other) const {
+ return major == other.major && minor == other.minor && patch == other.patch;
+ }
+
+ bool GsrVersion::operator!=(const GsrVersion &other) const {
+ return !operator==(other);
+ }
+
+ std::string GsrVersion::to_string() const {
+ std::string result;
+ if(major == 0 && minor == 0 && patch == 0)
+ result = "Unknown";
+ else
+ result = std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(patch);
+ return result;
+ }
+
+ /* Returns -1 on error */
+ static int parse_u8(const char *str, int size) {
+ if(size <= 0)
+ return -1;
+
+ int result = 0;
+ for(int i = 0; i < size; ++i) {
+ char c = str[i];
+ if(c >= '0' && c <= '9') {
+ result = result * 10 + (c - '0');
+ if(result > 255)
+ return -1;
+ } else {
+ return -1;
+ }
+ }
+ return result;
+ }
+
+ static GsrVersion parse_gsr_version(const std::string_view str) {
+ GsrVersion result;
+ uint8_t numbers[3];
+ int number_index = 0;
+
+ size_t index = 0;
+ while(true) {
+ size_t next_index = str.find('.', index);
+ if(next_index == std::string::npos)
+ next_index = str.size();
+
+ const int number = parse_u8(str.data() + index, next_index - index);
+ if(number == -1) {
+ fprintf(stderr, "Error: gpu-screen-recorder --info contains invalid gsr version: %.*s\n", (int)str.size(), str.data());
+ return {0, 0, 0};
+ }
+
+ if(number_index >= 3) {
+ fprintf(stderr, "Error: gpu-screen-recorder --info contains invalid gsr version: %.*s\n", (int)str.size(), str.data());
+ return {0, 0, 0};
+ }
+
+ numbers[number_index] = number;
+ ++number_index;
+ index = next_index + 1;
+ if(next_index == str.size())
+ break;
+ }
+
+ result.major = numbers[0];
+ result.minor = numbers[1];
+ result.patch = numbers[2];
+ return result;
+ }
+
static std::optional<KeyValue> parse_key_value(std::string_view line) {
const size_t space_index = line.find('|');
if(space_index == std::string_view::npos)
@@ -25,6 +112,8 @@ namespace gsr {
gsr_info->system_info.display_server = DisplayServer::WAYLAND;
} else if(key_value->key == "supports_app_audio") {
gsr_info->system_info.supports_app_audio = key_value->value == "yes";
+ } else if(key_value->key == "gsr_version") {
+ gsr_info->system_info.gsr_version = parse_gsr_version(key_value->value);
}
}
diff --git a/src/Hotplug.cpp b/src/Hotplug.cpp
new file mode 100644
index 0000000..84ed5bb
--- /dev/null
+++ b/src/Hotplug.cpp
@@ -0,0 +1,81 @@
+#include "../include/Hotplug.hpp"
+
+#include <string.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <linux/types.h>
+#include <linux/netlink.h>
+
+namespace gsr {
+ Hotplug::~Hotplug() {
+ if(fd > 0)
+ close(fd);
+ }
+
+ bool Hotplug::start() {
+ if(started)
+ return false;
+
+ struct sockaddr_nl nls = {
+ AF_NETLINK,
+ 0,
+ (unsigned int)getpid(),
+ (unsigned int)-1
+ };
+
+ fd = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);
+ if(fd == -1)
+ return false; /* Not root user */
+
+ if(bind(fd, (const struct sockaddr*)&nls, sizeof(struct sockaddr_nl))) {
+ close(fd);
+ fd = -1;
+ return false;
+ }
+
+ started = true;
+ return true;
+ }
+
+ int Hotplug::steal_fd() {
+ const int val = fd;
+ fd = -1;
+ return val;
+ }
+
+ void Hotplug::process_event_data(int fd, const HotplugEventCallback &callback) {
+ const int bytes_read = read(fd, event_data, sizeof(event_data));
+ if(bytes_read <= 0)
+ return;
+
+ /* Hotplug data ends with a newline and a null terminator */
+ int data_index = 0;
+ while(data_index < bytes_read) {
+ parse_netlink_data(event_data + data_index, callback);
+ data_index += strlen(event_data + data_index) + 1; /* Skip null terminator as well */
+ }
+ }
+
+ /* TODO: This assumes SUBSYSTEM= is output before DEVNAME=, is that always true? */
+ void Hotplug::parse_netlink_data(const char *line, const HotplugEventCallback &callback) {
+ const char *at_symbol = strchr(line, '@');
+ if(at_symbol) {
+ event_is_add = strncmp(line, "add@", 4) == 0;
+ event_is_remove = strncmp(line, "remove@", 7) == 0;
+ subsystem_is_input = false;
+ } else if(event_is_add || event_is_remove) {
+ if(strcmp(line, "SUBSYSTEM=input") == 0)
+ subsystem_is_input = true;
+
+ if(subsystem_is_input && strncmp(line, "DEVNAME=", 8) == 0) {
+ if(event_is_add)
+ callback(HotplugAction::ADD, line+8);
+ else if(event_is_remove)
+ callback(HotplugAction::REMOVE, line+8);
+
+ event_is_add = false;
+ event_is_remove = false;
+ }
+ }
+ }
+}
diff --git a/src/Overlay.cpp b/src/Overlay.cpp
index d71dd4e..c8e8b8f 100644
--- a/src/Overlay.cpp
+++ b/src/Overlay.cpp
@@ -164,7 +164,7 @@ namespace gsr {
return std::abs(a - b) <= difference;
}
- static bool is_window_fullscreen_on_monitor(Display *display, Window window, const mgl_monitor *monitor) {
+ static bool is_window_fullscreen_on_monitor(Display *display, Window window, const Monitor &monitor) {
if(!window)
return false;
@@ -173,8 +173,8 @@ namespace gsr {
return false;
const int margin = 2;
- return diff_int(geometry.x, monitor->pos.x, margin) && diff_int(geometry.y, monitor->pos.y, margin)
- && diff_int(geometry.width, monitor->size.x, margin) && diff_int(geometry.height, monitor->size.y, margin);
+ return diff_int(geometry.x, monitor.position.x, margin) && diff_int(geometry.y, monitor.position.y, margin)
+ && diff_int(geometry.width, monitor.size.x, margin) && diff_int(geometry.height, monitor.size.y, margin);
}
/*static bool is_window_fullscreen_on_monitor(Display *display, Window window, const mgl_monitor *monitors, int num_monitors) {
@@ -279,15 +279,13 @@ namespace gsr {
}
// Returns the first monitor if not found. Assumes there is at least one monitor connected.
- static const mgl_monitor* find_monitor_at_position(mgl::Window &window, mgl::vec2i pos) {
- const mgl_window *win = window.internal_window();
- assert(win->num_monitors > 0);
- for(int i = 0; i < win->num_monitors; ++i) {
- const mgl_monitor *mon = &win->monitors[i];
- if(mgl::IntRect({ mon->pos.x, mon->pos.y }, { mon->size.x, mon->size.y }).contains(pos))
- return mon;
+ static const Monitor* find_monitor_at_position(const std::vector<Monitor> &monitors, mgl::vec2i pos) {
+ assert(!monitors.empty());
+ for(const Monitor &monitor : monitors) {
+ if(mgl::IntRect(monitor.position, monitor.size).contains(pos))
+ return &monitor;
}
- return &win->monitors[0];
+ return &monitors.front();
}
static std::string get_power_supply_online_filepath() {
@@ -338,6 +336,79 @@ namespace gsr {
return true;
}
+ static Hotkey config_hotkey_to_hotkey(ConfigHotkey config_hotkey) {
+ return {
+ (uint32_t)mgl::Keyboard::key_to_x11_keysym((mgl::Keyboard::Key)config_hotkey.key),
+ config_hotkey.modifiers
+ };
+ }
+
+ static void bind_linux_hotkeys(GlobalHotkeysLinux *global_hotkeys, Overlay *overlay) {
+ global_hotkeys->bind_key_press(
+ config_hotkey_to_hotkey(overlay->get_config().main_config.show_hide_hotkey),
+ "show_hide", [overlay](const std::string &id) {
+ fprintf(stderr, "pressed %s\n", id.c_str());
+ overlay->toggle_show();
+ });
+
+ global_hotkeys->bind_key_press(
+ config_hotkey_to_hotkey(overlay->get_config().record_config.start_stop_hotkey),
+ "record", [overlay](const std::string &id) {
+ fprintf(stderr, "pressed %s\n", id.c_str());
+ overlay->toggle_record();
+ });
+
+ global_hotkeys->bind_key_press(
+ config_hotkey_to_hotkey(overlay->get_config().record_config.pause_unpause_hotkey),
+ "pause", [overlay](const std::string &id) {
+ fprintf(stderr, "pressed %s\n", id.c_str());
+ overlay->toggle_pause();
+ });
+
+ global_hotkeys->bind_key_press(
+ config_hotkey_to_hotkey(overlay->get_config().streaming_config.start_stop_hotkey),
+ "stream", [overlay](const std::string &id) {
+ fprintf(stderr, "pressed %s\n", id.c_str());
+ overlay->toggle_stream();
+ });
+
+ global_hotkeys->bind_key_press(
+ config_hotkey_to_hotkey(overlay->get_config().replay_config.start_stop_hotkey),
+ "replay_start", [overlay](const std::string &id) {
+ fprintf(stderr, "pressed %s\n", id.c_str());
+ overlay->toggle_replay();
+ });
+
+ global_hotkeys->bind_key_press(
+ config_hotkey_to_hotkey(overlay->get_config().replay_config.save_hotkey),
+ "replay_save", [overlay](const std::string &id) {
+ fprintf(stderr, "pressed %s\n", id.c_str());
+ overlay->save_replay();
+ });
+ }
+
+ static std::unique_ptr<GlobalHotkeysLinux> register_linux_hotkeys(Overlay *overlay, GlobalHotkeysLinux::GrabType grab_type) {
+ auto global_hotkeys = std::make_unique<GlobalHotkeysLinux>(grab_type);
+ if(!global_hotkeys->start())
+ fprintf(stderr, "error: failed to start global hotkeys\n");
+
+ bind_linux_hotkeys(global_hotkeys.get(), overlay);
+ return global_hotkeys;
+ }
+
+ static std::unique_ptr<GlobalHotkeysJoystick> register_joystick_hotkeys(Overlay *overlay) {
+ auto global_hotkeys_js = std::make_unique<GlobalHotkeysJoystick>();
+ if(!global_hotkeys_js->start())
+ fprintf(stderr, "Warning: failed to start joystick hotkeys\n");
+
+ global_hotkeys_js->bind_action("save_replay", [overlay](const std::string &id) {
+ fprintf(stderr, "pressed %s\n", id.c_str());
+ overlay->save_replay();
+ });
+
+ return global_hotkeys_js;
+ }
+
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)),
@@ -368,6 +439,20 @@ namespace gsr {
if(config.replay_config.turn_on_replay_automatically_mode == "turn_on_at_system_startup")
on_press_start_replay(true);
+
+ if(config.main_config.hotkeys_enable_option == "enable_hotkeys")
+ global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::ALL);
+ else if(config.main_config.hotkeys_enable_option == "enable_hotkeys_virtual_devices")
+ global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::VIRTUAL);
+
+ if(config.main_config.joystick_hotkeys_enable_option == "enable_hotkeys")
+ global_hotkeys_js = register_joystick_hotkeys(this);
+
+ x11_mapping_display = XOpenDisplay(nullptr);
+ if(x11_mapping_display)
+ XKeysymToKeycode(x11_mapping_display, XK_F1); // If we dont call we will never get a MappingNotify
+ else
+ fprintf(stderr, "Warning: XOpenDisplay failed to mapping notify\n");
}
Overlay::~Overlay() {
@@ -395,6 +480,9 @@ namespace gsr {
close_gpu_screen_recorder_output();
deinit_color_theme();
+
+ if(x11_mapping_display)
+ XCloseDisplay(x11_mapping_display);
}
void Overlay::xi_setup() {
@@ -537,7 +625,32 @@ namespace gsr {
}
}
- void Overlay::handle_events(gsr::GlobalHotkeys *global_hotkeys) {
+ void Overlay::handle_keyboard_mapping_event() {
+ if(!x11_mapping_display)
+ return;
+
+ bool mapping_updated = false;
+ while(XPending(x11_mapping_display)) {
+ XNextEvent(x11_mapping_display, &x11_mapping_xev);
+ if(x11_mapping_xev.type == MappingNotify) {
+ XRefreshKeyboardMapping(&x11_mapping_xev.xmapping);
+ mapping_updated = true;
+ }
+ }
+
+ if(mapping_updated)
+ rebind_all_keyboard_hotkeys();
+ }
+
+ void Overlay::handle_events() {
+ if(global_hotkeys)
+ global_hotkeys->poll_events();
+
+ if(global_hotkeys_js)
+ global_hotkeys_js->poll_events();
+
+ handle_keyboard_mapping_event();
+
if(!visible || !window)
return;
@@ -586,39 +699,43 @@ namespace gsr {
//force_window_on_top();
- window->clear(bg_color);
+ const bool draw_ui = show_overlay_clock.get_elapsed_time_seconds() >= show_overlay_timeout_seconds;
- if(window_texture_sprite.get_texture() && window_texture.texture_id) {
- window->draw(window_texture_sprite);
- window->draw(bg_screenshot_overlay);
- } else if(screenshot_texture.is_valid()) {
- window->draw(screenshot_sprite);
- window->draw(bg_screenshot_overlay);
- }
+ window->clear(draw_ui ? bg_color : mgl::Color(0, 0, 0, 0));
- window->draw(top_bar_background);
- window->draw(top_bar_text);
- window->draw(logo_sprite);
+ if(draw_ui) {
+ if(window_texture_sprite.get_texture() && window_texture.texture_id) {
+ window->draw(window_texture_sprite);
+ window->draw(bg_screenshot_overlay);
+ } else if(screenshot_texture.is_valid()) {
+ window->draw(screenshot_sprite);
+ window->draw(bg_screenshot_overlay);
+ }
- close_button_widget.draw(*window, mgl::vec2f(0.0f, 0.0f));
- page_stack.draw(*window, mgl::vec2f(0.0f, 0.0f));
+ window->draw(top_bar_background);
+ window->draw(top_bar_text);
+ window->draw(logo_sprite);
- if(cursor_texture.is_valid()) {
- cursor_sprite.set_position((window->get_mouse_position() - cursor_hotspot).to_vec2f());
- window->draw(cursor_sprite);
- }
+ close_button_widget.draw(*window, mgl::vec2f(0.0f, 0.0f));
+ page_stack.draw(*window, mgl::vec2f(0.0f, 0.0f));
- window->display();
+ if(cursor_texture.is_valid()) {
+ cursor_sprite.set_position((window->get_mouse_position() - cursor_hotspot).to_vec2f());
+ window->draw(cursor_sprite);
+ }
- if(!drawn_first_frame) {
- drawn_first_frame = true;
- mgl::Event event;
- event.type = mgl::Event::MouseMoved;
- event.mouse_move.x = window->get_mouse_position().x;
- event.mouse_move.y = window->get_mouse_position().y;
- on_event(event);
+ if(!drawn_first_frame) {
+ drawn_first_frame = true;
+ mgl::Event event;
+ event.type = mgl::Event::MouseMoved;
+ event.mouse_move.x = window->get_mouse_position().x;
+ event.mouse_move.y = window->get_mouse_position().y;
+ on_event(event);
+ }
}
+ window->display();
+
return true;
}
@@ -669,7 +786,15 @@ namespace gsr {
XcursorImageDestroy(cursor_image);
}
- void Overlay::xi_grab_all_devices() {
+ static bool device_is_mouse(const XIDeviceInfo *dev) {
+ for(int i = 0; i < dev->num_classes; ++i) {
+ if(dev->classes[i]->type == XIMasterPointer || dev->classes[i]->type == XISlavePointer)
+ return true;
+ }
+ return false;
+ }
+
+ void Overlay::xi_grab_all_mouse_devices() {
if(!xi_display)
return;
@@ -689,6 +814,9 @@ namespace gsr {
for (int i = 0; i < num_devices; ++i) {
const XIDeviceInfo *dev = &info[i];
+ if(!device_is_mouse(dev))
+ continue;
+
XIEventMask xi_masks;
xi_masks.deviceid = dev->deviceid;
xi_masks.mask_len = sizeof(mask);
@@ -712,21 +840,36 @@ namespace gsr {
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
+ const std::vector<Monitor> monitors = get_monitors(display);
+ if(monitors.empty()) {
+ fprintf(stderr, "gsr warning: no monitors found, not showing overlay\n");
+ window.reset();
+ return;
+ }
+
const std::string wm_name = get_window_manager_name(display);
const bool is_kwin = wm_name == "KWin";
+ const bool is_wlroots = wm_name.find("wlroots") != std::string::npos;
// The cursor position is wrong on wayland if an x11 window is not focused. On wayland we instead create a window and get the position where the wayland compositor puts it
Window x11_cursor_window = None;
const mgl::vec2i cursor_position = get_cursor_position(display, &x11_cursor_window);
const mgl::vec2i monitor_position_query_value = (x11_cursor_window || gsr_info.system_info.display_server != DisplayServer::WAYLAND) ? cursor_position : create_window_get_center_position(display);
+ const Monitor *focused_monitor = find_monitor_at_position(monitors, monitor_position_query_value);
+
// 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.
- const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || x11_cursor_window;
+ const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || x11_cursor_window || is_wlroots;
- window_size = { 32, 32 };
- window_pos = { 0, 0 };
+ if(prevent_game_minimizing) {
+ window_pos = focused_monitor->position;
+ window_size = focused_monitor->size;
+ } else {
+ window_pos = {0, 0};
+ window_size = focused_monitor->size / 2;
+ }
mgl::Window::CreateParams window_create_params;
window_create_params.size = window_size;
@@ -734,53 +877,52 @@ namespace gsr {
window_create_params.min_size = window_size;
window_create_params.max_size = window_size;
}
- window_create_params.position = window_pos;
+ window_create_params.position = focused_monitor->position + focused_monitor->size / 2 - window_size / 2;
window_create_params.hidden = prevent_game_minimizing;
window_create_params.override_redirect = prevent_game_minimizing;
- window_create_params.background_color = bg_color;
+ window_create_params.background_color = mgl::Color(0, 0, 0, 0);
window_create_params.support_alpha = true;
window_create_params.hide_decorations = true;
// MGL_WINDOW_TYPE_DIALOG is needed for kde plasma wayland in some cases, otherwise the window will pop up on another activity
// or may not be visible at all
window_create_params.window_type = (is_kwin && gsr_info.system_info.display_server == DisplayServer::WAYLAND) ? MGL_WINDOW_TYPE_DIALOG : MGL_WINDOW_TYPE_NORMAL;
- window_create_params.render_api = MGL_RENDER_API_EGL;
+ // Nvidia + Wayland + Egl doesn't work on some systems properly and it instead falls back to software rendering.
+ // Use Glx on Wayland to workaround this issue. This is fine since Egl is only needed for x11 to reliably get the texture of the fullscreen window on Nvidia
+ // when a compositor isn't running.
+ window_create_params.render_api = gsr_info.system_info.display_server == DisplayServer::WAYLAND ? MGL_RENDER_API_GLX : MGL_RENDER_API_EGL;
if(!window->create("gsr ui", window_create_params))
fprintf(stderr, "error: failed to create window\n");
+ //window->set_low_latency(true);
+
unsigned char data = 2; // Prefer being composed to allow transparency
XChangeProperty(display, window->get_system_handle(), XInternAtom(display, "_NET_WM_BYPASS_COMPOSITOR", False), XA_CARDINAL, 32, PropModeReplace, &data, 1);
data = 1;
XChangeProperty(display, window->get_system_handle(), XInternAtom(display, "GAMESCOPE_EXTERNAL_OVERLAY", False), XA_CARDINAL, 32, PropModeReplace, &data, 1);
+ const auto original_window_size = window_size;
+ window_pos = focused_monitor->position;
+ window_size = focused_monitor->size;
if(!init_theme(resources_path)) {
fprintf(stderr, "Error: failed to load theme\n");
- ::exit(1);
- }
-
- mgl_window *win = window->internal_window();
- if(win->num_monitors == 0) {
- fprintf(stderr, "gsr warning: no monitors found, not showing overlay\n");
window.reset();
return;
}
-
- const mgl_monitor *focused_monitor = find_monitor_at_position(*window, monitor_position_query_value);
- window_pos = {focused_monitor->pos.x, focused_monitor->pos.y};
- window_size = {focused_monitor->size.x, focused_monitor->size.y};
get_theme().set_window_size(window_size);
if(prevent_game_minimizing) {
window->set_size(window_size);
window->set_size_limits(window_size, window_size);
- window->set_position(window_pos);
}
+ window->set_position(focused_monitor->position + focused_monitor->size / 2 - original_window_size / 2);
+ mgl_window *win = window->internal_window();
win->cursor_position.x = cursor_position.x - window_pos.x;
win->cursor_position.y = cursor_position.y - window_pos.y;
- update_compositor_texture(focused_monitor);
+ update_compositor_texture(*focused_monitor);
bg_screenshot_overlay = mgl::Rectangle(mgl::vec2f(get_theme().window_width, get_theme().window_height));
top_bar_background = mgl::Rectangle(mgl::vec2f(get_theme().window_width, get_theme().window_height*0.06f).floor());
@@ -902,7 +1044,8 @@ namespace gsr {
button->set_bg_hover_color(mgl::Color(0, 0, 0, 255));
button->set_icon(&get_theme().settings_small_texture);
button->on_click = [&]() {
- auto settings_page = std::make_unique<GlobalSettingsPage>(&gsr_info, config, &page_stack);
+ auto settings_page = std::make_unique<GlobalSettingsPage>(this, &gsr_info, config, &page_stack);
+
settings_page->on_startup_changed = [&](bool enable, int exit_status) {
if(exit_status == 0)
return;
@@ -917,10 +1060,30 @@ namespace gsr {
show_notification("Failed to remove GPU Screen Recorder from system startup", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE);
}
};
- settings_page->on_click_exit_program_button = [&](const char *reason) {
+
+ settings_page->on_click_exit_program_button = [this](const char *reason) {
do_exit = true;
exit_reason = reason;
};
+
+ settings_page->on_keyboard_hotkey_changed = [this](const char *hotkey_option) {
+ global_hotkeys.reset();
+ if(strcmp(hotkey_option, "enable_hotkeys") == 0)
+ global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::ALL);
+ else if(strcmp(hotkey_option, "enable_hotkeys_virtual_devices") == 0)
+ global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::VIRTUAL);
+ else if(strcmp(hotkey_option, "disable_hotkeys") == 0)
+ global_hotkeys.reset();
+ };
+
+ settings_page->on_joystick_hotkey_changed = [this](const char *hotkey_option) {
+ global_hotkeys_js.reset();
+ if(strcmp(hotkey_option, "enable_hotkeys") == 0)
+ global_hotkeys_js = register_joystick_hotkeys(this);
+ else if(strcmp(hotkey_option, "disable_hotkeys") == 0)
+ global_hotkeys_js.reset();
+ };
+
page_stack.push(std::move(settings_page));
};
front_page_ptr->add_widget(std::move(button));
@@ -955,7 +1118,8 @@ namespace gsr {
// The focused application can be an xwayland application but the cursor can hover over a wayland application.
// This is even the case when hovering over the titlebar of the xwayland application.
- if(prevent_game_minimizing)
+ const bool fake_cursor = is_wlroots ? x11_cursor_window != None : prevent_game_minimizing;
+ if(fake_cursor)
xi_setup();
//window->set_fullscreen(true);
@@ -981,14 +1145,15 @@ namespace gsr {
// We want to grab all devices to prevent any other application below the UI from receiving events.
// Owlboy seems to use xi events and XGrabPointer doesn't prevent owlboy from receiving events.
- xi_grab_all_devices();
+ xi_grab_all_mouse_devices();
// if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) {
// set_focused_window(display, window->get_system_handle());
// XFlush(display);
// }
- window->set_fullscreen(true);
+ if(!is_wlroots)
+ window->set_fullscreen(true);
visible = true;
@@ -1010,6 +1175,12 @@ namespace gsr {
if(paused)
update_ui_recording_paused();
+
+ // Wayland compositors have retarded fullscreen animations that we cant disable in a proper way
+ // without messing up window position.
+ show_overlay_timeout_seconds = prevent_game_minimizing ? 0.0 : 0.15;
+ show_overlay_clock.restart();
+ draw();
}
void Overlay::hide() {
@@ -1073,6 +1244,16 @@ namespace gsr {
}
if(window) {
+ if(show_overlay_timeout_seconds > 0.0001) {
+ window->clear(mgl::Color(0, 0, 0, 0));
+ window->display();
+
+ mgl_context *context = mgl_get_context();
+ context->gl.glFlush();
+ context->gl.glFinish();
+ usleep(50 * 1000); // EGL doesn't do an immediate flush for some reason
+ }
+
window->set_visible(false);
window.reset();
}
@@ -1183,6 +1364,18 @@ namespace gsr {
return config;
}
+ void Overlay::unbind_all_keyboard_hotkeys() {
+ if(global_hotkeys)
+ global_hotkeys->unbind_all_keys();
+ }
+
+ void Overlay::rebind_all_keyboard_hotkeys() {
+ unbind_all_keyboard_hotkeys();
+ // TODO: Check if type is GlobalHotkeysLinux
+ if(global_hotkeys)
+ bind_linux_hotkeys(static_cast<GlobalHotkeysLinux*>(global_hotkeys.get()), this);
+ }
+
void Overlay::update_notification_process_status() {
if(notification_process <= 0)
return;
@@ -1673,6 +1866,11 @@ namespace gsr {
"-o", output_directory.c_str()
};
+ if(config.replay_config.restart_replay_on_save && gsr_info.system_info.gsr_version >= GsrVersion{5, 0, 3}) {
+ args.push_back("-restart-replay-on-save");
+ args.push_back("yes");
+ }
+
add_common_gpu_screen_recorder_args(args, config.replay_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged);
args.push_back(nullptr);
@@ -1951,7 +2149,7 @@ namespace gsr {
show_notification("Streaming has started", 3.0, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM);
}
- bool Overlay::update_compositor_texture(const mgl_monitor *monitor) {
+ bool Overlay::update_compositor_texture(const Monitor &monitor) {
window_texture_deinit(&window_texture);
window_texture_sprite.set_texture(nullptr);
screenshot_texture.clear();
@@ -1974,7 +2172,7 @@ namespace gsr {
window_texture_texture = mgl::Texture(window_texture.texture_id, MGL_TEXTURE_FORMAT_RGB);
window_texture_sprite.set_texture(&window_texture_texture);
} else {
- XImage *img = XGetImage(display, DefaultRootWindow(display), monitor->pos.x, monitor->pos.y, monitor->size.x, monitor->size.y, AllPlanes, ZPixmap);
+ XImage *img = XGetImage(display, DefaultRootWindow(display), monitor.position.x, monitor.position.y, monitor.size.x, monitor.size.y, AllPlanes, ZPixmap);
if(!img)
fprintf(stderr, "Error: failed to take a screenshot\n");
@@ -2000,4 +2198,4 @@ namespace gsr {
XFlush(display);
}
}
-} \ No newline at end of file
+}
diff --git a/src/Rpc.cpp b/src/Rpc.cpp
index 206b1cf..3eec98d 100644
--- a/src/Rpc.cpp
+++ b/src/Rpc.cpp
@@ -6,7 +6,7 @@
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
-#include <sys/fcntl.h>
+#include <fcntl.h>
namespace gsr {
static void get_runtime_filepath(char *buffer, size_t buffer_size, const char *filename) {
diff --git a/src/WindowUtils.cpp b/src/WindowUtils.cpp
index 7631e4d..d8e3508 100644
--- a/src/WindowUtils.cpp
+++ b/src/WindowUtils.cpp
@@ -4,11 +4,20 @@
#include <X11/Xatom.h>
#include <X11/Xutil.h>
+#include <mglpp/system/Utf8.hpp>
+
+extern "C" {
+#include <mgl/window/window.h>
+}
+
#include <stdbool.h>
+#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
+#include <optional>
+
#define MAX_PROPERTY_VALUE_LEN 4096
namespace gsr {
@@ -89,15 +98,16 @@ namespace gsr {
return found_window;
}
- static Window get_window_at_cursor_position(Display *dpy) {
+ mgl::vec2i get_cursor_position(Display *dpy, Window *window) {
Window root_window = None;
- Window window = None;
+ *window = None;
int dummy_i;
unsigned int dummy_u;
- XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &dummy_i, &dummy_i, &dummy_i, &dummy_i, &dummy_u);
+ mgl::vec2i root_pos;
+ XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, window, &root_pos.x, &root_pos.y, &dummy_i, &dummy_i, &dummy_u);
if(window)
- window = window_get_target_window_child(dpy, window);
- return window;
+ *window = window_get_target_window_child(dpy, *window);
+ return root_pos;
}
Window get_focused_window(Display *dpy, WindowCaptureType cap_type) {
@@ -127,14 +137,33 @@ namespace gsr {
return focused_window;
}
- focused_window = get_window_at_cursor_position(dpy);
+ get_cursor_position(dpy, &focused_window);
if(focused_window && focused_window != DefaultRootWindow(dpy))
return focused_window;
return None;
}
- static char* get_window_title(Display *dpy, Window window) {
+ static std::string utf8_sanitize(const uint8_t *str, int size) {
+ const uint32_t zero_width_space_codepoint = 0x200b; // Some games such as the finals has zero-width space characters
+ std::string result;
+ for(int i = 0; i < size;) {
+ // Some games such as the finals has utf8-bom between each character, wtf?
+ if(i + 3 < size && memcmp(str + i, "\xEF\xBB\xBF", 3) == 0) {
+ i += 3;
+ continue;
+ }
+
+ uint32_t codepoint = 0;
+ size_t codepoint_length = 1;
+ if(mgl::utf8_decode(str + i, size - i, &codepoint, &codepoint_length) && codepoint != zero_width_space_codepoint)
+ result.append((const char*)str + i, codepoint_length);
+ i += codepoint_length;
+ }
+ return result;
+ }
+
+ static std::optional<std::string> get_window_title(Display *dpy, Window window) {
const Atom net_wm_name_atom = XInternAtom(dpy, "_NET_WM_NAME", False);
const Atom wm_name_atom = XInternAtom(dpy, "WM_NAME", False);
const Atom utf8_string_atom = XInternAtom(dpy, "UTF8_STRING", False);
@@ -147,7 +176,7 @@ namespace gsr {
XGetWindowProperty(dpy, window, net_wm_name_atom, 0, 1024, False, utf8_string_atom, &type, &format, &num_items, &bytes_left, &data);
if(type == utf8_string_atom && format == 8 && data)
- return (char*)data;
+ return utf8_sanitize(data, num_items);
type = None;
format = 0;
@@ -157,16 +186,18 @@ namespace gsr {
XGetWindowProperty(dpy, window, wm_name_atom, 0, 1024, False, 0, &type, &format, &num_items, &bytes_left, &data);
if((type == XA_STRING || type == utf8_string_atom) && data)
- return (char*)data;
+ return utf8_sanitize(data, num_items);
- return NULL;
+ return std::nullopt;
}
- static const char* strip(const char *str, int *len) {
- int str_len = strlen(str);
+ static std::string strip(const std::string &str) {
+ int start_index = 0;
+ int str_len = str.size();
+
for(int i = 0; i < str_len; ++i) {
if(str[i] != ' ') {
- str += i;
+ start_index += i;
str_len -= i;
break;
}
@@ -179,14 +210,7 @@ namespace gsr {
}
}
- *len = str_len;
- return str;
- }
-
- static std::string strip_strip(const char *str) {
- int len = 0;
- str = strip(str, &len);
- return std::string(str, len);
+ return str.substr(start_index, str_len);
}
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type) {
@@ -196,52 +220,22 @@ namespace gsr {
return result;
// Window title is not always ideal (for example for a browser), but for games its pretty much required
- char *window_title = get_window_title(dpy, focused_window);
+ const std::optional<std::string> window_title = get_window_title(dpy, focused_window);
if(window_title) {
- result = strip_strip(window_title);
- XFree(window_title);
+ result = strip(window_title.value());
return result;
}
XClassHint class_hint = {nullptr, nullptr};
XGetClassHint(dpy, focused_window, &class_hint);
if(class_hint.res_class) {
- result = strip_strip(class_hint.res_class);
+ result = strip(class_hint.res_class);
return result;
}
return result;
}
- mgl::vec2i get_cursor_position(Display *dpy, Window *window) {
- Window root_window = None;
- *window = None;
- int dummy_i;
- unsigned int dummy_u;
- mgl::vec2i root_pos;
- XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, window, &root_pos.x, &root_pos.y, &dummy_i, &dummy_i, &dummy_u);
-
- // This dumb shit is done to satisfy gnome wayland. Only set |window| if a valid x11 window is focused
- if(window) {
- XWindowAttributes attr;
- if(XGetWindowAttributes(dpy, *window, &attr) && attr.override_redirect)
- *window = None;
-
- int revert_to = 0;
- Window input_focus_window = None;
- if(XGetInputFocus(dpy, &input_focus_window, &revert_to)) {
- if(input_focus_window) {
- if(XGetWindowAttributes(dpy, input_focus_window, &attr) && attr.override_redirect)
- *window = None;
- } else {
- *window = None;
- }
- }
- }
-
- return root_pos;
- }
-
typedef struct {
unsigned long flags;
unsigned long functions;
@@ -312,7 +306,7 @@ namespace gsr {
poll_fd.fd = x_fd;
poll_fd.events = POLLIN;
poll_fd.revents = 0;
- const int fds_ready = poll(&poll_fd, 1, 1000);
+ const int fds_ready = poll(&poll_fd, 1, 200);
if(fds_ready == 0) {
fprintf(stderr, "Error: timed out waiting for ConfigureNotify after XCreateWindow\n");
break;
@@ -320,15 +314,18 @@ namespace gsr {
continue;
}
- XNextEvent(display, &xev);
- if(xev.type == ConfigureNotify && xev.xconfigure.window == window) {
- got_data = xev.xconfigure.x > 0 && xev.xconfigure.y > 0;
- position.x = xev.xconfigure.x + xev.xconfigure.width / 2;
- position.y = xev.xconfigure.y + xev.xconfigure.height / 2;
- break;
+ while(XPending(display)) {
+ XNextEvent(display, &xev);
+ if(xev.type == ConfigureNotify && xev.xconfigure.window == window) {
+ got_data = xev.xconfigure.x > 0 && xev.xconfigure.y > 0;
+ position.x = xev.xconfigure.x + xev.xconfigure.width / 2;
+ position.y = xev.xconfigure.y + xev.xconfigure.height / 2;
+ goto done;
+ }
}
}
+ done:
XDestroyWindow(display, window);
XFlush(display);
@@ -373,7 +370,7 @@ namespace gsr {
poll_fd.fd = x_fd;
poll_fd.events = POLLIN;
poll_fd.revents = 0;
- const int fds_ready = poll(&poll_fd, 1, 1000);
+ const int fds_ready = poll(&poll_fd, 1, 200);
if(fds_ready == 0) {
fprintf(stderr, "Error: timed out waiting for MapNotify/ConfigureNotify after XCreateWindow\n");
break;
@@ -381,27 +378,30 @@ namespace gsr {
continue;
}
- XNextEvent(display, &xev);
- if(xev.type == MapNotify && xev.xmap.window == window) {
- int x = 0;
- int y = 0;
- Window w = None;
- XTranslateCoordinates(display, window, DefaultRootWindow(display), 0, 0, &x, &y, &w);
-
- got_data = x > 0 && y > 0;
- position.x = x + size / 2;
- position.y = y + size / 2;
- if(got_data)
- break;
- } else if(xev.type == ConfigureNotify && xev.xconfigure.window == window) {
- got_data = xev.xconfigure.x > 0 && xev.xconfigure.y > 0;
- position.x = xev.xconfigure.x + xev.xconfigure.width / 2;
- position.y = xev.xconfigure.y + xev.xconfigure.height / 2;
- if(got_data)
- break;
+ while(XPending(display)) {
+ XNextEvent(display, &xev);
+ if(xev.type == MapNotify && xev.xmap.window == window) {
+ int x = 0;
+ int y = 0;
+ Window w = None;
+ XTranslateCoordinates(display, window, DefaultRootWindow(display), 0, 0, &x, &y, &w);
+
+ got_data = x > 0 && y > 0;
+ position.x = x + size / 2;
+ position.y = y + size / 2;
+ if(got_data)
+ goto done;
+ } else if(xev.type == ConfigureNotify && xev.xconfigure.window == window) {
+ got_data = xev.xconfigure.x > 0 && xev.xconfigure.y > 0;
+ position.x = xev.xconfigure.x + xev.xconfigure.width / 2;
+ position.y = xev.xconfigure.y + xev.xconfigure.height / 2;
+ if(got_data)
+ goto done;
+ }
}
}
+ done:
XDestroyWindow(display, window);
XFlush(display);
@@ -442,11 +442,9 @@ namespace gsr {
if(!window)
return wm_name;
- char *window_title = get_window_title(display, window);
- if(window_title) {
- wm_name = strip_strip(window_title);
- XFree(window_title);
- }
+ const std::optional<std::string> window_title = get_window_title(display, window);
+ if(window_title)
+ wm_name = strip(window_title.value());
return wm_name;
}
@@ -454,7 +452,18 @@ namespace gsr {
bool is_compositor_running(Display *dpy, int screen) {
char prop_name[20];
snprintf(prop_name, sizeof(prop_name), "_NET_WM_CM_S%d", screen);
- Atom prop_atom = XInternAtom(dpy, prop_name, False);
+ const Atom prop_atom = XInternAtom(dpy, prop_name, False);
return XGetSelectionOwner(dpy, prop_atom) != None;
}
+
+ static void get_monitors_callback(const mgl_monitor *monitor, void *userdata) {
+ std::vector<Monitor> *monitors = (std::vector<Monitor>*)userdata;
+ monitors->push_back({mgl::vec2i(monitor->pos.x, monitor->pos.y), mgl::vec2i(monitor->size.x, monitor->size.y)});
+ }
+
+ std::vector<Monitor> get_monitors(Display *dpy) {
+ std::vector<Monitor> monitors;
+ mgl_for_each_active_monitor_output(dpy, get_monitors_callback, &monitors);
+ return monitors;
+ }
} \ No newline at end of file
diff --git a/src/gui/DropdownButton.cpp b/src/gui/DropdownButton.cpp
index 4a2ae3a..81bc015 100644
--- a/src/gui/DropdownButton.cpp
+++ b/src/gui/DropdownButton.cpp
@@ -20,7 +20,7 @@ namespace gsr {
{
if(icon_texture && icon_texture->is_valid()) {
icon_sprite.set_texture(icon_texture);
- icon_sprite.set_height((int)(size.y * 0.5f));
+ icon_sprite.set_height((int)(size.y * 0.45f));
}
this->description.set_color(mgl::Color(150, 150, 150));
}
@@ -242,4 +242,4 @@ namespace gsr {
update_if_dirty();
return size;
}
-} \ No newline at end of file
+}
diff --git a/src/gui/GlobalSettingsPage.cpp b/src/gui/GlobalSettingsPage.cpp
index d3d440d..81e1d9a 100644
--- a/src/gui/GlobalSettingsPage.cpp
+++ b/src/gui/GlobalSettingsPage.cpp
@@ -1,5 +1,7 @@
#include "../../include/gui/GlobalSettingsPage.hpp"
+#include "../../include/Overlay.hpp"
+#include "../../include/GlobalHotkeys.hpp"
#include "../../include/Theme.hpp"
#include "../../include/Process.hpp"
#include "../../include/gui/GsrPage.hpp"
@@ -9,6 +11,25 @@
#include "../../include/gui/List.hpp"
#include "../../include/gui/Label.hpp"
#include "../../include/gui/RadioButton.hpp"
+#include "../../include/gui/LineSeparator.hpp"
+#include "../../include/gui/CustomRendererWidget.hpp"
+
+#include <assert.h>
+#include <X11/Xlib.h>
+extern "C" {
+#include <mgl/mgl.h>
+}
+#include <mglpp/window/Window.hpp>
+#include <mglpp/graphics/Rectangle.hpp>
+#include <mglpp/graphics/Text.hpp>
+
+#ifndef GSR_UI_VERSION
+#define GSR_UI_VERSION "Unknown"
+#endif
+
+#ifndef GSR_FLATPAK_VERSION
+#define GSR_FLATPAK_VERSION "Unknown"
+#endif
namespace gsr {
static const char* gpu_vendor_to_color_name(GpuVendor vendor) {
@@ -21,8 +42,74 @@ namespace gsr {
return "amd";
}
- GlobalSettingsPage::GlobalSettingsPage(const GsrInfo *gsr_info, Config &config, PageStack *page_stack) :
+ static const char* gpu_vendor_to_string(GpuVendor vendor) {
+ switch(vendor) {
+ case GpuVendor::UNKNOWN: return "Unknown";
+ case GpuVendor::AMD: return "AMD";
+ case GpuVendor::INTEL: return "Intel";
+ case GpuVendor::NVIDIA: return "NVIDIA";
+ }
+ return "unknown";
+ }
+
+ static uint32_t mgl_modifier_to_hotkey_modifier(mgl::Keyboard::Key modifier_key) {
+ switch(modifier_key) {
+ case mgl::Keyboard::LControl: return HOTKEY_MOD_LCTRL;
+ case mgl::Keyboard::LShift: return HOTKEY_MOD_LSHIFT;
+ case mgl::Keyboard::LAlt: return HOTKEY_MOD_LALT;
+ case mgl::Keyboard::LSystem: return HOTKEY_MOD_LSUPER;
+ case mgl::Keyboard::RControl: return HOTKEY_MOD_RCTRL;
+ case mgl::Keyboard::RShift: return HOTKEY_MOD_RSHIFT;
+ case mgl::Keyboard::RAlt: return HOTKEY_MOD_RALT;
+ case mgl::Keyboard::RSystem: return HOTKEY_MOD_RSUPER;
+ default: return 0;
+ }
+ return 0;
+ }
+
+ static std::vector<mgl::Keyboard::Key> hotkey_modifiers_to_mgl_keys(uint32_t modifiers) {
+ std::vector<mgl::Keyboard::Key> result;
+ if(modifiers & HOTKEY_MOD_LCTRL)
+ result.push_back(mgl::Keyboard::LControl);
+ if(modifiers & HOTKEY_MOD_LSHIFT)
+ result.push_back(mgl::Keyboard::LShift);
+ if(modifiers & HOTKEY_MOD_LALT)
+ result.push_back(mgl::Keyboard::LAlt);
+ if(modifiers & HOTKEY_MOD_LSUPER)
+ result.push_back(mgl::Keyboard::LSystem);
+ if(modifiers & HOTKEY_MOD_RCTRL)
+ result.push_back(mgl::Keyboard::RControl);
+ if(modifiers & HOTKEY_MOD_RSHIFT)
+ result.push_back(mgl::Keyboard::RShift);
+ if(modifiers & HOTKEY_MOD_RALT)
+ result.push_back(mgl::Keyboard::RAlt);
+ if(modifiers & HOTKEY_MOD_RSUPER)
+ result.push_back(mgl::Keyboard::RSystem);
+ return result;
+ }
+
+ static std::string config_hotkey_to_string(ConfigHotkey config_hotkey) {
+ std::string result;
+
+ const std::vector<mgl::Keyboard::Key> modifier_keys = hotkey_modifiers_to_mgl_keys(config_hotkey.modifiers);
+ for(const mgl::Keyboard::Key modifier_key : modifier_keys) {
+ if(!result.empty())
+ result += " + ";
+ result += mgl::Keyboard::key_to_string(modifier_key);
+ }
+
+ if(config_hotkey.key != 0) {
+ if(!result.empty())
+ result += " + ";
+ result += mgl::Keyboard::key_to_string((mgl::Keyboard::Key)config_hotkey.key);
+ }
+
+ return result;
+ }
+
+ GlobalSettingsPage::GlobalSettingsPage(Overlay *overlay, const GsrInfo *gsr_info, Config &config, PageStack *page_stack) :
StaticPage(mgl::vec2f(get_theme().window_width, get_theme().window_height).floor()),
+ overlay(overlay),
config(config),
gsr_info(gsr_info),
page_stack(page_stack)
@@ -38,6 +125,48 @@ namespace gsr {
add_widgets();
load();
+
+ auto hotkey_overlay = std::make_unique<CustomRendererWidget>(get_size());
+ hotkey_overlay->draw_handler = [this](mgl::Window &window, mgl::vec2f, mgl::vec2f) {
+ Button *configure_hotkey_button = configure_hotkey_get_button_by_active_type();
+ if(!configure_hotkey_button)
+ return;
+
+ mgl::Text title_text("Press a key combination to use for the hotkey \"" + hotkey_configure_action_name + "\":", get_theme().title_font);
+ mgl::Text hotkey_text(configure_hotkey_button->get_text(), get_theme().top_bar_font);
+ mgl::Text description_text("The hotkey has to contain one or more of these keys: Alt, Ctrl, Shift and Super. Press Esc to cancel.", get_theme().body_font);
+ const float text_max_width = std::max(title_text.get_bounds().size.x, std::max(hotkey_text.get_bounds().size.x, description_text.get_bounds().size.x));
+
+ const float padding_horizontal = int(get_theme().window_height * 0.01f);
+ const float padding_vertical = int(get_theme().window_height * 0.01f);
+
+ const mgl::vec2f bg_size = mgl::vec2f(text_max_width + padding_horizontal*2.0f, get_theme().window_height * 0.1f).floor();
+ mgl::Rectangle bg_rect(mgl::vec2f(get_theme().window_width*0.5f - bg_size.x*0.5f, get_theme().window_height*0.5f - bg_size.y*0.5f).floor(), bg_size);
+ bg_rect.set_color(get_color_theme().page_bg_color);
+ window.draw(bg_rect);
+
+ const mgl::vec2f tint_size = mgl::vec2f(bg_size.x, 0.004f * get_theme().window_height).floor();
+ mgl::Rectangle tint_rect(bg_rect.get_position() - mgl::vec2f(0.0f, tint_size.y), tint_size);
+ tint_rect.set_color(get_color_theme().tint_color);
+ window.draw(tint_rect);
+
+ title_text.set_position(mgl::vec2f(bg_rect.get_position() + mgl::vec2f(bg_rect.get_size().x*0.5f - title_text.get_bounds().size.x*0.5f, padding_vertical)).floor());
+ window.draw(title_text);
+
+ hotkey_text.set_position(mgl::vec2f(bg_rect.get_position() + bg_rect.get_size()*0.5f - hotkey_text.get_bounds().size*0.5f).floor());
+ window.draw(hotkey_text);
+
+ const float caret_padding_x = int(0.001f * get_theme().window_height);
+ const mgl::vec2f caret_size = mgl::vec2f(std::max(2.0f, 0.002f * get_theme().window_height), hotkey_text.get_bounds().size.y).floor();
+ mgl::Rectangle caret_rect(hotkey_text.get_position() + mgl::vec2f(hotkey_text.get_bounds().size.x + caret_padding_x, hotkey_text.get_bounds().size.y*0.5f - caret_size.y*0.5f).floor(), caret_size);
+ window.draw(caret_rect);
+
+ description_text.set_position(mgl::vec2f(bg_rect.get_position() + mgl::vec2f(bg_rect.get_size().x*0.5f - description_text.get_bounds().size.x*0.5f, bg_rect.get_size().y - description_text.get_bounds().size.y - padding_vertical)).floor());
+ window.draw(description_text);
+ };
+ hotkey_overlay->set_visible(false);
+ hotkey_overlay_ptr = hotkey_overlay.get();
+ add_widget(std::move(hotkey_overlay));
}
std::unique_ptr<Subsection> GlobalSettingsPage::create_appearance_subsection(ScrollablePage *parent_page) {
@@ -88,29 +217,160 @@ namespace gsr {
return std::make_unique<Subsection>("Startup", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
}
- std::unique_ptr<Subsection> GlobalSettingsPage::create_hotkey_subsection(ScrollablePage *parent_page) {
- const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
-
- auto enable_hotkeys_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::VERTICAL);
- enable_hotkeys_radio_button_ptr = enable_hotkeys_radio_button.get();
- enable_hotkeys_radio_button->add_item("Enable hotkeys", "enable_hotkeys");
- if(!inside_flatpak)
- enable_hotkeys_radio_button->add_item("Disable hotkeys", "disable_hotkeys");
+ std::unique_ptr<RadioButton> GlobalSettingsPage::create_enable_keyboard_hotkeys_button() {
+ auto enable_hotkeys_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL);
+ enable_keyboard_hotkeys_radio_button_ptr = enable_hotkeys_radio_button.get();
+ enable_hotkeys_radio_button->add_item("Yes", "enable_hotkeys");
+ enable_hotkeys_radio_button->add_item("No", "disable_hotkeys");
enable_hotkeys_radio_button->add_item("Only grab virtual devices (supports input remapping software)", "enable_hotkeys_virtual_devices");
enable_hotkeys_radio_button->on_selection_changed = [&](const std::string&, const std::string &id) {
- if(!on_click_exit_program_button)
- return true;
-
- if(id == "enable_hotkeys")
- on_click_exit_program_button("restart");
- else if(id == "disable_hotkeys")
- on_click_exit_program_button("restart");
- else if(id == "enable_hotkeys_virtual_devices")
- on_click_exit_program_button("restart");
+ if(on_keyboard_hotkey_changed)
+ on_keyboard_hotkey_changed(id.c_str());
+ return true;
+ };
+ return enable_hotkeys_radio_button;
+ }
+ std::unique_ptr<RadioButton> GlobalSettingsPage::create_enable_joystick_hotkeys_button() {
+ auto enable_hotkeys_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL);
+ enable_joystick_hotkeys_radio_button_ptr = enable_hotkeys_radio_button.get();
+ enable_hotkeys_radio_button->add_item("Yes", "enable_hotkeys");
+ enable_hotkeys_radio_button->add_item("No", "disable_hotkeys");
+ enable_hotkeys_radio_button->on_selection_changed = [&](const std::string&, const std::string &id) {
+ if(on_joystick_hotkey_changed)
+ on_joystick_hotkey_changed(id.c_str());
return true;
};
- return std::make_unique<Subsection>("Hotkeys", std::move(enable_hotkeys_radio_button), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
+ return enable_hotkeys_radio_button;
+ }
+
+ std::unique_ptr<List> GlobalSettingsPage::create_show_hide_hotkey_options() {
+ auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
+
+ list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Show/hide UI:", get_color_theme().text_color));
+ auto show_hide_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
+ show_hide_button_ptr = show_hide_button.get();
+ list->add_widget(std::move(show_hide_button));
+
+ show_hide_button_ptr->on_click = [this] {
+ configure_hotkey_start(ConfigureHotkeyType::SHOW_HIDE);
+ };
+
+ return list;
+ }
+
+ std::unique_ptr<List> GlobalSettingsPage::create_replay_hotkey_options() {
+ auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
+
+ list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Turn replay on/off:", get_color_theme().text_color));
+ auto turn_replay_on_off_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
+ turn_replay_on_off_button_ptr = turn_replay_on_off_button.get();
+ list->add_widget(std::move(turn_replay_on_off_button));
+
+ list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Save replay:", get_color_theme().text_color));
+ auto save_replay_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
+ save_replay_button_ptr = save_replay_button.get();
+ list->add_widget(std::move(save_replay_button));
+
+ turn_replay_on_off_button_ptr->on_click = [this] {
+ configure_hotkey_start(ConfigureHotkeyType::REPLAY_START_STOP);
+ };
+
+ save_replay_button_ptr->on_click = [this] {
+ configure_hotkey_start(ConfigureHotkeyType::REPLAY_SAVE);
+ };
+
+ return list;
+ }
+
+ std::unique_ptr<List> GlobalSettingsPage::create_record_hotkey_options() {
+ auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
+
+ list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Start/stop recording:", get_color_theme().text_color));
+ auto start_stop_recording_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
+ start_stop_recording_button_ptr = start_stop_recording_button.get();
+ list->add_widget(std::move(start_stop_recording_button));
+
+ list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Pause/unpause recording:", get_color_theme().text_color));
+ auto pause_unpause_recording_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
+ pause_unpause_recording_button_ptr = pause_unpause_recording_button.get();
+ list->add_widget(std::move(pause_unpause_recording_button));
+
+ start_stop_recording_button_ptr->on_click = [this] {
+ configure_hotkey_start(ConfigureHotkeyType::RECORD_START_STOP);
+ };
+
+ pause_unpause_recording_button_ptr->on_click = [this] {
+ configure_hotkey_start(ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE);
+ };
+
+ return list;
+ }
+
+ std::unique_ptr<List> GlobalSettingsPage::create_stream_hotkey_options() {
+ auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
+
+ list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Start/stop streaming:", get_color_theme().text_color));
+ auto start_stop_streaming_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
+ start_stop_streaming_button_ptr = start_stop_streaming_button.get();
+ list->add_widget(std::move(start_stop_streaming_button));
+
+ start_stop_streaming_button_ptr->on_click = [this] {
+ configure_hotkey_start(ConfigureHotkeyType::STREAM_START_STOP);
+ };
+
+ return list;
+ }
+
+ std::unique_ptr<List> GlobalSettingsPage::create_hotkey_control_buttons() {
+ auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
+
+ // auto clear_hotkeys_button = std::make_unique<Button>(&get_theme().body_font, "Clear hotkeys", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
+ // clear_hotkeys_button->on_click = [this] {
+ // config.streaming_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
+ // config.record_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
+ // config.record_config.pause_unpause_hotkey = {mgl::Keyboard::Unknown, 0};
+ // config.replay_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
+ // config.replay_config.save_hotkey = {mgl::Keyboard::Unknown, 0};
+ // config.main_config.show_hide_hotkey = {mgl::Keyboard::Unknown, 0};
+ // load_hotkeys();
+ // overlay->rebind_all_keyboard_hotkeys();
+ // };
+ // list->add_widget(std::move(clear_hotkeys_button));
+
+ auto reset_hotkeys_button = std::make_unique<Button>(&get_theme().body_font, "Reset hotkeys to default", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
+ reset_hotkeys_button->on_click = [this] {
+ config.streaming_config.start_stop_hotkey = {mgl::Keyboard::F8, HOTKEY_MOD_LALT};
+ config.record_config.start_stop_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LALT};
+ config.record_config.pause_unpause_hotkey = {mgl::Keyboard::F7, HOTKEY_MOD_LALT};
+ config.replay_config.start_stop_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT | HOTKEY_MOD_LSHIFT};
+ config.replay_config.save_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT};
+ config.main_config.show_hide_hotkey = {mgl::Keyboard::Z, HOTKEY_MOD_LALT};
+ load_hotkeys();
+ overlay->rebind_all_keyboard_hotkeys();
+ };
+ list->add_widget(std::move(reset_hotkeys_button));
+
+ return list;
+ }
+
+ std::unique_ptr<Subsection> GlobalSettingsPage::create_hotkey_subsection(ScrollablePage *parent_page) {
+ auto list = std::make_unique<List>(List::Orientation::VERTICAL);
+ List *list_ptr = list.get();
+ auto subsection = std::make_unique<Subsection>("Hotkeys", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
+
+ list_ptr->add_widget(std::make_unique<Label>(&get_theme().body_font, "Enable keyboard hotkeys?", get_color_theme().text_color));
+ list_ptr->add_widget(create_enable_keyboard_hotkeys_button());
+ list_ptr->add_widget(std::make_unique<Label>(&get_theme().body_font, "Enable controller hotkeys?", get_color_theme().text_color));
+ list_ptr->add_widget(create_enable_joystick_hotkeys_button());
+ list_ptr->add_widget(std::make_unique<LineSeparator>(LineSeparator::Orientation::HORIZONTAL, subsection->get_inner_size().x));
+ list_ptr->add_widget(create_show_hide_hotkey_options());
+ list_ptr->add_widget(create_replay_hotkey_options());
+ list_ptr->add_widget(create_record_hotkey_options());
+ list_ptr->add_widget(create_stream_hotkey_options());
+ list_ptr->add_widget(std::make_unique<Label>(&get_theme().body_font, "Double-click the controller share button to save a replay", get_color_theme().text_color));
+ list_ptr->add_widget(create_hotkey_control_buttons());
+ return subsection;
}
std::unique_ptr<Button> GlobalSettingsPage::create_exit_program_button() {
@@ -140,6 +400,29 @@ namespace gsr {
return std::make_unique<Subsection>("Application options", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
}
+ std::unique_ptr<Subsection> GlobalSettingsPage::create_application_info_subsection(ScrollablePage *parent_page) {
+ const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
+ auto list = std::make_unique<List>(List::Orientation::VERTICAL);
+
+ char str[128];
+ const std::string gsr_version = gsr_info->system_info.gsr_version.to_string();
+ snprintf(str, sizeof(str), "GSR version: %s", gsr_version.c_str());
+ list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color));
+
+ snprintf(str, sizeof(str), "GSR-UI version: %s", GSR_UI_VERSION);
+ list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color));
+
+ if(inside_flatpak) {
+ snprintf(str, sizeof(str), "Flatpak version: %s", GSR_FLATPAK_VERSION);
+ list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color));
+ }
+
+ snprintf(str, sizeof(str), "GPU vendor: %s", gpu_vendor_to_string(gsr_info->gpu_info.vendor));
+ list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color));
+
+ return std::make_unique<Subsection>("Application info", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
+ }
+
void GlobalSettingsPage::add_widgets() {
auto scrollable_page = std::make_unique<ScrollablePage>(content_page_ptr->get_inner_size());
@@ -149,6 +432,7 @@ namespace gsr {
settings_list->add_widget(create_startup_subsection(scrollable_page.get()));
settings_list->add_widget(create_hotkey_subsection(scrollable_page.get()));
settings_list->add_widget(create_application_options_subsection(scrollable_page.get()));
+ settings_list->add_widget(create_application_info_subsection(scrollable_page.get()));
scrollable_page->add_widget(std::move(settings_list));
content_page_ptr->add_widget(std::move(scrollable_page));
@@ -169,12 +453,201 @@ namespace gsr {
const int exit_status = exec_program_on_host_get_stdout(args, stdout_str);
startup_radio_button_ptr->set_selected_item(exit_status == 0 ? "start_on_system_startup" : "dont_start_on_system_startup", false, false);
- enable_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);
+
+ load_hotkeys();
+ }
+
+ void GlobalSettingsPage::load_hotkeys() {
+ turn_replay_on_off_button_ptr->set_text(config_hotkey_to_string(config.replay_config.start_stop_hotkey));
+ save_replay_button_ptr->set_text(config_hotkey_to_string(config.replay_config.save_hotkey));
+
+ start_stop_recording_button_ptr->set_text(config_hotkey_to_string(config.record_config.start_stop_hotkey));
+ pause_unpause_recording_button_ptr->set_text(config_hotkey_to_string(config.record_config.pause_unpause_hotkey));
+
+ start_stop_streaming_button_ptr->set_text(config_hotkey_to_string(config.streaming_config.start_stop_hotkey));
+
+ show_hide_button_ptr->set_text(config_hotkey_to_string(config.main_config.show_hide_hotkey));
}
void GlobalSettingsPage::save() {
+ configure_hotkey_cancel();
config.main_config.tint_color = tint_color_radio_button_ptr->get_selected_id();
- config.main_config.hotkeys_enable_option = enable_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();
save_config(config);
}
-} \ No newline at end of file
+
+ bool GlobalSettingsPage::on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) {
+ if(!StaticPage::on_event(event, window, offset))
+ return false;
+
+ if(configure_hotkey_type == ConfigureHotkeyType::NONE)
+ return true;
+
+ Button *configure_hotkey_button = configure_hotkey_get_button_by_active_type();
+ if(!configure_hotkey_button)
+ return true;
+
+ if(event.type == mgl::Event::KeyPressed) {
+ if(event.key.code == mgl::Keyboard::Escape)
+ return false;
+
+ if(mgl::Keyboard::key_is_modifier(event.key.code)) {
+ configure_config_hotkey.modifiers |= mgl_modifier_to_hotkey_modifier(event.key.code);
+ configure_hotkey_button->set_text(config_hotkey_to_string(configure_config_hotkey));
+ } else if(configure_config_hotkey.modifiers != 0) {
+ configure_config_hotkey.key = event.key.code;
+ configure_hotkey_button->set_text(config_hotkey_to_string(configure_config_hotkey));
+ configure_hotkey_stop_and_save();
+ }
+
+ return false;
+ } else if(event.type == mgl::Event::KeyReleased) {
+ if(event.key.code == mgl::Keyboard::Escape) {
+ configure_hotkey_cancel();
+ return false;
+ }
+
+ if(mgl::Keyboard::key_is_modifier(event.key.code)) {
+ configure_config_hotkey.modifiers &= ~mgl_modifier_to_hotkey_modifier(event.key.code);
+ configure_hotkey_button->set_text(config_hotkey_to_string(configure_config_hotkey));
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
+ Button* GlobalSettingsPage::configure_hotkey_get_button_by_active_type() {
+ switch(configure_hotkey_type) {
+ case ConfigureHotkeyType::NONE:
+ return nullptr;
+ case ConfigureHotkeyType::REPLAY_START_STOP:
+ return turn_replay_on_off_button_ptr;
+ case ConfigureHotkeyType::REPLAY_SAVE:
+ return save_replay_button_ptr;
+ case ConfigureHotkeyType::RECORD_START_STOP:
+ return start_stop_recording_button_ptr;
+ case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE:
+ return pause_unpause_recording_button_ptr;
+ case ConfigureHotkeyType::STREAM_START_STOP:
+ return start_stop_streaming_button_ptr;
+ case ConfigureHotkeyType::SHOW_HIDE:
+ return show_hide_button_ptr;
+ }
+ return nullptr;
+ }
+
+ ConfigHotkey* GlobalSettingsPage::configure_hotkey_get_config_by_active_type() {
+ switch(configure_hotkey_type) {
+ case ConfigureHotkeyType::NONE:
+ return nullptr;
+ case ConfigureHotkeyType::REPLAY_START_STOP:
+ return &config.replay_config.start_stop_hotkey;
+ case ConfigureHotkeyType::REPLAY_SAVE:
+ return &config.replay_config.save_hotkey;
+ case ConfigureHotkeyType::RECORD_START_STOP:
+ return &config.record_config.start_stop_hotkey;
+ case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE:
+ return &config.record_config.pause_unpause_hotkey;
+ case ConfigureHotkeyType::STREAM_START_STOP:
+ return &config.streaming_config.start_stop_hotkey;
+ case ConfigureHotkeyType::SHOW_HIDE:
+ return &config.main_config.show_hide_hotkey;
+ }
+ return nullptr;
+ }
+
+ void GlobalSettingsPage::for_each_config_hotkey(std::function<void(ConfigHotkey *config_hotkey)> callback) {
+ ConfigHotkey *config_hotkeys[] = {
+ &config.replay_config.start_stop_hotkey,
+ &config.replay_config.save_hotkey,
+ &config.record_config.start_stop_hotkey,
+ &config.record_config.pause_unpause_hotkey,
+ &config.streaming_config.start_stop_hotkey,
+ &config.main_config.show_hide_hotkey
+ };
+ for(ConfigHotkey *config_hotkey : config_hotkeys) {
+ callback(config_hotkey);
+ }
+ }
+
+ void GlobalSettingsPage::configure_hotkey_start(ConfigureHotkeyType hotkey_type) {
+ assert(hotkey_type != ConfigureHotkeyType::NONE);
+ configure_config_hotkey = {0, 0};
+ configure_hotkey_type = hotkey_type;
+
+ content_page_ptr->set_visible(false);
+ hotkey_overlay_ptr->set_visible(true);
+ overlay->unbind_all_keyboard_hotkeys();
+ configure_hotkey_get_button_by_active_type()->set_text("");
+
+ switch(hotkey_type) {
+ case ConfigureHotkeyType::NONE:
+ hotkey_configure_action_name = "";
+ break;
+ case ConfigureHotkeyType::REPLAY_START_STOP:
+ hotkey_configure_action_name = "Turn replay on/off";
+ break;
+ case ConfigureHotkeyType::REPLAY_SAVE:
+ hotkey_configure_action_name = "Save replay";
+ break;
+ case ConfigureHotkeyType::RECORD_START_STOP:
+ hotkey_configure_action_name = "Start/stop recording";
+ break;
+ case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE:
+ hotkey_configure_action_name = "Pause/unpause recording";
+ break;
+ case ConfigureHotkeyType::STREAM_START_STOP:
+ hotkey_configure_action_name = "Start/stop streaming";
+ break;
+ case ConfigureHotkeyType::SHOW_HIDE:
+ hotkey_configure_action_name = "Show/hide UI";
+ break;
+ }
+ }
+
+ void GlobalSettingsPage::configure_hotkey_cancel() {
+ Button *config_hotkey_button = configure_hotkey_get_button_by_active_type();
+ ConfigHotkey *config_hotkey = configure_hotkey_get_config_by_active_type();
+ if(config_hotkey_button && config_hotkey)
+ config_hotkey_button->set_text(config_hotkey_to_string(*config_hotkey));
+
+ configure_config_hotkey = {0, 0};
+ configure_hotkey_type = ConfigureHotkeyType::NONE;
+ content_page_ptr->set_visible(true);
+ hotkey_overlay_ptr->set_visible(false);
+ overlay->rebind_all_keyboard_hotkeys();
+ }
+
+ void GlobalSettingsPage::configure_hotkey_stop_and_save() {
+ Button *config_hotkey_button = configure_hotkey_get_button_by_active_type();
+ ConfigHotkey *config_hotkey = configure_hotkey_get_config_by_active_type();
+ if(config_hotkey_button && config_hotkey) {
+ bool hotkey_used_by_another_action = false;
+ for_each_config_hotkey([&](ConfigHotkey *config_hotkey_item) {
+ if(config_hotkey_item != config_hotkey && *config_hotkey_item == configure_config_hotkey)
+ hotkey_used_by_another_action = true;
+ });
+
+ if(hotkey_used_by_another_action) {
+ const std::string error_msg = "The hotkey \"" + config_hotkey_to_string(configure_config_hotkey) + " is already used for something else";
+ overlay->show_notification(error_msg.c_str(), 3.0, mgl::Color(255, 0, 0, 255), mgl::Color(255, 0, 0, 255), NotificationType::NONE);
+ config_hotkey_button->set_text(config_hotkey_to_string(*config_hotkey));
+ configure_config_hotkey = {0, 0};
+ return;
+ }
+
+ *config_hotkey = configure_config_hotkey;
+ }
+
+ configure_config_hotkey = {0, 0};
+ configure_hotkey_type = ConfigureHotkeyType::NONE;
+ content_page_ptr->set_visible(true);
+ hotkey_overlay_ptr->set_visible(false);
+ overlay->rebind_all_keyboard_hotkeys();
+ }
+}
diff --git a/src/gui/SettingsPage.cpp b/src/gui/SettingsPage.cpp
index 5fdcc91..3d114ae 100644
--- a/src/gui/SettingsPage.cpp
+++ b/src/gui/SettingsPage.cpp
@@ -664,6 +664,12 @@ namespace gsr {
return checkbox;
}
+ std::unique_ptr<CheckBox> SettingsPage::create_restart_replay_on_save() {
+ auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Restart replay on save");
+ restart_replay_on_save = checkbox.get();
+ return checkbox;
+ }
+
std::unique_ptr<Label> SettingsPage::create_estimated_replay_file_size() {
auto label = std::make_unique<Label>(&get_theme().body_font, "Estimated video max file size in RAM: 57.60MB", get_color_theme().text_color);
estimated_file_size_ptr = label.get();
@@ -693,6 +699,8 @@ namespace gsr {
auto general_list = std::make_unique<List>(List::Orientation::VERTICAL);
general_list->add_widget(create_start_replay_automatically());
general_list->add_widget(create_save_replay_in_game_folder());
+ if(gsr_info->system_info.gsr_version >= GsrVersion{5, 0, 3})
+ general_list->add_widget(create_restart_replay_on_save());
settings_list_ptr->add_widget(std::make_unique<Subsection>("General", std::move(general_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL);
@@ -1065,6 +1073,8 @@ namespace gsr {
load_common(config.replay_config.record_options);
turn_on_replay_automatically_mode_ptr->set_selected_item(config.replay_config.turn_on_replay_automatically_mode);
save_replay_in_game_folder_ptr->set_checked(config.replay_config.save_video_in_game_folder);
+ if(restart_replay_on_save)
+ restart_replay_on_save->set_checked(config.replay_config.restart_replay_on_save);
show_replay_started_notification_checkbox_ptr->set_checked(config.replay_config.show_replay_started_notifications);
show_replay_stopped_notification_checkbox_ptr->set_checked(config.replay_config.show_replay_stopped_notifications);
show_replay_saved_notification_checkbox_ptr->set_checked(config.replay_config.show_replay_saved_notifications);
@@ -1193,6 +1203,8 @@ namespace gsr {
save_common(config.replay_config.record_options);
config.replay_config.turn_on_replay_automatically_mode = turn_on_replay_automatically_mode_ptr->get_selected_id();
config.replay_config.save_video_in_game_folder = save_replay_in_game_folder_ptr->is_checked();
+ if(restart_replay_on_save)
+ config.replay_config.restart_replay_on_save = restart_replay_on_save->is_checked();
config.replay_config.show_replay_started_notifications = show_replay_started_notification_checkbox_ptr->is_checked();
config.replay_config.show_replay_stopped_notifications = show_replay_stopped_notification_checkbox_ptr->is_checked();
config.replay_config.show_replay_saved_notifications = show_replay_saved_notification_checkbox_ptr->is_checked();
diff --git a/src/main.cpp b/src/main.cpp
index c81bc8c..17a73a5 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,17 +1,14 @@
#include "../include/GsrInfo.hpp"
#include "../include/Overlay.hpp"
-#include "../include/GlobalHotkeysX11.hpp"
-#include "../include/GlobalHotkeysLinux.hpp"
#include "../include/gui/Utils.hpp"
#include "../include/Process.hpp"
#include "../include/Rpc.hpp"
#include <unistd.h>
#include <signal.h>
-#include <thread>
#include <string.h>
+#include <limits.h>
-#include <X11/keysym.h>
#include <mglpp/mglpp.hpp>
#include <mglpp/system/Clock.hpp>
@@ -40,100 +37,6 @@ static void disable_prime_run() {
unsetenv("DRI_PRIME");
}
-static std::unique_ptr<gsr::GlobalHotkeysX11> register_x11_hotkeys(gsr::Overlay *overlay) {
- auto global_hotkeys = std::make_unique<gsr::GlobalHotkeysX11>();
- const bool show_hotkey_registered = global_hotkeys->bind_key_press({ XK_z, Mod1Mask }, "show_hide", [overlay](const std::string &id) {
- fprintf(stderr, "pressed %s\n", id.c_str());
- overlay->toggle_show();
- });
-
- const bool record_hotkey_registered = global_hotkeys->bind_key_press({ XK_F9, Mod1Mask }, "record", [overlay](const std::string &id) {
- fprintf(stderr, "pressed %s\n", id.c_str());
- overlay->toggle_record();
- });
-
- const bool pause_hotkey_registered = global_hotkeys->bind_key_press({ XK_F7, Mod1Mask }, "pause", [overlay](const std::string &id) {
- fprintf(stderr, "pressed %s\n", id.c_str());
- overlay->toggle_pause();
- });
-
- const bool stream_hotkey_registered = global_hotkeys->bind_key_press({ XK_F8, Mod1Mask }, "stream", [overlay](const std::string &id) {
- fprintf(stderr, "pressed %s\n", id.c_str());
- overlay->toggle_stream();
- });
-
- const bool replay_hotkey_registered = global_hotkeys->bind_key_press({ XK_F10, ShiftMask | Mod1Mask }, "replay_start", [overlay](const std::string &id) {
- fprintf(stderr, "pressed %s\n", id.c_str());
- overlay->toggle_replay();
- });
-
- const bool replay_save_hotkey_registered = global_hotkeys->bind_key_press({ XK_F10, Mod1Mask }, "replay_save", [overlay](const std::string &id) {
- fprintf(stderr, "pressed %s\n", id.c_str());
- overlay->save_replay();
- });
-
- if(!show_hotkey_registered)
- fprintf(stderr, "error: failed to register hotkey alt+z for showing the overlay because the hotkey is registered by another program\n");
-
- if(!record_hotkey_registered)
- fprintf(stderr, "error: failed to register hotkey alt+f9 for recording because the hotkey is registered by another program\n");
-
- if(!pause_hotkey_registered)
- fprintf(stderr, "error: failed to register hotkey alt+f7 for pausing because the hotkey is registered by another program\n");
-
- if(!stream_hotkey_registered)
- fprintf(stderr, "error: failed to register hotkey alt+f8 for streaming because the hotkey is registered by another program\n");
-
- if(!replay_hotkey_registered)
- fprintf(stderr, "error: failed to register hotkey alt+shift+f10 for starting replay because the hotkey is registered by another program\n");
-
- if(!replay_save_hotkey_registered)
- fprintf(stderr, "error: failed to register hotkey alt+f10 for saving replay because the hotkey is registered by another program\n");
-
- if(!show_hotkey_registered || !record_hotkey_registered || !pause_hotkey_registered || !stream_hotkey_registered || !replay_hotkey_registered || !replay_save_hotkey_registered)
- return nullptr;
-
- return global_hotkeys;
-}
-
-static std::unique_ptr<gsr::GlobalHotkeysLinux> register_linux_hotkeys(gsr::Overlay *overlay, gsr::GlobalHotkeysLinux::GrabType grab_type) {
- auto global_hotkeys = std::make_unique<gsr::GlobalHotkeysLinux>(grab_type);
- if(!global_hotkeys->start())
- fprintf(stderr, "error: failed to start global hotkeys\n");
-
- global_hotkeys->bind_action("show_hide", [overlay](const std::string &id) {
- fprintf(stderr, "pressed %s\n", id.c_str());
- overlay->toggle_show();
- });
-
- global_hotkeys->bind_action("record", [overlay](const std::string &id) {
- fprintf(stderr, "pressed %s\n", id.c_str());
- overlay->toggle_record();
- });
-
- global_hotkeys->bind_action("pause", [overlay](const std::string &id) {
- fprintf(stderr, "pressed %s\n", id.c_str());
- overlay->toggle_pause();
- });
-
- global_hotkeys->bind_action("stream", [overlay](const std::string &id) {
- fprintf(stderr, "pressed %s\n", id.c_str());
- overlay->toggle_stream();
- });
-
- global_hotkeys->bind_action("replay_start", [overlay](const std::string &id) {
- fprintf(stderr, "pressed %s\n", id.c_str());
- overlay->toggle_replay();
- });
-
- global_hotkeys->bind_action("replay_save", [overlay](const std::string &id) {
- fprintf(stderr, "pressed %s\n", id.c_str());
- overlay->save_replay();
- });
-
- return global_hotkeys;
-}
-
static void rpc_add_commands(gsr::Rpc *rpc, gsr::Overlay *overlay) {
rpc->add_handler("show_ui", [overlay](const std::string &name) {
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
@@ -189,6 +92,48 @@ static bool is_gsr_ui_virtual_keyboard_running() {
return virtual_keyboard_running;
}
+static void install_flatpak_systemd_service() {
+ const bool systemd_service_exists = system(
+ "data_home=$(flatpak-spawn --host -- /bin/sh -c 'echo \"${XDG_DATA_HOME:-$HOME/.local/share}\"') && "
+ "flatpak-spawn --host -- ls \"$data_home/systemd/user/gpu-screen-recorder-ui.service\"") == 0;
+ if(systemd_service_exists)
+ return;
+
+ bool service_install_successful = (system(
+ "data_home=$(flatpak-spawn --host -- /bin/sh -c 'echo \"${XDG_DATA_HOME:-$HOME/.local/share}\"') && "
+ "flatpak-spawn --host -- install -Dm644 /var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/share/gpu-screen-recorder/gpu-screen-recorder-ui.service \"$data_home/systemd/user/gpu-screen-recorder-ui.service\"") == 0);
+ service_install_successful &= (system("flatpak-spawn --host -- systemctl --user daemon-reload") == 0);
+ if(service_install_successful)
+ fprintf(stderr, "Info: the systemd service file was missing. It has now been installed\n");
+ else
+ fprintf(stderr, "Error: the systemd service file is missing and failed to install it again\n");
+}
+
+static void remove_flatpak_systemd_service() {
+ char systemd_service_path[PATH_MAX];
+ const char *xdg_data_home = getenv("XDG_DATA_HOME");
+ const char *home = getenv("HOME");
+ if(xdg_data_home) {
+ snprintf(systemd_service_path, sizeof(systemd_service_path), "%s/systemd/user/gpu-screen-recorder-ui.service", xdg_data_home);
+ } else if(home) {
+ snprintf(systemd_service_path, sizeof(systemd_service_path), "%s/.local/share/systemd/user/gpu-screen-recorder-ui.service", home);
+ } else {
+ fprintf(stderr, "Error: failed to get user home directory\n");
+ return;
+ }
+
+ if(access(systemd_service_path, F_OK) != 0)
+ return;
+
+ remove(systemd_service_path);
+ system("systemctl --user daemon-reload");
+ fprintf(stderr, "Info: conflicting flatpak version of the systemd service for gsr-ui was found at \"%s\", it has now been removed\n", systemd_service_path);
+}
+
+static bool is_flatpak() {
+ return getenv("FLATPAK_ID") != nullptr;
+}
+
static void usage() {
printf("usage: gsr-ui [action]\n");
printf("OPTIONS:\n");
@@ -228,6 +173,11 @@ int main(int argc, char **argv) {
usage();
}
+ if(is_flatpak())
+ install_flatpak_systemd_service();
+ else
+ remove_flatpak_systemd_service();
+
// TODO: This is a shitty method to detect if multiple instances of gsr-ui is running but this will work properly even in flatpak
// that uses pid sandboxing. Replace this with a better method once we no longer rely on linux global hotkeys on some platform.
// TODO: This method doesn't work when disabling hotkeys and the method below with pidof gsr-ui doesn't work in flatpak.
@@ -244,9 +194,6 @@ int main(int argc, char **argv) {
return 1;
}
- // Cant get window texture when prime-run is used
- disable_prime_run();
-
// Stop nvidia driver from buffering frames
setenv("__GL_MaxFramesAllowed", "1", true);
// If this is set to 1 then cuGraphicsGLRegisterImage will fail for egl context with error: invalid OpenGL or DirectX context,
@@ -259,11 +206,6 @@ int main(int argc, char **argv) {
signal(SIGINT, sigint_handler);
- if(mgl_init() != 0) {
- fprintf(stderr, "Error: failed to initialize mgl. Failed to either connect to the X11 server or setup opengl\n");
- exit(1);
- }
-
gsr::GsrInfo gsr_info;
// TODO: Show the error in ui
gsr::GsrInfoExitStatus gsr_info_exit_status = gsr::get_gpu_screen_recorder_info(&gsr_info);
@@ -273,8 +215,17 @@ int main(int argc, char **argv) {
}
const gsr::DisplayServer display_server = gsr_info.system_info.display_server;
- if(display_server == gsr::DisplayServer::WAYLAND)
- fprintf(stderr, "Warning: Wayland support is experimental and requires XWayland. Things may not work as expected.\n");
+ if(display_server == gsr::DisplayServer::WAYLAND) {
+ fprintf(stderr, "Warning: Wayland doesn't support this program properly and XWayland is required. Things may not work as expected. Use X11 if you experience issues.\n");
+ } else {
+ // Cant get window texture when prime-run is used
+ disable_prime_run();
+ }
+
+ if(mgl_init() != 0) {
+ fprintf(stderr, "Error: failed to initialize mgl. Failed to either connect to the X11 server or setup opengl\n");
+ exit(1);
+ }
gsr::SupportedCaptureOptions capture_options = gsr::get_supported_capture_options(gsr_info);
@@ -314,12 +265,6 @@ int main(int argc, char **argv) {
rpc_add_commands(rpc.get(), overlay.get());
- std::unique_ptr<gsr::GlobalHotkeys> global_hotkeys = nullptr;
- if(overlay->get_config().main_config.hotkeys_enable_option == "enable_hotkeys")
- global_hotkeys = register_linux_hotkeys(overlay.get(), gsr::GlobalHotkeysLinux::GrabType::ALL);
- else if(overlay->get_config().main_config.hotkeys_enable_option == "enable_hotkeys_virtual_devices")
- global_hotkeys = register_linux_hotkeys(overlay.get(), gsr::GlobalHotkeysLinux::GrabType::VIRTUAL);
-
// TODO: Add hotkeys in Overlay when using x11 global hotkeys. The hotkeys in Overlay should duplicate each key that is used for x11 global hotkeys.
std::string exit_reason;
@@ -330,21 +275,15 @@ int main(int argc, char **argv) {
gsr::set_frame_delta_seconds(frame_delta_seconds);
rpc->poll();
-
- if(global_hotkeys)
- global_hotkeys->poll_events();
-
- overlay->handle_events(global_hotkeys.get());
+ overlay->handle_events();
if(!overlay->draw()) {
- std::this_thread::sleep_for(std::chrono::milliseconds(100));
+ usleep(100 * 1000); // 100ms
mgl_ping_display_server();
}
}
fprintf(stderr, "Info: shutting down!\n");
rpc.reset();
- if(global_hotkeys)
- global_hotkeys.reset();
overlay.reset();
mgl_deinit();