diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/AudioPlayer.cpp | 86 | ||||
-rw-r--r-- | src/Config.cpp | 90 | ||||
-rw-r--r-- | src/GlobalHotkeysJoystick.cpp | 243 | ||||
-rw-r--r-- | src/GlobalHotkeysLinux.cpp | 188 | ||||
-rw-r--r-- | src/GlobalHotkeysX11.cpp | 84 | ||||
-rw-r--r-- | src/GsrInfo.cpp | 254 | ||||
-rw-r--r-- | src/Hotplug.cpp | 81 | ||||
-rw-r--r-- | src/Overlay.cpp | 1184 | ||||
-rw-r--r-- | src/Process.cpp | 158 | ||||
-rw-r--r-- | src/Rpc.cpp | 133 | ||||
-rw-r--r-- | src/Theme.cpp | 48 | ||||
-rw-r--r-- | src/WindowUtils.cpp | 530 | ||||
-rw-r--r-- | src/gui/Button.cpp | 44 | ||||
-rw-r--r-- | src/gui/ComboBox.cpp | 34 | ||||
-rw-r--r-- | src/gui/CustomRendererWidget.cpp | 14 | ||||
-rw-r--r-- | src/gui/DropdownButton.cpp | 4 | ||||
-rw-r--r-- | src/gui/FileChooser.cpp | 12 | ||||
-rw-r--r-- | src/gui/GlobalSettingsPage.cpp | 653 | ||||
-rw-r--r-- | src/gui/GsrPage.cpp | 13 | ||||
-rw-r--r-- | src/gui/RadioButton.cpp | 20 | ||||
-rw-r--r-- | src/gui/ScrollablePage.cpp | 39 | ||||
-rw-r--r-- | src/gui/SettingsPage.cpp | 296 | ||||
-rw-r--r-- | src/gui/StaticPage.cpp | 12 | ||||
-rw-r--r-- | src/gui/Utils.cpp | 17 | ||||
-rw-r--r-- | src/main.cpp | 316 |
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; } |