aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/AudioPlayer.cpp86
-rw-r--r--src/Config.cpp90
-rw-r--r--src/GlobalHotkeysJoystick.cpp243
-rw-r--r--src/GlobalHotkeysLinux.cpp188
-rw-r--r--src/GlobalHotkeysX11.cpp84
-rw-r--r--src/GsrInfo.cpp254
-rw-r--r--src/Hotplug.cpp81
-rw-r--r--src/Overlay.cpp1184
-rw-r--r--src/Process.cpp158
-rw-r--r--src/Rpc.cpp133
-rw-r--r--src/Theme.cpp48
-rw-r--r--src/WindowUtils.cpp530
-rw-r--r--src/gui/Button.cpp44
-rw-r--r--src/gui/ComboBox.cpp34
-rw-r--r--src/gui/CustomRendererWidget.cpp14
-rw-r--r--src/gui/DropdownButton.cpp4
-rw-r--r--src/gui/FileChooser.cpp12
-rw-r--r--src/gui/GlobalSettingsPage.cpp653
-rw-r--r--src/gui/GsrPage.cpp13
-rw-r--r--src/gui/RadioButton.cpp20
-rw-r--r--src/gui/ScrollablePage.cpp39
-rw-r--r--src/gui/SettingsPage.cpp296
-rw-r--r--src/gui/StaticPage.cpp12
-rw-r--r--src/gui/Utils.cpp17
-rw-r--r--src/main.cpp316
25 files changed, 3873 insertions, 680 deletions
diff --git a/src/AudioPlayer.cpp b/src/AudioPlayer.cpp
new file mode 100644
index 0000000..cb6d1c7
--- /dev/null
+++ b/src/AudioPlayer.cpp
@@ -0,0 +1,86 @@
+#include "../include/AudioPlayer.hpp"
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <pulse/simple.h>
+#include <pulse/error.h>
+
+#define BUFSIZE 4096
+
+namespace gsr {
+ AudioPlayer::~AudioPlayer() {
+ if(thread.joinable()) {
+ stop_playing_audio = true;
+ thread.join();
+ }
+
+ if(audio_file_fd > 0)
+ close(audio_file_fd);
+ }
+
+ bool AudioPlayer::play(const char *filepath) {
+ if(thread.joinable()) {
+ stop_playing_audio = true;
+ thread.join();
+ }
+
+ stop_playing_audio = false;
+ audio_file_fd = open(filepath, O_RDONLY);
+ if(audio_file_fd == -1)
+ return false;
+
+ thread = std::thread([this]() {
+ pa_sample_spec ss;
+ ss.format = PA_SAMPLE_S16LE;
+ ss.rate = 48000;
+ ss.channels = 2;
+
+ pa_simple *s = NULL;
+ int error;
+
+ /* Create a new playback stream */
+ if(!(s = pa_simple_new(NULL, "gsr-ui-audio-playback", PA_STREAM_PLAYBACK, NULL, "playback", &ss, NULL, NULL, &error))) {
+ fprintf(stderr, __FILE__": pa_simple_new() failed: %s\n", pa_strerror(error));
+ goto finish;
+ }
+
+ uint8_t buf[BUFSIZE];
+ for(;;) {
+ ssize_t r;
+
+ if(stop_playing_audio)
+ goto finish;
+
+ if((r = read(audio_file_fd, buf, sizeof(buf))) <= 0) {
+ if(r == 0) /* EOF */
+ break;
+
+ fprintf(stderr, __FILE__": read() failed: %s\n", strerror(errno));
+ goto finish;
+ }
+
+ if(pa_simple_write(s, buf, (size_t) r, &error) < 0) {
+ fprintf(stderr, __FILE__": pa_simple_write() failed: %s\n", pa_strerror(error));
+ goto finish;
+ }
+ }
+
+ if(pa_simple_drain(s, &error) < 0) {
+ fprintf(stderr, __FILE__": pa_simple_drain() failed: %s\n", pa_strerror(error));
+ goto finish;
+ }
+
+ finish:
+ if(s)
+ pa_simple_free(s);
+
+ close(audio_file_fd);
+ audio_file_fd = -1;
+ });
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/src/Config.cpp b/src/Config.cpp
index 112688a..b9e4cb7 100644
--- a/src/Config.cpp
+++ b/src/Config.cpp
@@ -1,38 +1,53 @@
#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 <mglpp/window/Keyboard.hpp>
#define FORMAT_I32 "%" PRIi32
#define FORMAT_I64 "%" PRIi64
#define FORMAT_U32 "%" PRIu32
-#define CONFIG_FILE_VERSION 1
-
namespace gsr {
- Config::Config(const GsrInfo &gsr_info) {
+ bool ConfigHotkey::operator==(const ConfigHotkey &other) const {
+ return key == other.key && modifiers == other.modifiers;
+ }
+
+ bool ConfigHotkey::operator!=(const ConfigHotkey &other) const {
+ return !operator==(other);
+ }
+
+ 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;
- if(!gsr_info.supported_capture_options.monitors.empty()) {
- streaming_config.record_options.record_area_option = gsr_info.supported_capture_options.monitors.front().name;
- record_config.record_options.record_area_option = gsr_info.supported_capture_options.monitors.front().name;
- replay_config.record_options.record_area_option = gsr_info.supported_capture_options.monitors.front().name;
+ 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;
+ replay_config.record_options.record_area_option = capture_options.monitors.front().name;
}
}
@@ -49,6 +64,10 @@ namespace gsr {
return {
{"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},
@@ -77,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},
@@ -104,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},
@@ -129,18 +148,51 @@ 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}
};
}
- std::optional<Config> read_config(const GsrInfo &gsr_info) {
+ bool Config::operator==(const Config &other) {
+ const auto config_options = get_config_options(*this);
+ const auto config_options_other = get_config_options(const_cast<Config&>(other));
+ for(auto it : config_options) {
+ auto it_other = config_options_other.find(it.first);
+ if(it_other == config_options_other.end() || it_other->second.index() != it.second.index())
+ return false;
+
+ if(std::holds_alternative<bool*>(it.second)) {
+ if(*std::get<bool*>(it.second) != *std::get<bool*>(it_other->second))
+ return false;
+ } else if(std::holds_alternative<std::string*>(it.second)) {
+ if(*std::get<std::string*>(it.second) != *std::get<std::string*>(it_other->second))
+ return false;
+ } else if(std::holds_alternative<int32_t*>(it.second)) {
+ if(*std::get<int32_t*>(it.second) != *std::get<int32_t*>(it_other->second))
+ return false;
+ } else if(std::holds_alternative<ConfigHotkey*>(it.second)) {
+ if(*std::get<ConfigHotkey*>(it.second) != *std::get<ConfigHotkey*>(it_other->second))
+ return false;
+ } else if(std::holds_alternative<std::vector<std::string>*>(it.second)) {
+ if(*std::get<std::vector<std::string>*>(it.second) != *std::get<std::vector<std::string>*>(it_other->second))
+ return false;
+ }
+ }
+ return true;
+ }
+
+ bool Config::operator!=(const Config &other) {
+ return !operator==(other);
+ }
+
+ std::optional<Config> read_config(const SupportedCaptureOptions &capture_options) {
std::optional<Config> config;
const std::string config_path = get_config_dir() + "/config_ui";
@@ -150,7 +202,7 @@ namespace gsr {
return config;
}
- config = Config(gsr_info);
+ config = Config(capture_options);
config->streaming_config.record_options.audio_tracks.clear();
config->record_config.record_options.audio_tracks.clear();
config->replay_config.record_options.audio_tracks.clear();
@@ -185,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)) {
@@ -198,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;
}
@@ -207,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";
@@ -236,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 b0e8e52..4df6390 100644
--- a/src/GlobalHotkeysLinux.cpp
+++ b/src/GlobalHotkeysLinux.cpp
@@ -2,22 +2,76 @@
#include <signal.h>
#include <sys/wait.h>
#include <fcntl.h>
+#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
namespace gsr {
- GlobalHotkeysLinux::GlobalHotkeysLinux() {
+ static const char* grab_type_to_arg(GlobalHotkeysLinux::GrabType grab_type) {
+ switch(grab_type) {
+ case GlobalHotkeysLinux::GrabType::ALL: return "--all";
+ case GlobalHotkeysLinux::GrabType::VIRTUAL: return "--virtual";
+ }
+ 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)
@@ -31,77 +85,159 @@ namespace gsr {
}
bool GlobalHotkeysLinux::start() {
+ const char *grab_type_arg = grab_type_to_arg(grab_type);
+ const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
+ const char *user_homepath = getenv("HOME");
+ if(!user_homepath)
+ user_homepath = "/tmp";
+
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]);
}
- const char *args[] = { "gsr-global-hotkeys", NULL };
- execvp(args[0], (char* const*)args);
- perror("execvp");
+ 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", "/var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/bin/kms-server-proxy", "launch-gsr-global-hotkeys", user_homepath, grab_type_arg, nullptr };
+ execvp(args[0], (char* const*)args);
+ } else {
+ const char *args[] = { "gsr-global-hotkeys", grab_type_arg, nullptr };
+ execvp(args[0], (char* const*)args);
+ }
+
+ perror("gsr-global-hotkeys");
_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, 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;
}
+ std::string action;
char buffer[256];
while(true) {
char *line = fgets(buffer, sizeof(buffer), read_file);
if(!line)
break;
- const int line_len = strlen(line);
+ int line_len = strlen(line);
if(line_len == 0)
continue;
- if(line[line_len - 1] == '\n')
+ if(line[line_len - 1] == '\n') {
line[line_len - 1] = '\0';
+ --line_len;
+ }
- const std::string action = line;
+ action = line;
auto it = bound_actions_by_id.find(action);
if(it != bound_actions_by_id.end())
it->second(action);
diff --git a/src/GlobalHotkeysX11.cpp b/src/GlobalHotkeysX11.cpp
index 6b01bfd..9af2607 100644
--- a/src/GlobalHotkeysX11.cpp
+++ b/src/GlobalHotkeysX11.cpp
@@ -1,6 +1,7 @@
#include "../include/GlobalHotkeysX11.hpp"
-#define XK_MISCELLANY
-#include <X11/keysymdef.h>
+#include <X11/keysym.h>
+#include <mglpp/window/Event.hpp>
+#include <assert.h>
namespace gsr {
static bool x_failed = false;
@@ -25,6 +26,51 @@ namespace gsr {
return numlockmask;
}
+ static KeySym mgl_key_to_key_sym(mgl::Keyboard::Key key) {
+ switch(key) {
+ case mgl::Keyboard::Z: return XK_z;
+ case mgl::Keyboard::F7: return XK_F7;
+ case mgl::Keyboard::F8: return XK_F8;
+ case mgl::Keyboard::F9: return XK_F9;
+ case mgl::Keyboard::F10: return XK_F10;
+ default: return None;
+ }
+ }
+
+ static uint32_t mgl_key_modifiers_to_x11_modifier_mask(const mgl::Event::KeyEvent &key_event) {
+ uint32_t mask = 0;
+ if(key_event.shift)
+ mask |= ShiftMask;
+ if(key_event.control)
+ mask |= ControlMask;
+ if(key_event.alt)
+ mask |= Mod1Mask;
+ if(key_event.system)
+ mask |= Mod4Mask;
+ 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)
@@ -49,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);
@@ -81,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);
@@ -102,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();
@@ -113,25 +162,40 @@ 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 });
}
}
}
+ bool GlobalHotkeysX11::on_event(mgl::Event &event) {
+ if(event.type != mgl::Event::KeyPressed)
+ return true;
+
+ // 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{(uint32_t)key_sym, modifiers});
+ }
+
static unsigned int key_state_without_locks(unsigned int key_state) {
return key_state & ~(Mod2Mask|LockMask);
}
- void GlobalHotkeysX11::call_hotkey_callback(Hotkey hotkey) const {
+ 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;
+ return true;
}
}
+ return false;
}
} \ No newline at end of file
diff --git a/src/GsrInfo.cpp b/src/GsrInfo.cpp
index 276870b..033757c 100644
--- a/src/GsrInfo.cpp
+++ b/src/GsrInfo.cpp
@@ -1,9 +1,98 @@
#include "../include/GsrInfo.hpp"
#include "../include/Utils.hpp"
+#include "../include/Process.hpp"
+
#include <optional>
#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)
@@ -23,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);
}
}
@@ -38,6 +129,8 @@ namespace gsr {
gsr_info->gpu_info.vendor = GpuVendor::INTEL;
else if(key_value->value == "nvidia")
gsr_info->gpu_info.vendor = GpuVendor::NVIDIA;
+ } else if(key_value->key == "card_path") {
+ gsr_info->gpu_info.card_path = key_value->value;
}
}
@@ -64,38 +157,6 @@ namespace gsr {
gsr_info->supported_video_codecs.vp9 = true;
}
- static std::optional<GsrMonitor> capture_option_line_to_monitor(std::string_view line) {
- std::optional<GsrMonitor> monitor;
- const std::optional<KeyValue> key_value = parse_key_value(line);
- if(!key_value)
- return monitor;
-
- char value_buffer[256];
- snprintf(value_buffer, sizeof(value_buffer), "%.*s", (int)key_value->value.size(), key_value->value.data());
-
- monitor = GsrMonitor{std::string(key_value->key), mgl::vec2i{0, 0}};
- if(sscanf(value_buffer, "%dx%d", &monitor->size.x, &monitor->size.y) != 2)
- monitor->size = {0, 0};
-
- return monitor;
- }
-
- static void parse_capture_options_line(GsrInfo *gsr_info, std::string_view line) {
- if(line == "window")
- gsr_info->supported_capture_options.window = true;
- else if(line == "focused")
- gsr_info->supported_capture_options.focused = true;
- else if(line == "screen")
- gsr_info->supported_capture_options.screen = true;
- else if(line == "portal")
- gsr_info->supported_capture_options.portal = true;
- else {
- std::optional<GsrMonitor> monitor = capture_option_line_to_monitor(line);
- if(monitor)
- gsr_info->supported_capture_options.monitors.push_back(std::move(monitor.value()));
- }
- }
-
enum class GsrInfoSection {
UNKNOWN,
SYSTEM_INFO,
@@ -112,23 +173,19 @@ namespace gsr {
GsrInfoExitStatus get_gpu_screen_recorder_info(GsrInfo *gsr_info) {
*gsr_info = GsrInfo{};
- FILE *f = popen("gpu-screen-recorder --info", "r");
- if(!f) {
- fprintf(stderr, "error: 'gpu-screen-recorder --info' failed\n");
- return GsrInfoExitStatus::FAILED_TO_RUN_COMMAND;
- }
-
- char output[8192];
- ssize_t bytes_read = fread(output, 1, sizeof(output) - 1, f);
- if(bytes_read < 0 || ferror(f)) {
- fprintf(stderr, "error: failed to read 'gpu-screen-recorder --info' output\n");
- pclose(f);
- return GsrInfoExitStatus::FAILED_TO_RUN_COMMAND;
+ std::string stdout_str;
+ const char *args[] = { "gpu-screen-recorder", "--info", nullptr };
+ const int exit_status = exec_program_get_stdout(args, stdout_str);
+ switch(exit_status) {
+ case 0: break;
+ case 14: return GsrInfoExitStatus::BROKEN_DRIVERS;
+ case 22: return GsrInfoExitStatus::OPENGL_FAILED;
+ case 23: return GsrInfoExitStatus::NO_DRM_CARD;
+ default: return GsrInfoExitStatus::FAILED_TO_RUN_COMMAND;
}
- output[bytes_read] = '\0';
GsrInfoSection section = GsrInfoSection::UNKNOWN;
- string_split_char({output, (size_t)bytes_read}, '\n', [&](std::string_view line) {
+ string_split_char(stdout_str, '\n', [&](std::string_view line) {
if(starts_with(line, "section=")) {
const std::string_view section_name = line.substr(8);
if(section_name == "system_info")
@@ -161,7 +218,7 @@ namespace gsr {
break;
}
case GsrInfoSection::CAPTURE_OPTIONS: {
- parse_capture_options_line(gsr_info, line);
+ // Intentionally ignore, get capture options with get_supported_capture_options instead
break;
}
}
@@ -169,18 +226,7 @@ namespace gsr {
return true;
});
- int status = pclose(f);
- if(WIFEXITED(status)) {
- switch(WEXITSTATUS(status)) {
- case 0: return GsrInfoExitStatus::OK;
- case 14: return GsrInfoExitStatus::BROKEN_DRIVERS;
- case 22: return GsrInfoExitStatus::OPENGL_FAILED;
- case 23: return GsrInfoExitStatus::NO_DRM_CARD;
- default: return GsrInfoExitStatus::FAILED_TO_RUN_COMMAND;
- }
- }
-
- return GsrInfoExitStatus::FAILED_TO_RUN_COMMAND;
+ return GsrInfoExitStatus::OK;
}
static std::optional<AudioDevice> parse_audio_device_line(std::string_view line) {
@@ -196,22 +242,14 @@ namespace gsr {
std::vector<AudioDevice> get_audio_devices() {
std::vector<AudioDevice> audio_devices;
- FILE *f = popen("gpu-screen-recorder --list-audio-devices", "r");
- if(!f) {
+ std::string stdout_str;
+ const char *args[] = { "gpu-screen-recorder", "--list-audio-devices", nullptr };
+ if(exec_program_get_stdout(args, stdout_str) != 0) {
fprintf(stderr, "error: 'gpu-screen-recorder --list-audio-devices' failed\n");
return audio_devices;
}
- char output[16384];
- ssize_t bytes_read = fread(output, 1, sizeof(output) - 1, f);
- if(bytes_read < 0 || ferror(f)) {
- fprintf(stderr, "error: failed to read 'gpu-screen-recorder --list-audio-devices' output\n");
- pclose(f);
- return audio_devices;
- }
- output[bytes_read] = '\0';
-
- string_split_char({output, (size_t)bytes_read}, '\n', [&](std::string_view line) {
+ string_split_char(stdout_str, '\n', [&](std::string_view line) {
std::optional<AudioDevice> audio_device = parse_audio_device_line(line);
if(audio_device)
audio_devices.push_back(std::move(audio_device.value()));
@@ -224,26 +262,76 @@ namespace gsr {
std::vector<std::string> get_application_audio() {
std::vector<std::string> application_audio;
- FILE *f = popen("gpu-screen-recorder --list-application-audio", "r");
- if(!f) {
+ std::string stdout_str;
+ const char *args[] = { "gpu-screen-recorder", "--list-application-audio", nullptr };
+ if(exec_program_get_stdout(args, stdout_str) != 0) {
fprintf(stderr, "error: 'gpu-screen-recorder --list-application-audio' failed\n");
return application_audio;
}
- char output[16384];
- ssize_t bytes_read = fread(output, 1, sizeof(output) - 1, f);
- if(bytes_read < 0 || ferror(f)) {
- fprintf(stderr, "error: failed to read 'gpu-screen-recorder --list-application-audio' output\n");
- pclose(f);
- return application_audio;
- }
- output[bytes_read] = '\0';
-
- string_split_char({output, (size_t)bytes_read}, '\n', [&](std::string_view line) {
+ string_split_char(stdout_str, '\n', [&](std::string_view line) {
application_audio.emplace_back(line);
return true;
});
return application_audio;
}
+
+ static std::optional<GsrMonitor> capture_option_line_to_monitor(std::string_view line) {
+ std::optional<GsrMonitor> monitor;
+ const std::optional<KeyValue> key_value = parse_key_value(line);
+ if(!key_value)
+ return monitor;
+
+ char value_buffer[256];
+ snprintf(value_buffer, sizeof(value_buffer), "%.*s", (int)key_value->value.size(), key_value->value.data());
+
+ monitor = GsrMonitor{std::string(key_value->key), mgl::vec2i{0, 0}};
+ if(sscanf(value_buffer, "%dx%d", &monitor->size.x, &monitor->size.y) != 2)
+ monitor->size = {0, 0};
+
+ return monitor;
+ }
+
+ static void parse_capture_options_line(SupportedCaptureOptions &capture_options, std::string_view line) {
+ if(line == "window")
+ capture_options.window = true;
+ else if(line == "focused")
+ capture_options.focused = true;
+ else if(line == "portal")
+ capture_options.portal = true;
+ else {
+ std::optional<GsrMonitor> monitor = capture_option_line_to_monitor(line);
+ if(monitor)
+ capture_options.monitors.push_back(std::move(monitor.value()));
+ }
+ }
+
+ 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";
+ }
+
+ SupportedCaptureOptions get_supported_capture_options(const GsrInfo &gsr_info) {
+ SupportedCaptureOptions capture_options;
+
+ std::string stdout_str;
+ const char *args[] = { "gpu-screen-recorder", "--list-capture-options", gsr_info.gpu_info.card_path.c_str(), gpu_vendor_to_string(gsr_info.gpu_info.vendor), nullptr };
+ if(exec_program_get_stdout(args, stdout_str) != 0) {
+ fprintf(stderr, "error: 'gpu-screen-recorder --list-capture-options' failed\n");
+ return capture_options;
+ }
+
+ string_split_char(stdout_str, '\n', [&](std::string_view line) {
+ parse_capture_options_line(capture_options, line);
+ return true;
+ });
+
+ return capture_options;
+ }
}
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 2475a77..919117d 100644
--- a/src/Overlay.cpp
+++ b/src/Overlay.cpp
@@ -7,20 +7,29 @@
#include "../include/gui/DropdownButton.hpp"
#include "../include/gui/CustomRendererWidget.hpp"
#include "../include/gui/SettingsPage.hpp"
+#include "../include/gui/GlobalSettingsPage.hpp"
#include "../include/gui/Utils.hpp"
#include "../include/gui/PageStack.hpp"
+#include "../include/gui/GsrPage.hpp"
+#include "../include/WindowUtils.hpp"
+#include "../include/GlobalHotkeys.hpp"
#include <string.h>
#include <assert.h>
#include <sys/wait.h>
#include <limits.h>
#include <fcntl.h>
+#include <poll.h>
#include <stdexcept>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/cursorfont.h>
+#include <X11/extensions/Xfixes.h>
+#include <X11/extensions/XInput2.h>
+#include <X11/extensions/shape.h>
+#include <X11/Xcursor/Xcursor.h>
#include <mglpp/system/Rect.hpp>
#include <mglpp/window/Event.hpp>
@@ -32,65 +41,7 @@ namespace gsr {
static const mgl::Color bg_color(0, 0, 0, 100);
static const double force_window_on_top_timeout_seconds = 1.0;
static const double replay_status_update_check_timeout_seconds = 1.0;
-
- static bool window_has_atom(Display *dpy, Window window, Atom atom) {
- Atom type;
- unsigned long len, bytes_left;
- int format;
- unsigned char *properties = NULL;
- if(XGetWindowProperty(dpy, window, atom, 0, 1024, False, AnyPropertyType, &type, &format, &len, &bytes_left, &properties) < Success)
- return false;
-
- if(properties)
- XFree(properties);
-
- return type != None;
- }
-
- static bool window_is_user_program(Display *dpy, Window window) {
- const Atom net_wm_state_atom = XInternAtom(dpy, "_NET_WM_STATE", False);
- const Atom wm_state_atom = XInternAtom(dpy, "WM_STATE", False);
- return window_has_atom(dpy, window, net_wm_state_atom) || window_has_atom(dpy, window, wm_state_atom);
- }
-
- static Window get_window_at_cursor_position(Display *dpy) {
- Window root_window = None;
- Window window = None;
- int dummy_i;
- unsigned int dummy_u;
- int cursor_pos_x = 0;
- int cursor_pos_y = 0;
- XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &dummy_i, &dummy_i, &cursor_pos_x, &cursor_pos_y, &dummy_u);
- return window;
- }
-
- static Window get_focused_window(Display *dpy) {
- const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False);
- Window focused_window = None;
-
- // Atom type = None;
- // int format = 0;
- // unsigned long num_items = 0;
- // unsigned long bytes_left = 0;
- // unsigned char *data = NULL;
- // XGetWindowProperty(dpy, DefaultRootWindow(dpy), net_active_window_atom, 0, 1, False, XA_WINDOW, &type, &format, &num_items, &bytes_left, &data);
-
- // fprintf(stderr, "focused window: %p\n", (void*)data);
-
- // if(type == XA_WINDOW && num_items == 1 && data)
- // return *(Window*)data;
-
- int revert_to = 0;
- XGetInputFocus(dpy, &focused_window, &revert_to);
- if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window))
- return focused_window;
-
- focused_window = get_window_at_cursor_position(dpy);
- if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window))
- return focused_window;
-
- return None;
- }
+ static const double replay_saving_notification_timeout_seconds = 0.5;
static mgl::Texture texture_from_ximage(XImage *img) {
uint8_t *texture_data = (uint8_t*)malloc(img->width * img->height * 3);
@@ -117,6 +68,53 @@ namespace gsr {
return texture;
}
+ static bool texture_from_x11_cursor(XcursorImage *x11_cursor_image, bool *visible, mgl::vec2i *hotspot, mgl::Texture &texture) {
+ uint8_t *cursor_data = NULL;
+ uint8_t *out = NULL;
+ const unsigned int *pixels = NULL;
+ *visible = false;
+
+ if(!x11_cursor_image)
+ return false;
+
+ if(!x11_cursor_image->pixels)
+ return false;
+
+ hotspot->x = x11_cursor_image->xhot;
+ hotspot->y = x11_cursor_image->yhot;
+
+ pixels = x11_cursor_image->pixels;
+ cursor_data = (uint8_t*)malloc((int)x11_cursor_image->width * (int)x11_cursor_image->height * 4);
+ if(!cursor_data)
+ return false;
+
+ out = cursor_data;
+ /* Un-premultiply alpha */
+ for(uint32_t y = 0; y < x11_cursor_image->height; ++y) {
+ for(uint32_t x = 0; x < x11_cursor_image->width; ++x) {
+ uint32_t pixel = *pixels++;
+ uint8_t *in = (uint8_t*)&pixel;
+ uint8_t alpha = in[3];
+ if(alpha == 0) {
+ alpha = 1;
+ } else {
+ *visible = true;
+ }
+
+ out[0] = (float)in[2] * 255.0/(float)alpha;
+ out[1] = (float)in[1] * 255.0/(float)alpha;
+ out[2] = (float)in[0] * 255.0/(float)alpha;
+ out[3] = in[3];
+ out += 4;
+ in += 4;
+ }
+ }
+
+ texture.load_from_memory(cursor_data, x11_cursor_image->width, x11_cursor_image->height, MGL_IMAGE_FORMAT_RGBA);
+ free(cursor_data);
+ return true;
+ }
+
static char hex_value_to_str(uint8_t v) {
if(v <= 9)
return '0' + v;
@@ -167,7 +165,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;
@@ -176,8 +174,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) {
@@ -231,15 +229,6 @@ namespace gsr {
return is_fullscreen;
}
- static void set_focused_window(Display *dpy, Window window) {
- XSetInputFocus(dpy, window, RevertToPointerRoot, CurrentTime);
-
- const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False);
- XChangeProperty(dpy, DefaultRootWindow(dpy), net_active_window_atom, XA_WINDOW, 32, PropModeReplace, (const unsigned char*)&window, 1);
-
- XFlush(dpy);
- }
-
#define _NET_WM_STATE_REMOVE 0
#define _NET_WM_STATE_ADD 1
#define _NET_WM_STATE_TOGGLE 2
@@ -265,6 +254,14 @@ namespace gsr {
return True;
}
+ static void make_window_click_through(Display *display, Window window) {
+ XRectangle rect;
+ memset(&rect, 0, sizeof(rect));
+ XserverRegion region = XFixesCreateRegion(display, &rect, 1);
+ XFixesSetWindowShapeRegion(display, window, ShapeInput, 0, 0, region);
+ XFixesDestroyRegion(display, region);
+ }
+
static Bool make_window_sticky(Display *dpy, Window window) {
return set_window_wm_state(dpy, window, XInternAtom(dpy, "_NET_WM_STATE_STICKY", False));
}
@@ -274,22 +271,13 @@ namespace gsr {
}
// Returns the first monitor if not found. Assumes there is at least one monitor connected.
- static const mgl_monitor* find_monitor_by_cursor_position(mgl::Window &window) {
- 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({ win->cursor_position.x, win->cursor_position.y }))
- 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];
- }
-
- static 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);
- return XGetSelectionOwner(dpy, prop_atom) != None;
+ return &monitors.front();
}
static std::string get_power_supply_online_filepath() {
@@ -320,17 +308,108 @@ namespace gsr {
return is_connected;
}
- Overlay::Overlay(std::string resources_path, GsrInfo gsr_info, egl_functions egl_funcs) :
+ static bool xinput_is_supported(Display *dpy, int *xi_opcode) {
+ *xi_opcode = 0;
+ int query_event = 0;
+ int query_error = 0;
+ if(!XQueryExtension(dpy, "XInputExtension", xi_opcode, &query_event, &query_error)) {
+ fprintf(stderr, "gsr-ui error: X Input extension not available\n");
+ return false;
+ }
+
+ int major = 2;
+ int minor = 1;
+ int retval = XIQueryVersion(dpy, &major, &minor);
+ if (retval != Success) {
+ fprintf(stderr, "gsr-ui error: XInput 2.1 is not supported\n");
+ return false;
+ }
+
+ 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(gsr_info),
+ gsr_info(std::move(gsr_info)),
egl_funcs(egl_funcs),
+ config(capture_options),
bg_screenshot_overlay({0.0f, 0.0f}),
top_bar_background({0.0f, 0.0f}),
- close_button_widget({0.0f, 0.0f}),
- config(gsr_info)
+ close_button_widget({0.0f, 0.0f})
{
- memset(&window_texture, 0, sizeof(window_texture));
-
key_bindings[0].key_event.code = mgl::Keyboard::Escape;
key_bindings[0].key_event.alt = false;
key_bindings[0].key_event.control = false;
@@ -340,19 +419,32 @@ namespace gsr {
page_stack.pop();
};
- std::optional<Config> new_config = read_config(gsr_info);
+ memset(&window_texture, 0, sizeof(window_texture));
+
+ std::optional<Config> new_config = read_config(capture_options);
if(new_config)
config = std::move(new_config.value());
- init_color_theme(gsr_info);
- // These environment variable are used by files in scripts/ folder
- const std::string notify_bg_color_str = color_to_hex_str(get_color_theme().tint_color);
- setenv("GSR_NOTIFY_BG_COLOR", notify_bg_color_str.c_str(), true);
+ init_color_theme(config, this->gsr_info);
power_supply_online_filepath = get_power_supply_online_filepath();
if(config.replay_config.turn_on_replay_automatically_mode == "turn_on_at_system_startup")
on_press_start_replay(true);
+
+ 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() {
@@ -377,6 +469,134 @@ namespace gsr {
}
gpu_screen_recorder_process = -1;
}
+
+ close_gpu_screen_recorder_output();
+ deinit_color_theme();
+
+ if(x11_mapping_display)
+ XCloseDisplay(x11_mapping_display);
+ }
+
+ void Overlay::xi_setup() {
+ xi_display = XOpenDisplay(nullptr);
+ if(!xi_display) {
+ fprintf(stderr, "gsr-ui error: failed to setup XI connection\n");
+ return;
+ }
+
+ if(!xinput_is_supported(xi_display, &xi_opcode))
+ goto error;
+
+ xi_input_xev = (XEvent*)calloc(1, sizeof(XEvent));
+ if(!xi_input_xev)
+ throw std::runtime_error("gsr-ui error: failed to allocate XEvent data");
+
+ xi_output_xev = (XEvent*)calloc(1, sizeof(XEvent));
+ if(!xi_output_xev)
+ throw std::runtime_error("gsr-ui error: failed to allocate XEvent data");
+
+ unsigned char mask[XIMaskLen(XI_LASTEVENT)];
+ memset(mask, 0, sizeof(mask));
+ XISetMask(mask, XI_Motion);
+ //XISetMask(mask, XI_RawMotion);
+ XISetMask(mask, XI_ButtonPress);
+ XISetMask(mask, XI_ButtonRelease);
+ XISetMask(mask, XI_KeyPress);
+ XISetMask(mask, XI_KeyRelease);
+
+ XIEventMask xi_masks;
+ xi_masks.deviceid = XIAllMasterDevices;
+ xi_masks.mask_len = sizeof(mask);
+ xi_masks.mask = mask;
+ if(XISelectEvents(xi_display, DefaultRootWindow(xi_display), &xi_masks, 1) != Success) {
+ fprintf(stderr, "gsr-ui error: XISelectEvents failed\n");
+ goto error;
+ }
+
+ XFlush(xi_display);
+ return;
+
+ error:
+ free(xi_input_xev);
+ xi_input_xev = nullptr;
+ free(xi_output_xev);
+ xi_output_xev = nullptr;
+ if(xi_display) {
+ XCloseDisplay(xi_display);
+ xi_display = nullptr;
+ }
+ }
+
+ void Overlay::close_gpu_screen_recorder_output() {
+ if(gpu_screen_recorder_process_output_file) {
+ fclose(gpu_screen_recorder_process_output_file);
+ gpu_screen_recorder_process_output_file = nullptr;
+ }
+
+ if(gpu_screen_recorder_process_output_fd > 0) {
+ close(gpu_screen_recorder_process_output_fd);
+ gpu_screen_recorder_process_output_fd = -1;
+ }
+ }
+
+ void Overlay::handle_xi_events() {
+ if(!xi_display)
+ return;
+
+ mgl_context *context = mgl_get_context();
+ Display *display = (Display*)context->connection;
+
+ while(XPending(xi_display)) {
+ XNextEvent(xi_display, xi_input_xev);
+ XGenericEventCookie *cookie = &xi_input_xev->xcookie;
+ if(cookie->type == GenericEvent && cookie->extension == xi_opcode && XGetEventData(xi_display, cookie)) {
+ const XIDeviceEvent *de = (XIDeviceEvent*)cookie->data;
+ if(cookie->evtype == XI_Motion) {
+ memset(xi_output_xev, 0, sizeof(*xi_output_xev));
+ xi_output_xev->type = MotionNotify;
+ xi_output_xev->xmotion.display = display;
+ xi_output_xev->xmotion.window = window->get_system_handle();
+ xi_output_xev->xmotion.subwindow = window->get_system_handle();
+ xi_output_xev->xmotion.x = de->root_x - window_pos.x;
+ xi_output_xev->xmotion.y = de->root_y - window_pos.y;
+ xi_output_xev->xmotion.x_root = de->root_x;
+ xi_output_xev->xmotion.y_root = de->root_y;
+ //xi_output_xev->xmotion.state = // modifiers // TODO:
+ if(window->inject_x11_event(xi_output_xev, event))
+ on_event(event);
+ } else if(cookie->evtype == XI_ButtonPress || cookie->evtype == XI_ButtonRelease) {
+ memset(xi_output_xev, 0, sizeof(*xi_output_xev));
+ xi_output_xev->type = cookie->evtype == XI_ButtonPress ? ButtonPress : ButtonRelease;
+ xi_output_xev->xbutton.display = display;
+ xi_output_xev->xbutton.window = window->get_system_handle();
+ xi_output_xev->xbutton.subwindow = window->get_system_handle();
+ xi_output_xev->xbutton.x = de->root_x - window_pos.x;
+ xi_output_xev->xbutton.y = de->root_y - window_pos.y;
+ xi_output_xev->xbutton.x_root = de->root_x;
+ xi_output_xev->xbutton.y_root = de->root_y;
+ //xi_output_xev->xbutton.state = // modifiers // TODO:
+ xi_output_xev->xbutton.button = de->detail;
+ if(window->inject_x11_event(xi_output_xev, event))
+ on_event(event);
+ } else if(cookie->evtype == XI_KeyPress || cookie->evtype == XI_KeyRelease) {
+ memset(xi_output_xev, 0, sizeof(*xi_output_xev));
+ xi_output_xev->type = cookie->evtype == XI_KeyPress ? KeyPress : KeyRelease;
+ xi_output_xev->xkey.display = display;
+ xi_output_xev->xkey.window = window->get_system_handle();
+ xi_output_xev->xkey.subwindow = window->get_system_handle();
+ xi_output_xev->xkey.x = de->root_x - window_pos.x;
+ xi_output_xev->xkey.y = de->root_y - window_pos.y;
+ xi_output_xev->xkey.x_root = de->root_x;
+ xi_output_xev->xkey.y_root = de->root_y;
+ xi_output_xev->xkey.state = de->mods.effective;
+ xi_output_xev->xkey.keycode = de->detail;
+ if(window->inject_x11_event(xi_output_xev, event))
+ on_event(event);
+ }
+ //fprintf(stderr, "got xi event: %d\n", cookie->evtype);
+ XFreeEventData(xi_display, cookie);
+ }
+ }
}
static uint32_t key_event_to_bitmask(mgl::Event::KeyEvent key_event) {
@@ -397,11 +617,42 @@ namespace gsr {
}
}
+ 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;
+ handle_xi_events();
+
while(window->poll_event(event)) {
+ if(global_hotkeys) {
+ if(!global_hotkeys->on_event(event))
+ continue;
+ }
on_event(event);
}
}
@@ -410,7 +661,9 @@ namespace gsr {
if(!visible || !window)
return;
- close_button_widget.on_event(event, *window, mgl::vec2f(0.0f, 0.0f));
+ if(!close_button_widget.on_event(event, *window, mgl::vec2f(0.0f, 0.0f)))
+ return;
+
if(!page_stack.on_event(event, *window, mgl::vec2f(0.0f, 0.0f)))
return;
@@ -419,6 +672,7 @@ namespace gsr {
bool Overlay::draw() {
update_notification_process_status();
+ update_gsr_replay_save();
update_gsr_process_status();
replay_status_update_status();
@@ -433,55 +687,229 @@ namespace gsr {
if(!window)
return false;
+ grab_mouse_and_keyboard();
+
//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));
+
+ 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);
+ }
+
+ window->draw(top_bar_background);
+ window->draw(top_bar_text);
+ window->draw(logo_sprite);
- window->draw(top_bar_background);
- window->draw(top_bar_text);
- window->draw(logo_sprite);
+ close_button_widget.draw(*window, mgl::vec2f(0.0f, 0.0f));
+ page_stack.draw(*window, mgl::vec2f(0.0f, 0.0f));
- close_button_widget.draw(*window, mgl::vec2f(0.0f, 0.0f));
- page_stack.draw(*window, mgl::vec2f(0.0f, 0.0f));
+ 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);
+ }
+ }
window->display();
return true;
}
+ void Overlay::grab_mouse_and_keyboard() {
+ // TODO: Remove these grabs when debugging with a debugger, or your X11 session will appear frozen.
+ // There should be a debug mode to not use these
+ mgl_context *context = mgl_get_context();
+ Display *display = (Display*)context->connection;
+ XGrabPointer(display, window->get_system_handle(), True,
+ ButtonPressMask | ButtonReleaseMask | PointerMotionMask |
+ Button1MotionMask | Button2MotionMask | Button3MotionMask | Button4MotionMask | Button5MotionMask |
+ ButtonMotionMask,
+ GrabModeAsync, GrabModeAsync, None, default_cursor, CurrentTime);
+ // TODO: This breaks global hotkeys (when using x11 global hotkeys)
+ XGrabKeyboard(display, window->get_system_handle(), True, GrabModeAsync, GrabModeAsync, CurrentTime);
+ XFlush(display);
+ }
+
+ void Overlay::xi_setup_fake_cursor() {
+ if(!xi_display)
+ return;
+
+ XFixesHideCursor(xi_display, DefaultRootWindow(xi_display));
+ XFlush(xi_display);
+
+ // TODO: XCURSOR_SIZE and XCURSOR_THEME environment variables
+ const char *cursor_theme = XcursorGetTheme(xi_display);
+ if(!cursor_theme) {
+ //fprintf(stderr, "Warning: failed to get cursor theme, using \"default\" theme instead\n");
+ cursor_theme = "default";
+ }
+
+ int cursor_size = XcursorGetDefaultSize(xi_display);
+ if(cursor_size <= 1)
+ cursor_size = 24;
+
+ XcursorImage *cursor_image = nullptr;
+ for(int cursor_size_test : {cursor_size, 24}) {
+ for(const char *cursor_theme_test : {cursor_theme, "default", "Adwaita"}) {
+ for(unsigned int shape : {XC_left_ptr, XC_arrow}) {
+ cursor_image = XcursorShapeLoadImage(shape, cursor_theme_test, cursor_size_test);
+ if(cursor_image)
+ goto done;
+ }
+ }
+ }
+
+ done:
+ if(!cursor_image) {
+ fprintf(stderr, "Error: failed to get cursor, loading bundled default cursor instead\n");
+ const std::string default_cursor_path = resources_path + "images/default.cur";
+ for(int cursor_size_test : {cursor_size, 24}) {
+ cursor_image = XcursorFilenameLoadImage(default_cursor_path.c_str(), cursor_size_test);
+ if(cursor_image)
+ break;
+ }
+ }
+
+ if(!cursor_image) {
+ fprintf(stderr, "Error: failed to get cursor\n");
+ XFixesShowCursor(xi_display, DefaultRootWindow(xi_display));
+ XFlush(xi_display);
+ return;
+ }
+
+ bool cursor_visible = false;
+ texture_from_x11_cursor(cursor_image, &cursor_visible, &cursor_hotspot, cursor_texture);
+ if(cursor_texture.is_valid())
+ cursor_sprite.set_texture(&cursor_texture);
+
+ XcursorImageDestroy(cursor_image);
+ }
+
+ 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;
+
+ int num_devices = 0;
+ XIDeviceInfo *info = XIQueryDevice(xi_display, XIAllDevices, &num_devices);
+ if(!info)
+ return;
+
+ unsigned char mask[XIMaskLen(XI_LASTEVENT)];
+ memset(mask, 0, sizeof(mask));
+ XISetMask(mask, XI_Motion);
+ //XISetMask(mask, XI_RawMotion);
+ XISetMask(mask, XI_ButtonPress);
+ XISetMask(mask, XI_ButtonRelease);
+ XISetMask(mask, XI_KeyPress);
+ XISetMask(mask, XI_KeyRelease);
+
+ 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);
+ xi_masks.mask = mask;
+ XIGrabDevice(xi_display, dev->deviceid, window->get_system_handle(), CurrentTime, None, XIGrabModeAsync, XIGrabModeAsync, XIOwnerEvents, &xi_masks);
+ }
+
+ XFlush(xi_display);
+ XIFreeDeviceInfo(info);
+ }
+
void Overlay::show() {
+ if(visible)
+ return;
+
+ drawn_first_frame = false;
window.reset();
window = std::make_unique<mgl::Window>();
deinit_theme();
- mgl::vec2i window_size = { 1280, 720 };
- mgl::vec2i window_pos = { 0, 0 };
+ 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 || is_wlroots;
+
+ 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;
- window_create_params.min_size = window_size;
- window_create_params.max_size = window_size;
- window_create_params.position = window_pos;
- window_create_params.hidden = true;
- window_create_params.override_redirect = true;
- window_create_params.background_color = bg_color;
+ if(prevent_game_minimizing) {
+ window_create_params.min_size = window_size;
+ window_create_params.max_size = window_size;
+ }
+ 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 = mgl::Color(0, 0, 0, 0);
window_create_params.support_alpha = true;
- window_create_params.window_type = MGL_WINDOW_TYPE_NOTIFICATION;
- window_create_params.render_api = MGL_RENDER_API_EGL;
+ 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;
+ // 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");
- mgl_context *context = mgl_get_context();
- Display *display = (Display*)context->connection;
+ //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);
@@ -489,30 +917,27 @@ namespace gsr {
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_by_cursor_position(*window);
- 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);
- window->set_size(window_size);
- window->set_size_limits(window_size, window_size);
- window->set_position(window_pos);
+ if(prevent_game_minimizing) {
+ window->set_size(window_size);
+ window->set_size_limits(window_size, window_size);
+ }
+ window->set_position(focused_monitor->position + focused_monitor->size / 2 - original_window_size / 2);
- update_compositor_texture(focused_monitor);
+ 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;
- top_bar_text = mgl::Text("GPU Screen Recorder", get_theme().top_bar_font);
- logo_sprite = mgl::Sprite(&get_theme().logo_texture);
+ 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());
@@ -545,6 +970,7 @@ namespace gsr {
const int button_width = button_height;
auto main_buttons_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
+ List * main_buttons_list_ptr = main_buttons_list.get();
main_buttons_list->set_spacing(0.0f);
{
auto button = std::make_unique<DropdownButton>(&get_theme().title_font, &get_theme().body_font, "Instant Replay", "Off", &get_theme().replay_button_texture,
@@ -555,9 +981,14 @@ namespace gsr {
button->add_item("Settings", "settings");
button->set_item_icon("start", &get_theme().play_texture);
button->set_item_icon("save", &get_theme().save_texture);
+ button->set_item_icon("settings", &get_theme().settings_small_texture);
button->on_click = [this](const std::string &id) {
if(id == "settings") {
- auto replay_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::REPLAY, gsr_info, config, &page_stack);
+ auto replay_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::REPLAY, &gsr_info, config, &page_stack);
+ replay_settings_page->on_config_changed = [this]() {
+ if(recording_status == RecordingStatus::REPLAY)
+ show_notification("Replay settings have been modified.\nYou may need to restart replay to apply the changes.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
+ };
page_stack.push(std::move(replay_settings_page));
} else if(id == "save") {
on_press_save_replay();
@@ -576,9 +1007,14 @@ namespace gsr {
button->add_item("Settings", "settings");
button->set_item_icon("start", &get_theme().play_texture);
button->set_item_icon("pause", &get_theme().pause_texture);
+ button->set_item_icon("settings", &get_theme().settings_small_texture);
button->on_click = [this](const std::string &id) {
if(id == "settings") {
- auto record_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::RECORD, gsr_info, config, &page_stack);
+ auto record_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::RECORD, &gsr_info, config, &page_stack);
+ record_settings_page->on_config_changed = [this]() {
+ if(recording_status == RecordingStatus::RECORD)
+ show_notification("Recording settings have been modified.\nYou may need to restart recording to apply the changes.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
+ };
page_stack.push(std::move(record_settings_page));
} else if(id == "pause") {
toggle_pause();
@@ -595,9 +1031,14 @@ namespace gsr {
button->add_item("Start", "start", "Alt+F8");
button->add_item("Settings", "settings");
button->set_item_icon("start", &get_theme().play_texture);
+ button->set_item_icon("settings", &get_theme().settings_small_texture);
button->on_click = [this](const std::string &id) {
if(id == "settings") {
- auto stream_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::STREAM, gsr_info, config, &page_stack);
+ auto stream_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::STREAM, &gsr_info, config, &page_stack);
+ stream_settings_page->on_config_changed = [this]() {
+ if(recording_status == RecordingStatus::STREAM)
+ show_notification("Streaming settings have been modified.\nYou may need to restart streaming to apply the changes.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
+ };
page_stack.push(std::move(stream_settings_page));
} else if(id == "start") {
on_press_start_stream();
@@ -610,6 +1051,59 @@ namespace gsr {
main_buttons_list->set_position((mgl::vec2f(window_size.x * 0.5f, window_size.y * 0.25f) - main_buttons_list_size * 0.5f).floor());
front_page_ptr->add_widget(std::move(main_buttons_list));
+ {
+ const mgl::vec2f main_buttons_size = main_buttons_list_ptr->get_size();
+ const int settings_button_size = main_buttons_size.y * 0.2f;
+ auto button = std::make_unique<Button>(&get_theme().title_font, "", mgl::vec2f(settings_button_size, settings_button_size), mgl::Color(0, 0, 0, 180));
+ button->set_position((main_buttons_list_ptr->get_position() + main_buttons_size - mgl::vec2f(0.0f, settings_button_size) + mgl::vec2f(settings_button_size * 0.333f, 0.0f)).floor());
+ button->set_bg_hover_color(mgl::Color(0, 0, 0, 255));
+ button->set_icon(&get_theme().settings_small_texture);
+ button->on_click = [&]() {
+ 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;
+
+ if(exit_status == 127) {
+ if(enable)
+ show_notification("Failed to add GPU Screen Recorder to system startup.\nThis option only works on systems that use systemd.\nYou have to manually add \"gsr-ui\" to system startup on systems that uses another init system.", 10.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE);
+ } else {
+ if(enable)
+ show_notification("Failed to add GPU Screen Recorder to system startup", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE);
+ else
+ show_notification("Failed to remove GPU Screen Recorder from system startup", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE);
+ }
+ };
+
+ 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));
+ }
+
close_button_widget.draw_handler = [&](mgl::Window &window, mgl::vec2f pos, mgl::vec2f size) {
const int border_size = std::max(1.0f, 0.0015f * get_theme().window_height);
const float padding_size = std::max(1.0f, 0.003f * get_theme().window_height);
@@ -637,8 +1131,18 @@ namespace gsr {
return true;
};
- window->set_fullscreen(true);
+ // The focused application can be an xwayland application but the cursor can hover over a wayland application.
+ // This is even the case when hovering over the titlebar of the xwayland application.
+ const bool fake_cursor = is_wlroots ? x11_cursor_window != None : prevent_game_minimizing;
+ if(fake_cursor)
+ xi_setup();
+
+ //window->set_fullscreen(true);
+ if(gsr_info.system_info.display_server == DisplayServer::X11)
+ make_window_click_through(display, window->get_system_handle());
+
window->set_visible(true);
+
make_window_sticky(display, window->get_system_handle());
hide_window_from_taskbar(display, window->get_system_handle());
@@ -646,33 +1150,23 @@ namespace gsr {
XFreeCursor(display, default_cursor);
default_cursor = 0;
}
- default_cursor = XCreateFontCursor(display, XC_arrow);
+ default_cursor = XCreateFontCursor(display, XC_left_ptr);
+ XFlush(display);
- // TODO: Retry if these fail.
- // TODO: Hmm, these dont work in owlboy. Maybe owlboy uses xi2 and that breaks this (does it?).
- // Remove these grabs when debugging with a debugger, or your X11 session will appear frozen
+ grab_mouse_and_keyboard();
- // XGrabPointer(display, window->get_system_handle(), True,
- // ButtonPressMask | ButtonReleaseMask | PointerMotionMask |
- // Button1MotionMask | Button2MotionMask | Button3MotionMask | Button4MotionMask | Button5MotionMask |
- // ButtonMotionMask,
- // GrabModeAsync, GrabModeAsync, None, default_cursor, CurrentTime);
- // TODO: This breaks global hotkeys
- //XGrabKeyboard(display, window->get_system_handle(), True, GrabModeAsync, GrabModeAsync, CurrentTime);
+ // The real cursor doesn't move when all devices are grabbed, so we create our own cursor and diplay that while grabbed
+ xi_setup_fake_cursor();
- set_focused_window(display, window->get_system_handle());
- XFlush(display);
+ // We want to grab all devices to prevent any other application below the UI from receiving events.
+ // Owlboy seems to use xi events and XGrabPointer doesn't prevent owlboy from receiving events.
+ xi_grab_all_mouse_devices();
- //window->set_fullscreen(true);
+ if(!is_wlroots)
+ window->set_fullscreen(true);
visible = 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(gpu_screen_recorder_process > 0) {
switch(recording_status) {
case RecordingStatus::NONE:
@@ -691,9 +1185,18 @@ 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() {
+ if(!visible)
+ return;
+
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
@@ -710,13 +1213,57 @@ namespace gsr {
XUngrabPointer(display, CurrentTime);
XFlush(display);
+ if(xi_display) {
+ cursor_texture.clear();
+ cursor_sprite.set_texture(nullptr);
+ }
+
window_texture_deinit(&window_texture);
window_texture_sprite.set_texture(nullptr);
screenshot_texture.clear();
screenshot_sprite.set_texture(nullptr);
visible = false;
+ drawn_first_frame = false;
+
+ if(xi_input_xev) {
+ free(xi_input_xev);
+ xi_input_xev = nullptr;
+ }
+
+ if(xi_output_xev) {
+ free(xi_output_xev);
+ xi_output_xev = nullptr;
+ }
+
+ if(xi_display) {
+ XCloseDisplay(xi_display);
+ xi_display = nullptr;
+
+ if(window) {
+ mgl_context *context = mgl_get_context();
+ Display *display = (Display*)context->connection;
+
+ const mgl::vec2i new_cursor_position = mgl::vec2i(window->internal_window()->pos.x, window->internal_window()->pos.y) + window->get_mouse_position();
+ XWarpPointer(display, DefaultRootWindow(display), DefaultRootWindow(display), 0, 0, 0, 0, new_cursor_position.x, new_cursor_position.y);
+ XFlush(display);
+
+ XFixesShowCursor(display, DefaultRootWindow(display));
+ XFlush(display);
+ }
+ }
+
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();
}
@@ -725,10 +1272,16 @@ namespace gsr {
}
void Overlay::toggle_show() {
- if(visible)
- hide();
- else
+ if(visible) {
+ //hide();
+ // We dont want to hide immediately because hide is called in mgl event callback, in which it destroys the mgl window.
+ // Instead remove all pages and wait until next iteration to close the UI (which happens when there are no pages to render).
+ while(!page_stack.empty()) {
+ page_stack.pop();
+ }
+ } else {
show();
+ }
}
void Overlay::toggle_record() {
@@ -787,7 +1340,7 @@ namespace gsr {
const char *notification_type_str = notification_type_to_string(notification_type);
if(notification_type_str) {
notification_args[9] = "--icon";
- notification_args[10] = "record",
+ notification_args[10] = notification_type_str;
notification_args[11] = nullptr;
} else {
notification_args[9] = nullptr;
@@ -799,13 +1352,40 @@ namespace gsr {
waitpid(notification_process, &status, 0);
}
- notification_process = exec_program(notification_args);
+ notification_process = exec_program(notification_args, NULL);
}
bool Overlay::is_open() const {
return visible;
}
+ bool Overlay::should_exit(std::string &reason) const {
+ reason.clear();
+ if(do_exit)
+ reason = exit_reason;
+ return do_exit;
+ }
+
+ void Overlay::exit() {
+ do_exit = true;
+ }
+
+ const Config& Overlay::get_config() const {
+ 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;
@@ -819,32 +1399,128 @@ namespace gsr {
notification_process = -1;
}
+ static void string_replace_characters(char *str, const char *characters_to_replace, char new_character) {
+ for(; *str != '\0'; ++str) {
+ for(const char *p = characters_to_replace; *p != '\0'; ++p) {
+ if(*str == *p)
+ *str = new_character;
+ }
+ }
+ }
+
+ static std::string filepath_get_directory(const char *filepath) {
+ std::string result = filepath;
+ const size_t last_slash_index = result.rfind('/');
+ if(last_slash_index == std::string::npos)
+ result = ".";
+ else
+ result.erase(last_slash_index);
+ return result;
+ }
+
+ static std::string filepath_get_filename(const char *filepath) {
+ std::string result = filepath;
+ const size_t last_slash_index = result.rfind('/');
+ if(last_slash_index != std::string::npos)
+ result.erase(0, last_slash_index + 1);
+ return result;
+ }
+
+ static void truncate_string(std::string &str, int max_length) {
+ if((int)str.size() > max_length)
+ str.replace(str.begin() + max_length, str.end(), "...");
+ }
+
+ void Overlay::save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type) {
+ mgl_context *context = mgl_get_context();
+ Display *display = (Display*)context->connection;
+ const std::string video_filename = filepath_get_filename(video_filepath);
+
+ const Window gsr_ui_window = window ? window->get_system_handle() : None;
+ std::string focused_window_name = get_window_name_at_cursor_position(display, gsr_ui_window);
+ if(focused_window_name.empty())
+ focused_window_name = get_focused_window_name(display, WindowCaptureType::FOCUSED);
+ if(focused_window_name.empty())
+ focused_window_name = "Game";
+
+ string_replace_characters(focused_window_name.data(), "/\\", '_');
+
+ std::string video_directory = filepath_get_directory(video_filepath) + "/" + focused_window_name;
+ create_directory_recursive(video_directory.data());
+
+ const std::string new_video_filepath = video_directory + "/" + video_filename;
+ rename(video_filepath, new_video_filepath.c_str());
+
+ truncate_string(focused_window_name, 20);
+ std::string text;
+ switch(notification_type) {
+ case NotificationType::RECORD: {
+ if(!config.record_config.show_video_saved_notifications)
+ return;
+ text = "Saved recording to '" + focused_window_name + "/" + video_filename + "'";
+ break;
+ }
+ case NotificationType::REPLAY: {
+ if(!config.replay_config.show_replay_saved_notifications)
+ return;
+ text = "Saved replay to '" + focused_window_name + "/" + video_filename + "'";
+ break;
+ }
+ case NotificationType::NONE:
+ case NotificationType::STREAM:
+ break;
+ }
+ show_notification(text.c_str(), 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, notification_type);
+ }
+
+ void Overlay::on_replay_saved(const char *replay_saved_filepath) {
+ replay_save_show_notification = false;
+ if(config.replay_config.save_video_in_game_folder) {
+ save_video_in_current_game_directory(replay_saved_filepath, NotificationType::REPLAY);
+ } else {
+ const std::string text = "Saved replay to '" + filepath_get_filename(replay_saved_filepath) + "'";
+ show_notification(text.c_str(), 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
+ }
+ }
+
+ void Overlay::update_gsr_replay_save() {
+ if(replay_save_show_notification && replay_save_clock.get_elapsed_time_seconds() >= replay_saving_notification_timeout_seconds) {
+ replay_save_show_notification = false;
+ show_notification("Saving replay, this might take some time", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
+ }
+
+ if(gpu_screen_recorder_process_output_file) {
+ char buffer[1024];
+ char *replay_saved_filepath = fgets(buffer, sizeof(buffer), gpu_screen_recorder_process_output_file);
+ if(!replay_saved_filepath || replay_saved_filepath[0] == '\0')
+ return;
+
+ const int line_len = strlen(replay_saved_filepath);
+ if(replay_saved_filepath[line_len - 1] == '\n')
+ replay_saved_filepath[line_len - 1] = '\0';
+
+ on_replay_saved(replay_saved_filepath);
+ } else if(gpu_screen_recorder_process_output_fd > 0) {
+ char buffer[1024];
+ read(gpu_screen_recorder_process_output_fd, buffer, sizeof(buffer));
+ }
+ }
+
void Overlay::update_gsr_process_status() {
if(gpu_screen_recorder_process <= 0)
return;
- errno = 0;
int status;
if(waitpid(gpu_screen_recorder_process, &status, WNOHANG) == 0) {
// Still running
return;
}
+ close_gpu_screen_recorder_output();
+
int exit_code = -1;
- // The process is no longer a child process since gsr ui has restarted
- if(errno == ECHILD) {
- errno = 0;
- kill(gpu_screen_recorder_process, 0);
- if(errno != ESRCH) {
- // Still running
- return;
- }
- // We cant know the exit status, so we assume it succeeded
- exit_code = 0;
- } else {
- if(WIFEXITED(status))
- exit_code = WEXITSTATUS(status);
- }
+ if(WIFEXITED(status))
+ exit_code = WEXITSTATUS(status);
switch(recording_status) {
case RecordingStatus::NONE:
@@ -856,16 +1532,13 @@ namespace gsr {
show_notification("Replay stopped", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
} else {
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
- show_notification("Replay stopped because of an error", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY);
+ show_notification("Replay stopped because of an error. Verify if settings are correct", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY);
}
break;
}
case RecordingStatus::RECORD: {
update_ui_recording_stopped();
- if(exit_code != 0) {
- fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
- show_notification("Failed to start/save recording", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD);
- }
+ on_stop_recording(exit_code);
break;
}
case RecordingStatus::STREAM: {
@@ -875,7 +1548,7 @@ namespace gsr {
show_notification("Streaming has stopped", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
} else {
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
- show_notification("Streaming stopped because of an error", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM);
+ show_notification("Streaming stopped because of an error. Verify if settings are correct", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM);
}
break;
}
@@ -901,7 +1574,7 @@ namespace gsr {
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
- const Window focused_window = get_focused_window(display);
+ const Window focused_window = get_focused_window(display, WindowCaptureType::FOCUSED);
if(window && focused_window == window->get_system_handle())
return;
@@ -915,6 +1588,7 @@ namespace gsr {
}
}
+ // TODO: Instead of checking power supply status periodically listen to power supply event
void Overlay::update_power_supply_status() {
if(config.replay_config.turn_on_replay_automatically_mode != "turn_on_at_power_supply_connected")
return;
@@ -929,6 +1603,20 @@ namespace gsr {
}
}
+ void Overlay::on_stop_recording(int exit_code) {
+ if(exit_code == 0) {
+ if(config.record_config.save_video_in_game_folder) {
+ save_video_in_current_game_directory(record_filepath.c_str(), NotificationType::RECORD);
+ } else {
+ const std::string text = "Saved recording to '" + filepath_get_filename(record_filepath.c_str()) + "'";
+ show_notification(text.c_str(), 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD);
+ }
+ } else {
+ fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
+ show_notification("Failed to start/save recording. Verify if settings are correct", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD);
+ }
+ }
+
void Overlay::update_ui_recording_paused() {
if(!visible || recording_status != RecordingStatus::RECORD)
return;
@@ -1088,12 +1776,35 @@ namespace gsr {
args.push_back(audio_track.c_str());
}
}
+
+ if(record_options.restore_portal_session) {
+ args.push_back("-restore-portal-session");
+ args.push_back("yes");
+ }
+ }
+
+ static bool validate_capture_target(const GsrInfo &gsr_info, const std::string &capture_target) {
+ const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info);
+ // TODO: Also check x11 window when enabled (check if capture_target is a decminal/hex number)
+ if(capture_target == "focused") {
+ return capture_options.focused;
+ } else if(capture_target == "portal") {
+ return capture_options.portal;
+ } else {
+ for(const GsrMonitor &monitor : capture_options.monitors) {
+ if(capture_target == monitor.name)
+ return true;
+ }
+ return false;
+ }
}
void Overlay::on_press_save_replay() {
if(recording_status != RecordingStatus::REPLAY || gpu_screen_recorder_process <= 0)
return;
+ replay_save_show_notification = true;
+ replay_save_clock.restart();
kill(gpu_screen_recorder_process, SIGUSR1);
}
@@ -1111,10 +1822,13 @@ namespace gsr {
}
paused = false;
+ replay_save_show_notification = false;
// window->close();
// usleep(1000 * 50); // 50 milliseconds
+ close_gpu_screen_recorder_output();
+
if(gpu_screen_recorder_process > 0) {
kill(gpu_screen_recorder_process, SIGINT);
int status;
@@ -1133,6 +1847,13 @@ namespace gsr {
return;
}
+ if(!validate_capture_target(gsr_info, config.replay_config.record_options.record_area_option)) {
+ char err_msg[256];
+ snprintf(err_msg, sizeof(err_msg), "Failed to start replay, capture target \"%s\" is invalid. Please change capture target in settings", config.replay_config.record_options.record_area_option.c_str());
+ show_notification(err_msg, 3.0, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::REPLAY);
+ return;
+ }
+
// TODO: Validate input, fallback to valid values
const std::string fps = std::to_string(config.replay_config.record_options.fps);
const std::string video_bitrate = std::to_string(config.replay_config.record_options.video_bitrate);
@@ -1149,7 +1870,9 @@ namespace gsr {
}
char region[64];
- snprintf(region, sizeof(region), "%dx%d", (int)config.replay_config.record_options.record_area_width, (int)config.replay_config.record_options.record_area_height);
+ region[0] = '\0';
+ if(config.replay_config.record_options.record_area_option == "focused")
+ snprintf(region, sizeof(region), "%dx%d", (int)config.replay_config.record_options.record_area_width, (int)config.replay_config.record_options.record_area_height);
if(config.replay_config.record_options.record_area_option != "focused" && config.replay_config.record_options.change_video_resolution)
snprintf(region, sizeof(region), "%dx%d", (int)config.replay_config.record_options.video_width, (int)config.replay_config.record_options.video_height);
@@ -1169,16 +1892,16 @@ namespace gsr {
"-o", output_directory.c_str()
};
- add_common_gpu_screen_recorder_args(args, config.replay_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged);
+ 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");
+ }
- setenv("GSR_SHOW_SAVED_NOTIFICATION", config.replay_config.show_replay_saved_notifications ? "1" : "0", true);
- const std::string script_to_run_on_save = resources_path + (config.replay_config.save_video_in_game_folder ? "scripts/save-video-in-game-folder.sh" : "scripts/notify-saved-name.sh");
- args.push_back("-sc");
- args.push_back(script_to_run_on_save.c_str());
+ add_common_gpu_screen_recorder_args(args, config.replay_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged);
args.push_back(nullptr);
- gpu_screen_recorder_process = exec_program(args.data());
+ gpu_screen_recorder_process = exec_program(args.data(), &gpu_screen_recorder_process_output_fd);
if(gpu_screen_recorder_process == -1) {
// TODO: Show notification failed to start
} else {
@@ -1186,6 +1909,12 @@ namespace gsr {
update_ui_replay_started();
}
+ const int fdl = fcntl(gpu_screen_recorder_process_output_fd, F_GETFL);
+ fcntl(gpu_screen_recorder_process_output_fd, F_SETFL, fdl | O_NONBLOCK);
+ gpu_screen_recorder_process_output_file = fdopen(gpu_screen_recorder_process_output_fd, "r");
+ if(gpu_screen_recorder_process_output_file)
+ gpu_screen_recorder_process_output_fd = -1;
+
// TODO: Start recording after this notification has disappeared to make sure it doesn't show up in the video.
// Make clear to the user that the recording starts after the notification is gone.
// Maybe have the option in notification to show timer until its getting hidden, then the notification can say:
@@ -1223,17 +1952,29 @@ namespace gsr {
if(waitpid(gpu_screen_recorder_process, &status, 0) == -1) {
perror("waitpid failed");
/* Ignore... */
+ } else {
+ int exit_code = -1;
+ if(WIFEXITED(status))
+ exit_code = WEXITSTATUS(status);
+ on_stop_recording(exit_code);
}
- // window->set_visible(false);
- // window->close();
- // return;
- //exit(0);
+
gpu_screen_recorder_process = -1;
recording_status = RecordingStatus::NONE;
update_ui_recording_stopped();
+ record_filepath.clear();
return;
}
+ if(!validate_capture_target(gsr_info, config.record_config.record_options.record_area_option)) {
+ char err_msg[256];
+ snprintf(err_msg, sizeof(err_msg), "Failed to start recording, capture target \"%s\" is invalid. Please change capture target in settings", config.record_config.record_options.record_area_option.c_str());
+ show_notification(err_msg, 3.0, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::RECORD);
+ return;
+ }
+
+ record_filepath.clear();
+
// TODO: Validate input, fallback to valid values
const std::string fps = std::to_string(config.record_config.record_options.fps);
const std::string video_bitrate = std::to_string(config.record_config.record_options.video_bitrate);
@@ -1249,7 +1990,9 @@ namespace gsr {
}
char region[64];
- snprintf(region, sizeof(region), "%dx%d", (int)config.record_config.record_options.record_area_width, (int)config.record_config.record_options.record_area_height);
+ region[0] = '\0';
+ if(config.record_config.record_options.record_area_option == "focused")
+ snprintf(region, sizeof(region), "%dx%d", (int)config.record_config.record_options.record_area_width, (int)config.record_config.record_options.record_area_height);
if(config.record_config.record_options.record_area_option != "focused" && config.record_config.record_options.change_video_resolution)
snprintf(region, sizeof(region), "%dx%d", (int)config.record_config.record_options.video_width, (int)config.record_config.record_options.video_height);
@@ -1270,14 +2013,10 @@ namespace gsr {
add_common_gpu_screen_recorder_args(args, config.record_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged);
- setenv("GSR_SHOW_SAVED_NOTIFICATION", config.record_config.show_video_saved_notifications ? "1" : "0", true);
- const std::string script_to_run_on_save = resources_path + (config.record_config.save_video_in_game_folder ? "scripts/save-video-in-game-folder.sh" : "scripts/notify-saved-name.sh");
- args.push_back("-sc");
- args.push_back(script_to_run_on_save.c_str());
-
args.push_back(nullptr);
- gpu_screen_recorder_process = exec_program(args.data());
+ record_filepath = output_file;
+ gpu_screen_recorder_process = exec_program(args.data(), nullptr);
if(gpu_screen_recorder_process == -1) {
// TODO: Show notification failed to start
} else {
@@ -1363,6 +2102,13 @@ namespace gsr {
return;
}
+ if(!validate_capture_target(gsr_info, config.streaming_config.record_options.record_area_option)) {
+ char err_msg[256];
+ snprintf(err_msg, sizeof(err_msg), "Failed to start streaming, capture target \"%s\" is invalid. Please change capture target in settings", config.streaming_config.record_options.record_area_option.c_str());
+ show_notification(err_msg, 3.0, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::STREAM);
+ return;
+ }
+
// TODO: Validate input, fallback to valid values
const std::string fps = std::to_string(config.streaming_config.record_options.fps);
const std::string video_bitrate = std::to_string(config.streaming_config.record_options.video_bitrate);
@@ -1383,7 +2129,9 @@ namespace gsr {
const std::string url = streaming_get_url(config);
char region[64];
- snprintf(region, sizeof(region), "%dx%d", (int)config.streaming_config.record_options.record_area_width, (int)config.streaming_config.record_options.record_area_height);
+ region[0] = '\0';
+ if(config.record_config.record_options.record_area_option == "focused")
+ snprintf(region, sizeof(region), "%dx%d", (int)config.streaming_config.record_options.record_area_width, (int)config.streaming_config.record_options.record_area_height);
if(config.record_config.record_options.record_area_option != "focused" && config.streaming_config.record_options.change_video_resolution)
snprintf(region, sizeof(region), "%dx%d", (int)config.streaming_config.record_options.video_width, (int)config.streaming_config.record_options.video_height);
@@ -1397,16 +2145,16 @@ namespace gsr {
"-fm", framerate_mode.c_str(),
"-encoder", encoder,
"-f", fps.c_str(),
- "-f", fps.c_str(),
"-v", "no",
"-o", url.c_str()
};
+ config.streaming_config.record_options.merge_audio_tracks = true;
add_common_gpu_screen_recorder_args(args, config.streaming_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged);
args.push_back(nullptr);
- gpu_screen_recorder_process = exec_program(args.data());
+ gpu_screen_recorder_process = exec_program(args.data(), nullptr);
if(gpu_screen_recorder_process == -1) {
// TODO: Show notification failed to start
} else {
@@ -1427,7 +2175,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();
@@ -1440,15 +2188,17 @@ namespace gsr {
return false;
bool window_texture_loaded = false;
- const Window window_at_cursor_position = get_window_at_cursor_position(display);
- if(is_window_fullscreen_on_monitor(display, window_at_cursor_position, monitor) && window_at_cursor_position)
- window_texture_loaded = window_texture_init(&window_texture, display, mgl_window_get_egl_display(window->internal_window()), window_at_cursor_position, egl_funcs) == 0;
+ Window focused_window = get_focused_window(display, WindowCaptureType::CURSOR);
+ if(!focused_window)
+ focused_window = get_focused_window(display, WindowCaptureType::FOCUSED);
+ if(focused_window && is_window_fullscreen_on_monitor(display, focused_window, monitor))
+ window_texture_loaded = window_texture_init(&window_texture, display, mgl_window_get_egl_display(window->internal_window()), focused_window, egl_funcs) == 0;
if(window_texture_loaded && window_texture.texture_id) {
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");
@@ -1474,4 +2224,4 @@ namespace gsr {
XFlush(display);
}
}
-} \ No newline at end of file
+}
diff --git a/src/Process.cpp b/src/Process.cpp
index 07d9dc6..c5fcf0f 100644
--- a/src/Process.cpp
+++ b/src/Process.cpp
@@ -9,6 +9,9 @@
#include <dirent.h>
#include <stdlib.h>
+#define PIPE_READ 0
+#define PIPE_WRITE 1
+
namespace gsr {
static void debug_print_args(const char **args) {
fprintf(stderr, "gsr-ui info: running command:");
@@ -19,6 +22,24 @@ namespace gsr {
fprintf(stderr, "\n");
}
+ static bool is_number(const char *str) {
+ for(int i = 0; str[i]; ++i) {
+ char c = str[i];
+ if(c < '0' || c > '9')
+ return false;
+ }
+ return true;
+ }
+
+ static int count_num_args(const char **args) {
+ int num_args = 0;
+ while(*args) {
+ ++num_args;
+ ++args;
+ }
+ return num_args;
+ }
+
bool exec_program_daemonized(const char **args) {
/* 1 argument */
if(args[0] == nullptr)
@@ -26,7 +47,7 @@ namespace gsr {
debug_print_args(args);
- pid_t pid = vfork();
+ const pid_t pid = vfork();
if(pid == -1) {
perror("Failed to vfork");
return false;
@@ -35,10 +56,10 @@ namespace gsr {
signal(SIGHUP, SIG_IGN);
// Daemonize child to make the parent the init process which will reap the zombie child
- pid_t second_child = vfork();
+ const pid_t second_child = vfork();
if(second_child == 0) { // child
execvp(args[0], (char* const*)args);
- perror("execvp");
+ perror(args[0]);
_exit(127);
} else if(second_child != -1) {
// TODO:
@@ -51,27 +72,112 @@ namespace gsr {
return true;
}
- pid_t exec_program(const char **args) {
+ pid_t exec_program(const char **args, int *read_fd) {
+ if(read_fd)
+ *read_fd = -1;
+
/* 1 argument */
if(args[0] == nullptr)
return -1;
+ int fds[2] = {-1, -1};
+ if(pipe(fds) == -1)
+ return -1;
+
debug_print_args(args);
- pid_t pid = vfork();
+ const pid_t pid = vfork();
if(pid == -1) {
+ close(fds[PIPE_READ]);
+ close(fds[PIPE_WRITE]);
perror("Failed to vfork");
return -1;
} else if(pid == 0) { /* child */
+ dup2(fds[PIPE_WRITE], STDOUT_FILENO);
+ close(fds[PIPE_READ]);
+ close(fds[PIPE_WRITE]);
+
execvp(args[0], (char* const*)args);
- perror("execvp");
+ perror(args[0]);
_exit(127);
} else { /* parent */
+ close(fds[PIPE_WRITE]);
+ if(read_fd)
+ *read_fd = fds[PIPE_READ];
+ else
+ close(fds[PIPE_READ]);
return pid;
}
}
- bool read_cmdline_arg0(const char *filepath, char *output_buffer) {
+ int exec_program_get_stdout(const char **args, std::string &result) {
+ result.clear();
+ int read_fd = -1;
+ const pid_t process_id = exec_program(args, &read_fd);
+ if(process_id == -1)
+ return -1;
+
+ int exit_status = 0;
+ char buffer[8192];
+ for(;;) {
+ ssize_t bytes_read = read(read_fd, buffer, sizeof(buffer));
+ if(bytes_read == 0) {
+ break;
+ } else if(bytes_read == -1) {
+ fprintf(stderr, "Failed to read from pipe to program %s, error: %s\n", args[0], strerror(errno));
+ exit_status = -1;
+ break;
+ }
+
+ buffer[bytes_read] = '\0';
+ result.append(buffer, bytes_read);
+ }
+
+ if(exit_status != 0)
+ kill(process_id, SIGKILL);
+
+ int status = 0;
+ if(waitpid(process_id, &status, 0) == -1) {
+ perror("waitpid failed");
+ exit_status = -1;
+ }
+
+ if(!WIFEXITED(status))
+ exit_status = -1;
+
+ if(exit_status == 0)
+ exit_status = WEXITSTATUS(status);
+
+ close(read_fd);
+ return exit_status;
+ }
+
+ int exec_program_on_host_get_stdout(const char **args, std::string &result) {
+ if(count_num_args(args) > 64 - 3) {
+ fprintf(stderr, "Error: too many arguments when trying to launch \"%s\"\n", args[0]);
+ return -1;
+ }
+
+ const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
+ if(inside_flatpak) {
+ // Assumes programs wont need more than 64 - 3 args
+ const char *modified_args[64] = { "flatpak-spawn", "--host", "--" };
+ for(int i = 3; i < 64; ++i) {
+ const char *arg = args[i - 3];
+ if(!arg) {
+ modified_args[i] = nullptr;
+ break;
+ }
+ modified_args[i] = arg;
+ }
+ return exec_program_get_stdout(modified_args, result);
+ } else {
+ return exec_program_get_stdout(args, result);
+ }
+ }
+
+ // |output_buffer| should be at least PATH_MAX in size
+ bool read_cmdline_arg0(const char *filepath, char *output_buffer, int output_buffer_size) {
output_buffer[0] = '\0';
const char *arg0_end = NULL;
@@ -88,13 +194,43 @@ namespace gsr {
if(!arg0_end)
goto err;
- memcpy(output_buffer, buffer, arg0_end - buffer);
- output_buffer[arg0_end - buffer] = '\0';
- close(fd);
- return true;
+ if((arg0_end - buffer) + 1 <= output_buffer_size) {
+ memcpy(output_buffer, buffer, arg0_end - buffer);
+ output_buffer[arg0_end - buffer] = '\0';
+ close(fd);
+ return true;
+ }
err:
close(fd);
return false;
}
+
+ pid_t pidof(const char *process_name, pid_t ignore_pid) {
+ pid_t result = -1;
+ DIR *dir = opendir("/proc");
+ if(!dir)
+ return -1;
+
+ char cmdline_filepath[PATH_MAX];
+ char arg0[PATH_MAX];
+
+ struct dirent *entry;
+ while((entry = readdir(dir)) != NULL) {
+ if(!is_number(entry->d_name))
+ continue;
+
+ snprintf(cmdline_filepath, sizeof(cmdline_filepath), "/proc/%s/cmdline", entry->d_name);
+ if(read_cmdline_arg0(cmdline_filepath, arg0, sizeof(arg0)) && strcmp(process_name, arg0) == 0) {
+ const pid_t pid = atoi(entry->d_name);
+ if(pid != ignore_pid) {
+ result = pid;
+ break;
+ }
+ }
+ }
+
+ closedir(dir);
+ return result;
+ }
} \ No newline at end of file
diff --git a/src/Rpc.cpp b/src/Rpc.cpp
new file mode 100644
index 0000000..3eec98d
--- /dev/null
+++ b/src/Rpc.cpp
@@ -0,0 +1,133 @@
+#include "../include/Rpc.hpp"
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <limits.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+namespace gsr {
+ static void get_runtime_filepath(char *buffer, size_t buffer_size, const char *filename) {
+ char dir[PATH_MAX];
+
+ const char *runtime_dir = getenv("XDG_RUNTIME_DIR");
+ if(runtime_dir)
+ snprintf(dir, sizeof(dir), "%s", runtime_dir);
+ else
+ snprintf(dir, sizeof(dir), "/run/user/%d", geteuid());
+
+ if(access(dir, F_OK) != 0)
+ snprintf(dir, sizeof(dir), "/tmp");
+
+ snprintf(buffer, buffer_size, "%s/%s", dir, filename);
+ }
+
+ Rpc::~Rpc() {
+ if(fd > 0)
+ close(fd);
+
+ if(file)
+ fclose(file);
+
+ if(!fifo_filepath.empty())
+ remove(fifo_filepath.c_str());
+ }
+
+ bool Rpc::create(const char *name) {
+ if(file) {
+ fprintf(stderr, "Error: Rpc::create: already created/opened\n");
+ return false;
+ }
+
+ char fifo_filepath_tmp[PATH_MAX];
+ get_runtime_filepath(fifo_filepath_tmp, sizeof(fifo_filepath_tmp), name);
+ fifo_filepath = fifo_filepath_tmp;
+ remove(fifo_filepath.c_str());
+
+ if(mkfifo(fifo_filepath.c_str(), 0600) != 0) {
+ fprintf(stderr, "Error: mkfifo failed, error: %s, %s\n", strerror(errno), fifo_filepath.c_str());
+ return false;
+ }
+
+ if(!open_filepath(fifo_filepath.c_str())) {
+ remove(fifo_filepath.c_str());
+ fifo_filepath.clear();
+ return false;
+ }
+
+ return true;
+ }
+
+ bool Rpc::open(const char *name) {
+ if(file) {
+ fprintf(stderr, "Error: Rpc::open: already created/opened\n");
+ return false;
+ }
+
+ char fifo_filepath_tmp[PATH_MAX];
+ get_runtime_filepath(fifo_filepath_tmp, sizeof(fifo_filepath_tmp), name);
+ return open_filepath(fifo_filepath_tmp);
+ }
+
+ bool Rpc::open_filepath(const char *filepath) {
+ fd = ::open(filepath, O_RDWR | O_NONBLOCK);
+ if(fd <= 0)
+ return false;
+
+ file = fdopen(fd, "r+");
+ if(!file) {
+ close(fd);
+ fd = 0;
+ return false;
+ }
+ fd = 0;
+ return true;
+ }
+
+ bool Rpc::write(const char *str, size_t size) {
+ if(!file) {
+ fprintf(stderr, "Error: Rpc::write: fifo not created/opened yet\n");
+ return false;
+ }
+
+ ssize_t offset = 0;
+ while(offset < (ssize_t)size) {
+ const ssize_t bytes_written = fwrite(str + offset, 1, size - offset, file);
+ fflush(file);
+ if(bytes_written > 0)
+ offset += bytes_written;
+ }
+ return true;
+ }
+
+ void Rpc::poll() {
+ if(!file) {
+ //fprintf(stderr, "Error: Rpc::poll: fifo not created/opened yet\n");
+ return;
+ }
+
+ std::string name;
+ char line[1024];
+ while(fgets(line, sizeof(line), file)) {
+ int line_len = strlen(line);
+ if(line_len == 0)
+ continue;
+
+ if(line[line_len - 1] == '\n') {
+ line[line_len - 1] = '\0';
+ --line_len;
+ }
+
+ name = line;
+ auto it = handlers_by_name.find(name);
+ if(it != handlers_by_name.end())
+ it->second(name);
+ }
+ }
+
+ bool Rpc::add_handler(const std::string &name, RpcCallback callback) {
+ return handlers_by_name.insert(std::make_pair(name, std::move(callback))).second;
+ }
+} \ No newline at end of file
diff --git a/src/Theme.cpp b/src/Theme.cpp
index a88aa1e..a6d1050 100644
--- a/src/Theme.cpp
+++ b/src/Theme.cpp
@@ -1,4 +1,5 @@
#include "../include/Theme.hpp"
+#include "../include/Config.hpp"
#include "../include/GsrInfo.hpp"
#include <assert.h>
@@ -7,6 +8,27 @@ namespace gsr {
static Theme *theme = nullptr;
static ColorTheme *color_theme = nullptr;
+ static mgl::Color gpu_vendor_to_color(GpuVendor vendor) {
+ switch(vendor) {
+ case GpuVendor::UNKNOWN: return mgl::Color(221, 0, 49);
+ case GpuVendor::AMD: return mgl::Color(221, 0, 49);
+ case GpuVendor::INTEL: return mgl::Color(8, 109, 183);
+ case GpuVendor::NVIDIA: return mgl::Color(118, 185, 0);
+ }
+ return mgl::Color(221, 0, 49);
+ }
+
+ static mgl::Color color_name_to_color(const std::string &color_name) {
+ GpuVendor vendor = GpuVendor::UNKNOWN;
+ if(color_name == "amd")
+ vendor = GpuVendor::AMD;
+ else if(color_name == "intel")
+ vendor = GpuVendor::INTEL;
+ else if(color_name == "nvidia")
+ vendor = GpuVendor::NVIDIA;
+ return gpu_vendor_to_color(vendor);
+ }
+
bool Theme::set_window_size(mgl::vec2i window_size) {
if(std::abs(window_size.x - window_width) < 0.1f && std::abs(window_size.y - window_height) < 0.1f)
return true;
@@ -44,6 +66,9 @@ namespace gsr {
if(!theme->settings_texture.load_from_file((resources_path + "images/settings.png").c_str()))
goto error;
+ if(!theme->settings_small_texture.load_from_file((resources_path + "images/settings_small.png").c_str()))
+ goto error;
+
if(!theme->folder_texture.load_from_file((resources_path + "images/folder.png").c_str()))
goto error;
@@ -102,29 +127,16 @@ namespace gsr {
return *theme;
}
- bool init_color_theme(const GsrInfo &gsr_info) {
+ bool init_color_theme(const Config &config, const GsrInfo &gsr_info) {
if(color_theme)
return true;
color_theme = new ColorTheme();
- switch(gsr_info.gpu_info.vendor) {
- case GpuVendor::UNKNOWN: {
- break;
- }
- case GpuVendor::AMD: {
- color_theme->tint_color = mgl::Color(221, 0, 49);
- break;
- }
- case GpuVendor::INTEL: {
- color_theme->tint_color = mgl::Color(8, 109, 183);
- break;
- }
- case GpuVendor::NVIDIA: {
- color_theme->tint_color = mgl::Color(118, 185, 0);
- break;
- }
- }
+ if(config.main_config.tint_color.empty())
+ color_theme->tint_color = gpu_vendor_to_color(gsr_info.gpu_info.vendor);
+ else
+ color_theme->tint_color = color_name_to_color(config.main_config.tint_color);
return true;
}
diff --git a/src/WindowUtils.cpp b/src/WindowUtils.cpp
new file mode 100644
index 0000000..ec01e26
--- /dev/null
+++ b/src/WindowUtils.cpp
@@ -0,0 +1,530 @@
+#include "../include/WindowUtils.hpp"
+
+#include <X11/Xlib.h>
+#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>
+
+#define MAX_PROPERTY_VALUE_LEN 4096
+
+namespace gsr {
+ static unsigned char* window_get_property(Display *dpy, Window window, Atom property_type, const char *property_name, unsigned int *property_size) {
+ Atom ret_property_type = None;
+ int ret_format = 0;
+ unsigned long num_items = 0;
+ unsigned long num_remaining_bytes = 0;
+ unsigned char *data = nullptr;
+ const Atom atom = XInternAtom(dpy, property_name, False);
+ if(XGetWindowProperty(dpy, window, atom, 0, MAX_PROPERTY_VALUE_LEN / 4, False, property_type, &ret_property_type, &ret_format, &num_items, &num_remaining_bytes, &data) != Success || !data) {
+ return nullptr;
+ }
+
+ if(ret_property_type != property_type) {
+ XFree(data);
+ return nullptr;
+ }
+
+ *property_size = (ret_format / (32 / sizeof(long))) * num_items;
+ return data;
+ }
+
+ static bool window_has_atom(Display *dpy, Window window, Atom atom) {
+ Atom type;
+ unsigned long len, bytes_left;
+ int format;
+ unsigned char *properties = NULL;
+ if(XGetWindowProperty(dpy, window, atom, 0, 1024, False, AnyPropertyType, &type, &format, &len, &bytes_left, &properties) < Success)
+ return false;
+
+ if(properties)
+ XFree(properties);
+
+ return type != None;
+ }
+
+ static bool window_is_user_program(Display *dpy, Window window) {
+ const Atom net_wm_state_atom = XInternAtom(dpy, "_NET_WM_STATE", False);
+ const Atom wm_state_atom = XInternAtom(dpy, "WM_STATE", False);
+ return window_has_atom(dpy, window, net_wm_state_atom) || window_has_atom(dpy, window, wm_state_atom);
+ }
+
+ static Window window_get_target_window_child(Display *display, Window window) {
+ if(window == None)
+ return None;
+
+ if(window_is_user_program(display, window))
+ return window;
+
+ Window root;
+ Window parent;
+ Window *children = nullptr;
+ unsigned int num_children = 0;
+ if(!XQueryTree(display, window, &root, &parent, &children, &num_children) || !children)
+ return None;
+
+ Window found_window = None;
+ for(int i = num_children - 1; i >= 0; --i) {
+ if(children[i] && window_is_user_program(display, children[i])) {
+ found_window = children[i];
+ goto finished;
+ }
+ }
+
+ for(int i = num_children - 1; i >= 0; --i) {
+ if(children[i]) {
+ Window win = window_get_target_window_child(display, children[i]);
+ if(win) {
+ found_window = win;
+ goto finished;
+ }
+ }
+ }
+
+ finished:
+ XFree(children);
+ return found_window;
+ }
+
+ 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);
+
+ const Window direct_window = *window;
+ *window = window_get_target_window_child(dpy, *window);
+ // HACK: Count some other x11 windows as having an x11 window focused. Some games seem to create an Input window and that gets focused.
+ if(!*window) {
+ XWindowAttributes attr;
+ memset(&attr, 0, sizeof(attr));
+ XGetWindowAttributes(dpy, direct_window, &attr);
+ if(attr.c_class == InputOnly && !get_window_title(dpy, direct_window))
+ *window = direct_window;
+ }
+ return root_pos;
+ }
+
+ Window get_focused_window(Display *dpy, WindowCaptureType cap_type) {
+ //const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False);
+ Window focused_window = None;
+
+ if(cap_type == WindowCaptureType::FOCUSED) {
+ // Atom type = None;
+ // int format = 0;
+ // unsigned long num_items = 0;
+ // unsigned long bytes_left = 0;
+ // unsigned char *data = NULL;
+ // XGetWindowProperty(dpy, DefaultRootWindow(dpy), net_active_window_atom, 0, 1, False, XA_WINDOW, &type, &format, &num_items, &bytes_left, &data);
+
+ // if(type == XA_WINDOW && num_items == 1 && data)
+ // focused_window = *(Window*)data;
+
+ // if(data)
+ // XFree(data);
+
+ // if(focused_window)
+ // return focused_window;
+
+ int revert_to = 0;
+ XGetInputFocus(dpy, &focused_window, &revert_to);
+ if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window))
+ return focused_window;
+ }
+
+ get_cursor_position(dpy, &focused_window);
+ if(focused_window && focused_window != DefaultRootWindow(dpy))
+ return focused_window;
+
+ return None;
+ }
+
+ 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;
+ }
+
+ std::optional<std::string> get_window_title(Display *dpy, Window window) {
+ std::optional<std::string> result;
+ const Atom net_wm_name_atom = XInternAtom(dpy, "_NET_WM_NAME", False);
+ const Atom wm_name_atom = XInternAtom(dpy, "WM_NAME", False);
+ const Atom utf8_string_atom = XInternAtom(dpy, "UTF8_STRING", False);
+
+ Atom type = None;
+ int format = 0;
+ unsigned long num_items = 0;
+ unsigned long bytes_left = 0;
+ unsigned char *data = NULL;
+ XGetWindowProperty(dpy, window, net_wm_name_atom, 0, 1024, False, utf8_string_atom, &type, &format, &num_items, &bytes_left, &data);
+
+ if(type == utf8_string_atom && format == 8 && data) {
+ result = utf8_sanitize(data, num_items);
+ goto done;
+ }
+
+ if(data)
+ XFree(data);
+
+ type = None;
+ format = 0;
+ num_items = 0;
+ bytes_left = 0;
+ data = NULL;
+ XGetWindowProperty(dpy, window, wm_name_atom, 0, 1024, False, 0, &type, &format, &num_items, &bytes_left, &data);
+
+ if((type == XA_STRING || type == utf8_string_atom) && data) {
+ result = utf8_sanitize(data, num_items);
+ goto done;
+ }
+
+ done:
+ if(data)
+ XFree(data);
+ return result;
+ }
+
+ 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] != ' ') {
+ start_index += i;
+ str_len -= i;
+ break;
+ }
+ }
+
+ for(int i = str_len - 1; i >= 0; --i) {
+ if(str[i] != ' ') {
+ str_len = i + 1;
+ break;
+ }
+ }
+
+ return str.substr(start_index, str_len);
+ }
+
+ std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type) {
+ std::string result;
+ const Window focused_window = get_focused_window(dpy, window_capture_type);
+ if(focused_window == None)
+ return result;
+
+ // Window title is not always ideal (for example for a browser), but for games its pretty much required
+ const std::optional<std::string> window_title = get_window_title(dpy, focused_window);
+ if(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(class_hint.res_class);
+ return result;
+ }
+
+ return result;
+ }
+
+ std::string get_window_name_at_position(Display *dpy, mgl::vec2i position, Window ignore_window) {
+ std::string result;
+
+ Window root;
+ Window parent;
+ Window *children = nullptr;
+ unsigned int num_children = 0;
+ if(!XQueryTree(dpy, DefaultRootWindow(dpy), &root, &parent, &children, &num_children) || !children)
+ return result;
+
+ for(int i = (int)num_children - 1; i >= 0; --i) {
+ if(children[i] == ignore_window)
+ continue;
+
+ XWindowAttributes attr;
+ memset(&attr, 0, sizeof(attr));
+ XGetWindowAttributes(dpy, children[i], &attr);
+ if(attr.override_redirect || attr.c_class != InputOutput || attr.map_state != IsViewable)
+ continue;
+
+ if(position.x >= attr.x && position.x <= attr.x + attr.width && position.y >= attr.y && position.y <= attr.y + attr.height) {
+ const Window real_window = window_get_target_window_child(dpy, children[i]);
+ if(!real_window || real_window == ignore_window)
+ continue;
+
+ const std::optional<std::string> window_title = get_window_title(dpy, real_window);
+ if(window_title)
+ result = strip(window_title.value());
+
+ break;
+ }
+ }
+
+ XFree(children);
+ return result;
+ }
+
+ std::string get_window_name_at_cursor_position(Display *dpy, Window ignore_window) {
+ Window cursor_window;
+ const mgl::vec2i cursor_position = get_cursor_position(dpy, &cursor_window);
+ return get_window_name_at_position(dpy, cursor_position, ignore_window);
+ }
+
+ typedef struct {
+ unsigned long flags;
+ unsigned long functions;
+ unsigned long decorations;
+ long input_mode;
+ unsigned long status;
+ } MotifHints;
+
+ #define MWM_HINTS_DECORATIONS 2
+
+ #define MWM_DECOR_NONE 0
+ #define MWM_DECOR_ALL 1
+
+ static void window_set_decorations_visible(Display *display, Window window, bool visible) {
+ const Atom motif_wm_hints_atom = XInternAtom(display, "_MOTIF_WM_HINTS", False);
+ MotifHints motif_hints;
+ memset(&motif_hints, 0, sizeof(motif_hints));
+ motif_hints.flags = MWM_HINTS_DECORATIONS;
+ motif_hints.decorations = visible ? MWM_DECOR_ALL : MWM_DECOR_NONE;
+ XChangeProperty(display, window, motif_wm_hints_atom, motif_wm_hints_atom, 32, PropModeReplace, (unsigned char*)&motif_hints, sizeof(motif_hints) / sizeof(long));
+ }
+
+ static bool create_window_get_center_position_kde(Display *display, mgl::vec2i &position) {
+ const int size = 1;
+ XSetWindowAttributes window_attr;
+ window_attr.event_mask = StructureNotifyMask;
+ window_attr.background_pixel = 0;
+ const Window window = XCreateWindow(display, DefaultRootWindow(display), 0, 0, size, size, 0, CopyFromParent, InputOutput, CopyFromParent, CWBackPixel | CWEventMask, &window_attr);
+ if(!window)
+ return false;
+
+ const Atom net_wm_window_type_atom = XInternAtom(display, "_NET_WM_WINDOW_TYPE", False);
+ const Atom net_wm_window_type_notification_atom = XInternAtom(display, "_NET_WM_WINDOW_TYPE_NOTIFICATION", False);
+ const Atom net_wm_window_type_utility = XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", False);
+ const Atom net_wm_window_opacity = XInternAtom(display, "_NET_WM_WINDOW_OPACITY", False);
+
+ const Atom window_type_atoms[2] = {
+ net_wm_window_type_notification_atom,
+ net_wm_window_type_utility
+ };
+ XChangeProperty(display, window, net_wm_window_type_atom, XA_ATOM, 32, PropModeReplace, (const unsigned char*)window_type_atoms, 2L);
+
+ const double alpha = 0.0;
+ const unsigned long opacity = (unsigned long)(0xFFFFFFFFul * alpha);
+ XChangeProperty(display, window, net_wm_window_opacity, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&opacity, 1L);
+
+ window_set_decorations_visible(display, window, false);
+
+ XSizeHints *size_hints = XAllocSizeHints();
+ size_hints->width = size;
+ size_hints->height = size;
+ size_hints->min_width = size;
+ size_hints->min_height = size;
+ size_hints->max_width = size;
+ size_hints->max_height = size;
+ size_hints->flags = PSize | PMinSize | PMaxSize;
+ XSetWMNormalHints(display, window, size_hints);
+ XFree(size_hints);
+
+ XMapWindow(display, window);
+ XFlush(display);
+
+ bool got_data = false;
+ const int x_fd = XConnectionNumber(display);
+ XEvent xev;
+ while(true) {
+ struct pollfd poll_fd;
+ poll_fd.fd = x_fd;
+ poll_fd.events = POLLIN;
+ poll_fd.revents = 0;
+ 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;
+ } else if(fds_ready == -1 || !(poll_fd.revents & POLLIN)) {
+ continue;
+ }
+
+ 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);
+
+ return got_data;
+ }
+
+ static bool create_window_get_center_position_gnome(Display *display, mgl::vec2i &position) {
+ const int size = 32;
+ XSetWindowAttributes window_attr;
+ window_attr.event_mask = StructureNotifyMask | ExposureMask;
+ window_attr.background_pixel = 0;
+ const Window window = XCreateWindow(display, DefaultRootWindow(display), 0, 0, size, size, 0, CopyFromParent, InputOutput, CopyFromParent, CWBackPixel | CWEventMask, &window_attr);
+ if(!window)
+ return false;
+
+ const Atom net_wm_window_opacity = XInternAtom(display, "_NET_WM_WINDOW_OPACITY", False);
+ const double alpha = 0.0;
+ const unsigned long opacity = (unsigned long)(0xFFFFFFFFul * alpha);
+ XChangeProperty(display, window, net_wm_window_opacity, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&opacity, 1L);
+
+ window_set_decorations_visible(display, window, false);
+
+ XSizeHints *size_hints = XAllocSizeHints();
+ size_hints->width = size;
+ size_hints->height = size;
+ size_hints->min_width = size;
+ size_hints->min_height = size;
+ size_hints->max_width = size;
+ size_hints->max_height = size;
+ size_hints->flags = PSize | PMinSize | PMaxSize;
+ XSetWMNormalHints(display, window, size_hints);
+ XFree(size_hints);
+
+ XMapWindow(display, window);
+ XFlush(display);
+
+ bool got_data = false;
+ const int x_fd = XConnectionNumber(display);
+ XEvent xev;
+ while(true) {
+ struct pollfd poll_fd;
+ poll_fd.fd = x_fd;
+ poll_fd.events = POLLIN;
+ poll_fd.revents = 0;
+ 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;
+ } else if(fds_ready == -1 || !(poll_fd.revents & POLLIN)) {
+ continue;
+ }
+
+ 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);
+
+ return got_data;
+ }
+
+ mgl::vec2i create_window_get_center_position(Display *display) {
+ mgl::vec2i pos;
+ if(!create_window_get_center_position_kde(display, pos)) {
+ pos.x = 0;
+ pos.y = 0;
+ create_window_get_center_position_gnome(display, pos);
+ }
+ return pos;
+ }
+
+ std::string get_window_manager_name(Display *display) {
+ std::string wm_name;
+ unsigned int property_size = 0;
+ Window window = None;
+
+ unsigned char *net_supporting_wm_check = window_get_property(display, DefaultRootWindow(display), XA_WINDOW, "_NET_SUPPORTING_WM_CHECK", &property_size);
+ if(net_supporting_wm_check) {
+ if(property_size == 8)
+ window = *(Window*)net_supporting_wm_check;
+ XFree(net_supporting_wm_check);
+ }
+
+ if(!window) {
+ unsigned char *win_supporting_wm_check = window_get_property(display, DefaultRootWindow(display), XA_WINDOW, "_WIN_SUPPORTING_WM_CHECK", &property_size);
+ if(win_supporting_wm_check) {
+ if(property_size == 8)
+ window = *(Window*)win_supporting_wm_check;
+ XFree(win_supporting_wm_check);
+ }
+ }
+
+ if(!window)
+ return wm_name;
+
+ const std::optional<std::string> window_title = get_window_title(display, window);
+ if(window_title)
+ wm_name = strip(window_title.value());
+
+ return wm_name;
+ }
+
+ bool is_compositor_running(Display *dpy, int screen) {
+ char prop_name[20];
+ snprintf(prop_name, sizeof(prop_name), "_NET_WM_CM_S%d", screen);
+ 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/Button.cpp b/src/gui/Button.cpp
index fbf5cdd..54d1854 100644
--- a/src/gui/Button.cpp
+++ b/src/gui/Button.cpp
@@ -12,7 +12,15 @@ namespace gsr {
static const float padding_left_scale = 0.007f;
static const float padding_right_scale = 0.007f;
- Button::Button(mgl::Font *font, const char *text, mgl::vec2f size, mgl::Color bg_color) : size(size), bg_color(bg_color), text(text, *font) {
+ // These are relative to the button size
+ static const float padding_top_icon_scale = 0.25f;
+ static const float padding_bottom_icon_scale = 0.25f;
+ static const float padding_left_icon_scale = 0.25f;
+ static const float padding_right_icon_scale = 0.25f;
+
+ Button::Button(mgl::Font *font, const char *text, mgl::vec2f size, mgl::Color bg_color) :
+ size(size), bg_color(bg_color), bg_hover_color(bg_color), text(text, *font)
+ {
}
@@ -37,17 +45,23 @@ namespace gsr {
return;
const mgl::vec2f draw_pos = position + offset;
-
const mgl::vec2f item_size = get_size().floor();
+ const bool mouse_inside = mgl::FloatRect(draw_pos, item_size).contains(window.get_mouse_position().to_vec2f()) && !has_parent_with_selected_child_widget();
+
mgl::Rectangle background(item_size);
background.set_position(draw_pos.floor());
- background.set_color(bg_color);
+ background.set_color(mouse_inside ? bg_hover_color : bg_color);
window.draw(background);
text.set_position((draw_pos + item_size * 0.5f - text.get_bounds().size * 0.5f).floor());
window.draw(text);
- const bool mouse_inside = mgl::FloatRect(draw_pos, item_size).contains(window.get_mouse_position().to_vec2f()) && !has_parent_with_selected_child_widget();
+ if(sprite.get_texture() && sprite.get_texture()->is_valid()) {
+ scale_sprite_to_button_size();
+ sprite.set_position((background.get_position() + background.get_size() * 0.5f - sprite.get_size() * 0.5f).floor());
+ window.draw(sprite);
+ }
+
if(mouse_inside) {
const mgl::Color outline_color = (bg_color == get_color_theme().tint_color) ? mgl::Color(255, 255, 255) : get_color_theme().tint_color;
draw_rectangle_outline(window, draw_pos, item_size, outline_color, std::max(1.0f, border_scale * get_theme().window_height));
@@ -76,6 +90,14 @@ namespace gsr {
border_scale = scale;
}
+ void Button::set_bg_hover_color(mgl::Color color) {
+ bg_hover_color = color;
+ }
+
+ void Button::set_icon(mgl::Texture *texture) {
+ sprite.set_texture(texture);
+ }
+
const std::string& Button::get_text() const {
return text.get_string();
}
@@ -83,4 +105,18 @@ namespace gsr {
void Button::set_text(std::string str) {
text.set_string(std::move(str));
}
+
+ void Button::scale_sprite_to_button_size() {
+ if(!sprite.get_texture() || !sprite.get_texture()->is_valid())
+ return;
+
+ const mgl::vec2f button_size = get_size();
+ const int padding_icon_top = padding_top_icon_scale * button_size.y;
+ const int padding_icon_bottom = padding_bottom_icon_scale * button_size.y;
+ const int padding_icon_left = padding_left_icon_scale * button_size.y;
+ const int padding_icon_right = padding_right_icon_scale * button_size.y;
+
+ const mgl::vec2f desired_size = button_size - mgl::vec2f(padding_icon_left + padding_icon_right, padding_icon_top + padding_icon_bottom);
+ sprite.set_size(scale_keep_aspect_ratio(sprite.get_texture()->get_size().to_vec2f(), desired_size).floor());
+ }
} \ No newline at end of file
diff --git a/src/gui/ComboBox.cpp b/src/gui/ComboBox.cpp
index 62b2086..948e3a4 100644
--- a/src/gui/ComboBox.cpp
+++ b/src/gui/ComboBox.cpp
@@ -26,16 +26,21 @@ namespace gsr {
return true;
if(event.type == mgl::Event::MouseButtonPressed && event.mouse_button.button == mgl::Mouse::Left) {
+ const int padding_top = padding_top_scale * get_theme().window_height;
+ const int padding_bottom = padding_bottom_scale * get_theme().window_height;
+
const mgl::vec2f mouse_pos = { (float)event.mouse_button.x, (float)event.mouse_button.y };
- const mgl::vec2f item_size = get_size();
+ mgl::vec2f item_size = get_size();
if(show_dropdown) {
for(size_t i = 0; i < items.size(); ++i) {
Item &item = items[i];
+ item_size.y = padding_top + item.text.get_bounds().size.y + padding_bottom;
if(mgl::FloatRect(item.position, item_size).contains(mouse_pos)) {
const size_t prev_selected_item = selected_item;
selected_item = i;
show_dropdown = false;
+ dirty = true;
remove_widget_as_selected_in_parent();
if(selected_item != prev_selected_item && on_selection_changed)
@@ -47,6 +52,7 @@ namespace gsr {
}
const mgl::vec2f draw_pos = position + offset;
+ item_size = get_size();
if(mgl::FloatRect(draw_pos, item_size).contains(mouse_pos)) {
show_dropdown = !show_dropdown;
if(show_dropdown)
@@ -66,9 +72,10 @@ namespace gsr {
if(!visible)
return;
+ //const mgl::Scissor scissor = window.get_scissor();
update_if_dirty();
-
const mgl::vec2f draw_pos = (position + offset).floor();
+ //max_size.x = std::min((scissor.position.x + scissor.size.x) - draw_pos.x, max_size.x);
if(show_dropdown)
draw_selected(window, draw_pos);
@@ -78,6 +85,8 @@ namespace gsr {
void ComboBox::add_item(const std::string &text, const std::string &id) {
items.push_back({mgl::Text(text, *font), id, {0.0f, 0.0f}});
+ items.back().text.set_max_width(font->get_character_size() * 22); // TODO: Make a proper solution
+ //items.back().text.set_max_rows(1);
dirty = true;
}
@@ -87,6 +96,7 @@ namespace gsr {
if(item.id == id) {
const size_t prev_selected_item = selected_item;
selected_item = i;
+ dirty = true;
if(trigger_event && (trigger_event_even_if_selection_not_changed || selected_item != prev_selected_item) && on_selection_changed)
on_selection_changed(item.text.get_string(), item.id);
@@ -107,13 +117,13 @@ namespace gsr {
void ComboBox::draw_selected(mgl::Window &window, mgl::vec2f draw_pos) {
const int padding_top = padding_top_scale * get_theme().window_height;
+ const int padding_bottom = padding_bottom_scale * get_theme().window_height;
const int padding_left = padding_left_scale * get_theme().window_height;
- mgl_scissor scissor;
- mgl_window_get_scissor(window.internal_window(), &scissor);
+ const mgl::Scissor scissor = window.get_scissor();
const bool bottom_is_outside_scissor = draw_pos.y + max_size.y > scissor.position.y + scissor.size.y;
- const mgl::vec2f item_size = get_size();
+ mgl::vec2f item_size = get_size();
mgl::vec2f items_draw_pos = draw_pos + mgl::vec2f(0.0f, item_size.y);
mgl::Rectangle background(draw_pos, item_size.floor());
@@ -137,6 +147,9 @@ namespace gsr {
const mgl::vec2f mouse_pos = window.get_mouse_position().to_vec2f();
for(size_t i = 0; i < items.size(); ++i) {
+ Item &item = items[i];
+ item_size.y = padding_top + item.text.get_bounds().size.y + padding_bottom;
+
if(!cursor_inside) {
cursor_inside = mgl::FloatRect(items_draw_pos, item_size).contains(mouse_pos);
if(cursor_inside) {
@@ -146,7 +159,6 @@ namespace gsr {
}
}
- Item &item = items[i];
item.text.set_position((items_draw_pos + mgl::vec2f(padding_left, padding_top)).floor());
window.draw(item.text);
@@ -160,7 +172,7 @@ namespace gsr {
const int padding_left = padding_left_scale * get_theme().window_height;
const int padding_right = padding_right_scale * get_theme().window_height;
- const mgl::vec2f item_size = get_size();
+ mgl::vec2f item_size = get_size();
mgl::Rectangle background(draw_pos.floor(), item_size.floor());
background.set_color(mgl::Color(0, 0, 0, 120));
window.draw(background);
@@ -197,11 +209,12 @@ namespace gsr {
const int padding_left = padding_left_scale * get_theme().window_height;
const int padding_right = padding_right_scale * get_theme().window_height;
- max_size = { 0.0f, font->get_character_size() + (float)padding_top + (float)padding_bottom };
+ Item *selected_item_ptr = (selected_item < items.size()) ? &items[selected_item] : nullptr;
+ max_size = { 0.0f, padding_top + padding_bottom + (selected_item_ptr ? selected_item_ptr->text.get_bounds().size.y : 0.0f) };
for(Item &item : items) {
const mgl::vec2f bounds = item.text.get_bounds().size;
max_size.x = std::max(max_size.x, bounds.x + padding_left + padding_right);
- max_size.y += bounds.y + padding_top + padding_bottom;
+ max_size.y += padding_top + bounds.y + padding_bottom;
}
if(max_size.x <= 0.001f)
@@ -219,7 +232,8 @@ namespace gsr {
const int padding_top = padding_top_scale * get_theme().window_height;
const int padding_bottom = padding_bottom_scale * get_theme().window_height;
- return { max_size.x, font->get_character_size() + (float)padding_top + (float)padding_bottom };
+ Item *selected_item_ptr = (selected_item < items.size()) ? &items[selected_item] : nullptr;
+ return { max_size.x, padding_top + padding_bottom + (selected_item_ptr ? selected_item_ptr->text.get_bounds().size.y : 0.0f) };
}
float ComboBox::get_dropdown_arrow_height() const {
diff --git a/src/gui/CustomRendererWidget.cpp b/src/gui/CustomRendererWidget.cpp
index cfb113b..5b6c809 100644
--- a/src/gui/CustomRendererWidget.cpp
+++ b/src/gui/CustomRendererWidget.cpp
@@ -17,19 +17,11 @@ namespace gsr {
const mgl::vec2f draw_pos = position + offset;
- mgl_scissor prev_scissor;
- mgl_window_get_scissor(window.internal_window(), &prev_scissor);
-
- mgl_scissor new_scissor = {
- mgl_vec2i{(int)draw_pos.x, (int)draw_pos.y},
- mgl_vec2i{(int)size.x, (int)size.y}
- };
- mgl_window_set_scissor(window.internal_window(), &new_scissor);
-
+ const mgl::Scissor prev_scissor = window.get_scissor();
+ window.set_scissor({draw_pos.to_vec2i(), size.to_vec2i()});
if(draw_handler)
draw_handler(window, draw_pos, size);
-
- mgl_window_set_scissor(window.internal_window(), &prev_scissor);
+ window.set_scissor(prev_scissor);
}
mgl::vec2f CustomRendererWidget::get_size() {
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/FileChooser.cpp b/src/gui/FileChooser.cpp
index a58a582..ceb8c94 100644
--- a/src/gui/FileChooser.cpp
+++ b/src/gui/FileChooser.cpp
@@ -65,8 +65,7 @@ namespace gsr {
if(!visible)
return;
- mgl_scissor scissor;
- mgl_window_get_scissor(window.internal_window(), &scissor);
+ const mgl::Scissor scissor = window.get_scissor();
const mgl::vec2f draw_pos = position + offset;
const mgl::vec2f mouse_pos = window.get_mouse_position().to_vec2f();
@@ -96,7 +95,12 @@ namespace gsr {
selected_item_background.set_color(get_color_theme().tint_color);
window.draw(selected_item_background);
}
- if(!has_parent_with_selected_child_widget() && mouse_over_item == -1 && mgl::FloatRect(item_pos, item_size).contains(mouse_pos)) {
+
+ if(!has_parent_with_selected_child_widget() && mouse_over_item == -1 &&
+ mouse_pos.x >= scissor.position.x && mouse_pos.x <= scissor.position.x + scissor.size.x &&
+ mouse_pos.y >= scissor.position.y && mouse_pos.y <= scissor.position.y + scissor.size.y &&
+ mgl::FloatRect(item_pos, item_size).contains(mouse_pos))
+ {
// mgl::Rectangle selected_item_background(item_size.floor());
// selected_item_background.set_position(item_pos.floor());
// selected_item_background.set_color(mgl::Color(20, 20, 20, 150));
@@ -106,7 +110,7 @@ namespace gsr {
mouse_over_item = i;
}
- if(item_pos.y + item_size.y >= draw_pos.y && item_pos.y < scissor.position.y + scissor.size.y) {
+ if(item_pos.y + item_size.y >= scissor.position.y && item_pos.y < scissor.position.y + scissor.size.y) {
window.draw(folder_sprite);
// TODO: Dont allow text to go further left/right than item_pos (on the left side) and item_pos + item_size (on the right side).
diff --git a/src/gui/GlobalSettingsPage.cpp b/src/gui/GlobalSettingsPage.cpp
new file mode 100644
index 0000000..d00ad49
--- /dev/null
+++ b/src/gui/GlobalSettingsPage.cpp
@@ -0,0 +1,653 @@
+#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"
+#include "../../include/gui/PageStack.hpp"
+#include "../../include/gui/ScrollablePage.hpp"
+#include "../../include/gui/Subsection.hpp"
+#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) {
+ switch(vendor) {
+ case GpuVendor::UNKNOWN: return "amd";
+ case GpuVendor::AMD: return "amd";
+ case GpuVendor::INTEL: return "intel";
+ case GpuVendor::NVIDIA: return "nvidia";
+ }
+ return "amd";
+ }
+
+ 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)
+ {
+ auto content_page = std::make_unique<GsrPage>();
+ content_page->add_button("Back", "back", get_color_theme().page_bg_color);
+ content_page->on_click = [page_stack](const std::string &id) {
+ if(id == "back")
+ page_stack->pop();
+ };
+ content_page_ptr = content_page.get();
+ add_widget(std::move(content_page));
+
+ add_widgets();
+ load();
+
+ 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) {
+ auto list = std::make_unique<List>(List::Orientation::VERTICAL);
+ list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Accent color", get_color_theme().text_color));
+ auto tint_color_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL);
+ tint_color_radio_button_ptr = tint_color_radio_button.get();
+ tint_color_radio_button->add_item("Red", "amd");
+ tint_color_radio_button->add_item("Green", "nvidia");
+ tint_color_radio_button->add_item("blue", "intel");
+ tint_color_radio_button->on_selection_changed = [](const std::string&, const std::string &id) {
+ if(id == "amd")
+ get_color_theme().tint_color = mgl::Color(221, 0, 49);
+ else if(id == "nvidia")
+ get_color_theme().tint_color = mgl::Color(118, 185, 0);
+ else if(id == "intel")
+ get_color_theme().tint_color = mgl::Color(8, 109, 183);
+ return true;
+ };
+ list->add_widget(std::move(tint_color_radio_button));
+ return std::make_unique<Subsection>("Appearance", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
+ }
+
+ std::unique_ptr<Subsection> GlobalSettingsPage::create_startup_subsection(ScrollablePage *parent_page) {
+ auto list = std::make_unique<List>(List::Orientation::VERTICAL);
+ list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Start program on system startup?", get_color_theme().text_color));
+ auto startup_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL);
+ startup_radio_button_ptr = startup_radio_button.get();
+ startup_radio_button->add_item("Yes", "start_on_system_startup");
+ startup_radio_button->add_item("No", "dont_start_on_system_startup");
+ startup_radio_button->on_selection_changed = [&](const std::string&, const std::string &id) {
+ bool enable = false;
+ if(id == "dont_start_on_system_startup")
+ enable = false;
+ else if(id == "start_on_system_startup")
+ enable = true;
+ else
+ return false;
+
+ const char *args[] = { "systemctl", enable ? "enable" : "disable", "--user", "gpu-screen-recorder-ui", nullptr };
+ std::string stdout_str;
+ const int exit_status = exec_program_on_host_get_stdout(args, stdout_str);
+ if(on_startup_changed)
+ on_startup_changed(enable, exit_status);
+ return exit_status == 0;
+ };
+ list->add_widget(std::move(startup_radio_button));
+ return std::make_unique<Subsection>("Startup", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
+ }
+
+ 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_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 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() {
+ auto exit_program_button = std::make_unique<Button>(&get_theme().body_font, "Exit program", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
+ exit_program_button->on_click = [&]() {
+ if(on_click_exit_program_button)
+ on_click_exit_program_button("exit");
+ };
+ return exit_program_button;
+ }
+
+ std::unique_ptr<Button> GlobalSettingsPage::create_go_back_to_old_ui_button() {
+ auto exit_program_button = std::make_unique<Button>(&get_theme().body_font, "Go back to the old UI", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
+ exit_program_button->on_click = [&]() {
+ if(on_click_exit_program_button)
+ on_click_exit_program_button("back-to-old-ui");
+ };
+ return exit_program_button;
+ }
+
+ std::unique_ptr<Subsection> GlobalSettingsPage::create_application_options_subsection(ScrollablePage *parent_page) {
+ const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
+ auto list = std::make_unique<List>(List::Orientation::HORIZONTAL);
+ list->add_widget(create_exit_program_button());
+ if(inside_flatpak)
+ list->add_widget(create_go_back_to_old_ui_button());
+ return std::make_unique<Subsection>("Application options", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
+ }
+
+ 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());
+
+ auto settings_list = std::make_unique<List>(List::Orientation::VERTICAL);
+ settings_list->set_spacing(0.018f);
+ settings_list->add_widget(create_appearance_subsection(scrollable_page.get()));
+ 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));
+ }
+
+ void GlobalSettingsPage::on_navigate_away_from_page() {
+ save();
+ }
+
+ void GlobalSettingsPage::load() {
+ if(config.main_config.tint_color.empty())
+ tint_color_radio_button_ptr->set_selected_item(gpu_vendor_to_color_name(gsr_info->gpu_info.vendor));
+ else
+ tint_color_radio_button_ptr->set_selected_item(config.main_config.tint_color);
+
+ const char *args[] = { "systemctl", "is-enabled", "--quiet", "--user", "gpu-screen-recorder-ui", nullptr };
+ std::string stdout_str;
+ 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_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_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);
+ }
+
+ 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/GsrPage.cpp b/src/gui/GsrPage.cpp
index e6ee5fc..c5fa263 100644
--- a/src/gui/GsrPage.cpp
+++ b/src/gui/GsrPage.cpp
@@ -102,15 +102,8 @@ namespace gsr {
void GsrPage::draw_children(mgl::Window &window, mgl::vec2f position) {
Widget *selected_widget = selected_child_widget;
- mgl_scissor prev_scissor;
- mgl_window_get_scissor(window.internal_window(), &prev_scissor);
-
- const mgl::vec2f inner_size = get_inner_size();
- mgl_scissor new_scissor = {
- mgl_vec2i{(int)position.x, (int)position.y},
- mgl_vec2i{(int)inner_size.x, (int)inner_size.y}
- };
- mgl_window_set_scissor(window.internal_window(), &new_scissor);
+ const mgl::Scissor prev_scissor = window.get_scissor();
+ window.set_scissor({position.to_vec2i(), get_inner_size().to_vec2i()});
for(size_t i = 0; i < widgets.size(); ++i) {
auto &widget = widgets[i];
@@ -121,7 +114,7 @@ namespace gsr {
if(selected_widget)
selected_widget->draw(window, position);
- mgl_window_set_scissor(window.internal_window(), &prev_scissor);
+ window.set_scissor(prev_scissor);
}
mgl::vec2f GsrPage::get_size() {
diff --git a/src/gui/RadioButton.cpp b/src/gui/RadioButton.cpp
index 061d811..a6ef96a 100644
--- a/src/gui/RadioButton.cpp
+++ b/src/gui/RadioButton.cpp
@@ -35,12 +35,12 @@ namespace gsr {
const bool mouse_inside = mgl::FloatRect(draw_pos, item_size).contains(mgl::vec2f(event.mouse_button.x, event.mouse_button.y));
if(mouse_inside) {
- const size_t prev_selected_item = selected_item;
- selected_item = i;
-
- if(selected_item != prev_selected_item && on_selection_changed)
- on_selection_changed(item.text.get_string(), item.id);
+ if(selected_item != i && on_selection_changed) {
+ if(!on_selection_changed(item.text.get_string(), item.id))
+ return false;
+ }
+ selected_item = i;
return false;
}
@@ -158,12 +158,12 @@ namespace gsr {
for(size_t i = 0; i < items.size(); ++i) {
auto &item = items[i];
if(item.id == id) {
- const size_t prev_selected_item = selected_item;
- selected_item = i;
-
- if(trigger_event && (trigger_event_even_if_selection_not_changed || selected_item != prev_selected_item) && on_selection_changed)
- on_selection_changed(item.text.get_string(), item.id);
+ if(trigger_event && (trigger_event_even_if_selection_not_changed || selected_item != i) && on_selection_changed) {
+ if(!on_selection_changed(item.text.get_string(), item.id))
+ break;
+ }
+ selected_item = i;
break;
}
}
diff --git a/src/gui/ScrollablePage.cpp b/src/gui/ScrollablePage.cpp
index 74dd715..d5e92d0 100644
--- a/src/gui/ScrollablePage.cpp
+++ b/src/gui/ScrollablePage.cpp
@@ -19,15 +19,37 @@ namespace gsr {
if(!visible)
return true;
- offset = position + offset + mgl::vec2f(0.0f, scroll_y);
+ offset = position + offset;
+
+ const mgl::vec2f content_size = get_inner_size();
+ const mgl::vec2i scissor_pos(offset.x, offset.y);
+ const mgl::vec2i scissor_size(content_size.x, content_size.y);
+
+ offset.y += scroll_y;
Widget *selected_widget = selected_child_widget;
+ if(event.type == mgl::Event::MouseButtonPressed && scrollbar_rect.contains(mgl::vec2f(event.mouse_button.x, event.mouse_button.y))) {
+ set_widget_as_selected_in_parent();
+ moving_scrollbar_with_cursor = true;
+ scrollbar_move_cursor_start_pos = mgl::vec2f(event.mouse_button.x, event.mouse_button.y);
+ scrollbar_move_cursor_scroll_y_start = scroll_y;
+ return false;
+ }
+
if(event.type == mgl::Event::MouseButtonReleased && moving_scrollbar_with_cursor) {
moving_scrollbar_with_cursor = false;
remove_widget_as_selected_in_parent();
return false;
}
+ if(event.type == mgl::Event::MouseButtonPressed || event.type == mgl::Event::MouseButtonReleased) {
+ if(!mgl::IntRect(scissor_pos, scissor_size).contains({event.mouse_button.x, event.mouse_button.y}))
+ return true;
+ } else if(event.type == mgl::Event::MouseMoved) {
+ if(!mgl::IntRect(scissor_pos, scissor_size).contains({event.mouse_move.x, event.mouse_move.y}))
+ return true;
+ }
+
if(selected_widget) {
if(!selected_widget->on_event(event, window, offset))
return false;
@@ -51,14 +73,6 @@ namespace gsr {
return false;
}
- if(event.type == mgl::Event::MouseButtonPressed && scrollbar_rect.contains(mgl::vec2f(event.mouse_button.x, event.mouse_button.y))) {
- set_widget_as_selected_in_parent();
- moving_scrollbar_with_cursor = true;
- scrollbar_move_cursor_start_pos = mgl::vec2f(event.mouse_button.x, event.mouse_button.y);
- scrollbar_move_cursor_scroll_y_start = scroll_y;
- return false;
- }
-
return true;
}
@@ -75,11 +89,10 @@ namespace gsr {
offset = position + offset;
- mgl_scissor prev_scissor;
- mgl_window_get_scissor(window.internal_window(), &prev_scissor);
+ const mgl::Scissor prev_scissor = window.get_scissor();
const mgl::vec2f content_size = get_inner_size();
- mgl_scissor new_scissor = {
+ const mgl_scissor new_scissor = {
mgl_vec2i{(int)offset.x, (int)offset.y},
mgl_vec2i{(int)content_size.x, (int)content_size.y}
};
@@ -136,7 +149,7 @@ namespace gsr {
apply_animation();
limit_scroll(child_height);
- mgl_window_set_scissor(window.internal_window(), &prev_scissor);
+ window.set_scissor(prev_scissor);
double scrollbar_height = 1.0;
if(child_height > 0.001)
diff --git a/src/gui/SettingsPage.cpp b/src/gui/SettingsPage.cpp
index 79f6c52..4d1109a 100644
--- a/src/gui/SettingsPage.cpp
+++ b/src/gui/SettingsPage.cpp
@@ -22,15 +22,16 @@ namespace gsr {
APPLICATION_CUSTOM
};
- SettingsPage::SettingsPage(Type type, const GsrInfo &gsr_info, Config &config, PageStack *page_stack) :
+ SettingsPage::SettingsPage(Type type, const GsrInfo *gsr_info, Config &config, PageStack *page_stack) :
StaticPage(mgl::vec2f(get_theme().window_width, get_theme().window_height).floor()),
type(type),
config(config),
- page_stack(page_stack),
- settings_title_text("Settings", get_theme().title_font)
+ gsr_info(gsr_info),
+ page_stack(page_stack)
{
audio_devices = get_audio_devices();
application_audio = get_application_audio();
+ capture_options = get_supported_capture_options(*gsr_info);
auto content_page = std::make_unique<GsrPage>();
content_page->add_button("Back", "back", get_color_theme().page_bg_color);
@@ -41,9 +42,9 @@ namespace gsr {
content_page_ptr = content_page.get();
add_widget(std::move(content_page));
- add_widgets(gsr_info);
- add_page_specific_widgets(gsr_info);
- load(gsr_info);
+ add_widgets();
+ add_page_specific_widgets();
+ load();
}
std::unique_ptr<RadioButton> SettingsPage::create_view_radio_button() {
@@ -55,31 +56,29 @@ namespace gsr {
return view_radio_button;
}
- std::unique_ptr<ComboBox> SettingsPage::create_record_area_box(const GsrInfo &gsr_info) {
+ std::unique_ptr<ComboBox> SettingsPage::create_record_area_box() {
auto record_area_box = std::make_unique<ComboBox>(&get_theme().body_font);
// TODO: Show options not supported but disable them
// TODO: Enable this
- //if(gsr_info.supported_capture_options.window)
+ //if(capture_options.window)
// record_area_box->add_item("Window", "window");
- if(gsr_info.supported_capture_options.focused)
+ if(capture_options.focused)
record_area_box->add_item("Follow focused window", "focused");
- if(gsr_info.supported_capture_options.screen)
- record_area_box->add_item("All monitors", "screen");
- for(const auto &monitor : gsr_info.supported_capture_options.monitors) {
+ for(const auto &monitor : capture_options.monitors) {
char name[256];
snprintf(name, sizeof(name), "Monitor %s (%dx%d)", monitor.name.c_str(), monitor.size.x, monitor.size.y);
record_area_box->add_item(name, monitor.name);
}
- if(gsr_info.supported_capture_options.portal)
+ if(capture_options.portal)
record_area_box->add_item("Desktop portal", "portal");
record_area_box_ptr = record_area_box.get();
return record_area_box;
}
- std::unique_ptr<Widget> SettingsPage::create_record_area(const GsrInfo &gsr_info) {
+ std::unique_ptr<Widget> SettingsPage::create_record_area() {
auto record_area_list = std::make_unique<List>(List::Orientation::VERTICAL);
record_area_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Capture target:", get_color_theme().text_color));
- record_area_list->add_widget(create_record_area_box(gsr_info));
+ record_area_list->add_widget(create_record_area_box());
return record_area_list;
}
@@ -172,11 +171,11 @@ namespace gsr {
return checkbox;
}
- std::unique_ptr<Widget> SettingsPage::create_capture_target(const GsrInfo &gsr_info) {
+ std::unique_ptr<Widget> SettingsPage::create_capture_target() {
auto ll = std::make_unique<List>(List::Orientation::VERTICAL);
auto capture_target_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
- capture_target_list->add_widget(create_record_area(gsr_info));
+ capture_target_list->add_widget(create_record_area());
capture_target_list->add_widget(create_select_window());
capture_target_list->add_widget(create_area_size_section());
capture_target_list->add_widget(create_video_resolution_section());
@@ -305,7 +304,8 @@ namespace gsr {
std::unique_ptr<Widget> SettingsPage::create_audio_section() {
auto audio_device_section_list = std::make_unique<List>(List::Orientation::VERTICAL);
audio_device_section_list->add_widget(create_audio_track_section());
- audio_device_section_list->add_widget(create_merge_audio_tracks_checkbox());
+ if(type != Type::STREAM)
+ audio_device_section_list->add_widget(create_merge_audio_tracks_checkbox());
audio_device_section_list->add_widget(create_application_audio_invert_checkbox());
audio_device_section_list->add_widget(create_audio_codec());
return std::make_unique<Subsection>("Audio", std::move(audio_device_section_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
@@ -339,11 +339,27 @@ namespace gsr {
return list;
}
- std::unique_ptr<Entry> SettingsPage::create_video_bitrate_entry() {
+ std::unique_ptr<List> SettingsPage::create_video_bitrate_entry() {
+ auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
auto video_bitrate_entry = std::make_unique<Entry>(&get_theme().body_font, "15000", (int)(get_theme().body_font.get_character_size() * 4.0f));
video_bitrate_entry->validate_handler = create_entry_validator_integer_in_range(1, 500000);
video_bitrate_entry_ptr = video_bitrate_entry.get();
- return video_bitrate_entry;
+ list->add_widget(std::move(video_bitrate_entry));
+
+ if(type == Type::STREAM) {
+ auto size_mb_label = std::make_unique<Label>(&get_theme().body_font, "1.92MB", get_color_theme().text_color);
+ Label *size_mb_label_ptr = size_mb_label.get();
+ list->add_widget(std::move(size_mb_label));
+
+ video_bitrate_entry_ptr->on_changed = [size_mb_label_ptr](const std::string &text) {
+ const double video_bitrate_mb_per_seconds = (double)atoi(text.c_str()) / 1000LL / 8LL * 1.024;
+ char buffer[32];
+ snprintf(buffer, sizeof(buffer), "%.2fMB", video_bitrate_mb_per_seconds);
+ size_mb_label_ptr->set_text(buffer);
+ };
+ }
+
+ return list;
}
std::unique_ptr<List> SettingsPage::create_video_bitrate() {
@@ -378,40 +394,40 @@ namespace gsr {
return quality_list;
}
- std::unique_ptr<ComboBox> SettingsPage::create_video_codec_box(const GsrInfo &gsr_info) {
+ std::unique_ptr<ComboBox> SettingsPage::create_video_codec_box() {
auto video_codec_box = std::make_unique<ComboBox>(&get_theme().body_font);
// TODO: Show options not supported but disable them.
// TODO: Show error if no encoders are supported.
// TODO: Show warning (once) if only software encoder is available.
video_codec_box->add_item("Auto (Recommended)", "auto");
- if(gsr_info.supported_video_codecs.h264)
+ if(gsr_info->supported_video_codecs.h264)
video_codec_box->add_item("H264", "h264");
- if(gsr_info.supported_video_codecs.hevc)
+ if(gsr_info->supported_video_codecs.hevc)
video_codec_box->add_item("HEVC", "hevc");
- if(gsr_info.supported_video_codecs.av1)
+ if(gsr_info->supported_video_codecs.hevc_10bit)
+ video_codec_box->add_item("HEVC (10 bit, reduces banding)", "hevc_10bit");
+ if(gsr_info->supported_video_codecs.hevc_hdr)
+ video_codec_box->add_item("HEVC (HDR)", "hevc_hdr");
+ if(gsr_info->supported_video_codecs.av1)
video_codec_box->add_item("AV1", "av1");
- if(gsr_info.supported_video_codecs.vp8)
+ if(gsr_info->supported_video_codecs.av1_10bit)
+ video_codec_box->add_item("AV1 (10 bit, reduces banding)", "av1_10bit");
+ if(gsr_info->supported_video_codecs.av1_hdr)
+ video_codec_box->add_item("AV1 (HDR)", "av1_hdr");
+ if(gsr_info->supported_video_codecs.vp8)
video_codec_box->add_item("VP8", "vp8");
- if(gsr_info.supported_video_codecs.vp9)
+ if(gsr_info->supported_video_codecs.vp9)
video_codec_box->add_item("VP9", "vp9");
- if(gsr_info.supported_video_codecs.hevc_hdr)
- video_codec_box->add_item("HEVC (HDR)", "hevc_hdr");
- if(gsr_info.supported_video_codecs.hevc_10bit)
- video_codec_box->add_item("HEVC (10 bit, reduces banding)", "hevc_10bit");
- if(gsr_info.supported_video_codecs.av1_hdr)
- video_codec_box->add_item("AV1 (HDR)", "av1_hdr");
- if(gsr_info.supported_video_codecs.av1_10bit)
- video_codec_box->add_item("AV1 (10 bit, reduces banding)", "av1_10bit");
- if(gsr_info.supported_video_codecs.h264_software)
+ if(gsr_info->supported_video_codecs.h264_software)
video_codec_box->add_item("H264 Software Encoder (Slow, not recommended)", "h264_software");
video_codec_box_ptr = video_codec_box.get();
return video_codec_box;
}
- std::unique_ptr<List> SettingsPage::create_video_codec(const GsrInfo &gsr_info) {
+ std::unique_ptr<List> SettingsPage::create_video_codec() {
auto video_codec_list = std::make_unique<List>(List::Orientation::VERTICAL);
video_codec_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Video codec:", get_color_theme().text_color));
- video_codec_list->add_widget(create_video_codec_box(gsr_info));
+ video_codec_list->add_widget(create_video_codec_box());
video_codec_ptr = video_codec_list.get();
return video_codec_list;
}
@@ -477,16 +493,16 @@ namespace gsr {
return record_cursor_checkbox;
}
- std::unique_ptr<Widget> SettingsPage::create_video_section(const GsrInfo &gsr_info) {
+ std::unique_ptr<Widget> SettingsPage::create_video_section() {
auto video_section_list = std::make_unique<List>(List::Orientation::VERTICAL);
video_section_list->add_widget(create_video_quality_section());
- video_section_list->add_widget(create_video_codec(gsr_info));
+ video_section_list->add_widget(create_video_codec());
video_section_list->add_widget(create_framerate_section());
video_section_list->add_widget(create_record_cursor_section());
return std::make_unique<Subsection>("Video", std::move(video_section_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
- std::unique_ptr<Widget> SettingsPage::create_settings(const GsrInfo &gsr_info) {
+ std::unique_ptr<Widget> SettingsPage::create_settings() {
auto page_list = std::make_unique<List>(List::Orientation::VERTICAL);
page_list->set_spacing(0.018f);
page_list->add_widget(create_view_radio_button());
@@ -496,16 +512,16 @@ namespace gsr {
auto settings_list = std::make_unique<List>(List::Orientation::VERTICAL);
settings_list->set_spacing(0.018f);
- settings_list->add_widget(create_capture_target(gsr_info));
+ settings_list->add_widget(create_capture_target());
settings_list->add_widget(create_audio_section());
- settings_list->add_widget(create_video_section(gsr_info));
+ settings_list->add_widget(create_video_section());
settings_list_ptr = settings_list.get();
settings_scrollable_page_ptr->add_widget(std::move(settings_list));
return page_list;
}
- void SettingsPage::add_widgets(const GsrInfo &gsr_info) {
- content_page_ptr->add_widget(create_settings(gsr_info));
+ void SettingsPage::add_widgets() {
+ content_page_ptr->add_widget(create_settings());
record_area_box_ptr->on_selection_changed = [this](const std::string &text, const std::string &id) {
(void)text;
@@ -517,6 +533,7 @@ namespace gsr {
video_resolution_list_ptr->set_visible(!focused_selected && change_video_resolution_checkbox_ptr->is_checked());
change_video_resolution_checkbox_ptr->set_visible(!focused_selected);
restore_portal_session_list_ptr->set_visible(portal_selected);
+ return true;
};
change_video_resolution_checkbox_ptr->on_changed = [this](bool checked) {
@@ -531,35 +548,37 @@ namespace gsr {
if(estimated_file_size_ptr)
estimated_file_size_ptr->set_visible(custom_selected);
+
+ return true;
};
video_quality_box_ptr->on_selection_changed("", video_quality_box_ptr->get_selected_id());
- if(!gsr_info.supported_capture_options.monitors.empty())
- record_area_box_ptr->set_selected_item(gsr_info.supported_capture_options.monitors.front().name);
- else if(gsr_info.supported_capture_options.portal)
+ if(!capture_options.monitors.empty())
+ record_area_box_ptr->set_selected_item(capture_options.monitors.front().name);
+ else if(capture_options.portal)
record_area_box_ptr->set_selected_item("portal");
- else if(gsr_info.supported_capture_options.window)
+ else if(capture_options.window)
record_area_box_ptr->set_selected_item("window");
else
record_area_box_ptr->on_selection_changed("", "");
- if(!gsr_info.system_info.supports_app_audio) {
+ if(!gsr_info->system_info.supports_app_audio) {
add_application_audio_button_ptr->set_visible(false);
add_custom_application_audio_button_ptr->set_visible(false);
application_audio_invert_checkbox_ptr->set_visible(false);
}
}
- void SettingsPage::add_page_specific_widgets(const GsrInfo &gsr_info) {
+ void SettingsPage::add_page_specific_widgets() {
switch(type) {
case Type::REPLAY:
- add_replay_widgets(gsr_info);
+ add_replay_widgets();
break;
case Type::RECORD:
- add_record_widgets(gsr_info);
+ add_record_widgets();
break;
case Type::STREAM:
- add_stream_widgets(gsr_info);
+ add_stream_widgets();
break;
}
}
@@ -579,10 +598,12 @@ namespace gsr {
select_directory_page->add_widget(std::move(file_chooser));
select_directory_page->on_click = [this, file_chooser_ptr](const std::string &id) {
- if(id == "save")
+ if(id == "save") {
save_directory_button_ptr->set_text(file_chooser_ptr->get_current_directory());
- else if(id == "cancel")
page_stack->pop();
+ } else if(id == "cancel") {
+ page_stack->pop();
+ }
};
page_stack->push(std::move(select_directory_page));
@@ -608,70 +629,100 @@ namespace gsr {
return container_list;
}
- std::unique_ptr<Entry> SettingsPage::create_replay_time_entry() {
+ std::unique_ptr<List> SettingsPage::create_replay_time_entry() {
+ auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
+
auto replay_time_entry = std::make_unique<Entry>(&get_theme().body_font, "60", get_theme().body_font.get_character_size() * 3);
- replay_time_entry->validate_handler = create_entry_validator_integer_in_range(1, 1200);
+ replay_time_entry->validate_handler = create_entry_validator_integer_in_range(1, 10800);
replay_time_entry_ptr = replay_time_entry.get();
- return replay_time_entry;
+ list->add_widget(std::move(replay_time_entry));
+
+ auto replay_time_label = std::make_unique<Label>(&get_theme().body_font, "00h:00m:00s", get_color_theme().text_color);
+ replay_time_label_ptr = replay_time_label.get();
+ list->add_widget(std::move(replay_time_label));
+
+ return list;
}
std::unique_ptr<List> SettingsPage::create_replay_time() {
auto replay_time_list = std::make_unique<List>(List::Orientation::VERTICAL);
- replay_time_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Replay time in seconds:", get_color_theme().text_color));
+ replay_time_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Replay duration in seconds:", get_color_theme().text_color));
replay_time_list->add_widget(create_replay_time_entry());
return replay_time_list;
}
- std::unique_ptr<RadioButton> SettingsPage::create_start_replay_automatically(const GsrInfo &gsr_info) {
+ std::unique_ptr<RadioButton> SettingsPage::create_start_replay_automatically() {
char fullscreen_text[256];
- snprintf(fullscreen_text, sizeof(fullscreen_text), "Turn on replay when starting a fullscreen application%s", gsr_info.system_info.display_server == DisplayServer::X11 ? "" : " (X11 only)");
+ snprintf(fullscreen_text, sizeof(fullscreen_text), "Turn on replay when starting a fullscreen application%s", gsr_info->system_info.display_server == DisplayServer::X11 ? "" : " (X11 applications only)");
auto radiobutton = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::VERTICAL);
radiobutton->add_item("Don't turn on replay automatically", "dont_turn_on_automatically");
- radiobutton->add_item("Turn on replay at system startup", "turn_on_at_system_startup");
+ radiobutton->add_item("Turn on replay when this program starts", "turn_on_at_system_startup");
radiobutton->add_item(fullscreen_text, "turn_on_at_fullscreen");
radiobutton->add_item("Turn on replay when power supply is connected", "turn_on_at_power_supply_connected");
turn_on_replay_automatically_mode_ptr = radiobutton.get();
return radiobutton;
}
- std::unique_ptr<CheckBox> SettingsPage::create_save_replay_in_game_folder(const GsrInfo &gsr_info) {
+ std::unique_ptr<CheckBox> SettingsPage::create_save_replay_in_game_folder() {
char text[256];
- snprintf(text, sizeof(text), "Save video in a folder with the name of the game%s", gsr_info.system_info.display_server == DisplayServer::X11 ? "" : " (X11 only)");
+ snprintf(text, sizeof(text), "Save video in a folder with the name of the game%s", gsr_info->system_info.display_server == DisplayServer::X11 ? "" : " (X11 applications only)");
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, text);
save_replay_in_game_folder_ptr = checkbox.get();
return checkbox;
}
- std::unique_ptr<Label> SettingsPage::create_estimated_file_size() {
- auto label = std::make_unique<Label>(&get_theme().body_font, "Estimated video max file size in RAM: 5.23MB", get_color_theme().text_color);
+ 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();
return label;
}
- void SettingsPage::update_estimated_file_size() {
+ void SettingsPage::update_estimated_replay_file_size() {
const int64_t replay_time_seconds = atoi(replay_time_entry_ptr->get_text().c_str());
const int64_t video_bitrate_bps = atoi(video_bitrate_entry_ptr->get_text().c_str()) * 1000LL / 8LL;
- const double video_filesize_mb = ((double)replay_time_seconds * (double)video_bitrate_bps) / 1024.0 / 1024.0;
+ const double video_filesize_mb = ((double)replay_time_seconds * (double)video_bitrate_bps) / 1000.0 / 1000.0 * 1.024;
- char buffer[512];
- snprintf(buffer, sizeof(buffer), "Estimated video max file size in RAM: %.2fMB", video_filesize_mb);
+ char buffer[256];
+ snprintf(buffer, sizeof(buffer), "Estimated video max file size in RAM: %.2fMB.\nChange video bitrate or replay duration to change file size.", video_filesize_mb);
estimated_file_size_ptr->set_text(buffer);
}
- void SettingsPage::add_replay_widgets(const GsrInfo &gsr_info) {
+ void SettingsPage::update_replay_time_text() {
+ int seconds = atoi(replay_time_entry_ptr->get_text().c_str());
+
+ const int hours = seconds / 60 / 60;
+ seconds -= (hours * 60 * 60);
+
+ const int minutes = seconds / 60;
+ seconds -= (minutes * 60);
+
+ char buffer[256];
+ snprintf(buffer, sizeof(buffer), "%02dh:%02dm:%02ds", hours, minutes, seconds);
+ replay_time_label_ptr->set_text(buffer);
+ }
+
+ void SettingsPage::add_replay_widgets() {
auto file_info_list = std::make_unique<List>(List::Orientation::VERTICAL);
auto file_info_data_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
file_info_data_list->add_widget(create_save_directory("Directory to save replays:"));
file_info_data_list->add_widget(create_container_section());
file_info_data_list->add_widget(create_replay_time());
file_info_list->add_widget(std::move(file_info_data_list));
- file_info_list->add_widget(create_estimated_file_size());
+ file_info_list->add_widget(create_estimated_replay_file_size());
settings_list_ptr->add_widget(std::make_unique<Subsection>("File info", std::move(file_info_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
auto general_list = std::make_unique<List>(List::Orientation::VERTICAL);
- general_list->add_widget(create_start_replay_automatically(gsr_info));
- general_list->add_widget(create_save_replay_in_game_folder(gsr_info));
+ 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);
@@ -704,34 +755,54 @@ namespace gsr {
framerate_mode_list_ptr->set_visible(advanced_view);
notifications_subsection_ptr->set_visible(advanced_view);
settings_scrollable_page_ptr->reset_scroll();
+ return true;
};
view_radio_button_ptr->on_selection_changed("Simple", "simple");
replay_time_entry_ptr->on_changed = [this](const std::string&) {
- update_estimated_file_size();
+ update_estimated_replay_file_size();
+ update_replay_time_text();
};
video_bitrate_entry_ptr->on_changed = [this](const std::string&) {
- update_estimated_file_size();
+ update_estimated_replay_file_size();
};
}
- std::unique_ptr<CheckBox> SettingsPage::create_save_recording_in_game_folder(const GsrInfo &gsr_info) {
+ std::unique_ptr<CheckBox> SettingsPage::create_save_recording_in_game_folder() {
char text[256];
- snprintf(text, sizeof(text), "Save video in a folder with the name of the game%s", gsr_info.system_info.display_server == DisplayServer::X11 ? "" : " (X11 only)");
+ snprintf(text, sizeof(text), "Save video in a folder with the name of the game%s", gsr_info->system_info.display_server == DisplayServer::X11 ? "" : " (X11 applications only)");
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, text);
save_recording_in_game_folder_ptr = checkbox.get();
return checkbox;
}
- void SettingsPage::add_record_widgets(const GsrInfo &gsr_info) {
- auto file_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
- file_list->add_widget(create_save_directory("Directory to save the video:"));
- file_list->add_widget(create_container_section());
- settings_list_ptr->add_widget(std::make_unique<Subsection>("File info", std::move(file_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
+ std::unique_ptr<Label> SettingsPage::create_estimated_record_file_size() {
+ auto label = std::make_unique<Label>(&get_theme().body_font, "Estimated video file size per minute (excluding audio): 345.60MB", get_color_theme().text_color);
+ estimated_file_size_ptr = label.get();
+ return label;
+ }
+
+ void SettingsPage::update_estimated_record_file_size() {
+ const int64_t video_bitrate_bps = atoi(video_bitrate_entry_ptr->get_text().c_str()) * 1000LL / 8LL;
+ const double video_filesize_mb_per_minute = (60.0 * (double)video_bitrate_bps) / 1000.0 / 1000.0 * 1.024;
+
+ char buffer[512];
+ snprintf(buffer, sizeof(buffer), "Estimated video file size per minute (excluding audio): %.2fMB", video_filesize_mb_per_minute);
+ estimated_file_size_ptr->set_text(buffer);
+ }
+
+ void SettingsPage::add_record_widgets() {
+ auto file_info_list = std::make_unique<List>(List::Orientation::VERTICAL);
+ auto file_info_data_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
+ file_info_data_list->add_widget(create_save_directory("Directory to save the video:"));
+ file_info_data_list->add_widget(create_container_section());
+ file_info_list->add_widget(std::move(file_info_data_list));
+ file_info_list->add_widget(create_estimated_record_file_size());
+ settings_list_ptr->add_widget(std::make_unique<Subsection>("File info", std::move(file_info_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
auto general_list = std::make_unique<List>(List::Orientation::VERTICAL);
- general_list->add_widget(create_save_recording_in_game_folder(gsr_info));
+ general_list->add_widget(create_save_recording_in_game_folder());
settings_list_ptr->add_widget(std::make_unique<Subsection>("General", std::move(general_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL);
@@ -759,8 +830,13 @@ namespace gsr {
framerate_mode_list_ptr->set_visible(advanced_view);
notifications_subsection_ptr->set_visible(advanced_view);
settings_scrollable_page_ptr->reset_scroll();
+ return true;
};
view_radio_button_ptr->on_selection_changed("Simple", "simple");
+
+ video_bitrate_entry_ptr->on_changed = [this](const std::string&) {
+ update_estimated_record_file_size();
+ };
}
std::unique_ptr<ComboBox> SettingsPage::create_streaming_service_box() {
@@ -825,7 +901,7 @@ namespace gsr {
return container_list;
}
- void SettingsPage::add_stream_widgets(const GsrInfo&) {
+ void SettingsPage::add_stream_widgets() {
auto streaming_info_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
streaming_info_list->add_widget(create_streaming_service_section());
streaming_info_list->add_widget(create_stream_key_section());
@@ -859,6 +935,7 @@ namespace gsr {
container_list_ptr->set_visible(custom_option);
twitch_stream_key_entry_ptr->set_visible(twitch_option);
youtube_stream_key_entry_ptr->set_visible(youtube_option);
+ return true;
};
streaming_service_box_ptr->on_selection_changed("Twitch", "twitch");
@@ -871,6 +948,7 @@ namespace gsr {
framerate_mode_list_ptr->set_visible(advanced_view);
notifications_subsection_ptr->set_visible(advanced_view);
settings_scrollable_page_ptr->reset_scroll();
+ return true;
};
view_radio_button_ptr->on_selection_changed("Simple", "simple");
}
@@ -879,21 +957,22 @@ namespace gsr {
save();
}
- void SettingsPage::load(const GsrInfo &gsr_info) {
+ void SettingsPage::load() {
switch(type) {
case Type::REPLAY:
- load_replay(gsr_info);
+ load_replay();
break;
case Type::RECORD:
- load_record(gsr_info);
+ load_record();
break;
case Type::STREAM:
- load_stream(gsr_info);
+ load_stream();
break;
}
}
void SettingsPage::save() {
+ Config prev_config = config;
switch(type) {
case Type::REPLAY:
save_replay();
@@ -906,6 +985,9 @@ namespace gsr {
break;
}
save_config(config);
+
+ if(on_config_changed && config != prev_config)
+ on_config_changed();
}
static const std::string* get_application_audio_by_name_case_insensitive(const std::vector<std::string> &application_audio, const std::string &name) {
@@ -921,11 +1003,11 @@ namespace gsr {
return str.size() >= len && memcmp(str.data(), substr, len) == 0;
}
- void SettingsPage::load_audio_tracks(const RecordOptions &record_options, const GsrInfo &gsr_info) {
+ void SettingsPage::load_audio_tracks(const RecordOptions &record_options) {
audio_track_list_ptr->clear();
for(const std::string &audio_track : record_options.audio_tracks) {
if(starts_with(audio_track, "app:")) {
- if(!gsr_info.system_info.supports_app_audio)
+ if(!gsr_info->system_info.supports_app_audio)
continue;
std::string audio_track_name = audio_track.substr(4);
@@ -955,12 +1037,13 @@ namespace gsr {
}
}
- void SettingsPage::load_common(RecordOptions &record_options, const GsrInfo &gsr_info) {
+ void SettingsPage::load_common(RecordOptions &record_options) {
record_area_box_ptr->set_selected_item(record_options.record_area_option);
- merge_audio_tracks_checkbox_ptr->set_checked(record_options.merge_audio_tracks);
+ if(merge_audio_tracks_checkbox_ptr)
+ merge_audio_tracks_checkbox_ptr->set_checked(record_options.merge_audio_tracks);
application_audio_invert_checkbox_ptr->set_checked(record_options.application_audio_invert);
change_video_resolution_checkbox_ptr->set_checked(record_options.change_video_resolution);
- load_audio_tracks(record_options, gsr_info);
+ load_audio_tracks(record_options);
color_range_box_ptr->set_selected_item(record_options.color_range);
video_quality_box_ptr->set_selected_item(record_options.video_quality);
video_codec_box_ptr->set_selected_item(record_options.video_codec);
@@ -1009,23 +1092,27 @@ namespace gsr {
video_bitrate_entry_ptr->set_text(std::to_string(record_options.video_bitrate));
}
- void SettingsPage::load_replay(const GsrInfo &gsr_info) {
- load_common(config.replay_config.record_options, gsr_info);
+ void SettingsPage::load_replay() {
+ 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);
save_directory_button_ptr->set_text(config.replay_config.save_directory);
container_box_ptr->set_selected_item(config.replay_config.container);
- if(config.replay_config.replay_time < 5)
- config.replay_config.replay_time = 5;
+ if(config.replay_config.replay_time < 2)
+ config.replay_config.replay_time = 2;
+ if(config.replay_config.replay_time > 10800)
+ config.replay_config.replay_time = 10800;
replay_time_entry_ptr->set_text(std::to_string(config.replay_config.replay_time));
}
- void SettingsPage::load_record(const GsrInfo &gsr_info) {
- load_common(config.record_config.record_options, gsr_info);
+ void SettingsPage::load_record() {
+ load_common(config.record_config.record_options);
save_recording_in_game_folder_ptr->set_checked(config.record_config.save_video_in_game_folder);
show_recording_started_notification_checkbox_ptr->set_checked(config.record_config.show_recording_started_notifications);
show_video_saved_notification_checkbox_ptr->set_checked(config.record_config.show_video_saved_notifications);
@@ -1033,8 +1120,8 @@ namespace gsr {
container_box_ptr->set_selected_item(config.record_config.container);
}
- void SettingsPage::load_stream(const GsrInfo &gsr_info) {
- load_common(config.streaming_config.record_options, gsr_info);
+ void SettingsPage::load_stream() {
+ load_common(config.streaming_config.record_options);
show_streaming_started_notification_checkbox_ptr->set_checked(config.streaming_config.show_streaming_started_notifications);
show_streaming_stopped_notification_checkbox_ptr->set_checked(config.streaming_config.show_streaming_stopped_notifications);
streaming_service_box_ptr->set_selected_item(config.streaming_config.streaming_service);
@@ -1078,7 +1165,8 @@ namespace gsr {
record_options.video_height = atoi(video_height_entry_ptr->get_text().c_str());
record_options.fps = atoi(framerate_entry_ptr->get_text().c_str());
record_options.video_bitrate = atoi(video_bitrate_entry_ptr->get_text().c_str());
- record_options.merge_audio_tracks = merge_audio_tracks_checkbox_ptr->is_checked();
+ if(merge_audio_tracks_checkbox_ptr)
+ record_options.merge_audio_tracks = merge_audio_tracks_checkbox_ptr->is_checked();
record_options.application_audio_invert = application_audio_invert_checkbox_ptr->is_checked();
record_options.change_video_resolution = change_video_resolution_checkbox_ptr->is_checked();
save_audio_tracks(record_options.audio_tracks, audio_track_list_ptr);
@@ -1140,6 +1228,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/gui/StaticPage.cpp b/src/gui/StaticPage.cpp
index a89fc42..182464c 100644
--- a/src/gui/StaticPage.cpp
+++ b/src/gui/StaticPage.cpp
@@ -36,14 +36,8 @@ namespace gsr {
offset = draw_pos;
Widget *selected_widget = selected_child_widget;
- mgl_scissor prev_scissor;
- mgl_window_get_scissor(window.internal_window(), &prev_scissor);
-
- mgl_scissor new_scissor = {
- mgl_vec2i{(int)draw_pos.x, (int)draw_pos.y},
- mgl_vec2i{(int)size.x, (int)size.y}
- };
- mgl_window_set_scissor(window.internal_window(), &new_scissor);
+ const mgl::Scissor prev_scissor = window.get_scissor();
+ window.set_scissor({draw_pos.to_vec2i(), size.to_vec2i()});
for(size_t i = 0; i < widgets.size(); ++i) {
auto &widget = widgets[i];
@@ -54,7 +48,7 @@ namespace gsr {
if(selected_widget)
selected_widget->draw(window, offset);
- mgl_window_set_scissor(window.internal_window(), &prev_scissor);
+ window.set_scissor(prev_scissor);
}
mgl::vec2f StaticPage::get_size() {
diff --git a/src/gui/Utils.cpp b/src/gui/Utils.cpp
index e000b7a..d1643f2 100644
--- a/src/gui/Utils.cpp
+++ b/src/gui/Utils.cpp
@@ -50,4 +50,21 @@ namespace gsr {
void set_frame_delta_seconds(double frame_delta) {
frame_delta_seconds = frame_delta;
}
+
+ mgl::vec2f scale_keep_aspect_ratio(mgl::vec2f from, mgl::vec2f to) {
+ if(std::abs(from.x) <= 0.0001f || std::abs(from.y) <= 0.0001f)
+ return {0.0f, 0.0f};
+
+ const double height_to_width_ratio = (double)from.y / (double)from.x;
+ from.x = to.x;
+ from.y = from.x * height_to_width_ratio;
+
+ if(from.y > to.y) {
+ const double width_height_ratio = (double)from.x / (double)from.y;
+ from.y = to.y;
+ from.x = from.y * width_height_ratio;
+ }
+
+ return from;
+ }
} \ No newline at end of file
diff --git a/src/main.cpp b/src/main.cpp
index 4a36fe7..ffaf596 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,21 +1,18 @@
#include "../include/GsrInfo.hpp"
-#include "../include/Theme.hpp"
-#include "../include/window_texture.h"
#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 <sys/socket.h>
-#include <thread>
+#include <string.h>
+#include <limits.h>
-#include <X11/keysym.h>
#include <mglpp/mglpp.hpp>
#include <mglpp/system/Clock.hpp>
-// TODO: Make keyboard controllable for steam deck (and other controllers).
+// TODO: Make keyboard/controller controllable for steam deck (and other controllers).
// TODO: Keep track of gpu screen recorder run by other programs to not allow recording at the same time, or something.
// TODO: Add systray by using org.kde.StatusNotifierWatcher/etc dbus directly.
// TODO: Make sure the overlay always stays on top. Test with starting the overlay and then opening youtube in fullscreen.
@@ -37,16 +34,121 @@ static void disable_prime_run() {
unsetenv("__NV_PRIME_RENDER_OFFLOAD_PROVIDER");
unsetenv("__GLX_VENDOR_LIBRARY_NAME");
unsetenv("__VK_LAYER_NV_optimus");
+ unsetenv("DRI_PRIME");
}
-static bool is_socket_disconnected(int socket) {
- char buf = '\0';
- errno = 0;
- const ssize_t bytes_read = recv(socket, &buf, 1, MSG_PEEK | MSG_DONTWAIT);
- return bytes_read == 0 || (bytes_read == -1 && (errno == EBADF || errno == ENOTCONN));
+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());
+ overlay->show();
+ });
+
+ rpc->add_handler("toggle-show", [overlay](const std::string &name) {
+ fprintf(stderr, "rpc command executed: %s\n", name.c_str());
+ overlay->toggle_show();
+ });
+
+ rpc->add_handler("toggle-record", [overlay](const std::string &name) {
+ fprintf(stderr, "rpc command executed: %s\n", name.c_str());
+ overlay->toggle_record();
+ });
+
+ rpc->add_handler("toggle-pause", [overlay](const std::string &name) {
+ fprintf(stderr, "rpc command executed: %s\n", name.c_str());
+ overlay->toggle_pause();
+ });
+
+ rpc->add_handler("toggle-stream", [overlay](const std::string &name) {
+ fprintf(stderr, "rpc command executed: %s\n", name.c_str());
+ overlay->toggle_stream();
+ });
+
+ rpc->add_handler("toggle-replay", [overlay](const std::string &name) {
+ fprintf(stderr, "rpc command executed: %s\n", name.c_str());
+ overlay->toggle_replay();
+ });
+
+ rpc->add_handler("replay-save", [overlay](const std::string &name) {
+ fprintf(stderr, "rpc command executed: %s\n", name.c_str());
+ overlay->save_replay();
+ });
}
-int main(void) {
+static bool is_gsr_ui_virtual_keyboard_running() {
+ FILE *f = fopen("/proc/bus/input/devices", "rb");
+ if(!f)
+ return false;
+
+ bool virtual_keyboard_running = false;
+ char line[1024];
+ while(fgets(line, sizeof(line), f)) {
+ if(strstr(line, "gsr-ui virtual keyboard")) {
+ virtual_keyboard_running = true;
+ break;
+ }
+ }
+
+ fclose(f);
+ return virtual_keyboard_running;
+}
+
+static void install_flatpak_systemd_service() {
+ 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");
+ printf(" action The launch action. Should be either \"launch-show\" or \"launch-hide\". Optional, defaults to \"launch-hide\".\n");
+ printf(" If \"launch-show\" is used then the program starts and the UI is immediately opened and can be shown/hidden with Alt+Z.\n");
+ printf(" If \"launch-hide\" is used then the program starts but the UI is not opened until Alt+Z is pressed.\n");
+ exit(1);
+}
+
+enum class LaunchAction {
+ LAUNCH_SHOW,
+ LAUNCH_HIDE
+};
+
+int main(int argc, char **argv) {
setlocale(LC_ALL, "C"); // Sigh... stupid C
if(geteuid() == 0) {
@@ -54,8 +156,44 @@ int main(void) {
return 1;
}
- // Cant get window texture when prime-run is used
- disable_prime_run();
+ LaunchAction launch_action = LaunchAction::LAUNCH_HIDE;
+ if(argc == 1) {
+ launch_action = LaunchAction::LAUNCH_HIDE;
+ } else if(argc == 2) {
+ const char *launch_action_opt = argv[1];
+ if(strcmp(launch_action_opt, "launch-show") == 0) {
+ launch_action = LaunchAction::LAUNCH_SHOW;
+ } else if(strcmp(launch_action_opt, "launch-hide") == 0) {
+ launch_action = LaunchAction::LAUNCH_HIDE;
+ } else {
+ printf("error: invalid action \"%s\", expected \"launch-show\" or \"launch-hide\".\n", launch_action_opt);
+ usage();
+ }
+ } else {
+ 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.
+ // What do? creating a pid file doesn't work in flatpak either.
+ // TODO: This doesn't work in flatpak when disabling hotkeys.
+ if(is_gsr_ui_virtual_keyboard_running() || gsr::pidof("gsr-ui", getpid()) != -1) {
+ gsr::Rpc rpc;
+ if(rpc.open("gsr-ui") && rpc.write("show_ui\n", 8)) {
+ fprintf(stderr, "Error: another instance of gsr-ui is already running, opening that one instead\n");
+ } else {
+ fprintf(stderr, "Error: failed to send command to running gsr-ui instance, user will have to open the UI manually with Alt+Z\n");
+ const char *args[] = { "gsr-notify", "--text", "Another instance of GPU Screen Recorder UI is already running.\nPress Alt+Z to open the UI.", "--timeout", "5.0", "--icon-color", "ff0000", "--bg-color", "ff0000", nullptr };
+ gsr::exec_program_daemonized(args);
+ }
+ return 1;
+ }
// Stop nvidia driver from buffering frames
setenv("__GL_MaxFramesAllowed", "1", true);
@@ -69,24 +207,31 @@ int main(void) {
signal(SIGINT, sigint_handler);
- if(mgl_init() != 0) {
- fprintf(stderr, "error: failed to initialize mgl. Either failed to connec to the X11 server or failed to 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);
if(gsr_info_exit_status != gsr::GsrInfoExitStatus::OK) {
- fprintf(stderr, "error: failed to get gpu-screen-recorder info, error: %d\n", (int)gsr_info_exit_status);
+ fprintf(stderr, "Error: failed to get gpu-screen-recorder info, error: %d\n", (int)gsr_info_exit_status);
+ exit(1);
+ }
+
+ const gsr::DisplayServer display_server = gsr_info.system_info.display_server;
+ 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);
}
- if(gsr_info.system_info.display_server == gsr::DisplayServer::WAYLAND)
- fprintf(stderr, "warning: Wayland support is experimental and requires XWayland. Things may not work as expected.\n");
+ gsr::SupportedCaptureOptions capture_options = gsr::get_supported_capture_options(gsr_info);
std::string resources_path;
- if(access("sibs-build", F_OK) == 0) {
+ if(access("sibs-build/linux_x86_64/debug/gsr-ui", F_OK) == 0) {
resources_path = "./";
} else {
#ifdef GSR_UI_RESOURCES_PATH
@@ -97,7 +242,6 @@ int main(void) {
}
mgl_context *context = mgl_get_context();
- const int x11_socket = XConnectionNumber((Display*)context->connection);
egl_functions egl_funcs;
egl_funcs.eglGetError = (decltype(egl_funcs.eglGetError))context->gl.eglGetProcAddress("eglGetError");
@@ -110,115 +254,47 @@ int main(void) {
exit(1);
}
- fprintf(stderr, "info: gsr ui is now ready, waiting for inputs. Press alt+z to show/hide the overlay\n");
+ fprintf(stderr, "Info: gsr ui is now ready, waiting for inputs. Press alt+z to show/hide the overlay\n");
- auto overlay = std::make_unique<gsr::Overlay>(resources_path, gsr_info, egl_funcs);
- //overlay.show();
+ auto overlay = std::make_unique<gsr::Overlay>(resources_path, std::move(gsr_info), std::move(capture_options), egl_funcs);
+ if(launch_action == LaunchAction::LAUNCH_SHOW)
+ overlay->show();
- // gsr::GlobalHotkeysX11 global_hotkeys;
- // const bool show_hotkey_registered = global_hotkeys.bind_key_press({ XK_z, Mod1Mask }, "show_hide", [&](const std::string &id) {
- // fprintf(stderr, "pressed %s\n", id.c_str());
- // overlay->toggle_show();
- // });
+ auto rpc = std::make_unique<gsr::Rpc>();
+ if(!rpc->create("gsr-ui"))
+ fprintf(stderr, "Error: Failed to create rpc, commands won't be received\n");
- // const bool record_hotkey_registered = global_hotkeys.bind_key_press({ XK_F9, Mod1Mask }, "record", [&](const std::string &id) {
- // fprintf(stderr, "pressed %s\n", id.c_str());
- // overlay->toggle_record();
- // });
+ rpc_add_commands(rpc.get(), overlay.get());
- // const bool pause_hotkey_registered = global_hotkeys.bind_key_press({ XK_F7, Mod1Mask }, "pause", [&](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", [&](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", [&](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", [&](const std::string &id) {
- // fprintf(stderr, "pressed %s\n", id.c_str());
- // overlay->save_replay();
- // });
-
- gsr::GlobalHotkeysLinux global_hotkeys;
- if(!global_hotkeys.start())
- fprintf(stderr, "error: failed to start global hotkeys\n");
-
- const bool show_hotkey_registered = global_hotkeys.bind_action("show_hide", [&](const std::string &id) {
- fprintf(stderr, "pressed %s\n", id.c_str());
- overlay->toggle_show();
- });
-
- const bool record_hotkey_registered = global_hotkeys.bind_action("record", [&](const std::string &id) {
- fprintf(stderr, "pressed %s\n", id.c_str());
- overlay->toggle_record();
- });
-
- const bool pause_hotkey_registered = global_hotkeys.bind_action("pause", [&](const std::string &id) {
- fprintf(stderr, "pressed %s\n", id.c_str());
- overlay->toggle_pause();
- });
-
- const bool stream_hotkey_registered = global_hotkeys.bind_action("stream", [&](const std::string &id) {
- fprintf(stderr, "pressed %s\n", id.c_str());
- overlay->toggle_stream();
- });
-
- const bool replay_hotkey_registered = global_hotkeys.bind_action("replay_start", [&](const std::string &id) {
- fprintf(stderr, "pressed %s\n", id.c_str());
- overlay->toggle_replay();
- });
-
- const bool replay_save_hotkey_registered = global_hotkeys.bind_action("replay_save", [&](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");
+ // 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;
mgl::Clock frame_delta_clock;
- while(running) {
- if(is_socket_disconnected(x11_socket)) {
- fprintf(stderr, "info: the X11 server has shutdown\n");
- break;
- }
+ while(running && mgl_is_connected_to_display_server() && !overlay->should_exit(exit_reason)) {
const double frame_delta_seconds = frame_delta_clock.restart();
gsr::set_frame_delta_seconds(frame_delta_seconds);
- global_hotkeys.poll_events();
+ rpc->poll();
overlay->handle_events();
- if(!overlay->draw())
- std::this_thread::sleep_for(std::chrono::milliseconds(100));
+ if(!overlay->draw()) {
+ usleep(100 * 1000); // 100ms
+ mgl_ping_display_server();
+ }
}
- fprintf(stderr, "info: shutting down!\n");
+ fprintf(stderr, "Info: shutting down!\n");
+ rpc.reset();
overlay.reset();
- gsr::deinit_theme();
- gsr::deinit_color_theme();
mgl_deinit();
+ if(exit_reason == "back-to-old-ui") {
+ const char *args[] = { "gpu-screen-recorder-gtk", "use-old-ui", nullptr };
+ execvp(args[0], (char* const*)args);
+ } else if(exit_reason == "restart") {
+ const char *args[] = { "gsr-ui", "launch-show", nullptr };
+ execvp(args[0], (char* const*)args);
+ }
+
return 0;
}